Compare commits

..

172 Commits

Author SHA1 Message Date
zjs81 ea2354712d Fix formatting of cryptocurrency addresses in README 2026-03-06 15:41:02 -07:00
zach 7a0b8aad3d Added more crypto payment options 2026-03-06 15:39:54 -07:00
zjs81 bd34bb5e88 Merge pull request #264 from zjs81/dev-guessed-locations
Dev guessed locations
2026-03-06 15:19:03 -07:00
zach 81548fdc21 ai fixes 2026-03-06 15:18:48 -07:00
zach b2770ef028 fix ai suggestions 2026-03-06 15:11:21 -07:00
zach 7c479f9121 Formatted 2026-03-06 15:03:12 -07:00
zach 1f2dfc555b Add guessed node location map keys and translations
Adds map_showGuessedLocations and map_guessedLocation to app_en.arb and translates them across all 14 supported locales. Regenerates dart localizations.
2026-03-06 15:02:37 -07:00
zjs81 8eb6f32fef Merge pull request #239 from zjs81/dev-notifyListener
Implements a debounced notification listener in MeshCoreConnector
2026-03-06 14:52:27 -07:00
zjs81 d96cd34771 Merge pull request #251 from zjs81/dev-discoverScreen
Contact discovery
2026-03-06 11:59:24 -07:00
Winston Lowe 7d8e049745 Enhance message parsing and error handling in MeshCoreConnector (#260)
* Enhance readString method to include Latin-1 fallback for decoding errors

* Refactor _parseContactMessage to improve error handling and message parsing logic

* Update lib/connector/meshcore_connector.dart

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

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 22:56:39 -08:00
Winston Lowe d53465d13b persist discovered contacts when all are removed 2026-03-03 17:57:56 -08:00
Winston Lowe a0efbbe4bd Persist Discovered Contacts when updated 2026-03-03 17:44:28 -08:00
zjs81 bd5db9a9d5 Merge pull request #253 from ericszimmermann/ez_search-displayed-prefix
Allow search for prefix as Displayed in contact list
2026-03-02 18:44:04 -07:00
zjs81 79b17b53a0 Merge pull request #246 from Specter242/codex/signal-ui-consistency
Unify signal indicator UI across RSSI and SNR
2026-03-02 18:42:21 -07:00
ericz 647fe1523e make it that even combination <0x90 is allowed. 2026-03-02 21:42:44 +01:00
ericz b7d5ee5754 Allow search for prefix as Displayed in contact list. 2026-03-02 21:35:16 +01:00
Winston Lowe 38856c67e5 feat: Add functionality to delete all discovered contacts
- Implemented a new method to remove all discovered contacts from the list.
- Added confirmation dialog for deleting all discovered contacts in the discovery screen.
- Updated localization files to include new strings for deleting all discovered contacts.
- Refactored contact import logic to streamline the process.
- Enhanced the discovery handling to notify users appropriately based on settings.
2026-03-02 10:23:14 -08:00
zjs81 6bd3c17cdf Merge pull request #217 from MeshEnvy/chrome/main
enh: Chrome compatibility
2026-03-01 20:02:29 -07:00
zjs81 6d0712c450 Merge pull request #240 from ericszimmermann/ez_removeDevicenameBrackets
Show name of connected companion
2026-03-01 19:48:54 -07:00
Winston Lowe ddeb1edc2e refactor(discovery): simplify sorting logic for last seen contacts 2026-03-01 14:40:26 -08:00
Winston Lowe 8d73602509 add flags for manual contact addition and telemetry mode handling 2026-03-01 14:36:04 -08:00
Winston Lowe fcab69f9f0 refactor(connector): adjust frame length check and simplify contact handling logic
refactor(settings): extract settings sending logic into a separate method
refactor(ble_debug_log_service): remove unused command case for radio settings
refactor(app_bar): update compact width threshold for app bar display
2026-03-01 13:05:57 -08:00
Winston Lowe d2640e1294 feat(localization): update 'overwrite oldest contact' subtitle for multiple languages 2026-03-01 10:52:19 -08:00
Winston Lowe b02225c02e refactor(connector): remove unused radio settings frame and update command constant 2026-03-01 10:41:31 -08:00
Winston Lowe 128e99e3e7 refactor(settings): remove unused import for adaptive_app_bar_title 2026-03-01 10:35:32 -08:00
Winston Lowe 12bf46bba1 feat(localization): update contact settings translations for multiple languages
- Translated contact settings and related strings in Slovenian, Swedish, Ukrainian, Chinese, Dutch, Polish, Portuguese, Russian, and Slovak.
- Added new strings for discovered contacts actions such as adding, copying, and deleting contacts.
- Enhanced the DiscoveryContact model to include a rawPacket field for better data handling.
- Updated the contacts screen to support new actions in the context menu for discovered contacts.
- Improved the contact discovery store to handle the serialization of the new rawPacket field.
2026-03-01 10:13:17 -08:00
Winston Lowe 92d8e7cd0b Refactor contact search functionality to use DiscoveryContact model and simplify query matching 2026-02-28 19:14:22 -08:00
Winston Lowe 75610695c2 Add contact settings and discovery features
- Implemented contact settings in localization files for Swedish, Ukrainian, and Chinese.
- Added new DiscoveryContact model to handle discovered contacts.
- Created DiscoveryScreen to display discovered contacts with filtering and sorting options.
- Integrated contact discovery storage to persist discovered contacts.
- Updated settings screen to include options for automatic contact addition.
- Enhanced app bar and list filter widgets for better user experience.
- Fixed variable naming inconsistencies in contact model.
2026-02-28 19:11:11 -08:00
Specter242 57ea30cae9 Unify signal indicator UI 2026-02-27 14:30:15 -05:00
Winston Lowe e139383335 Add localized search functionality for contacts (#244)
- Introduced new localization keys for searching contacts, users, favorites, repeaters, and room servers in multiple languages.
- Updated localization files for Italian, Bulgarian, German, English, Spanish, French, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Enhanced the contacts screen to dynamically display search hints based on the selected contact type filter.
- Modified the map screen to utilize the new search functionality for contacts without a number.
2026-02-26 22:53:52 -08:00
ZIER 64428294c9 info • Unnecessary use of string interpolation • lib/widgets/app_bar.dart:43:23 • unnecessary_string_interpolations 2026-02-26 08:59:58 +01:00
ZIER e7a8c36bc4 more aesthetically pleasing display of Companionname 2026-02-26 08:51:57 +01:00
Winston Lowe 2a62390903 Implement debounced notification listener updates in MeshCoreConnector 2026-02-25 21:58:35 -08:00
Ben Allfree 75d25f6312 Merge branch 'main' into chrome/main 2026-02-24 22:51:51 -08:00
Ben Allfree 2a3119544c Merge branch 'main' of github.com:MeshEnvy/meshcore-open 2026-02-24 22:50:20 -08:00
Ben Allfree fb41a5bf10 Merge branch 'zjs81:main' into main 2026-02-24 22:47:48 -08:00
Ben Allfree d88786bb0f ble filtering 2026-02-24 22:41:03 -08:00
Ben Allfree e3148dd449 Merge main into chrome/main 2026-02-24 21:17:33 -08:00
Ben Allfree 96371c03ae pub lock upate 2026-02-24 21:17:24 -08:00
Ben Allfree cac65face6 Merge main into chrome/main 2026-02-24 21:15:49 -08:00
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
Ben Allfree a777236cd9 Merge branch 'zjs81:main' into main 2026-02-24 13:26:23 -08: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 7740698cde Merge branch 'main' into chrome/main 2026-02-23 15:03:20 -08: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 c8f93f9902 code cleanup 2026-02-23 04:30:13 -08:00
Ben Allfree c34be44950 merge from chat trace 2026-02-23 04:25:04 -08:00
Ben Allfree 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 549fc62632 chat fixes 2026-02-23 04:09:27 -08:00
Ben Allfree 53d073d8f2 deprecation fix 2026-02-23 03:43:49 -08:00
Ben Allfree 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 4975b5366e formatting fixes 2026-02-22 11:34:37 -08: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 c284e571b0 hide message tracing 2026-02-22 11:27:06 -08:00
Ben Allfree a1ee0789a6 deploy on tag only 2026-02-22 11:04:54 -08:00
Ben Allfree 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 40ac95e8e6 wrangler deploy 2026-02-22 10:48:22 -08:00
Ben Allfree 377f1df445 fix: browser detection 2026-02-22 10:47:51 -08:00
Ben Allfree 9865a03c53 fix: <enter> to send giphy 2026-02-22 09:20:20 -08:00
Ben Allfree a5555bd606 fix: return cursor to message window after send 2026-02-22 09:16:07 -08:00
Ben Allfree 1b4d31a36e gitignore update 2026-02-22 09:11:49 -08:00
Ben Allfree 8e07440114 BLE fix 2026-02-22 08:38:22 -08:00
Ben Allfree 71129bdf4d chrome BLE load fix 2026-02-22 08:37:07 -08:00
Ben Allfree ab05cf8b3e chrome BLE sync 2026-02-22 08:33:45 -08:00
Ben Allfree 452e5337f0 chrome connect 2026-02-22 08:31:29 -08:00
Ben Allfree 6ac987e7cf select BLE device 2026-02-22 08:10:16 -08:00
Ben Allfree 5522f9a236 BLE select cancel 2026-02-22 08:05:19 -08:00
Ben Allfree b4f79c1aae Merge branch 'enh/filter-ble-at-os' into chrome/main 2026-02-22 07:41:36 -08:00
Ben Allfree b08defcff4 Merge branch 'chrome/4-chrome-required-screen' into chrome/main 2026-02-22 07:40:57 -08:00
Ben Allfree 5676cbd84e chrome required screen 2026-02-22 07:40:40 -08:00
Ben Allfree cf8f01128b filter BLE at OS level 2026-02-22 07:15:09 -08:00
Ben Allfree b5e47ce44f filter BLE at OS level 2026-02-22 07:09:35 -08:00
Ben Allfree 7b2f75047c Merge branch 'chrome/1-readme' into chrome/main 2026-02-22 06:59:05 -08:00
Ben Allfree 6d63e49938 add platforminfo helper 2026-02-22 06:54:27 -08:00
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
102 changed files with 17518 additions and 3933 deletions
+38
View File
@@ -0,0 +1,38 @@
name: Deploy to Cloudflare Workers
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
# Match local development version which provides Dart 3.11.0
flutter-version: '3.41.2'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Get dependencies
run: flutter pub get
- name: Build Web
run: bun run build
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
+4
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.flutter-plugins-dependencies
.pub-cache/
.pub/
pubspec.lock
/build/
/coverage/
@@ -82,3 +83,6 @@ keystore.properties
# IDE
.vscode/launch.json
.vscode/settings.json
# Cloudflare Wrangler
.wrangler
+1
View File
@@ -0,0 +1 @@
4.0.0
+6
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
@@ -230,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
Your support helps maintain and improve this open-source project!
## Acknowledgments
+3 -3
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 {
+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

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

File diff suppressed because it is too large Load Diff
+88 -35
View File
@@ -34,8 +34,14 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readString() {
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readCString(int maxLength) {
final value = <int>[];
@@ -114,25 +120,27 @@ class BufferWriter {
}
void writeHex(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException(
'Invalid hex characters at position $i: $hexByte',
);
}
result.add(byte);
}
writeBytes(Uint8List.fromList(result));
writeBytes(hex2Uint8List(hex));
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
const int cmdAppStart = 1;
const int cmdSendTxtMsg = 2;
@@ -162,11 +170,13 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdGetRadioSettings = 57;
const int cmdSendAnonReq = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
// Text message types
const int txtTypePlain = 0;
@@ -200,8 +210,8 @@ const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
const int respCodeAutoAddConfig = 25;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -247,6 +257,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
//auto-add flags
const int autoAddOverwriteOldestFlag =
1 << 0; // 0x01 - overwrite oldest non-favourite when full
const int autoAddChatFlag =
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
const int autoAddRepeaterFlag =
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
const int autoAddRoomServerFlag =
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
const int autoAddSensorFlag =
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// Sizes
const int pubKeySize = 32;
const int maxPathSize = 64;
@@ -290,13 +312,14 @@ 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;
const int contactTimestampOffset = 132;
const int contactLatOffset = 136;
const int contactLonOffset = 140;
const int contactLastmodOffset = 144;
const int contactLastModOffset = 144;
const int contactFrameSize = 148;
// Message frame offsets
@@ -585,18 +608,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();
}
@@ -663,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
return writer.toBytes();
}
// Build CMD_GET_RADIO_SETTINGS frame
Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
Uint8List buildGetAutoAddFlagsFrame() {
return Uint8List.fromList([cmdGetAutoAddConfig]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -797,10 +830,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
writer.writeBytes(contactFrame);
return writer.toBytes();
}
@@ -814,20 +847,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
writer.writeByte(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
writer.writeByte(0x01);
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
// Build CMD_SET_AUTO_ADD_CONFIG frame
// Format: [cmd][flags]
Uint8List buildSetAutoAddConfigFrame({
required bool autoAddChat,
required bool autoAddRepeater,
required bool autoAddRoomServer,
required bool autoAddSensor,
required bool overwriteOldest,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetAutoAddConfig);
int flags = 0;
if (autoAddChat) flags |= autoAddChatFlag;
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
if (autoAddSensor) flags |= autoAddSensorFlag;
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
writer.writeByte(flags);
return writer.toBytes();
}
+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);
}
}
+234 -6
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": {
@@ -1557,6 +1562,8 @@
"contacts_clipboardEmpty": "Клипборда е празна.",
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
"appSettings_languageRu": "Руски",
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
"contacts_contactImported": "Контактът е импортиран.",
"contacts_zeroHopAdvert": "Реклама без скок",
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
@@ -1599,6 +1606,227 @@
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"scanner_chromeRequired": "Изисква се браузър Chrome",
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства"
"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} потребители...",
"contactsSettings_title": "Настройки на контактите",
"contactsSettings_autoAddTitle": "Автоматично откриване",
"contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители",
"contactsSettings_otherTitle": "Други настройки свързани с контакти",
"settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.",
"settings_contactSettings": "Настройки за контакти",
"contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици",
"contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите",
"contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.",
"contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи",
"contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.",
"contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.",
"contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.",
"contactsSettings_overwriteOldestTitle": "Премахни най-старото",
"discoveredContacts_Title": "Открити контакти",
"discoveredContacts_searchHint": "Търсене на открити контакти",
"discoveredContacts_noMatching": "Няма съвпадащи контакти",
"discoveredContacts_contactAdded": "Контакт добавен",
"discoveredContacts_copyContact": "Копирай контакт в клипборда",
"discoveredContacts_deleteContact": "Изтрий контакт",
"discoveredContacts_addContact": "Добави контакт",
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
"common_deleteAll": "Изтрий всичко",
"map_guessedLocation": "Предполагано местоположение",
"map_showGuessedLocations": "Покажете местоположенията на предположените възли."
}
+234 -6
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",
@@ -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": {
@@ -1344,6 +1349,9 @@
"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",
@@ -1557,6 +1565,8 @@
"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-Ankündigung",
@@ -1625,8 +1635,226 @@
"pathTrace_clearTooltip": "Pfad löschen",
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
"scanner_chromeRequired": "Chrome Browser erforderlich",
"scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.",
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
"scanner_enableBluetooth": "Bluetooth aktivieren",
"snrIndicator_lastSeen": "Zuletzt gesehen",
"snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater"
"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...",
"settings_contactSettings": "Kontakteinstellungen",
"contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten",
"contactsSettings_title": "Kontakteinstellungen",
"contactsSettings_autoAddTitle": "Automatische Erkennung",
"contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern",
"settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten",
"contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen",
"contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen",
"contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen",
"contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen",
"contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen",
"contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.",
"discoveredContacts_noMatching": "Keine passenden Kontakte",
"discoveredContacts_searchHint": "Entdeckte Kontakte suchen",
"discoveredContacts_addContact": "Kontakt hinzufügen",
"discoveredContacts_contactAdded": "Kontakt hinzugefügt",
"discoveredContacts_deleteContact": "Kontakt löschen",
"discoveredContacts_Title": "Entdeckte Kontakte",
"discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren",
"contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten",
"contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen",
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
"common_deleteAll": "Alles löschen",
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
"map_guessedLocation": "Geschätzter Ort"
}
+641 -172
View File
File diff suppressed because it is too large Load Diff
+234 -6
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": {
@@ -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.",
@@ -1625,8 +1632,229 @@
"map_removeLast": "Eliminar último",
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
"scanner_chromeRequired": "Navegador Chrome requerido",
"scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.",
"scanner_bluetoothOff": "Bluetooth está desactivado.",
"scanner_enableBluetooth": "Habilitar Bluetooth",
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
"snrIndicator_lastSeen": "Visto por última vez"
"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...",
"contactsSettings_autoAddTitle": "Detección automática",
"settings_contactSettings": "Configuración de contacto",
"contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente",
"contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto",
"contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.",
"contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente",
"contactsSettings_title": "Configuración de contactos",
"settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.",
"contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente",
"contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo",
"contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala",
"discoveredContacts_noMatching": "No se encontraron contactos coincidentes",
"discoveredContacts_contactAdded": "Contacto agregado",
"discoveredContacts_copyContact": "Copiar contacto al portapapeles",
"discoveredContacts_deleteContact": "Eliminar contacto",
"discoveredContacts_Title": "Contactos descubiertos",
"discoveredContacts_searchHint": "Buscar contactos descubiertos",
"discoveredContacts_addContact": "Agregar contacto",
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
"common_deleteAll": "Eliminar todo",
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
"map_guessedLocation": "Ubicación estimada",
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos."
}
+237 -9
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": {
@@ -267,7 +270,7 @@
}
},
"contacts_manageRepeater": "Gérer le répéteur",
"contacts_roomLogin": "Connexion Salle",
"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": {
@@ -801,7 +806,7 @@
"dialog_disconnect": "Déconnecter",
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
"login_repeaterLogin": "Connexion au répéteur",
"login_roomLogin": "Connexion Salle",
"login_roomLogin": "Connexion Room Server",
"login_password": "Mot de passe",
"login_enterPassword": "Entrez votre mot de passe",
"login_savePassword": "Sauvegarder le mot de passe",
@@ -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,6 +1561,8 @@
"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 à tout le réseau",
@@ -1597,8 +1604,229 @@
"map_removeLast": "Supprimer le dernier",
"map_runTrace": "Exécuter la traçage de chemin",
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
"scanner_chromeRequired": "Navigateur Chrome requis",
"scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.",
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
"scanner_enableBluetooth": "Activer le Bluetooth",
"snrIndicator_lastSeen": "Dernière fois vu",
"snrIndicator_nearByRepeaters": "Répéteurs à proximité"
"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...",
"settings_contactSettings": "Paramètres de contact",
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
"contactsSettings_title": "Paramètres des contacts",
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
"contactsSettings_autoAddTitle": "Découverte automatique",
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
"discoveredContacts_noMatching": "Aucun contact correspondant",
"discoveredContacts_contactAdded": "Contact ajouté",
"discoveredContacts_addContact": "Ajouter un contact",
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
"discoveredContacts_deleteContact": "Supprimer le contact",
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
"discoveredContacts_Title": "Contacts découverts",
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
"common_deleteAll": "Supprimer tout",
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
"map_guessedLocation": "Lieu deviné"
}
+234 -6
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": {
@@ -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",
@@ -1598,7 +1605,228 @@
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_chromeRequired": "Browser Chrome richiesto",
"scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.",
"scanner_enableBluetooth": "Abilita il Bluetooth",
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
"snrIndicator_lastSeen": "Ultimo accesso"
"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...",
"contactsSettings_title": "Impostazioni dei contatti",
"settings_contactSettings": "Impostazioni di contatto",
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
"contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.",
"contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente",
"contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.",
"contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori",
"settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti",
"contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente",
"contactsSettings_autoAddTitle": "Scoperta automatica",
"contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti",
"discoveredContacts_noMatching": "Nessun contatto corrispondente",
"contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.",
"discoveredContacts_searchHint": "Cerca contatti scoperti",
"contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze",
"discoveredContacts_addContact": "Aggiungi contatto",
"contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio",
"discoveredContacts_Title": "Contatti scoperti",
"discoveredContacts_contactAdded": "Contatto aggiunto",
"discoveredContacts_deleteContact": "Elimina Contatto",
"discoveredContacts_copyContact": "Copia contatto negli appunti",
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
"common_deleteAll": "Elimina tutto",
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
"map_guessedLocation": "Località indovinata",
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi"
}
+533 -26
View File
@@ -184,6 +184,12 @@ abstract class AppLocalizations {
/// **'Delete'**
String get common_delete;
/// No description provided for @common_deleteAll.
///
/// In en, this message translates to:
/// **'Delete All'**
String get common_deleteAll;
/// No description provided for @common_close.
///
/// In en, this message translates to:
@@ -388,6 +394,18 @@ abstract class AppLocalizations {
/// **'Please turn on Bluetooth to scan for devices'**
String get scanner_bluetoothOffMessage;
/// No description provided for @scanner_chromeRequired.
///
/// In en, this message translates to:
/// **'Chrome Browser Required'**
String get scanner_chromeRequired;
/// No description provided for @scanner_chromeRequiredMessage.
///
/// In en, this message translates to:
/// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'**
String get scanner_chromeRequiredMessage;
/// No description provided for @scanner_enableBluetooth.
///
/// In en, this message translates to:
@@ -544,6 +562,18 @@ abstract class AppLocalizations {
/// **'Longitude'**
String get settings_longitude;
/// No description provided for @settings_contactSettings.
///
/// In en, this message translates to:
/// **'Contact Settings'**
String get settings_contactSettings;
/// No description provided for @settings_contactSettingsSubtitle.
///
/// In en, this message translates to:
/// **'Settings for how contacts are added.'**
String get settings_contactSettingsSubtitle;
/// No description provided for @settings_privacyMode.
///
/// In en, this message translates to:
@@ -700,6 +730,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:
@@ -748,24 +784,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:
@@ -820,17 +838,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.
///
@@ -976,6 +1000,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:
@@ -1252,6 +1288,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:
@@ -1312,11 +1366,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.
///
@@ -1534,6 +1624,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:
@@ -1546,6 +1648,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:
@@ -2044,6 +2152,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:
@@ -2296,6 +2410,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:
@@ -2512,6 +2638,18 @@ abstract class AppLocalizations {
/// **'Show shared markers'**
String get map_showSharedMarkers;
/// No description provided for @map_showGuessedLocations.
///
/// In en, this message translates to:
/// **'Show guessed node locations'**
String get map_showGuessedLocations;
/// No description provided for @map_guessedLocation.
///
/// In en, this message translates to:
/// **'Guessed location'**
String get map_guessedLocation;
/// No description provided for @map_lastSeenTime.
///
/// In en, this message translates to:
@@ -4712,6 +4850,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:
@@ -4778,6 +4934,225 @@ abstract class AppLocalizations {
/// **'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:
@@ -5047,6 +5422,138 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Last seen'**
String get snrIndicator_lastSeen;
/// No description provided for @contactsSettings_title.
///
/// In en, this message translates to:
/// **'Contacts settings'**
String get contactsSettings_title;
/// No description provided for @contactsSettings_autoAddTitle.
///
/// In en, this message translates to:
/// **'Automatic Discovery'**
String get contactsSettings_autoAddTitle;
/// No description provided for @contactsSettings_otherTitle.
///
/// In en, this message translates to:
/// **'Other contact related settings'**
String get contactsSettings_otherTitle;
/// No description provided for @contactsSettings_autoAddUsersTitle.
///
/// In en, this message translates to:
/// **'Auto-add users'**
String get contactsSettings_autoAddUsersTitle;
/// No description provided for @contactsSettings_autoAddUsersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered users.'**
String get contactsSettings_autoAddUsersSubtitle;
/// No description provided for @contactsSettings_autoAddRepeatersTitle.
///
/// In en, this message translates to:
/// **'Auto-add repeaters'**
String get contactsSettings_autoAddRepeatersTitle;
/// No description provided for @contactsSettings_autoAddRepeatersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered repeaters.'**
String get contactsSettings_autoAddRepeatersSubtitle;
/// No description provided for @contactsSettings_autoAddRoomServersTitle.
///
/// In en, this message translates to:
/// **'Auto-add room servers'**
String get contactsSettings_autoAddRoomServersTitle;
/// No description provided for @contactsSettings_autoAddRoomServersSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered room servers.'**
String get contactsSettings_autoAddRoomServersSubtitle;
/// No description provided for @contactsSettings_autoAddSensorsTitle.
///
/// In en, this message translates to:
/// **'Auto-add sensors'**
String get contactsSettings_autoAddSensorsTitle;
/// No description provided for @contactsSettings_autoAddSensorsSubtitle.
///
/// In en, this message translates to:
/// **'Allow the companion to automatically add discovered sensors.'**
String get contactsSettings_autoAddSensorsSubtitle;
/// No description provided for @contactsSettings_overwriteOldestTitle.
///
/// In en, this message translates to:
/// **'Overwrite Oldest'**
String get contactsSettings_overwriteOldestTitle;
/// No description provided for @contactsSettings_overwriteOldestSubtitle.
///
/// In en, this message translates to:
/// **'When the contact list is full, the oldest non-favorited contact will be replaced.'**
String get contactsSettings_overwriteOldestSubtitle;
/// No description provided for @discoveredContacts_Title.
///
/// In en, this message translates to:
/// **'Discovered Contacts'**
String get discoveredContacts_Title;
/// No description provided for @discoveredContacts_noMatching.
///
/// In en, this message translates to:
/// **'No matching contacts'**
String get discoveredContacts_noMatching;
/// No description provided for @discoveredContacts_searchHint.
///
/// In en, this message translates to:
/// **'Search discovered contacts'**
String get discoveredContacts_searchHint;
/// No description provided for @discoveredContacts_contactAdded.
///
/// In en, this message translates to:
/// **'Contact added'**
String get discoveredContacts_contactAdded;
/// No description provided for @discoveredContacts_addContact.
///
/// In en, this message translates to:
/// **'Add Contact'**
String get discoveredContacts_addContact;
/// No description provided for @discoveredContacts_copyContact.
///
/// In en, this message translates to:
/// **'Copy Contact to clipboard'**
String get discoveredContacts_copyContact;
/// No description provided for @discoveredContacts_deleteContact.
///
/// In en, this message translates to:
/// **'Delete Discovered Contact'**
String get discoveredContacts_deleteContact;
/// No description provided for @discoveredContacts_deleteContactAll.
///
/// In en, this message translates to:
/// **'Delete All Discovered Contacts'**
String get discoveredContacts_deleteContactAll;
/// No description provided for @discoveredContacts_deleteContactAllContent.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete all discovered contacts?'**
String get discoveredContacts_deleteContactAllContent;
}
class _AppLocalizationsDelegate
+326 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_delete => 'Изтрий';
@override
String get common_deleteAll => 'Изтрий всичко';
@override
String get common_close => 'Затвори';
@@ -150,6 +153,13 @@ class AppLocalizationsBg extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@@ -234,6 +244,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_longitude => 'Дължина';
@override
String get settings_contactSettings => 'Настройки за контакти';
@override
String get settings_contactSettingsSubtitle =>
'Настройки за добавяне на контакти.';
@override
String get settings_privacyMode => 'Режим на поверителност';
@@ -326,6 +343,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 => 'Име';
@@ -350,15 +371,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)';
@@ -387,10 +399,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) {
@@ -466,6 +483,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 => 'Уведомления';
@@ -626,6 +651,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 => 'Няма избрана област';
@@ -664,7 +698,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 => 'Няма непрочетени контакти';
@@ -789,6 +851,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 => 'Изтрий канала';
@@ -797,6 +865,11 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Неуспешно изтриване на канала \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Каналът \"$name\" е изтрит';
@@ -1084,6 +1157,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление на пътища';
@override
String get chat_ShowAllPaths => 'Покажи всички пътища';
@override
String get chat_routingMode => 'Режим на маршрутизиране';
@@ -1244,6 +1320,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 => 'Няма възли с данни за местоположение.';
@@ -1361,6 +1443,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Покажи споделени маркери';
@override
String get map_showGuessedLocations =>
'Покажете местоположенията на предположените възли.';
@override
String get map_guessedLocation => 'Предполагано местоположение';
@override
String get map_lastSeenTime => 'Последна видяна дата';
@@ -2691,6 +2780,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 => 'Потребители';
@@ -2725,6 +2823,144 @@ class AppLocalizationsBg extends AppLocalizations {
@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 => 'Пътен проследяване';
@@ -2900,4 +3136,82 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последно видян';
@override
String get contactsSettings_title => 'Настройки на контактите';
@override
String get contactsSettings_autoAddTitle => 'Автоматично откриване';
@override
String get contactsSettings_otherTitle =>
'Други настройки свързани с контакти';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматично добавяне на потребители';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Позволи на спътника да добавя автоматично откритите потребители.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматично добавяне на повтарящи се елементи';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматично добавяне на сървъри на стаите';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Позволи на спътника да добавя автоматично откритите сървъри на стаите.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматично добавяне на датчици';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Позволи на спътника да добавя автоматично откритите датчици.';
@override
String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.';
@override
String get discoveredContacts_Title => 'Открити контакти';
@override
String get discoveredContacts_noMatching => 'Няма съвпадащи контакти';
@override
String get discoveredContacts_searchHint => 'Търсене на открити контакти';
@override
String get discoveredContacts_contactAdded => 'Контакт добавен';
@override
String get discoveredContacts_addContact => 'Добави контакт';
@override
String get discoveredContacts_copyContact => 'Копирай контакт в клипборда';
@override
String get discoveredContacts_deleteContact => 'Изтрий контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Изтриване на Всички Открити Контакти';
@override
String get discoveredContacts_deleteContactAllContent =>
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
}
+329 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get common_delete => 'Löschen';
@override
String get common_deleteAll => 'Alles löschen';
@override
String get common_close => 'Schließen';
@@ -150,6 +153,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_chromeRequired => 'Chrome Browser erforderlich';
@override
String get scanner_chromeRequiredMessage =>
'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.';
@override
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
@@ -233,6 +243,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_longitude => 'Längengrad';
@override
String get settings_contactSettings => 'Kontakteinstellungen';
@override
String get settings_contactSettingsSubtitle =>
'Einstellungen für das Hinzufügen von Kontakten';
@override
String get settings_privacyMode => 'Privatsphäreeinstellung';
@@ -320,6 +337,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';
@@ -344,15 +365,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)';
@@ -381,10 +393,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) {
@@ -460,6 +477,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';
@@ -623,6 +648,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';
@@ -660,7 +694,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';
@@ -786,6 +848,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';
@@ -794,6 +862,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';
@@ -1084,6 +1157,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';
@@ -1243,6 +1319,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';
@@ -1360,6 +1442,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
@override
String get map_showGuessedLocations =>
'Zeige die vermuteten Knotenpositionen';
@override
String get map_guessedLocation => 'Geschätzter Ort';
@override
String get map_lastSeenTime => 'Letzte Sichtung';
@@ -2696,6 +2785,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';
@@ -2730,6 +2828,145 @@ class AppLocalizationsDe extends AppLocalizations {
@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';
@@ -2908,4 +3145,84 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zuletzt gesehen';
@override
String get contactsSettings_title => 'Kontakteinstellungen';
@override
String get contactsSettings_autoAddTitle => 'Automatische Erkennung';
@override
String get contactsSettings_otherTitle =>
'Weitere Einstellungen zu Kontakten';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automatische Hinzufügung von Benutzern';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatisch Repeater hinzufügen';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatisch Raumservers hinzufügen';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatisch Sensoren hinzufügen';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen';
@override
String get contactsSettings_overwriteOldestTitle =>
'Überschreiben des Ältesten';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.';
@override
String get discoveredContacts_Title => 'Entdeckte Kontakte';
@override
String get discoveredContacts_noMatching => 'Keine passenden Kontakte';
@override
String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen';
@override
String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt';
@override
String get discoveredContacts_addContact => 'Kontakt hinzufügen';
@override
String get discoveredContacts_copyContact =>
'Kontakt in die Zwischenablage kopieren';
@override
String get discoveredContacts_deleteContact => 'Kontakt löschen';
@override
String get discoveredContacts_deleteContactAll =>
'Alle entdeckten Kontakte löschen';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
}
+319 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_delete => 'Delete';
@override
String get common_deleteAll => 'Delete All';
@override
String get common_close => 'Close';
@@ -149,6 +152,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_chromeRequired => 'Chrome Browser Required';
@override
String get scanner_chromeRequiredMessage =>
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@@ -232,6 +242,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Contact Settings';
@override
String get settings_contactSettingsSubtitle =>
'Settings for how contacts are added.';
@override
String get settings_privacyMode => 'Privacy Mode';
@@ -318,6 +335,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';
@@ -342,15 +363,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)';
@@ -379,10 +391,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) {
@@ -458,6 +475,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';
@@ -618,6 +642,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';
@@ -654,7 +687,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';
@@ -778,6 +839,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';
@@ -786,6 +853,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';
@@ -1069,6 +1141,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';
@@ -1223,6 +1298,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';
@@ -1340,6 +1421,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Show shared markers';
@override
String get map_showGuessedLocations => 'Show guessed node locations';
@override
String get map_guessedLocation => 'Guessed location';
@override
String get map_lastSeenTime => 'Last Seen Time';
@@ -2650,6 +2737,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';
@@ -2684,6 +2780,143 @@ class AppLocalizationsEn extends AppLocalizations {
@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';
@@ -2855,4 +3088,78 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Last seen';
@override
String get contactsSettings_title => 'Contacts settings';
@override
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
@override
String get contactsSettings_otherTitle => 'Other contact related settings';
@override
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Allow the companion to automatically add discovered users.';
@override
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Allow the companion to automatically add discovered repeaters.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Auto-add room servers';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Allow the companion to automatically add discovered room servers.';
@override
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Allow the companion to automatically add discovered sensors.';
@override
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'When the contact list is full, the oldest non-favorited contact will be replaced.';
@override
String get discoveredContacts_Title => 'Discovered Contacts';
@override
String get discoveredContacts_noMatching => 'No matching contacts';
@override
String get discoveredContacts_searchHint => 'Search discovered contacts';
@override
String get discoveredContacts_contactAdded => 'Contact added';
@override
String get discoveredContacts_addContact => 'Add Contact';
@override
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
@override
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
@override
String get discoveredContacts_deleteContactAll =>
'Delete All Discovered Contacts';
@override
String get discoveredContacts_deleteContactAllContent =>
'Are you sure you want to delete all discovered contacts?';
}
+331 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_delete => 'Eliminar';
@override
String get common_deleteAll => 'Eliminar todo';
@override
String get common_close => 'Cerrar';
@@ -150,6 +153,13 @@ class AppLocalizationsEs extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome requerido';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@@ -233,6 +243,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_longitude => 'Longitud';
@override
String get settings_contactSettings => 'Configuración de contacto';
@override
String get settings_contactSettingsSubtitle =>
'Configuración de cómo se agregan los contactos.';
@override
String get settings_privacyMode => 'Modo Privacidad';
@@ -323,6 +340,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';
@@ -347,15 +368,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)';
@@ -384,10 +396,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) {
@@ -463,6 +480,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';
@@ -624,6 +649,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';
@@ -661,7 +695,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';
@@ -787,6 +849,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';
@@ -795,6 +863,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';
@@ -1083,6 +1156,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';
@@ -1241,6 +1317,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';
@@ -1358,6 +1440,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
@override
String get map_showGuessedLocations =>
'Mostrar las ubicaciones estimadas de los nodos.';
@override
String get map_guessedLocation => 'Ubicación estimada';
@override
String get map_lastSeenTime => 'Última vez que se vio';
@@ -2689,6 +2778,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';
@@ -2723,6 +2821,146 @@ class AppLocalizationsEs extends AppLocalizations {
@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';
@@ -2899,4 +3137,85 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto por última vez';
@override
String get contactsSettings_title => 'Configuración de contactos';
@override
String get contactsSettings_autoAddTitle => 'Detección automática';
@override
String get contactsSettings_otherTitle =>
'Otras configuraciones relacionadas con el contacto';
@override
String get contactsSettings_autoAddUsersTitle =>
'Agregar usuarios automáticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Agregar repetidores automáticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Agregar automáticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Agregar sensores automáticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobreescribir el más antiguo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
@override
String get discoveredContacts_Title => 'Contactos descubiertos';
@override
String get discoveredContacts_noMatching =>
'No se encontraron contactos coincidentes';
@override
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
@override
String get discoveredContacts_contactAdded => 'Contacto agregado';
@override
String get discoveredContacts_addContact => 'Agregar contacto';
@override
String get discoveredContacts_copyContact =>
'Copiar contacto al portapapeles';
@override
String get discoveredContacts_deleteContact => 'Eliminar contacto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminar Todos los Contactos Descubiertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
}
+332 -15
View File
@@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_delete => 'Supprimer';
@override
String get common_deleteAll => 'Supprimer tout';
@override
String get common_close => 'Fermer';
@@ -150,6 +153,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Veuillez activer le Bluetooth pour rechercher des appareils.';
@override
String get scanner_chromeRequired => 'Navigateur Chrome requis';
@override
String get scanner_chromeRequiredMessage =>
'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.';
@override
String get scanner_enableBluetooth => 'Activer le Bluetooth';
@@ -234,6 +244,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Paramètres de contact';
@override
String get settings_contactSettingsSubtitle =>
'Paramètres pour l\'ajout de contacts';
@override
String get settings_privacyMode => 'Mode de confidentialité';
@@ -324,6 +341,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';
@@ -348,15 +369,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)';
@@ -385,10 +397,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) {
@@ -464,6 +481,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';
@@ -626,6 +651,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';
@@ -664,7 +698,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';
@@ -687,7 +749,7 @@ class AppLocalizationsFr extends AppLocalizations {
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';
@@ -789,6 +851,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';
@@ -797,6 +865,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é';
@@ -1086,6 +1159,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';
@@ -1247,6 +1323,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';
@@ -1365,6 +1447,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
@override
String get map_showGuessedLocations =>
'Afficher les emplacements des nœuds estimés';
@override
String get map_guessedLocation => 'Lieu deviné';
@override
String get map_lastSeenTime => 'Dernière fois vu';
@@ -1535,7 +1624,7 @@ class AppLocalizationsFr extends AppLocalizations {
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';
@@ -1660,7 +1749,7 @@ class AppLocalizationsFr extends AppLocalizations {
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';
@@ -2705,6 +2794,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';
@@ -2739,6 +2837,145 @@ class AppLocalizationsFr extends AppLocalizations {
@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';
@@ -2922,4 +3159,84 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Dernière fois vu';
@override
String get contactsSettings_title => 'Paramètres des contacts';
@override
String get contactsSettings_autoAddTitle => 'Découverte automatique';
@override
String get contactsSettings_otherTitle =>
'Autres paramètres liés aux contacts';
@override
String get contactsSettings_autoAddUsersTitle =>
'Ajouter automatiquement les utilisateurs';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Ajouter automatiquement les répéteurs';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Ajouter automatiquement les serveurs de salle';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Ajouter automatiquement les capteurs';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.';
@override
String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.';
@override
String get discoveredContacts_Title => 'Contacts découverts';
@override
String get discoveredContacts_noMatching => 'Aucun contact correspondant';
@override
String get discoveredContacts_searchHint =>
'Rechercher des contacts découverts';
@override
String get discoveredContacts_contactAdded => 'Contact ajouté';
@override
String get discoveredContacts_addContact => 'Ajouter un contact';
@override
String get discoveredContacts_copyContact =>
'Copier le contact dans le presse-papiers';
@override
String get discoveredContacts_deleteContact => 'Supprimer le contact';
@override
String get discoveredContacts_deleteContactAll =>
'Supprimer tous les contacts découverts';
@override
String get discoveredContacts_deleteContactAllContent =>
'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?';
}
+327 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_delete => 'Elimina';
@override
String get common_deleteAll => 'Elimina tutto';
@override
String get common_close => 'Chiudi';
@@ -150,6 +153,13 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_chromeRequired => 'Browser Chrome richiesto';
@override
String get scanner_chromeRequiredMessage =>
'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@@ -233,6 +243,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_longitude => 'Longitudine';
@override
String get settings_contactSettings => 'Impostazioni di contatto';
@override
String get settings_contactSettingsSubtitle =>
'Impostazioni per l\'aggiunta dei contatti';
@override
String get settings_privacyMode => 'Modalità Privacy';
@@ -322,6 +339,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';
@@ -346,15 +367,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)';
@@ -383,10 +395,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) {
@@ -462,6 +479,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';
@@ -623,6 +648,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';
@@ -660,7 +694,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';
@@ -785,6 +847,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';
@@ -793,6 +861,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';
@@ -1081,6 +1154,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';
@@ -1240,6 +1316,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';
@@ -1357,6 +1439,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
@override
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
@override
String get map_guessedLocation => 'Località indovinata';
@override
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
@@ -2689,6 +2777,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';
@@ -2724,6 +2821,145 @@ class AppLocalizationsIt extends AppLocalizations {
@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';
@@ -2903,4 +3139,83 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Ultimo accesso';
@override
String get contactsSettings_title => 'Impostazioni dei contatti';
@override
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
@override
String get contactsSettings_otherTitle =>
'Altre impostazioni relative ai contatti';
@override
String get contactsSettings_autoAddUsersTitle =>
'Aggiungere utenti automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Aggiungere ripetitori automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Aggiungere automaticamente i server delle stanze';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Aggiungere automaticamente i sensori';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sostituisci il più vecchio';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
@override
String get discoveredContacts_Title => 'Contatti scoperti';
@override
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
@override
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
@override
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
@override
String get discoveredContacts_addContact => 'Aggiungi contatto';
@override
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
@override
String get discoveredContacts_deleteContact => 'Elimina Contatto';
@override
String get discoveredContacts_deleteContactAll =>
'Eliminare tutti i contatti scoperti';
@override
String get discoveredContacts_deleteContactAllContent =>
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
}
+326 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_delete => 'Verwijderen';
@override
String get common_deleteAll => 'Alles verwijderen';
@override
String get common_close => 'Sluiten';
@@ -149,6 +152,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
@override
String get scanner_chromeRequired => 'Chrome-browser vereist';
@override
String get scanner_chromeRequiredMessage =>
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
@override
String get scanner_enableBluetooth => 'Activeer Bluetooth';
@@ -233,6 +243,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_longitude => 'Lengtegraad';
@override
String get settings_contactSettings => 'Contactinstellingen';
@override
String get settings_contactSettingsSubtitle =>
'Instellingen voor het toevoegen van contacten';
@override
String get settings_privacyMode => 'Privacy Mode';
@@ -320,6 +337,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';
@@ -344,15 +365,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)';
@@ -381,10 +393,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) {
@@ -460,6 +477,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';
@@ -621,6 +645,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';
@@ -658,7 +691,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';
@@ -783,6 +844,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';
@@ -791,6 +858,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';
@@ -1078,6 +1150,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';
@@ -1236,6 +1311,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';
@@ -1353,6 +1434,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
@override
String get map_showGuessedLocations =>
'Toon de voorspelde locaties van de knopen';
@override
String get map_guessedLocation => 'Geroerde locatie';
@override
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
@@ -2681,6 +2769,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';
@@ -2715,6 +2812,145 @@ class AppLocalizationsNl extends AppLocalizations {
@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';
@@ -2891,4 +3127,82 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Laatst gezien';
@override
String get contactsSettings_title => 'Instellingen voor contacten';
@override
String get contactsSettings_autoAddTitle => 'Automatische detectie';
@override
String get contactsSettings_otherTitle =>
'Andere instellingen voor contactgerelateerde zaken';
@override
String get contactsSettings_autoAddUsersTitle =>
'Gebruikers automatisch toevoegen';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatisch herhalingstoestellen toevoegen';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatisch kamerservers toevoegen';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatisch sensoren toevoegen';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
@override
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
@override
String get discoveredContacts_Title => 'Ontdekte contacten';
@override
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
@override
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
@override
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
@override
String get discoveredContacts_addContact => 'Contact toevoegen';
@override
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
@override
String get discoveredContacts_deleteContact => 'Contact verwijderen';
@override
String get discoveredContacts_deleteContactAll =>
'Verwijder alle ontdekte contacten';
@override
String get discoveredContacts_deleteContactAllContent =>
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
}
+325 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_delete => 'Usuń';
@override
String get common_deleteAll => 'Usuń wszystko';
@override
String get common_close => 'Zamknąć';
@@ -150,6 +153,13 @@ class AppLocalizationsPl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.';
@override
String get scanner_enableBluetooth => 'Włącz Bluetooth';
@@ -235,6 +245,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_longitude => 'Długość';
@override
String get settings_contactSettings => 'Ustawienia kontaktowe';
@override
String get settings_contactSettingsSubtitle =>
'Ustawienia dotyczące sposobu dodawania kontaktów';
@override
String get settings_privacyMode => 'Tryb Prywatny';
@@ -323,6 +340,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ę';
@@ -347,15 +368,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)';
@@ -385,10 +397,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) {
@@ -464,6 +481,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';
@@ -625,6 +649,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.';
@@ -662,7 +695,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';
@@ -788,6 +849,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ł';
@@ -796,6 +863,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';
@@ -1083,6 +1155,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';
@@ -1242,6 +1317,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';
@@ -1359,6 +1440,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
@override
String get map_showGuessedLocations =>
'Wyświetl lokalizacje zgadanych węzłów';
@override
String get map_guessedLocation => 'Wydana lokalizacja';
@override
String get map_lastSeenTime => 'Ostatni raz widiany';
@@ -2688,6 +2776,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';
@@ -2722,6 +2819,144 @@ class AppLocalizationsPl extends AppLocalizations {
@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';
@@ -2905,4 +3140,82 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
@override
String get contactsSettings_title => 'Ustawienia kontaktów';
@override
String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie';
@override
String get contactsSettings_otherTitle =>
'Inne ustawienia związane z kontaktami';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automatycznie dodaj użytkowników';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automatyczne dodawanie powtarzalników';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatycznie dodaj serwery pokojowe';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automatycznie dodaj czujniki';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.';
@override
String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.';
@override
String get discoveredContacts_Title => 'Odkryte Kontakty';
@override
String get discoveredContacts_noMatching => 'Brak pasujących kontaktów';
@override
String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty';
@override
String get discoveredContacts_contactAdded => 'Kontakt dodany';
@override
String get discoveredContacts_addContact => 'Dodaj kontakt';
@override
String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka';
@override
String get discoveredContacts_deleteContact => 'Usuń kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Usuń wszystkie odkryte kontakty';
@override
String get discoveredContacts_deleteContactAllContent =>
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
}
+328 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_delete => 'Excluir';
@override
String get common_deleteAll => 'Excluir Tudo';
@override
String get common_close => 'Fechar';
@@ -150,6 +153,13 @@ class AppLocalizationsPt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_chromeRequired => 'Navegador Chrome necessário';
@override
String get scanner_chromeRequiredMessage =>
'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@@ -234,6 +244,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_longitude => 'Longitude';
@override
String get settings_contactSettings => 'Configurações de Contato';
@override
String get settings_contactSettingsSubtitle =>
'Configurações para como os contatos são adicionados';
@override
String get settings_privacyMode => 'Modo de Privacidade';
@@ -324,6 +341,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';
@@ -348,15 +369,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)';
@@ -385,10 +397,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) {
@@ -464,6 +481,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';
@@ -624,6 +649,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';
@@ -662,7 +696,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.';
@@ -788,6 +850,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';
@@ -796,6 +864,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';
@@ -1083,6 +1156,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';
@@ -1241,6 +1317,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.';
@@ -1359,6 +1441,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
@override
String get map_showGuessedLocations =>
'Mostrar as localizações dos nós estimados';
@override
String get map_guessedLocation => 'Localização estimada';
@override
String get map_lastSeenTime => 'Último Tempo de Visualização';
@@ -2690,6 +2779,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';
@@ -2724,6 +2822,144 @@ class AppLocalizationsPt extends AppLocalizations {
@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';
@@ -2899,4 +3135,84 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Visto pela última vez';
@override
String get contactsSettings_title => 'Configurações de contatos';
@override
String get contactsSettings_autoAddTitle => 'Descoberta Automática';
@override
String get contactsSettings_otherTitle =>
'Outras configurações relacionadas a contatos';
@override
String get contactsSettings_autoAddUsersTitle =>
'Adicionar usuários automaticamente';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Permitir que o companheiro adicione automaticamente os usuários descobertos.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Adicionar repetidores automaticamente';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Permitir que o companheiro adicione automaticamente os repetidores descobertos.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Adicionar automaticamente servidores de sala';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Adicionar sensores automaticamente';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Permitir que o companheiro adicione automaticamente sensores descobertos.';
@override
String get contactsSettings_overwriteOldestTitle =>
'Sobrescrever o Mais Antigo';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.';
@override
String get discoveredContacts_Title => 'Contatos Descobertos';
@override
String get discoveredContacts_noMatching => 'Nenhum contato correspondente';
@override
String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos';
@override
String get discoveredContacts_contactAdded => 'Contato adicionado';
@override
String get discoveredContacts_addContact => 'Adicionar Contato';
@override
String get discoveredContacts_copyContact =>
'Copiar Contato para a área de transferência';
@override
String get discoveredContacts_deleteContact => 'Excluir Contato';
@override
String get discoveredContacts_deleteContactAll =>
'Excluir Todos os Contatos Descobertos';
@override
String get discoveredContacts_deleteContactAllContent =>
'Tem certeza de que deseja excluir todos os contatos descobertos?';
}
+328 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get common_delete => 'Удалить';
@override
String get common_deleteAll => 'Удалить все';
@override
String get common_close => 'Закрыть';
@@ -149,6 +152,13 @@ class AppLocalizationsRu extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
@override
String get scanner_chromeRequired => 'Требуется браузер Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.';
@override
String get scanner_enableBluetooth => 'Включите Bluetooth';
@@ -232,6 +242,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get settings_longitude => 'Долгота';
@override
String get settings_contactSettings => 'Настройки контактов';
@override
String get settings_contactSettingsSubtitle =>
'Настройки добавления контактов';
@override
String get settings_privacyMode => 'Режим конфиденциальности';
@@ -321,6 +338,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 => 'Имя';
@@ -345,15 +366,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 => 'Частота (МГц)';
@@ -383,10 +395,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) {
@@ -462,6 +479,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 => 'Уведомления';
@@ -624,6 +649,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 => 'Область не выбрана';
@@ -661,7 +695,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 => 'Нет непрочитанных контактов';
@@ -786,6 +848,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 => 'Удалить канал';
@@ -794,6 +862,11 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Удалить \"$name\"? Это действие нельзя отменить.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не удалось удалить канал $name.';
}
@override
String channels_channelDeleted(String name) {
return 'Канал \"$name\" удалён';
@@ -1081,6 +1154,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление маршрутами';
@override
String get chat_ShowAllPaths => 'Показать все пути';
@override
String get chat_routingMode => 'Режим маршрутизации';
@@ -1243,6 +1319,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 => 'Нет нод с данными о местоположении';
@@ -1360,6 +1442,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Показывать общие метки';
@override
String get map_showGuessedLocations =>
'Отобразить предполагаемые места расположения узлов';
@override
String get map_guessedLocation => 'Угаданное место';
@override
String get map_lastSeenTime => 'Время последнего появления';
@@ -2693,6 +2782,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 => 'Пользователи';
@@ -2727,6 +2825,144 @@ class AppLocalizationsRu extends AppLocalizations {
@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 => 'Трассировка пути';
@@ -2911,4 +3147,84 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Последний раз видели';
@override
String get contactsSettings_title => 'Настройки контактов';
@override
String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение';
@override
String get contactsSettings_otherTitle =>
'Другие настройки, связанные с контактами';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматически добавлять пользователей';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженных пользователей';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматически добавлять ретрансляторы';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматически добавлять серверы комнат';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматически добавлять датчики';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Разрешить компаньону автоматически добавлять обнаруженные датчики';
@override
String get contactsSettings_overwriteOldestTitle =>
'Перезаписать самое старое';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.';
@override
String get discoveredContacts_Title => 'Обнаруженные контакты';
@override
String get discoveredContacts_noMatching => 'Нет совпадающих контактов';
@override
String get discoveredContacts_searchHint => 'Найденные контакты поиска';
@override
String get discoveredContacts_contactAdded => 'Контакт добавлен';
@override
String get discoveredContacts_addContact => 'Добавить контакт';
@override
String get discoveredContacts_copyContact =>
'Копировать контакт в буфер обмена';
@override
String get discoveredContacts_deleteContact => 'Удалить контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Удалить Все Обнаруженные Контакты';
@override
String get discoveredContacts_deleteContactAllContent =>
'Вы уверены, что хотите удалить все обнаруженные контакты?';
}
+325 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_delete => 'Odstrániť';
@override
String get common_deleteAll => 'Zmazať všetko';
@override
String get common_close => 'Zavrieť';
@@ -150,6 +153,13 @@ class AppLocalizationsSk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@@ -233,6 +243,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_longitude => 'Dĺžka';
@override
String get settings_contactSettings => 'Nastavenia kontaktov';
@override
String get settings_contactSettingsSubtitle =>
'Nastavenia pre pridávanie kontaktov.';
@override
String get settings_privacyMode => 'Režim ochrany súkromia';
@@ -320,6 +337,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';
@@ -344,15 +365,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)';
@@ -381,10 +393,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) {
@@ -460,6 +477,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';
@@ -618,6 +642,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ť';
@@ -655,7 +688,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';
@@ -783,6 +844,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';
@@ -791,6 +858,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ý';
@@ -1078,6 +1150,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';
@@ -1237,6 +1312,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';
@@ -1354,6 +1435,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
@override
String get map_showGuessedLocations =>
'Zobraziť umiestnenia odhadnutých uzlov';
@override
String get map_guessedLocation => 'Odhadnutá lokalita';
@override
String get map_lastSeenTime => 'Posledný čas sledovania';
@@ -2676,6 +2764,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';
@@ -2710,6 +2807,144 @@ class AppLocalizationsSk extends AppLocalizations {
@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';
@@ -2887,4 +3122,82 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Naposledy videný';
@override
String get contactsSettings_title => 'Nastavenia kontaktov';
@override
String get contactsSettings_autoAddTitle => 'Automatické zisťovanie';
@override
String get contactsSettings_otherTitle =>
'Ďalšie nastavenia súvisiace s kontaktami';
@override
String get contactsSettings_autoAddUsersTitle =>
'Automaticky pridávať užívateľov';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Automaticky pridávať opakovače';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automaticky pridávať server miestnosti';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Automaticky pridávať senzory';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Povoliť spoločníkovi automaticky pridávať objavené senzory.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.';
@override
String get discoveredContacts_Title => 'Objavené kontakty';
@override
String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty';
@override
String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty';
@override
String get discoveredContacts_contactAdded => 'Kontakt bol pridaný';
@override
String get discoveredContacts_addContact => 'Pridať kontakt';
@override
String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky';
@override
String get discoveredContacts_deleteContact => 'Zmazať kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Zmazať všetky objavené kontakty';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
}
+323 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_delete => 'Izbrisati';
@override
String get common_deleteAll => 'Izbriši vse';
@override
String get common_close => 'Zapri';
@@ -150,6 +153,13 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
@override
String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.';
@override
String get scanner_enableBluetooth => 'Omogočite Bluetooth';
@@ -233,6 +243,13 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_longitude => 'Dolžina';
@override
String get settings_contactSettings => 'Nastavitve stika';
@override
String get settings_contactSettingsSubtitle =>
'Nastavitve za dodajanje stikov.';
@override
String get settings_privacyMode => 'Zasebnost';
@@ -319,6 +336,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';
@@ -343,15 +364,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)';
@@ -380,10 +392,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) {
@@ -459,6 +476,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';
@@ -619,6 +643,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';
@@ -656,7 +689,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.';
@@ -781,6 +842,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';
@@ -789,6 +856,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.';
@@ -1076,6 +1148,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';
@@ -1232,6 +1307,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.';
@@ -1350,6 +1431,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Pokaži skupno označenja';
@override
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
@override
String get map_guessedLocation => 'Predpostavljena lokacija';
@override
String get map_lastSeenTime => 'Datum zadnjega vpogleda';
@@ -2679,6 +2766,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';
@@ -2713,6 +2809,144 @@ class AppLocalizationsSl extends AppLocalizations {
@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';
@@ -2892,4 +3126,81 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Zadnjič videno';
@override
String get contactsSettings_title => 'Nastavitve stikov';
@override
String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje';
@override
String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki';
@override
String get contactsSettings_autoAddUsersTitle =>
'Avtomatsko dodaj uporabnike';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Avtomatsko dodaj ponovitelje';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Avtomatsko dodaj strežnike sob';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Avtomatsko dodaj senzorje';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.';
@override
String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.';
@override
String get discoveredContacts_Title => 'Odkriti stiki';
@override
String get discoveredContacts_noMatching => 'Ni ujemajočih stikov';
@override
String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju';
@override
String get discoveredContacts_contactAdded => 'Kontakt dodan';
@override
String get discoveredContacts_addContact => 'Dodaj stik';
@override
String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče';
@override
String get discoveredContacts_deleteContact => 'Izbriši stik';
@override
String get discoveredContacts_deleteContactAll =>
'Izbriši vse odkrite kontakte';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
}
+323 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_delete => 'Radera';
@override
String get common_deleteAll => 'Ta bort alla';
@override
String get common_close => 'Stänga';
@@ -149,6 +152,13 @@ class AppLocalizationsSv extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_chromeRequired => 'Chrome-webbläsare krävs';
@override
String get scanner_chromeRequiredMessage =>
'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@@ -232,6 +242,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_longitude => 'Längdgrad';
@override
String get settings_contactSettings => 'Kontaktinställningar';
@override
String get settings_contactSettingsSubtitle =>
'Inställningar för hur kontakter läggs till.';
@override
String get settings_privacyMode => 'Privatläge';
@@ -317,6 +334,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';
@@ -341,15 +362,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)';
@@ -378,10 +390,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) {
@@ -457,6 +474,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';
@@ -614,6 +638,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';
@@ -651,7 +684,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';
@@ -777,6 +838,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';
@@ -785,6 +852,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';
@@ -1073,6 +1145,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';
@@ -1229,6 +1304,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';
@@ -1346,6 +1427,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Visa delade markörer';
@override
String get map_showGuessedLocations =>
'Visa upp de antagna nodernas placeringar';
@override
String get map_guessedLocation => 'Gissad plats';
@override
String get map_lastSeenTime => 'Senaste Visats Tid';
@@ -2664,6 +2752,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';
@@ -2698,6 +2795,142 @@ class AppLocalizationsSv extends AppLocalizations {
@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';
@@ -2872,4 +3105,82 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Senast sedd';
@override
String get contactsSettings_title => 'Kontaktinställningar';
@override
String get contactsSettings_autoAddTitle => 'Automatisk upptäckt';
@override
String get contactsSettings_otherTitle =>
'Andra inställningar relaterade till kontakt';
@override
String get contactsSettings_autoAddUsersTitle =>
'Lägg till användare automatiskt';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta användare';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Lägg till upprepande enheter automatiskt';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Lägg automatiskt till rumsservrar';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Lägg till sensorer automatiskt';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.';
@override
String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.';
@override
String get discoveredContacts_Title => 'Upptäckta kontakter';
@override
String get discoveredContacts_noMatching => 'Inga matchande kontakter';
@override
String get discoveredContacts_searchHint => 'Sök uppfunna kontakter';
@override
String get discoveredContacts_contactAdded => 'Kontakt tillagd';
@override
String get discoveredContacts_addContact => 'Lägg till kontakt';
@override
String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp';
@override
String get discoveredContacts_deleteContact => 'Ta bort kontakt';
@override
String get discoveredContacts_deleteContactAll =>
'Ta bort alla upptäckta kontakter';
@override
String get discoveredContacts_deleteContactAllContent =>
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
}
+329 -12
View File
@@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get common_delete => 'Видалити';
@override
String get common_deleteAll => 'Видалити все';
@override
String get common_close => 'Закрити';
@@ -150,6 +153,13 @@ class AppLocalizationsUk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
@override
String get scanner_chromeRequired => 'Потрібен браузер Chrome';
@override
String get scanner_chromeRequiredMessage =>
'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.';
@override
String get scanner_enableBluetooth => 'Увімкніть Bluetooth';
@@ -232,6 +242,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_longitude => 'Довгота';
@override
String get settings_contactSettings => 'Налаштування контактів';
@override
String get settings_contactSettingsSubtitle =>
'Налаштування для додавання контактів';
@override
String get settings_privacyMode => 'Режим приватності';
@@ -322,6 +339,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 => 'Ім\'я';
@@ -346,15 +367,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 => 'Частота (МГц)';
@@ -383,10 +395,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) {
@@ -462,6 +479,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 => 'Сповіщення';
@@ -622,6 +647,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 => 'Область не вибрано';
@@ -659,7 +693,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 => 'Немає непрочитаних контактів';
@@ -784,6 +846,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 => 'Видалити канал';
@@ -792,6 +860,11 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Видалити $name? Це не можна скасувати.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Не вдалося видалити канал \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Канал «$name» видалено';
@@ -1079,6 +1152,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_pathManagement => 'Керування шляхами';
@override
String get chat_ShowAllPaths => 'Показати всі шляхи';
@override
String get chat_routingMode => 'Режим маршрутизації';
@@ -1241,6 +1317,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 =>
'Немає вузлів з даними про розташування';
@@ -1359,6 +1441,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_showSharedMarkers => 'Показувати спільні маркери';
@override
String get map_showGuessedLocations =>
'Показати місцезнаходження передбачених вузлів';
@override
String get map_guessedLocation => 'Визначено місцезнаходження';
@override
String get map_lastSeenTime => 'Час останньої активності';
@@ -2700,6 +2789,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 => 'Користувачі';
@@ -2734,6 +2832,145 @@ class AppLocalizationsUk extends AppLocalizations {
@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 => 'Трасування шляхів';
@@ -2917,4 +3154,84 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get snrIndicator_lastSeen => 'Останній раз бачили';
@override
String get contactsSettings_title => 'Налаштування контактів';
@override
String get contactsSettings_autoAddTitle => 'Автоматичне виявлення';
@override
String get contactsSettings_otherTitle =>
'Інші налаштування, пов\'язані з контактами';
@override
String get contactsSettings_autoAddUsersTitle =>
'Автоматично додавати користувачів';
@override
String get contactsSettings_autoAddUsersSubtitle =>
'Дозволити супутникові автоматично додавати виявлених користувачів';
@override
String get contactsSettings_autoAddRepeatersTitle =>
'Автоматично додавати повторювачі';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
'Дозволити супутнику автоматично додавати виявлені ретранслятори';
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Автоматично додавати сервери кімнат';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.';
@override
String get contactsSettings_autoAddSensorsTitle =>
'Автоматично додавати датчики';
@override
String get contactsSettings_autoAddSensorsSubtitle =>
'Дозволити супровіднику автоматично додавати виявлені сенсори';
@override
String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше';
@override
String get contactsSettings_overwriteOldestSubtitle =>
'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.';
@override
String get discoveredContacts_Title => 'Виявлені контакти';
@override
String get discoveredContacts_noMatching =>
'Відповідних контактів не знайдено';
@override
String get discoveredContacts_searchHint => 'Знайти виявлені контакти';
@override
String get discoveredContacts_contactAdded => 'Контакт додано';
@override
String get discoveredContacts_addContact => 'Додати контакт';
@override
String get discoveredContacts_copyContact =>
'Копіювати контакт у буфер обміну';
@override
String get discoveredContacts_deleteContact => 'Видалити контакт';
@override
String get discoveredContacts_deleteContactAll =>
'Видалити всі виявлені контакти';
@override
String get discoveredContacts_deleteContactAllContent =>
'Ви впевнені, що хочете видалити всі виявлені контакти?';
}
File diff suppressed because it is too large Load Diff
+234 -6
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": {
@@ -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.",
@@ -1598,7 +1605,228 @@
"map_runTrace": "Padeshulp traceren",
"scanner_enableBluetooth": "Activeer Bluetooth",
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
"scanner_chromeRequired": "Chrome-browser vereist",
"scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.",
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden"
"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...",
"contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen",
"contactsSettings_title": "Instellingen voor contacten",
"settings_contactSettings": "Contactinstellingen",
"contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken",
"contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt",
"contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen",
"contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.",
"contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen",
"settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten",
"contactsSettings_autoAddTitle": "Automatische detectie",
"contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt",
"contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt",
"contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen",
"contactsSettings_overwriteOldestTitle": "Overschrijf Oudste",
"discoveredContacts_noMatching": "Geen overeenkomende contacten",
"discoveredContacts_addContact": "Contact toevoegen",
"discoveredContacts_copyContact": "Kopieer contact naar klembord",
"discoveredContacts_deleteContact": "Contact verwijderen",
"discoveredContacts_Title": "Ontdekte contacten",
"discoveredContacts_contactAdded": "Contact toegevoegd",
"discoveredContacts_searchHint": "Ontdekte contacten zoeken",
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
"common_deleteAll": "Alles verwijderen",
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
"map_guessedLocation": "Geroerde locatie",
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen"
}
+234 -6
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": {
@@ -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",
@@ -1597,8 +1604,229 @@
"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_chromeRequired": "Wymagana przeglądarka Chrome",
"scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.",
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu"
"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...",
"contactsSettings_title": "Ustawienia kontaktów",
"settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów",
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników",
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
"settings_contactSettings": "Ustawienia kontaktowe",
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
"discoveredContacts_contactAdded": "Kontakt dodany",
"discoveredContacts_addContact": "Dodaj kontakt",
"discoveredContacts_copyContact": "Kopiuj kontakt do schowka",
"contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy",
"discoveredContacts_Title": "Odkryte Kontakty",
"contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.",
"discoveredContacts_noMatching": "Brak pasujących kontaktów",
"discoveredContacts_deleteContact": "Usuń kontakt",
"contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.",
"common_deleteAll": "Usuń wszystko",
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
"map_guessedLocation": "Wydana lokalizacja",
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów"
}
+234 -6
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": {
@@ -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.",
@@ -1599,6 +1606,227 @@
"scanner_enableBluetooth": "Ative o Bluetooth",
"scanner_bluetoothOff": "Bluetooth está desativado",
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
"scanner_chromeRequired": "Navegador Chrome necessário",
"scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.",
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
"snrIndicator_lastSeen": "Visto pela última vez"
"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...",
"settings_contactSettings": "Configurações de Contato",
"contactsSettings_otherTitle": "Outras configurações relacionadas a contatos",
"contactsSettings_title": "Configurações de contatos",
"contactsSettings_autoAddTitle": "Descoberta Automática",
"settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados",
"contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente",
"contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.",
"contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala",
"contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo",
"contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente",
"discoveredContacts_Title": "Contatos Descobertos",
"contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.",
"contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente",
"discoveredContacts_noMatching": "Nenhum contato correspondente",
"contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.",
"discoveredContacts_searchHint": "Pesquisar contatos descobertos",
"contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.",
"discoveredContacts_copyContact": "Copiar Contato para a área de transferência",
"discoveredContacts_deleteContact": "Excluir Contato",
"discoveredContacts_contactAdded": "Contato adicionado",
"discoveredContacts_addContact": "Adicionar Contato",
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
"common_deleteAll": "Excluir Tudo",
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
"map_guessedLocation": "Localização estimada",
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados"
}
+234 -6
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}\" удалён",
@@ -799,6 +804,8 @@
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
"contacts_zeroHopAdvert": "Реклама Zero Hop",
"appSettings_languageUk": "Українська",
"appSettings_enableMessageTracing": "Включить трассировку сообщений",
"appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений",
"contacts_floodAdvert": "Рекламный поток",
"contacts_clipboardEmpty": "Буфер обмена пуст.",
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
@@ -839,6 +846,227 @@
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"scanner_chromeRequired": "Требуется браузер Chrome",
"scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.",
"snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы",
"snrIndicator_lastSeen": "Последний раз видели"
"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} пользователей...",
"settings_contactSettings": "Настройки контактов",
"settings_contactSettingsSubtitle": "Настройки добавления контактов",
"contactsSettings_autoAddTitle": "Автоматическое обнаружение",
"contactsSettings_title": "Настройки контактов",
"contactsSettings_otherTitle": "Другие настройки, связанные с контактами",
"contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей",
"contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат",
"contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики",
"contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики",
"contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей",
"contactsSettings_overwriteOldestTitle": "Перезаписать самое старое",
"contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы",
"contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы",
"contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.",
"discoveredContacts_noMatching": "Нет совпадающих контактов",
"discoveredContacts_searchHint": "Найденные контакты поиска",
"discoveredContacts_contactAdded": "Контакт добавлен",
"discoveredContacts_copyContact": "Копировать контакт в буфер обмена",
"discoveredContacts_addContact": "Добавить контакт",
"discoveredContacts_Title": "Обнаруженные контакты",
"discoveredContacts_deleteContact": "Удалить контакт",
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
"common_deleteAll": "Удалить все",
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
"map_guessedLocation": "Угаданное место",
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов"
}
+234 -6
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": {
@@ -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.",
@@ -1597,8 +1604,229 @@
"map_runTrace": "Spustiť trasovaním cesty",
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
"scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome",
"scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.",
"scanner_bluetoothOff": "Bluetooth je vypnutý",
"scanner_enableBluetooth": "Povolte Bluetooth",
"snrIndicator_lastSeen": "Naposledy videný",
"snrIndicator_nearByRepeaters": "Miestne opakovače"
"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é",
"settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.",
"contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov",
"contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.",
"contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače",
"contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti",
"contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.",
"contactsSettings_autoAddTitle": "Automatické zisťovanie",
"contactsSettings_title": "Nastavenia kontaktov",
"contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami",
"settings_contactSettings": "Nastavenia kontaktov",
"contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory",
"discoveredContacts_noMatching": "Žiadne zhodné kontakty",
"discoveredContacts_searchHint": "Vyhľadať objavené kontakty",
"contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.",
"discoveredContacts_contactAdded": "Kontakt bol pridaný",
"discoveredContacts_copyContact": "Kopírovať kontakt do schránky",
"discoveredContacts_deleteContact": "Zmazať kontakt",
"contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.",
"discoveredContacts_Title": "Objavené kontakty",
"contactsSettings_overwriteOldestTitle": "Prepísať najstaršie",
"discoveredContacts_addContact": "Pridať kontakt",
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
"common_deleteAll": "Zmazať všetko",
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
"map_guessedLocation": "Odhadnutá lokalita"
}
+234 -6
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": {
@@ -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",
@@ -1598,7 +1605,228 @@
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
"scanner_enableBluetooth": "Omogočite Bluetooth",
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
"scanner_chromeRequired": "Zahtevan brskalnik Chrome",
"scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.",
"scanner_bluetoothOff": "Bluetooth je izklopljen",
"snrIndicator_lastSeen": "Zadnjič videno",
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji"
"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...",
"settings_contactSettings": "Nastavitve stika",
"contactsSettings_autoAddTitle": "Avtomatsko odkrivanje",
"contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike",
"contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje",
"contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.",
"contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob",
"contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.",
"contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki",
"settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.",
"contactsSettings_title": "Nastavitve stikov",
"contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje",
"contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.",
"discoveredContacts_noMatching": "Ni ujemajočih stikov",
"contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.",
"discoveredContacts_addContact": "Dodaj stik",
"discoveredContacts_contactAdded": "Kontakt dodan",
"discoveredContacts_copyContact": "Kopiraj stik v odložišče",
"contactsSettings_overwriteOldestTitle": "Prepiši najstarejše",
"discoveredContacts_Title": "Odkriti stiki",
"discoveredContacts_searchHint": "Najdeni stiki po iskanju",
"discoveredContacts_deleteContact": "Izbriši stik",
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
"common_deleteAll": "Izbriši vse",
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
"map_guessedLocation": "Predpostavljena lokacija",
"map_showGuessedLocations": "Pokaži lokacije domnevnih not."
}
+234 -6
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": {
@@ -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.",
@@ -1598,7 +1605,228 @@
"map_removeLast": "Ta bort sista",
"scanner_enableBluetooth": "Aktivera Bluetooth",
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
"scanner_chromeRequired": "Chrome-webbläsare krävs",
"scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.",
"scanner_bluetoothOff": "Bluetooth är avstängt",
"snrIndicator_lastSeen": "Senast sedd",
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer"
"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...",
"settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.",
"settings_contactSettings": "Kontaktinställningar",
"contactsSettings_autoAddTitle": "Automatisk upptäckt",
"contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt",
"contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare",
"contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt",
"contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.",
"contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt",
"contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt",
"contactsSettings_title": "Kontaktinställningar",
"contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.",
"contactsSettings_overwriteOldestTitle": "Skriv över äldst",
"contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.",
"contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar",
"discoveredContacts_noMatching": "Inga matchande kontakter",
"discoveredContacts_searchHint": "Sök uppfunna kontakter",
"discoveredContacts_deleteContact": "Ta bort kontakt",
"discoveredContacts_Title": "Upptäckta kontakter",
"discoveredContacts_contactAdded": "Kontakt tillagd",
"discoveredContacts_addContact": "Lägg till kontakt",
"discoveredContacts_copyContact": "Kopiera kontakt till urklipp",
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
"common_deleteAll": "Ta bort alla",
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
"map_guessedLocation": "Gissad plats",
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar"
}
+234 -6
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": {
@@ -1562,6 +1567,8 @@
"contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну",
"contacts_clipboardEmpty": "Буфер обміну порожній",
"appSettings_languageRu": "Російська",
"appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень",
"appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень",
"contacts_ShareContact": "Копіювати контакт у буфер обміну",
"contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.",
"contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.",
@@ -1598,7 +1605,228 @@
"map_pathTraceCancelled": "Відмінується трасування шляху",
"scanner_enableBluetooth": "Увімкніть Bluetooth",
"scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.",
"scanner_chromeRequired": "Потрібен браузер Chrome",
"scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.",
"scanner_bluetoothOff": "Bluetooth вимкнено",
"snrIndicator_lastSeen": "Останній раз бачили",
"snrIndicator_nearByRepeaters": "Ближні ретранслятори"
"snrIndicator_nearByRepeaters": "Ближні ретранслятори",
"chat_ShowAllPaths": "Показати всі шляхи",
"settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.",
"settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.",
"settings_clientRepeat": "Автономна система",
"settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "одиниці",
"appSettings_unitsMetric": "Метричний (м / км)",
"appSettings_unitsImperial": "Імперська (ft / mi)",
"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": "Непрочитане",
"settings_contactSettingsSubtitle": "Налаштування для додавання контактів",
"settings_contactSettings": "Налаштування контактів",
"contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів",
"contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі",
"contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори",
"contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат",
"contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами",
"contactsSettings_autoAddTitle": "Автоматичне виявлення",
"contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів",
"contactsSettings_title": "Налаштування контактів",
"contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.",
"contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики",
"discoveredContacts_searchHint": "Знайти виявлені контакти",
"discoveredContacts_contactAdded": "Контакт додано",
"contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори",
"contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше",
"discoveredContacts_Title": "Виявлені контакти",
"discoveredContacts_noMatching": "Відповідних контактів не знайдено",
"discoveredContacts_deleteContact": "Видалити контакт",
"discoveredContacts_copyContact": "Копіювати контакт у буфер обміну",
"discoveredContacts_addContact": "Додати контакт",
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
"common_deleteAll": "Видалити все",
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
"map_guessedLocation": "Визначено місцезнаходження"
}
+720 -487
View File
File diff suppressed because it is too large Load Diff
+37 -1
View File
@@ -1,8 +1,12 @@
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';
import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart';
import 'screens/scanner_screen.dart';
import 'services/storage_service.dart';
@@ -14,6 +18,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 +38,7 @@ void main() async {
final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
// Load settings
await appSettingsService.loadSettings();
@@ -47,6 +53,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 +85,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 +120,7 @@ class MeshCoreApp extends StatelessWidget {
final BleDebugLogService bleDebugLogService;
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
const MeshCoreApp({
super.key,
@@ -100,6 +132,7 @@ class MeshCoreApp extends StatelessWidget {
required this.bleDebugLogService,
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
});
@override
@@ -112,6 +145,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),
],
@@ -156,7 +190,9 @@ class MeshCoreApp extends StatelessWidget {
NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink();
},
home: const ScannerScreen(),
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen()
: const ScannerScreen(),
);
},
),
+64 -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,8 @@ class AppSettings {
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
final int mapCacheMaxZoom;
@@ -21,6 +36,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 +49,8 @@ class AppSettings {
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.mapShowGuessedLocations = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15,
@@ -43,7 +63,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 +80,8 @@ class AppSettings {
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'map_show_guessed_locations': mapShowGuessedLocations,
'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom,
@@ -67,10 +94,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 +118,9 @@ 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,
mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
),
@@ -101,6 +141,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 +164,8 @@ class AppSettings {
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? mapShowGuessedLocations,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
int? mapCacheMaxZoom,
@@ -125,6 +178,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 +191,9 @@ class AppSettings {
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapShowGuessedLocations:
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
: mapCacheBounds as Map<String, double>?,
@@ -154,6 +213,10 @@ class AppSettings {
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
batteryChemistryByDeviceId:
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
batteryChemistryByRepeaterId:
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
);
}
}
+8 -1
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
@@ -167,6 +172,7 @@ class Contact {
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)
@@ -177,7 +183,7 @@ class Contact {
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
final lastmod = readUint32LE(data, contactLastModOffset);
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
@@ -191,6 +197,7 @@ class Contact {
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
+105
View File
@@ -0,0 +1,105 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
class DiscoveryContact {
final Uint8List rawPacket;
final Uint8List publicKey;
final String name;
final int type;
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
final Uint8List path; // Path bytes from device
final double? latitude;
final double? longitude;
final DateTime lastSeen;
DiscoveryContact({
required this.rawPacket,
required this.publicKey,
required this.name,
required this.type,
required this.pathLength,
required this.path,
this.latitude,
this.longitude,
required this.lastSeen,
});
String get publicKeyHex => pubKeyToHex(publicKey);
String get typeLabel {
switch (type) {
case advTypeChat:
return 'Chat';
case advTypeRepeater:
return 'Repeater';
case advTypeRoom:
return 'Room';
case advTypeSensor:
return 'Sensor';
default:
return 'Unknown';
}
}
String get pathLabel {
if (pathLength < 0) return 'Flood';
if (pathLength == 0) return 'Direct';
return '$pathLength hops';
}
bool get hasLocation => latitude != null && longitude != null;
DiscoveryContact copyWith({
Uint8List? rawPacket,
Uint8List? publicKey,
String? name,
int? type,
int? pathLength,
Uint8List? path,
double? latitude,
double? longitude,
DateTime? lastSeen,
}) {
return DiscoveryContact(
rawPacket: rawPacket ?? this.rawPacket,
publicKey: publicKey ?? this.publicKey,
name: name ?? this.name,
type: type ?? this.type,
pathLength: pathLength ?? this.pathLength,
path: path ?? this.path,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen,
);
}
String get pathIdList {
final pathBytes = path;
if (pathBytes.isEmpty) return '';
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < pathBytes.length; i += groupSize) {
final end = (i + groupSize) <= pathBytes.length
? (i + groupSize)
: pathBytes.length;
final chunk = pathBytes.sublist(i, end);
parts.add(
chunk
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(),
);
}
return parts.join(',');
}
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
@override
bool operator ==(Object other) =>
other is DiscoveryContact && publicKeyHex == other.publicKeyHex;
@override
int get hashCode => publicKeyHex.hashCode;
}
+194 -40
View File
@@ -59,46 +59,200 @@ class RadioSettings {
required this.txPowerDbm,
});
// Preset configurations
static RadioSettings get preset915MHz => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get preset868MHz => RadioSettings(
frequencyMHz: 868.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
);
static RadioSettings get preset433MHz => RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
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,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
// 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,
),
),
(
'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,
),
),
(
'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.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'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;
+2 -1
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(
+67 -1
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),
@@ -706,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,
+2 -1
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,
+508 -167
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,37 +221,50 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Stack(
children: [
ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: itemCount,
itemBuilder: (context, index) {
// Loading indicator now appears at end (bottom) of reversed list
if (_isLoadingOlder && index == itemCount - 1) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
ChatZoomWrapper(
child: ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: itemCount,
itemBuilder: (context, index) {
// Loading indicator now appears at end (bottom) of reversed list
if (_isLoadingOlder && index == itemCount - 1) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: Builder(
builder: (context) {
final textScale = context
.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildMessageBubble(
message,
textScale,
);
},
),
);
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
},
),
),
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,107 +291,184 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
? message.pathVariants.first
: Uint8List(0));
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
_buildAvatar(message.senderName),
const SizedBox(width: 8),
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0;
final messageBody = Column(
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
_buildAvatar(message.senderName),
const SizedBox(width: 8),
],
Flexible(
child: GestureDetector(
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
: 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,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
top: 4,
bottom: 4,
)
: EdgeInsets.zero,
child: Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
top: 4,
bottom: 4,
)
: EdgeInsets.zero,
child: Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message, textScale),
const SizedBox(height: 8),
],
if (poi != null)
_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(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: isOutgoing
? Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.6),
),
),
),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
else if (gifId != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: isOutgoing
? Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.7)
: 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
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Linkify(
text: message.text,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) => LinkHandler.handleLinkTap(
context,
link.url,
),
),
),
)
else
Linkify(
text: message.text,
style: const TextStyle(fontSize: 14),
linkStyle: const TextStyle(
fontSize: 14,
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) =>
LinkHandler.handleLinkTap(context, link.url),
),
if (!enableTracing && isOutgoing) ...[
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,25 +540,81 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
),
],
),
],
),
),
),
],
),
if (message.reactions.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
child: _buildReactionsDisplay(message),
),
],
),
if (message.reactions.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
child: _buildReactionsDisplay(message),
),
],
),
],
);
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,30 +970,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
maxSize: 160,
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(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
maxSize: 160,
),
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
),
],
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
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) {
@@ -1007,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;
+138 -44
View File
@@ -9,11 +9,14 @@ 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;
@@ -48,7 +51,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
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),
@@ -297,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() {
@@ -333,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,
@@ -362,9 +371,7 @@ class _ChannelMessagePathMapScreenState
);
final points = <LatLng>[];
print(
'outgoing: ${widget.message.isOutgoing}, channelMsg: ${widget.channelMessage}',
);
if ((widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
@@ -395,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;
@@ -404,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(
@@ -426,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(
@@ -437,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)
@@ -460,7 +488,7 @@ class _ChannelMessagePathMapScreenState
),
),
),
_buildLegendCard(context, hops),
_buildLegendCard(context, hops, isImperial),
],
),
),
@@ -532,45 +560,61 @@ class _ChannelMessagePathMapScreenState
);
}
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
return [
for (final hop in hops)
if (hop.hasLocation)
Marker(
point: hop.position!,
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
hop.index.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
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: point,
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
hop.index.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
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(
@@ -597,10 +641,60 @@ class _ChannelMessagePathMapScreenState
),
),
),
];
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
return markers;
}
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
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);
@@ -619,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),
),
),
+99 -25
View File
@@ -3,12 +3,14 @@ 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';
@@ -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)) {
@@ -303,6 +306,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
showDragHandle: true,
dragIndex: index,
@@ -322,6 +326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return _buildChannelTile(
context,
connector,
channelMessageStore,
channel,
);
},
@@ -351,6 +356,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
Widget _buildChannelTile(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel, {
bool showDragHandle = false,
int? dragIndex,
@@ -467,7 +473,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
);
}
},
onLongPress: () => _showChannelActions(context, connector, channel),
onLongPress: () => _showChannelActions(
context,
connector,
channelMessageStore,
channel,
),
),
);
}
@@ -475,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: [
@@ -487,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);
}
},
),
@@ -501,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,
);
}
},
),
@@ -1414,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();
@@ -1431,13 +1472,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
}
Navigator.pop(dialogContext);
connector.setChannel(channel.index, name, psk);
connector.setChannelSmazEnabled(channel.index, smazEnabled);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.channels_channelUpdated(name)),
),
);
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),
),
@@ -1450,6 +1503,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
void _confirmDeleteChannel(
BuildContext context,
MeshCoreConnector connector,
ChannelMessageStore channelMessageStore,
Channel channel,
) {
showDialog(
@@ -1465,16 +1519,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel),
),
TextButton(
onPressed: () {
onPressed: () async {
Navigator.pop(dialogContext);
connector.deleteChannel(channel.index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleted(channel.name),
try {
await connector.deleteChannel(channel.index);
channelMessageStore.clearChannelMessages(channel.index);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleted(channel.name),
),
),
),
);
);
} catch (e, st) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleteFailed(channel.name),
),
),
);
// Preserve existing logging (if it was there)
debugPrint('Failed to delete channel: $e\n$st');
}
},
child: Text(
dialogContext.l10n.common_delete,
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
class ChromeRequiredScreen extends StatelessWidget {
const ChromeRequiredScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
body: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 32),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)]
: [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.browser_not_supported_rounded,
size: 80,
color: Colors.orange,
),
),
const SizedBox(height: 32),
Text(
l10n.scanner_chromeRequired,
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 16),
Text(
l10n.scanner_chromeRequiredMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: isDark ? Colors.white70 : Colors.black54,
height: 1.5,
),
),
const SizedBox(height: 48),
// We can't really "fix" it for them other than telling them to use Chrome
// but we can provide a nice visual.
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 20, color: Colors.blue),
const SizedBox(width: 12),
Text(
"Web Bluetooth requires a Chromium browser",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
);
}
}
+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
+121 -27
View File
@@ -26,6 +26,7 @@ import '../widgets/room_login_dialog.dart';
import '../widgets/unread_badge.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'discovery_screen.dart';
import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
@@ -183,14 +184,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 {
@@ -215,9 +219,10 @@ class _ContactsScreenState extends State<ContactsScreen>
}
final hexString = text.substring('meshcore://'.length);
try {
final importContactFrame = buildImportContactFrame(hexString);
final bytes = hex2Uint8List(hexString);
final importContactFrame = buildImportContactFrame(bytes);
_pendingOperations.add(ContactOperationType.import);
await connector.sendFrame(importContactFrame);
connector.importContact(importContactFrame);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -315,6 +320,21 @@ class _ContactsScreenState extends State<ContactsScreen>
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text("Discovered Contacts"),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DiscoveryScreen(),
),
),
),
PopupMenuItem(
child: Row(
children: [
@@ -399,6 +419,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(
@@ -406,7 +461,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,
@@ -478,6 +533,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),
@@ -514,6 +570,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;
@@ -588,6 +646,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:
@@ -978,6 +1038,7 @@ class _ContactsScreenState extends State<ContactsScreen>
) {
final isRepeater = contact.type == advTypeRepeater;
final isRoom = contact.type == advTypeRoom;
final isFavorite = contact.isFavorite;
showModalBottomSheet(
context: context,
@@ -1070,6 +1131,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact.name,
),
path: contact.traceRouteBytes ?? Uint8List(0),
targetContact: contact,
),
),
);
@@ -1084,6 +1146,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),
@@ -1152,6 +1229,7 @@ class _ContactTile extends StatelessWidget {
final Contact contact;
final DateTime lastSeen;
final int unreadCount;
final bool isFavorite;
final VoidCallback onTap;
final VoidCallback onLongPress;
@@ -1159,6 +1237,7 @@ class _ContactTile extends StatelessWidget {
required this.contact,
required this.lastSeen,
required this.unreadCount,
required this.isFavorite,
required this.onTap,
required this.onLongPress,
});
@@ -1170,12 +1249,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
@@ -1186,26 +1270,36 @@ class _ContactTile extends StatelessWidget {
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
Text(
_formatLastSeen(context, lastSeen),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
),
],
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]),
],
),
],
),
),
),
onTap: onTap,
+419
View File
@@ -0,0 +1,419 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/discovery_contact.dart';
import '../utils/contact_search.dart';
import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart';
enum DiscoverySortOption { lastSeen, name, type }
class DiscoveryScreen extends StatefulWidget {
const DiscoveryScreen({super.key});
@override
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
}
class _DiscoveryScreenState extends State<DiscoveryScreen> {
final TextEditingController _searchController = TextEditingController();
String searchQuery = '';
ContactSortOption sortOption = ContactSortOption.lastSeen;
bool showUnreadOnly = false;
ContactTypeFilter typeFilter = ContactTypeFilter.all;
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
Timer? _searchDebounce;
@override
void dispose() {
_searchController.dispose();
_searchDebounce?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final discoveredContacts = connector.discoveredContacts;
final filteredAndSorted = _filterAndSortContacts(
discoveredContacts,
connector,
);
return Scaffold(
appBar: AppBar(
title: AppBarTitle(
l10n.discoveredContacts_Title,
indicators: false,
subtitle: false,
),
centerTitle: true,
actions: [
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.delete, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.discoveredContacts_deleteContactAll),
],
),
onTap: () {
_deleteContacts(context, connector);
},
),
],
icon: const Icon(Icons.more_vert),
),
],
),
body: Column(
children: [
_buildFilters(filteredAndSorted, connector),
Expanded(
child: discoveredContacts.isEmpty
? Center(child: Text(l10n.contacts_noContacts))
: filteredAndSorted.isEmpty
? Center(child: Text(l10n.discoveredContacts_noMatching))
: ListView.builder(
itemCount: filteredAndSorted.length,
itemBuilder: (context, index) {
final contact = filteredAndSorted[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: Icon(
_getTypeIcon(contact.type),
color: Colors.white,
size: 20,
),
),
title: Text(
contact.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
contact.shortPubKeyHex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
_formatLastSeen(context, contact.lastSeen),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
onTap: () {
connector.importDiscoveredContact(contact);
},
onLongPress: () =>
_showContactContextMenu(contact, connector),
);
},
),
),
],
),
);
}
Future<void> _showContactContextMenu(
DiscoveryContact contact,
MeshCoreConnector connector,
) async {
final action = await showModalBottomSheet<String>(
context: context,
showDragHandle: true,
builder: (sheetContext) {
final l10n = context.l10n;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.add_reaction_sharp),
title: Text(l10n.discoveredContacts_addContact),
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(l10n.discoveredContacts_copyContact),
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
),
ListTile(
leading: const Icon(Icons.delete),
title: Text(l10n.discoveredContacts_deleteContact),
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
),
],
),
);
},
);
if (!mounted || action == null) return;
switch (action) {
case 'import_contact':
connector.importDiscoveredContact(contact);
break;
case 'copy_contact':
final hexString = pubKeyToHex(contact.rawPacket);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
);
break;
case 'delete_contact':
connector.removeDiscoveredContact(contact);
break;
}
}
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.common_deleteAll),
content: Text(l10n.discoveredContacts_deleteContactAllContent),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
connector.removeAllDiscoveredContacts();
},
child: Text(l10n.common_deleteAll),
),
],
),
);
}
Widget _buildFilters(
List<DiscoveryContact> filteredAndSorted,
MeshCoreConnector connector,
) {
String hintText = "";
switch (typeFilter) {
case ContactTypeFilter.all:
hintText = context.l10n.contacts_searchContacts(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.users:
hintText = context.l10n.contacts_searchUsers(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.repeaters:
hintText = context.l10n.contacts_searchRepeaters(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.rooms:
hintText = context.l10n.contacts_searchRoomServers(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
case ContactTypeFilter.favorites:
hintText = context.l10n.contacts_searchFavorites(
filteredAndSorted.length,
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
);
break;
}
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (searchQuery.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
searchQuery = '';
});
},
),
_buildFilterButton(context, connector),
],
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
if (!mounted) return;
setState(() {
searchQuery = value.toLowerCase();
});
});
},
),
),
],
);
}
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
return DiscoveryContactsFilterMenu(
sortOption: sortOption,
typeFilter: typeFilter,
onSortChanged: (value) {
setState(() {
sortOption = value;
});
},
onTypeFilterChanged: (value) {
setState(() {
typeFilter = value;
});
},
);
}
List<DiscoveryContact> _filterAndSortContacts(
List<DiscoveryContact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) {
if (searchQuery.isEmpty) return true;
return matchesDiscoveryContactQuery(contact, searchQuery);
}).toList();
filtered = filtered.where((contact) {
return !connector.knownContactKeys.contains(contact.publicKeyHex);
}).toList();
// Filter out own node from the list
if (connector.selfPublicKey != null) {
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
filtered = filtered.where((contact) {
return contact.publicKeyHex != selfPubKeyHex;
}).toList();
}
if (typeFilter != ContactTypeFilter.all) {
filtered = filtered.where(_matchesTypeFilter).toList();
}
switch (sortOption) {
case ContactSortOption.lastSeen:
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
break;
case ContactSortOption.name:
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
default:
break;
}
return filtered;
}
bool _matchesTypeFilter(DiscoveryContact contact) {
switch (typeFilter) {
case ContactTypeFilter.all:
return true;
case ContactTypeFilter.users:
return contact.type == advTypeChat;
case ContactTypeFilter.repeaters:
return contact.type == advTypeRepeater;
case ContactTypeFilter.rooms:
return contact.type == advTypeRoom;
default:
return false;
}
}
IconData _getTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color _getTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) {
return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
}
final days = diff.inDays;
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
}
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(
+446 -26
View File
@@ -11,14 +11,17 @@ 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';
import '../services/path_history_service.dart';
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/quick_switch_bar.dart';
import '../icons/los_icon.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
import 'contacts_screen.dart';
@@ -26,6 +29,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;
@@ -46,6 +50,8 @@ 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 = {};
@@ -58,6 +64,9 @@ class _MapScreenState extends State<MapScreen> {
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
bool _showNodeLabels = true;
List<_GuessedLocation> _cachedGuessedLocations = [];
String _guessedLocationsCacheKey = '';
@override
void initState() {
@@ -113,8 +122,8 @@ class _MapScreenState extends State<MapScreen> {
@override
Widget build(BuildContext context) {
return Consumer2<MeshCoreConnector, AppSettingsService>(
builder: (context, connector, settingsService, child) {
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings;
final contacts = connector.contacts;
@@ -154,6 +163,40 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation)
.toList();
// All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = contacts
.where((c) => c.hasLocation)
.toList();
// Compute guessed locations with caching
final maxRangeKm = _estimateLoRaRangeKm(connector);
final filteredKeys = filteredByKeyPrefix
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
.join(',');
final anchorKeys = allContactsWithLocation
.map(
(c) =>
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
)
.join(',');
final cacheKey =
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
if (cacheKey != _guessedLocationsCacheKey) {
_guessedLocationsCacheKey = cacheKey;
_cachedGuessedLocations = settings.mapShowGuessedLocations
? _computeGuessedLocations(
filteredByKeyPrefix,
allContactsWithLocation,
pathHistory,
maxRangeKm,
)
: [];
}
final guessedLocations = settings.mapShowGuessedLocations
? _cachedGuessedLocations
: <_GuessedLocation>[];
_polylines.clear();
_polylines.addAll(
_points.length > 1
@@ -247,6 +290,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) {
@@ -272,6 +316,47 @@ class _MapScreenState extends State<MapScreen> {
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(
@@ -350,6 +435,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(
@@ -374,7 +467,13 @@ class _MapScreenState extends State<MapScreen> {
size: 34,
),
),
..._buildMarkers(contactsWithLocation, settings),
if (!_isBuildingPathTrace)
...guessedLocations.map(_buildGuessedMarker),
..._buildMarkers(
contactsWithLocation,
settings,
showLabels: _showNodeLabels,
),
...sharedMarkers.map(_buildSharedMarker),
if (connector.selfLatitude != null &&
connector.selfLongitude != null)
@@ -403,24 +502,33 @@ 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,
),
],
),
],
),
if (!_isBuildingPathTrace)
_buildLegend(
contactsWithLocation.length,
contactsWithLocation,
settings,
sharedMarkers.length,
guessedLocations.length,
),
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
],
@@ -444,20 +552,222 @@ class _MapScreenState extends State<MapScreen> {
);
}
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
List<_GuessedLocation> _computeGuessedLocations(
List<Contact> allContacts,
List<Contact> withLocation,
PathHistoryService pathHistory,
double? maxRangeKm,
) {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
repeaterByHash[c.publicKey[0]] =
null; // collision: can't disambiguate
} else {
repeaterByHash[c.publicKey[0]] = c;
}
}
}
final result = <_GuessedLocation>[];
for (final contact in allContacts) {
if (contact.hasLocation) continue;
final anchorSet = <LatLng>{};
// Collect the contact-side (last-hop) repeater from every known path.
// path = [device-side hop, ..., contact-side hop]
// Only path.last is actually within radio range of the contact using
// earlier bytes would anchor against our own side of the network.
final pathSets = <List<int>>[
contact.path.toList(),
...pathHistory
.getRecentPaths(contact.publicKeyHex)
.map((r) => r.pathBytes),
];
final lastHopBytes = <int>{};
for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue;
final lastHop = pathBytes.last;
lastHopBytes.add(lastHop);
final r = repeaterByHash[lastHop];
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
}
// Fallback: for any last-hop byte with no GPS repeater, average the
// positions of contacts with known GPS that share the same last hop.
// Those contacts are all adjacent to the same unknown repeater, so their
// centroid is a reasonable proxy for its location.
for (final byte in lastHopBytes) {
if (repeaterByHash.containsKey(byte)) continue;
for (final c in withLocation) {
if (c.path.isNotEmpty && c.path.last == byte) {
anchorSet.add(LatLng(c.latitude!, c.longitude!));
}
}
}
// Filter anchors that are geometrically inconsistent with radio range.
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
// range of the same node, so isolated outliers are removed.
final anchors = maxRangeKm != null && anchorSet.length > 1
? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm)
: anchorSet.toList();
if (anchors.isEmpty) continue;
final LatLng position;
if (anchors.length == 1) {
// Offset single-anchor guesses so they don't overlap the repeater marker.
// Use the contact's public key byte as a deterministic angle seed.
const offsetDeg = 0.003; // ~330 m at the equator
final angle = (contact.publicKey[1] / 255.0) * 2 * pi;
position = LatLng(
anchors[0].latitude + offsetDeg * cos(angle),
anchors[0].longitude + offsetDeg * sin(angle),
);
} else {
double lat = 0, lon = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
}
position = LatLng(lat / anchors.length, lon / anchors.length);
}
result.add(
_GuessedLocation(
contact: contact,
position: position,
highConfidence: anchors.length >= 2,
),
);
}
return result;
}
/// Estimates the free-space maximum LoRa range in km from the connected
/// device's current radio parameters. Returns null if parameters are unknown.
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
final freqHz = connector.currentFreqHz;
final bwHz = connector.currentBwHz;
final sf = connector.currentSf;
final txPower = connector.currentTxPower;
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
return null;
}
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
const noiseFigureDb = 6.0;
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
final sensitivityDbm =
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
// FSPL at max range equals link budget:
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
final exponent =
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
return pow(10, exponent) / 1000;
}
double _sfToRequiredSnrDb(int sf) {
switch (sf) {
case 5:
return -2.5;
case 6:
return -5.0;
case 7:
return -7.5;
case 8:
return -10.0;
case 9:
return -12.5;
case 10:
return -15.0;
case 11:
return -17.5;
case 12:
return -20.0;
default:
return -10.0;
}
}
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
/// A node cannot be simultaneously in radio range of two points farther apart
/// than twice the expected maximum range.
List<LatLng> _filterConsistentAnchors(
List<LatLng> anchors,
double maxRangeKm,
) {
const distance = Distance();
final maxDistM = maxRangeKm * 2000;
return anchors
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
.toList();
}
Marker _buildGuessedMarker(_GuessedLocation guess) {
final color = _getNodeColor(guess.contact.type);
return Marker(
point: guess.position,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
),
),
);
}
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;
}
@@ -499,11 +809,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:
@@ -534,7 +887,27 @@ class _MapScreenState extends State<MapScreen> {
}
}
Widget _buildLegend(int nodeCount, int markerCount) {
Widget _buildLegend(
List<Contact> contactsWithLocation,
settings,
int markerCount,
int guessedCount,
) {
int nodeCount = 0;
for (final contact in contactsWithLocation) {
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
!settings.mapShowOtherNodes) {
continue;
}
nodeCount++;
}
return Positioned(
top: 16,
right: 16,
@@ -558,7 +931,12 @@ class _MapScreenState extends State<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.map_nodesCount(nodeCount),
context.l10n.map_nodesCount(
nodeCount +
(settings.mapShowGuessedLocations
? guessedCount
: 0),
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
@@ -626,6 +1004,12 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_pinPublic,
Colors.orange,
),
if (settings.mapShowGuessedLocations && guessedCount > 0)
_buildLegendItem(
Icons.not_listed_location,
context.l10n.map_guessedLocation,
Colors.grey,
),
],
),
),
@@ -814,7 +1198,11 @@ class _MapScreenState extends State<MapScreen> {
);
}
void _showNodeInfo(BuildContext context, Contact contact) {
void _showNodeInfo(
BuildContext context,
Contact contact, {
LatLng? guessedPosition,
}) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -834,10 +1222,16 @@ class _MapScreenState extends State<MapScreen> {
children: [
_buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel),
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
),
if (contact.hasLocation)
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
'Est. Location',
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
@@ -1163,7 +1557,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),
@@ -1342,6 +1737,14 @@ class _MapScreenState extends State<MapScreen> {
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showGuessedLocations),
value: settings.mapShowGuessedLocations,
onChanged: (value) {
service.setMapShowGuessedLocations(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@@ -1519,6 +1922,9 @@ class _MapScreenState extends State<MapScreen> {
Widget _buildPathTraceOverlay() {
final l10n = context.l10n;
final isImperial =
context.read<AppSettingsService>().settings.unitSystem ==
UnitSystem.imperial;
return Positioned(
top: 16,
left: 16,
@@ -1539,7 +1945,7 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 6),
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}",
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
SelectableText(
@@ -1549,8 +1955,10 @@ class _MapScreenState extends State<MapScreen> {
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Row(
mainAxisSize: MainAxisSize.min,
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
@@ -1600,6 +2008,18 @@ class _MapScreenState extends State<MapScreen> {
}
}
class _GuessedLocation {
final Contact contact;
final LatLng position;
final bool highConfidence;
_GuessedLocation({
required this.contact,
required this.position,
required this.highConfidence,
});
}
class _MarkerPayload {
final LatLng position;
final String label;
+277 -70
View File
@@ -8,7 +8,9 @@ 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';
@@ -27,8 +29,11 @@ double getPathDistanceMeters(List<LatLng> points) {
return distanceMeters;
}
String formatDistance(double distanceMeters) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)';
String formatDistance(double distanceMeters, {required bool isImperial}) {
if (isImperial) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
}
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
}
class PathTraceData {
@@ -49,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget {
final int? repeaterId;
final bool flipPathRound;
final bool reversePathRound;
final Contact? targetContact;
const PathTraceMapScreen({
super.key,
@@ -57,6 +63,7 @@ class PathTraceMapScreen extends StatefulWidget {
this.repeaterId,
this.flipPathRound = false,
this.reversePathRound = false,
this.targetContact,
});
@override
@@ -64,6 +71,8 @@ class PathTraceMapScreen extends StatefulWidget {
}
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
@@ -71,6 +80,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
bool _failed2Loaded = false;
bool _hasData = false;
PathTraceData? _traceData;
// Inferred positions for hops that have no GPS location, keyed by hop byte.
Map<int, LatLng> _inferredHopPositions = {};
// Endpoint position for the target contact (GPS or guessed).
LatLng? _targetContactPosition;
bool _targetContactIsGuessed = false;
List<LatLng> _points = <LatLng>[];
List<Polyline> _polylines = [];
LatLng? _initialCenter = LatLng(0, 0);
@@ -78,6 +92,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
LatLngBounds? _bounds;
ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
@@ -132,8 +147,6 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
path = pathTmp;
}
print('Initiating path trace with path: ${_formatPathPrefixes(path)}');
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
@@ -157,17 +170,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
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), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
});
_timeoutTimer = Timer(
Duration(milliseconds: timeoutMilliseconds),
() {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
},
);
}
if (code == respCodeErr) {
@@ -213,7 +229,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
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 = {};
@@ -230,25 +249,91 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
}
});
// For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte.
final Map<int, LatLng> inferredPositions = {};
for (final hop in pathData) {
final contact = pathContacts[hop];
if (contact != null && contact.hasLocation) continue;
final peers = connector.contacts
.where(
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
inferredPositions[hop] = LatLng(lat, lon);
}
}
setState(() {
_isLoading = false;
_hasData = true;
_inferredHopPositions = inferredPositions;
_traceData = PathTraceData(
pathData: pathData,
snrData: snrData.map((e) => e.toSigned(8).toDouble() / 4).toList(),
snrData: snrData,
pathContacts: pathContacts,
);
// Compute endpoint position for the target contact.
LatLng? targetPos;
bool targetGuessed = false;
final target = widget.targetContact;
if (target != null) {
if (target.hasLocation) {
targetPos = LatLng(target.latitude!, target.longitude!);
} else if (pathData.isNotEmpty) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathRound), the target-side hop sits
// in the middle of the symmetric sequence; .last is the local side.
final lastHop = (widget.flipPathRound && pathData.length > 1)
? pathData[(pathData.length - 1) ~/ 2]
: pathData.last;
final peers = connector.contacts
.where(
(c) =>
c.hasLocation &&
c.path.isNotEmpty &&
c.path.last == lastHop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
const offsetDeg = 0.003;
final angle = (target.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
}
}
}
_targetContactPosition = targetPos;
_targetContactIsGuessed = targetGuessed;
_points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
for (final hop in _traceData!.pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact != null &&
contact.hasLocation &&
contact.latitude != null &&
contact.longitude != null) {
if (contact != null && contact.hasLocation) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
final inferred = inferredPositions[hop];
if (inferred != null) _points.add(inferred);
}
}
if (targetPos != null) _points.add(targetPos);
_polylines = _points.length > 1
? [
Polyline(
@@ -287,6 +372,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
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(
@@ -351,7 +438,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
if (_hasData) _buildLegendCard(context, _traceData!),
if (_hasData)
_buildLegendCard(context, _traceData!, isImperial),
],
),
),
@@ -360,55 +448,70 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
);
}
List<Marker> _buildHopMarkers(List<int> pathData) {
return [
for (final hop in pathData)
if (_traceData!.pathContacts[hop] != null &&
_traceData!.pathContacts[hop]!.hasLocation)
Marker(
point: LatLng(
_traceData!.pathContacts[hop]!.latitude!,
_traceData!.pathContacts[hop]!.longitude!,
),
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
_traceData!.pathContacts[hop]!.publicKey
.sublist(0, 1)
.map(
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
)
.join(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
List<Marker> _buildHopMarkers(
List<int> pathData, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop];
final inferred = _inferredHopPositions[hop];
final hasGps = contact != null && contact.hasLocation;
if (!hasGps && inferred == null) continue;
final point = hasGps
? LatLng(contact.latitude!, contact.longitude!)
: inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
markers.add(
Marker(
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
point: point,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
hasGps ? label : '~$label',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: point,
label: contact?.name ?? '~$label',
),
);
}
}
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(
@@ -436,7 +539,94 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
),
),
];
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
// Add target contact endpoint marker.
final targetPos = _targetContactPosition;
if (targetPos != null) {
final isGuessed = _targetContactIsGuessed;
final targetName = widget.targetContact?.name ?? '?';
markers.add(
Marker(
point: targetPos,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
? Colors.purple.withValues(alpha: 0.55)
: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: const Icon(Icons.person, color: Colors.white, size: 18),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: targetPos,
label: isGuessed ? '~$targetName' : targetName,
),
);
}
}
return markers;
}
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) {
@@ -516,6 +706,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
minZoom: 2.0,
maxZoom: 18.0,
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
@@ -526,12 +724,21 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)),
MarkerLayer(
markers: _buildHopMarkers(
_traceData!.pathData,
showLabels: _showNodeLabels,
),
),
],
);
}
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
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);
@@ -550,7 +757,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}',
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
+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), () {
+62
View File
@@ -1,7 +1,9 @@
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';
@@ -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),
+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() {
+24 -15
View File
@@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import '../utils/platform_info.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
@@ -45,17 +45,22 @@ 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());
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
(state) {
if (mounted) {
setState(() {
_bluetoothState = state;
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
unawaited(connector.stopScan());
}
}
}
});
},
onError: (Object e) {
debugPrint("Scanner adapterState stream error: $e");
},
);
}
@override
@@ -70,7 +75,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,
),
@@ -107,7 +112,11 @@ class _ScannerScreenState extends State<ScannerScreen> {
if (isScanning) {
connector.stopScan();
} else {
connector.startScan();
unawaited(
connector.startScan().catchError((e) {
debugPrint("Scanner screen startScan error: $e");
}),
);
}
},
icon: isScanning
@@ -264,7 +273,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
],
),
),
if (Platform.isAndroid)
if (PlatformInfo.isAndroid)
TextButton(
onPressed: () => FlutterBluePlus.turnOn(),
child: Text(context.l10n.scanner_enableBluetooth),
+182 -43
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/app_bar.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
@@ -41,7 +42,13 @@ 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: AppBarTitle(
l10n.settings_title,
indicators: false,
subtitle: false,
),
),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
@@ -270,6 +277,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _editLocation(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.group_add_outlined),
title: Text(l10n.settings_contactSettings),
subtitle: Text(l10n.settings_contactSettingsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _editAutoAddConfig(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
title: Text(l10n.settings_privacyMode),
@@ -845,6 +860,121 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
bool autoAddChat = false;
bool autoAddRepeater = false;
bool autoAddRoomServer = false;
bool autoAddSensor = false;
bool overwriteOldest = false;
final connector = context.read<MeshCoreConnector>();
autoAddChat = connector.autoAddUsers ?? false;
autoAddRepeater = connector.autoAddRepeaters ?? false;
autoAddRoomServer = connector.autoAddRoomServers ?? false;
autoAddSensor = connector.autoAddSensors ?? false;
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.contactsSettings_autoAddTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FeatureToggleRow(
title: l10n.contactsSettings_autoAddUsersTitle,
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
value: autoAddChat,
onChanged: (value) {
setDialogState(() => autoAddChat = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRepeatersTitle,
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
value: autoAddRepeater,
onChanged: (value) {
setDialogState(() => autoAddRepeater = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRoomServersTitle,
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
value: autoAddRoomServer,
onChanged: (value) {
setDialogState(() => autoAddRoomServer = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddSensorsTitle,
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
value: autoAddSensor,
onChanged: (value) {
setDialogState(() => autoAddSensor = value);
},
),
Divider(height: 4),
FeatureToggleRow(
title: l10n.contactsSettings_overwriteOldestTitle,
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
value: overwriteOldest,
onChanged: (value) {
setDialogState(() => overwriteOldest = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
_sendSettings(
connector,
autoAddChat,
autoAddRepeater,
autoAddRoomServer,
autoAddSensor,
overwriteOldest,
);
Navigator.pop(context);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
void _sendSettings(
MeshCoreConnector connector,
bool autoAddChat,
bool autoAddRepeater,
bool autoAddRoomServer,
bool autoAddSensor,
bool overwriteOldest,
) async {
final frame = buildSetAutoAddConfigFrame(
autoAddChat: autoAddChat,
autoAddRepeater: autoAddRepeater,
autoAddRoomServer: autoAddRoomServer,
autoAddSensor: autoAddSensor,
overwriteOldest: overwriteOldest,
);
await connector.sendFrame(frame);
await connector.sendFrame(buildGetAutoAddFlagsFrame());
}
}
class _RadioSettingsDialog extends StatefulWidget {
@@ -862,6 +992,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() {
@@ -911,6 +1042,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (widget.connector.currentTxPower != null) {
_txPowerController.text = widget.connector.currentTxPower.toString();
}
_clientRepeat = widget.connector.clientRepeat ?? false;
}
@override
@@ -960,9 +1093,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();
@@ -1001,37 +1154,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
),
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),
),
DropdownButtonFormField<int>(
decoration: InputDecoration(
labelText: l10n.settings_presets,
border: const OutlineInputBorder(),
),
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(
@@ -1103,6 +1244,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,
),
],
],
),
),
@@ -1116,15 +1267,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';
}
}
+46
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,14 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowMarkers: value));
}
Future<void> setMapShowGuessedLocations(bool value) async {
await updateSettings(_settings.copyWith(mapShowGuessedLocations: 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 +146,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));
}
}
+4 -5
View File
@@ -1,12 +1,11 @@
import 'dart:io';
import '../utils/platform_info.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
class BackgroundService {
bool _initialized = false;
Future<void> initialize() async {
if (!Platform.isAndroid || _initialized) return;
if (!PlatformInfo.isAndroid || _initialized) return;
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'meshcore_background',
@@ -29,7 +28,7 @@ class BackgroundService {
}
Future<void> start() async {
if (!Platform.isAndroid) return;
if (!PlatformInfo.isAndroid) return;
if (!_initialized) {
await initialize();
}
@@ -43,7 +42,7 @@ class BackgroundService {
}
Future<void> stop() async {
if (!Platform.isAndroid) return;
if (!PlatformInfo.isAndroid) return;
final running = await FlutterForegroundTask.isRunningService;
if (!running) return;
await FlutterForegroundTask.stopService();
+24 -6
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();
notifyListeners();
_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(
@@ -152,8 +172,6 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_GET_CHANNEL';
case cmdSetChannel:
return 'CMD_SET_CHANNEL';
case cmdGetRadioSettings:
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
case cmdSendTracePath:
@@ -195,8 +213,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_MSG_RECV_V3';
case respCodeChannelInfo:
return 'RESP_CODE_CHANNEL_INFO';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
case respCodeAutoAddConfig:
return 'RESP_CODE_AUTO_ADD_CONFIG';
case pushCodeTraceData:
return 'PUSH_CODE_TRACE_DATA';
default:
+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), () {
+89 -62
View File
@@ -58,11 +58,17 @@ 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 {
@@ -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,
);
await _notifications.show(
id: contactId?.hashCode ?? 0,
title: contactName,
body: message,
notificationDetails: notificationDetails,
payload: 'message:$contactId',
);
try {
await _notifications.show(
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,
);
await _notifications.show(
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName,
notificationDetails: notificationDetails,
payload: 'advert:$contactId',
);
try {
await _notifications.show(
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;
await _notifications.show(
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: channelName,
body: body,
notificationDetails: notificationDetails,
payload: 'channel:$channelIndex',
);
try {
await _notifications.show(
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.
@@ -396,35 +415,39 @@ class NotificationService {
Future<void> _showNotificationImmediately(
_PendingNotification notification,
) async {
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
contactName: notification.title,
message: notification.body,
contactId: notification.id,
badgeCount: notification.badgeCount,
);
break;
case _NotificationType.advert:
await _showAdvertNotificationImpl(
contactName: notification.body,
contactType: notification.title,
contactId: notification.id,
);
break;
case _NotificationType.channelMessage:
await _showChannelMessageNotificationImpl(
channelName: notification.title,
message: notification.body,
channelIndex: int.tryParse(notification.id ?? ''),
badgeCount: notification.badgeCount,
);
break;
try {
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
contactName: notification.title,
message: notification.body,
contactId: notification.id,
badgeCount: notification.badgeCount,
);
break;
case _NotificationType.advert:
await _showAdvertNotificationImpl(
contactName: notification.body,
contactType: notification.title,
contactId: notification.id,
);
break;
case _NotificationType.channelMessage:
await _showChannelMessageNotificationImpl(
channelName: notification.title,
message: notification.body,
channelIndex: int.tryParse(notification.id ?? ''),
badgeCount: notification.badgeCount,
);
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);
await _notifications.show(
id: 'batch_summary'.hashCode,
title: _l10n.notification_activityTitle,
body: parts.join(', '),
notificationDetails: notificationDetails,
payload: 'batch',
);
try {
await _notifications.show(
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');
}
}
}
+7
View File
@@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier {
final List<String> _cacheAccessOrder = [];
static const int _maxHistoryEntries = 100;
int _version = 0;
int get version => _version;
static const int _autoRotationTopCount = 3;
PathHistoryService(this._storage);
@@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier {
) {
var history = _cache[contactPubKeyHex];
if (history == null) return;
_version++;
final existing = _findPathRecord(contactPubKeyHex, pathBytes);
if (existing != null) {
@@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier {
_cache[contactPubKeyHex] = loaded;
_trackAccess(contactPubKeyHex);
_evictIfNeeded();
_version++;
notifyListeners();
}
});
@@ -276,6 +281,7 @@ class PathHistoryService extends ChangeNotifier {
_autoRotationIndex.remove(contactPubKeyHex);
_floodStats.remove(contactPubKeyHex);
await _storage.clearPathHistory(contactPubKeyHex);
_version++;
notifyListeners();
}
@@ -295,6 +301,7 @@ class PathHistoryService extends ChangeNotifier {
);
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
_version++;
notifyListeners();
}
+61
View File
@@ -0,0 +1,61 @@
import 'dart:convert';
import 'dart:typed_data';
import '../models/discovery_contact.dart';
import 'prefs_manager.dart';
class ContactDiscoveryStore {
static const String _key = 'discovered_contacts';
Future<List<DiscoveryContact>> loadContacts() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
} catch (_) {
return [];
}
}
Future<void> saveContacts(List<DiscoveryContact> contacts) async {
final prefs = PrefsManager.instance;
final jsonList = contacts.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(DiscoveryContact contact) {
return {
'rawPacket': base64Encode(contact.rawPacket),
'publicKey': base64Encode(contact.publicKey),
'name': contact.name,
'type': contact.type,
'pathLength': contact.pathLength,
'path': base64Encode(contact.path),
'latitude': contact.latitude,
'longitude': contact.longitude,
'lastSeen': contact.lastSeen.millisecondsSinceEpoch,
};
}
DiscoveryContact _fromJson(Map<String, dynamic> json) {
final lastSeenMs = json['lastSeen'] as int? ?? 0;
return DiscoveryContact(
rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)),
publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)),
name: json['name'] as String? ?? 'Unknown',
type: json['type'] as int? ?? 0,
pathLength: json['pathLength'] as int? ?? -1,
path: json['path'] != null
? Uint8List.fromList(base64Decode(json['path'] as String))
: Uint8List(0),
latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
);
}
}
+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);
}
+2
View File
@@ -0,0 +1,2 @@
export 'browser_detection_stub.dart'
if (dart.library.js_interop) 'browser_detection_web.dart';
+3
View File
@@ -0,0 +1,3 @@
class BrowserDetection {
static bool get isChrome => false;
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:web/web.dart' as web;
class BrowserDetection {
static bool get isChrome {
final userAgent = web.window.navigator.userAgent.toLowerCase();
final isChrome = userAgent.contains('chrome');
return isChrome;
}
}
+19
View File
@@ -1,3 +1,5 @@
import 'package:meshcore_open/models/discovery_contact.dart';
import '../models/contact.dart';
bool matchesContactQuery(Contact contact, String query) {
@@ -14,8 +16,25 @@ bool matchesContactQuery(Contact contact, String query) {
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
}
bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) {
final normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.isEmpty) return true;
if (contact.name.toLowerCase().contains(normalizedQuery)) {
return true;
}
final hexPrefix = _extractHexPrefix(normalizedQuery);
if (hexPrefix == null) return false;
return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix);
}
String? _extractHexPrefix(String query) {
var cleaned = query;
if (cleaned.startsWith('<')) {
cleaned = cleaned.substring(1).replaceAll(">", "");
}
if (cleaned.startsWith('0x')) {
cleaned = cleaned.substring(2);
}
+5
View File
@@ -4,6 +4,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import '../utils/platform_info.dart';
import 'package:share_plus/share_plus.dart';
@@ -109,6 +110,10 @@ class GpxExport {
String shareText,
String subject,
) async {
if (PlatformInfo.isWeb) {
debugPrint("GPX export is not supported on Web.");
return gpxExportNotAvailable;
}
if (_contacts.isEmpty) {
debugPrint("No repeaters to export nothing to share.");
return gpxExportNoContacts;
+36
View File
@@ -0,0 +1,36 @@
import 'package:flutter/foundation.dart';
import 'dart:io' show Platform;
import 'browser_detection.dart';
/// Utility class to safely check the current platform across web and native.
///
/// Using `Platform` from `dart:io` directly on Web causes a crash.
/// This class handles the `kIsWeb` check first to avoid those crashes.
class PlatformInfo {
/// Whether the app is running in a web browser.
static bool get isWeb => kIsWeb;
/// Whether the app is running in the Chrome browser (only relevant if [isWeb] is true).
static bool get isChrome => isWeb && BrowserDetection.isChrome;
/// Whether the app is running on Android.
static bool get isAndroid => !kIsWeb && Platform.isAndroid;
/// Whether the app is running on iOS.
static bool get isIOS => !kIsWeb && Platform.isIOS;
/// Whether the app is running on macOS.
static bool get isMacOS => !kIsWeb && Platform.isMacOS;
/// Whether the app is running on Windows.
static bool get isWindows => !kIsWeb && Platform.isWindows;
/// Whether the app is running on Linux.
static bool get isLinux => !kIsWeb && Platform.isLinux;
/// Whether the app is running on a mobile platform (Android or iOS).
static bool get isMobile => isAndroid || isIOS;
/// Whether the app is running on a desktop platform (macOS, Windows, or Linux).
static bool get isDesktop => isMacOS || isWindows || isLinux;
}
+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)),
),
);
}
}
+55 -33
View File
@@ -9,46 +9,68 @@ class AppBarTitle extends StatelessWidget {
final String title;
final Widget? leading;
final Widget? trailing;
const AppBarTitle(this.title, {this.leading, this.trailing, super.key});
final bool indicators;
final bool subtitle;
const AppBarTitle(
this.title, {
this.leading,
this.trailing,
this.indicators = true,
this.subtitle = true,
super.key,
});
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final selfName = connector.selfName;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
leading ?? const SizedBox.shrink(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
leading ?? const SizedBox.shrink(),
Text(
title,
overflow: TextOverflow.ellipsis,
),
if (connector.isConnected && connector.selfName != null)
Center(
child: Text(
'(${connector.selfName})',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
Row(
mainAxisSize: MainAxisSize.min,
return LayoutBuilder(
builder: (context, constraints) {
final availableWidth = constraints.hasBoundedWidth
? constraints.maxWidth
: MediaQuery.sizeOf(context).width;
final compact = availableWidth < 170;
final showSubtitle =
!compact && connector.isConnected && selfName != null && subtitle;
final showBattery = availableWidth >= 60;
final showSnr = availableWidth >= 110;
final showIndicators = (showBattery || showSnr) && indicators;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
BatteryIndicator(connector: connector),
SNRIndicator(connector: connector),
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(),
],
),
trailing ?? const SizedBox.shrink(),
],
);
},
);
}
}
+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,
);
}
}
+16 -18
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../l10n/l10n.dart';
import 'signal_ui.dart';
/// A reusable tile widget for displaying a MeshCore device in a list
class DeviceTile extends StatelessWidget {
@@ -33,28 +34,25 @@ class DeviceTile extends StatelessWidget {
}
Widget _buildSignalIcon(int rssi) {
IconData icon;
Color color;
if (rssi >= -60) {
icon = Icons.signal_cellular_4_bar;
color = Colors.green;
} else if (rssi >= -70) {
icon = Icons.signal_cellular_alt;
color = Colors.lightGreen;
} else if (rssi >= -80) {
icon = Icons.signal_cellular_alt_2_bar;
color = Colors.orange;
} else {
icon = Icons.signal_cellular_alt_1_bar;
color = Colors.red;
}
final tier = rssi >= -60
? 0
: rssi >= -70
? 1
: rssi >= -80
? 2
: rssi >= -90
? 3
: 4;
final signalUi = signalUiForStrengthTier(tier);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Text('$rssi dBm', style: TextStyle(fontSize: 10, color: color)),
Icon(signalUi.icon, color: signalUi.color),
Text(
'$rssi dBm',
style: TextStyle(fontSize: 10, color: signalUi.color),
),
],
);
}
+105 -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;
@@ -215,3 +224,93 @@ class ContactsFilterMenu extends StatelessWidget {
);
}
}
class DiscoveryContactsFilterMenu extends StatelessWidget {
final ContactSortOption sortOption;
final ContactTypeFilter typeFilter;
final ValueChanged<ContactSortOption> onSortChanged;
final ValueChanged<ContactTypeFilter> onTypeFilterChanged;
const DiscoveryContactsFilterMenu({
super.key,
required this.sortOption,
required this.typeFilter,
required this.onSortChanged,
required this.onTypeFilterChanged,
});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return SortFilterMenu(
tooltip: l10n.listFilter_tooltip,
sections: [
SortFilterMenuSection(
title: l10n.listFilter_sortBy,
options: [
SortFilterMenuOption(
value: _actionSortLastSeen,
label: l10n.listFilter_heardRecently,
checked: sortOption == ContactSortOption.lastSeen,
),
SortFilterMenuOption(
value: _actionSortName,
label: l10n.listFilter_az,
checked: sortOption == ContactSortOption.name,
),
],
),
SortFilterMenuSection(
title: l10n.listFilter_filters,
options: [
SortFilterMenuOption(
value: _actionFilterAll,
label: l10n.listFilter_all,
checked: typeFilter == ContactTypeFilter.all,
),
SortFilterMenuOption(
value: _actionFilterUsers,
label: l10n.listFilter_users,
checked: typeFilter == ContactTypeFilter.users,
),
SortFilterMenuOption(
value: _actionFilterRepeaters,
label: l10n.listFilter_repeaters,
checked: typeFilter == ContactTypeFilter.repeaters,
),
SortFilterMenuOption(
value: _actionFilterRooms,
label: l10n.listFilter_roomServers,
checked: typeFilter == ContactTypeFilter.rooms,
),
],
),
],
onSelected: (action) {
switch (action) {
case _actionSortName:
onSortChanged(ContactSortOption.name);
break;
case _actionSortLastSeen:
onSortChanged(ContactSortOption.lastSeen);
break;
case _actionFilterAll:
onTypeFilterChanged(ContactTypeFilter.all);
break;
case _actionFilterUsers:
onTypeFilterChanged(ContactTypeFilter.users);
break;
case _actionFilterFavorites:
onTypeFilterChanged(ContactTypeFilter.favorites);
break;
case _actionFilterRepeaters:
onTypeFilterChanged(ContactTypeFilter.repeaters);
break;
case _actionFilterRooms:
onTypeFilterChanged(ContactTypeFilter.rooms);
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),
);
}
}
+148 -103
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,
);
}
@@ -70,6 +79,7 @@ class _PathManagementDialog extends StatelessWidget {
title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes),
flipPathRound: true,
targetContact: widget.contact,
),
),
),
@@ -137,6 +147,10 @@ class _PathManagementDialog extends StatelessWidget {
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;
@@ -147,6 +161,42 @@ class _PathManagementDialog extends StatelessWidget {
? 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(
@@ -160,6 +210,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(
@@ -167,7 +228,7 @@ class _PathManagementDialog extends StatelessWidget {
fontSize: 12,
),
),
if (paths.length >= 100) ...[
if (pathsWithRepeaters.length >= 100) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
@@ -186,115 +247,99 @@ class _PathManagementDialog extends StatelessWidget {
),
],
const SizedBox(height: 8),
...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;
...pathsWithRepeaters.map((entry) {
final path = entry.value.value;
final color = entry.value.key;
Color color = Colors.grey;
if (isDirectRepeater) {
color = Colors.green;
} else if (isSecondDirectRepeater) {
color = Colors.yellow;
} else if (isThirdDirectRepeater) {
color = Colors.red;
} else if (path.wasFloodDiscovery) {
color = Colors.blue;
}
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: color,
child: Text(
'${path.hopCount}',
style: const TextStyle(fontSize: 12),
),
),
title: Text(
l10n.chat_hopsCount(path.hopCount),
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}${path.successCount} ${l10n.chat_successes}',
style: const TextStyle(fontSize: 11),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close, size: 16),
tooltip: l10n.chat_removePath,
onPressed: () async {
await pathService.removePathRecord(
currentContact.publicKeyHex,
path.pathBytes,
);
},
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: color,
child: Text(
'${path.hopCount}',
style: const TextStyle(fontSize: 12),
),
path.wasFloodDiscovery
? const Icon(
Icons.waves,
size: 16,
color: Colors.grey,
)
: const Icon(
Icons.route,
size: 16,
color: Colors.grey,
),
title: Text(
l10n.chat_hopsCount(path.hopCount),
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}${path.successCount} ${l10n.chat_successes}',
style: const TextStyle(fontSize: 11),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close, size: 16),
tooltip: l10n.chat_removePath,
onPressed: () async {
await pathService.removePathRecord(
currentContact.publicKeyHex,
path.pathBytes,
);
},
),
path.wasFloodDiscovery
? const Icon(
Icons.waves,
size: 16,
color: Colors.grey,
)
: const Icon(
Icons.route,
size: 16,
color: Colors.grey,
),
],
),
onLongPress: () =>
_showFullPathDialog(context, path.pathBytes),
onTap: () async {
if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.chat_pathDetailsNotAvailable,
),
],
),
onLongPress: () =>
_showFullPathDialog(context, path.pathBytes),
onTap: () async {
if (path.pathBytes.isEmpty) {
duration: const Duration(seconds: 2),
),
);
return;
}
final pathBytes = Uint8List.fromList(
path.pathBytes,
);
final pathLength = path.pathBytes.length;
await connector.setPathOverride(
currentContact,
pathLen: pathLength,
pathBytes: pathBytes,
);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.chat_pathDetailsNotAvailable,
l10n.path_usingHopsPath(path.hopCount),
),
duration: const Duration(seconds: 2),
),
);
return;
}
final pathBytes = Uint8List.fromList(path.pathBytes);
final pathLength = path.pathBytes.length;
await connector.setPathOverride(
currentContact,
pathLen: pathLength,
pathBytes: pathBytes,
);
if (!context.mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
l10n.path_usingHopsPath(path.hopCount),
),
duration: const Duration(seconds: 2),
),
);
},
),
);
},
),
);
}
}),
const Divider(),
] else ...[
+2 -2
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import '../utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
@@ -326,8 +327,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
},
onSubmitted: (_) => _handleLogin(),
autofocus:
!(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
!PlatformInfo.isMobile &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
+2 -2
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import '../utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
@@ -274,8 +275,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
),
onSubmitted: (_) => _handleLogin(),
autofocus:
!(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
!PlatformInfo.isMobile &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
class SignalUi {
final IconData icon;
final Color color;
const SignalUi({required this.icon, required this.color});
}
SignalUi signalUiForStrengthTier(int tier) {
switch (tier) {
case 0:
return const SignalUi(
icon: Icons.signal_cellular_4_bar,
color: Colors.green,
);
case 1:
return const SignalUi(
icon: Icons.signal_cellular_alt,
color: Colors.lightGreen,
);
case 2:
return const SignalUi(
icon: Icons.signal_cellular_alt_2_bar,
color: Colors.amber,
);
case 3:
return const SignalUi(
icon: Icons.signal_cellular_alt_1_bar,
color: Colors.orange,
);
default:
return const SignalUi(
icon: Icons.signal_cellular_alt_1_bar,
color: Colors.red,
);
}
}
+14 -22
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import 'signal_ui.dart';
class SNRUi {
final IconData icon;
@@ -38,28 +39,19 @@ SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) {
final snrLevels = getSNRfromSF(spreadingFactor);
IconData icon;
Color color;
String text = '${snr.toStringAsFixed(1)} dB';
final tier = snr >= snrLevels[0]
? 0
: snr >= snrLevels[1]
? 1
: snr >= snrLevels[2]
? 2
: snr >= snrLevels[3]
? 3
: 4;
final signalUi = signalUiForStrengthTier(tier);
if (snr >= snrLevels[0]) {
icon = Icons.signal_cellular_alt;
color = Colors.green;
} else if (snr >= snrLevels[1]) {
icon = Icons.signal_cellular_alt;
color = Colors.lightGreen;
} else if (snr >= snrLevels[2]) {
icon = Icons.signal_cellular_alt;
color = Colors.yellow;
} else if (snr >= snrLevels[3]) {
icon = Icons.signal_cellular_alt_2_bar;
color = Colors.orange;
} else {
icon = Icons.signal_cellular_alt_1_bar;
color = Colors.red;
}
return SNRUi(icon, color, text);
return SNRUi(signalUi.icon, signalUi.color, text);
}
class SNRIndicator extends StatefulWidget {
@@ -139,10 +131,10 @@ class _SNRIndicatorState extends State<SNRIndicator> {
}
if (diff.inHours < 24) {
final hours = diff.inHours;
return hours == 1 ? "1h" : "${hours}hs";
return "${hours}h";
}
final days = diff.inDays;
return days == 1 ? "1d" : "${days}ds";
return "${days}d";
}
void _showFullPathDialog(
+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
+7
View File
@@ -0,0 +1,7 @@
{
"name": "meshcore-open",
"scripts": {
"build": "dart run build_pipe:build",
"deploy": "bun x wrangler deploy"
}
}
-1095
View File
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -50,7 +50,7 @@ dependencies:
cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1
flutter_foreground_task: ^9.2.0
wakelock_plus: ^1.2.8
wakelock_plus: ^1.4.0
characters: ^1.4.0
package_info_plus: ^9.0.0
mobile_scanner: ^7.1.4 # QR/barcode scanning
@@ -60,6 +60,10 @@ dependencies:
gpx: ^2.3.0
path_provider: ^2.1.5
share_plus: ^12.0.1
build_pipe: ^0.3.1
material_symbols_icons: ^4.2906.0
web: ^1.1.1
flutter_svg: ^2.0.10+1
dev_dependencies:
flutter_test:
@@ -87,6 +91,7 @@ flutter:
assets:
- assets/images/
- assets/icons/
flutter_launcher_icons:
android: true
@@ -124,3 +129,16 @@ flutter_launcher_icons:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
build_pipe:
workflows:
default:
clean_flutter: true # Optional: Cleans old build artifacts before running
generate_log: true # Optional: Outputs a build log file for debugging
platforms:
web:
build:
build_command: flutter build web --release --pwa-strategy=none
# Strongly recommended: disables the default service worker which often causes more cache headaches
add_version_query_param: true
# This is the key flag! It appends ?v=<your pubspec version> to bootstrap/JS files
@@ -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);
});
}

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