Compare commits

...

302 Commits

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

* format

* Cleaned up code

* format

* remove my gitignore change - ignore this

* fix

* Update lib/screens/channel_chat_screen.dart

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

* Update lib/screens/channel_chat_screen.dart

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

* Refactor onHorizontalDragStart for readability

fixed formating.

* Fix swipe end handling in channel chat screen

* Refactor swipe gesture handling in chat screen

* Update lib/screens/channel_chat_screen.dart

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

* Update lib/screens/channel_chat_screen.dart

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

* Refactor swipe handling for reply functionality

* Adjust swipe thresholds and logic in chat screen

* Conditionally render reply bubble or padding

---------

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

* Disambiguate RESP_CODE_SENT handling for direct vs channel

* Handle channel sent feedback when firmware returns RESP_CODE_OK

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

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

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

- Add repeater-specific battery chemistry persistence and service accessors

- Add repeater chemistry selector in Repeater Hub

- Ensure telemetry and status compute percentages consistently from same chemistry

- Add focused battery utility tests

Refs #116

Refs #174

* fix: Flutter Analyzer Errors fixed Recent Merge Compatible

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

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

* l10n: revert unrelated locale churn for LOS feature

* feat: keep LOS with app-wide unit settings

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

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

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

* Fix trace route bytes generation logic in Contact model

* Ignore advertisements from self in MeshCoreConnector

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

* Add SNRIndicator to AppBar and refactor BatteryIndicator layout

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

* Remove unused import from SNR indicator widget

* Update lib/models/contact.dart

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

* Update lib/connector/meshcore_connector.dart

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

* Update lib/connector/meshcore_connector.dart

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

* Update lib/screens/path_trace_map.dart

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

* Update lib/widgets/battery_indicator.dart

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

* Update lib/helpers/cayenne_lpp.dart

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

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

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

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

* Throw an exception for unsupported LPP types in CayenneLpp class

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

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

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

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

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

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

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

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

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

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

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

* Initial plan

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

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

---------

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

* Refactor AppBarTitle widget to simplify Text widget initialization

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

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

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

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

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

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

---------

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

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

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

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

in early testing the ble remains connected with display off and also
when switching apps
2026-02-10 19:38:46 -08:00
ericz 607583060a translations to german updated. 2026-02-10 22:55:39 +01:00
Ded 71cf556b61 Merge pull request #148 from spfmoby/more-better-french-translations
Still better french translations
2026-02-10 11:24:47 -08:00
Zach c26174ad18 Chore bump version 2026-02-10 09:01:56 -07:00
spfmoby 04021a39a1 Better french translations 2026-02-10 08:12:51 +01:00
446564 fe23e9f7a0 add support for whipseros
needed a new ble prefix
2026-02-09 05:36:25 -08:00
Ded d7ec1876af Merge pull request #143 from zjs81/alpha6
chore: update version to alpha 6
2026-02-08 19:07:29 -08:00
446564 87a2807f5b chore: update version to alpha 6 2026-02-08 18:56:24 -08:00
Ryan Malloy daca42701c Notification rate limiting (#110)
* Add notification rate limiting with privacy-safe debug logging

- Add batching system to prevent notification storms (3s rate limit, 5s batch window)
- Queue rapid notifications and show batch summaries
- Debug logs show device names for adverts, sender/channel for messages (no content leaks)
- Remove unused _maxBatchSize constant

Context: Added after getting notification-flooded while evaluating RF flood management. The irony.

* Update notification_service.dart

I made a mistake and removed this

* Add l10n support for notification strings

Addresses PR #110 review feedback to use the translations system:
- Add notification strings to app_en.arb (plurals for batch summary)
- Update NotificationService to use lookupAppLocalizations()
- Wire locale from MaterialApp to NotificationService
- Regenerate localization files

New strings added (English only, translations needed):
- notification_activityTitle: "MeshCore Activity"
- notification_messagesCount: "{count} message(s)"
- notification_channelMessagesCount: "{count} channel message(s)"
- notification_newNodesCount: "{count} new node(s)"
- notification_newTypeDiscovered: "New {type} discovered"
- notification_receivedNewMessage: "Received new message"

* Add notification string translations for all supported languages

Translated notification_activityTitle, notification_messagesCount,
notification_channelMessagesCount, notification_newNodesCount,
notification_newTypeDiscovered, and notification_receivedNewMessage
to: bg, de, es, fr, it, nl, pl, pt, ru, sk, sl, sv, uk, zh

Includes proper ICU plural forms for Slavic languages (few/many/other)
and Slovenian dual form.

* Apply dart format to notification_service.dart

---------

Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-08 18:42:15 -08:00
Ded ea43cf17eb reduce map marker size (#131)
* reduce map marker size

reduces map markers from 80 to 60 px to improve visibility with higher density areas

* add flutter test to actions

* Add GPX export functionality and related UI components

* Refactor GPX export constants to use lowercase naming convention and improve export function error handling

* ran formating

* Enhance GPX export functionality with customizable parameters and improved metadata

* Implement PathTraceMapScreen and refactor path tracing functionality across screens

* Add localization for missing location error in path tracing

* Updated GPX export functionality for contacts and repeaters in multiple languages.

* Add scrollbar to path trace details list for improved navigation

* Integrate SharePlus plugin for enhanced sharing functionality across platforms

* reduce map marker size

reduces map markers from 80 to 60 px to improve visibility with higher density areas

* reduce marker size to improve map clarity and add path trace navigation to path management

---------

Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-08 18:40:58 -08:00
Ded 8ef6e2c656 Merge pull request #130 from zjs81/path-map-rotation
remove rotation in path map
2026-02-08 18:39:48 -08:00
Winston Lowe 24de98d5ee Merge pull request #134 from zjs81/dev-gpx
Added a export to a GPX
2026-02-08 17:15:26 -08:00
Winston Lowe 0fd841b5b5 Merge branch 'main' into dev-gpx 2026-02-08 17:13:18 -08:00
Winston Lowe c365b7889b Merge pull request #141 from zjs81/dev-NewPathTracing
Implement PathTraceMapScreen and refactor path tracing functionality
2026-02-08 17:10:16 -08:00
Winston Lowe 2db30ace6a Integrate SharePlus plugin for enhanced sharing functionality across platforms 2026-02-08 12:26:49 -08:00
Winston Lowe 0d8801fa75 Add scrollbar to path trace details list for improved navigation 2026-02-08 12:25:51 -08:00
Winston Lowe bcae6ac19f Updated GPX export functionality for contacts and repeaters in multiple languages. 2026-02-08 12:14:03 -08:00
Winston Lowe 2f4b230b31 Add localization for missing location error in path tracing 2026-02-08 11:57:04 -08:00
Winston Lowe 98e0b05e73 Implement PathTraceMapScreen and refactor path tracing functionality across screens 2026-02-08 11:32:36 -08:00
Winston Lowe 2a909e6081 Enhance GPX export functionality with customizable parameters and improved metadata 2026-02-07 19:45:02 -08:00
Winston Lowe d1009d3c20 ran formating 2026-02-07 11:07:57 -08:00
Ded 91b1696bc5 Merge pull request #132 from zjs81/add-test-action
add flutter test to actions
2026-02-07 08:33:16 -08:00
Winston Lowe 978ea4790d Refactor GPX export constants to use lowercase naming convention and improve export function error handling 2026-02-05 13:46:05 -08:00
Winston Lowe 8b1228bf8d Add GPX export functionality and related UI components 2026-02-05 13:38:49 -08:00
446564 ddee76ced2 add flutter test to actions 2026-02-05 09:40:31 -08:00
446564 6a3c59fa2c remove rotation in path map
when zooming on the path map view window the rotation was too easy to trigger and
provided little value to understanding the path
2026-02-05 09:24:24 -08:00
Ded a54cc78691 Merge pull request #129 from zjs81/remove-msg-prefix
remove direct msg notification prefix
2026-02-05 08:58:43 -08:00
446564 05fb5a13fa remove direct msg notification prefix
The prefix "New message from " takes up a lot of space and was not localized anyway.
2026-02-05 08:33:07 -08:00
zjs81 c320378be1 Refactor unread message tracking and implement channel caching (#126)
* Refactor unread message tracking and implement channel caching

* formatted files
2026-02-04 20:34:03 -07:00
Ded b3645481c7 Merge pull request #125 from zjs81/reduce-build-steps
stop building twice for pull requests on branches from this repo
2026-02-04 12:42:24 -08:00
Ded 589707aa13 Merge pull request #123 from zjs81/dart-format
This formats the project and adds a workflow to check that each contribution has also been formatted.

plus small fix to get rid of analyzer errors which could be changed to warnings but that's another day.
2026-02-04 12:41:46 -08:00
446564 6070802213 stop building twice for pull requests
we should only run the build steps on a pull request OR a push to main
2026-02-04 09:02:03 -08:00
446564 2525b9425b reduce jobs for flutter and dart
no need to setup the env twice the exact same way as they don't conflict
2026-02-04 08:59:29 -08:00
446564 b786c90514 combine flutter and dart actions
reduce time to complete and stop running twice for pull requests
2026-02-04 08:56:40 -08:00
446564 a35590a407 fix dart format workflow install deps step
needs to use flutter pub get not dart pub get
2026-02-04 08:40:19 -08:00
446564 8d15f7cef6 wrap returns from if blocks
fixes two analyzer errors for return blocks on new lines from if blocks
2026-02-04 08:34:37 -08:00
446564 e449f5e1d5 add dart format workflow
checks code has been formatted with dart format on push and pull request

adds a note in README for contributors
2026-02-04 08:33:49 -08:00
446564 b34d684e67 format dart files
formats all dart files using `dart format .` from the root project dir

this makes the code style repeatable by new contributors and makes PR review easier
2026-02-04 08:32:35 -08:00
Ded 488a286701 Merge pull request #59 from 446564/community-#-names
add community to hashtag channel name
2026-02-03 20:08:42 -08:00
Zach c742d98fbb issue #112 fixes and more 2026-02-01 18:37:14 -07:00
zjs81 1d4c9ad9bd Merge pull request #115 from zjs81/advert-intervals
allow disable repeater adverts
2026-02-01 17:10:46 -07:00
Zach 818f514702 The first issue was that the toggle switch states weren't being initialized when settings were refreshed from the device. The code would correctly update the interval values themselves, but failed to set the corresponding boolean flags that control whether the toggles appear as "on" or "off". This meant that if you refreshed settings from a device that had advertisements disabled (with an interval of zero), the toggles would incorrectly show as enabled even though the device was actually broadcasting no advertisements. We fixed this by adding two lines that explicitly set _advertEnable = _advertInterval > 0 and _floodAdvertEnable = _floodAdvertInterval > 0 after parsing the interval values from device responses.
The second critical bug was in the validation logic that checks whether responses from the device contain valid data. The validator was rejecting any interval values of zero because it checked interval > 0, but zero is now a meaningful and valid value that indicates advertisements are disabled. Without this fix, any time a device reported back that advertisements were disabled, the app would silently discard that information as invalid, leaving the UI out of sync with reality. We changed the validation to use interval >= 0 instead and updated the comment to explicitly document that zero means disabled.

The third fix was a minor code style issue where a single-line if statement was missing braces, causing a linter warning. This doesn't affect functionality but ensures the code meets project standards.
2026-02-01 17:08:53 -07:00
Zach be54419e5b Merge remote-tracking branch 'origin/main' into advert-intervals 2026-02-01 17:03:53 -07:00
zjs81 00eb1a68a6 Merge pull request #118 from wel97459/dev-shareContact
Adds contact shearing
2026-02-01 16:59:21 -07:00
Zach 79ffc21bd6 fix commit 2026-02-01 16:57:17 -07:00
Zach 0374f4f5da Merge remote-tracking branch 'origin/main' into dev-shareContact 2026-02-01 14:18:35 -07:00
Winston Lowe 4650584f9b Merge pull request #117 from wel97459/dev-reconnection
This cures a race condition that was messing up the disconnection handler.
Before the bluetooth device was fully connected _handleDisconnection() was being called from the lisener.
2026-01-31 22:28:35 -08:00
Winston Lowe 8d8b938878 Ran translation script 2026-01-31 22:19:01 -08:00
Ded e3a0bd3b13 Merge pull request #114 from zjs81/obtainum-btn
add obtainium badge
2026-01-31 20:07:08 -08:00
446564 4f83d87f8c use switch for advert enable/disable
move style to align with other toggles and use a switch instead of a checkbox
2026-01-31 17:07:24 -08:00
Winston Lowe 6d7d51f0a4 _requestDeviceInfo added isConnected not already _awaitingSelfInfo 2026-01-31 16:03:05 -08:00
Winston Lowe 33680f0cb9 Replace action buttons with a popup menu for better UI/UX on channels and map screens 2026-01-31 15:25:34 -08:00
Winston Lowe 5115d8bbe3 Added zero-hop contact sharing functionality and related UI updates 2026-01-31 15:00:33 -08:00
Winston Lowe d30e7c4e2c Prevent disconnection handling when already disconnected, curing a race condition. 2026-01-31 14:55:55 -08:00
Winston Lowe 8470171e88 Merge branch 'dev-shareContacts' into dev-shareContact 2026-01-31 08:02:35 -08:00
446564 ede3142d40 allow disable repeater adverts
Adds checkbox to disable adverts and flood adverts

Also updates flood avert range to new max of 168 hours
2026-01-30 11:05:57 -08:00
446564 6712088fcd add obtainium badge
allow users to easily add app to obtainium
https://apps.obtainium.imranr.dev
2026-01-30 08:44:03 -08:00
Ded 7b519854d7 Merge branch 'main' into community-#-names 2026-01-29 08:07:05 -08:00
Zach 90ce46392a feat: optimize reaction message format to reduce airtime
- Reduce reaction payload from ~44 bytes to 9 bytes (5x smaller)
- Use 4-char hex hash (timestamp + sender + first 5 chars) for message ID
- Use 2-char hex emoji index instead of multi-byte UTF-8 emoji
- Format: r:HASH:INDEX (e.g., r:a1b2:00)
- For 1:1 chats, sender is implicit (null) for shorter hash
- Prevent users from reacting to their own messages
- Add room server reaction support with sender identification
- Make emoji lists public in EmojiPicker for shared indexing
- Add 💪 and 🚀 emojis to picker
- Add comprehensive unit tests for reaction helpers
- Update minor dependencies
2026-01-28 23:21:04 -07:00
Zach d61ec217fc feat: add Russian and Ukrainian to language selector
These languages had translation files but were missing from the
settings UI. Adds appSettings_languageRu and appSettings_languageUk
strings and corresponding RadioListTile entries.

Fixes missing languages in app settings.
2026-01-28 22:26:14 -07:00
Zach 3ac81a5448 Merge origin/main into pr-106
Resolve conflict in app_de.arb: keep improved German translation
for community_updateSecret while adding path trace strings from main.
2026-01-28 22:22:43 -07:00
zjs81 7004067839 Merge pull request #108 from wel97459/dev-pathtrace
Path tracing. This adds support to ping and trace route repeaters and room server.
2026-01-28 22:07:14 -07:00
Zach 935b7b07eb Add path trace localizations for all languages
- Translate path trace strings to all 14 supported locales
- Regenerate localization Dart files
- Fix translate.py to also detect empty string values as missing
2026-01-28 22:05:04 -07:00
Zach cdacc54421 Merge remote-tracking branch 'origin/main' into dev-pathtrace 2026-01-28 21:43:07 -07:00
zjs81 bf8f002d55 Merge pull request #111 from wel97459/dev-reconntion
Added disconnection handling, and fixed state changing of navigation on connection.
2026-01-28 21:39:42 -07:00
Zach 998ff50495 fix: restore _handleDisconnection() on battery request failure
This was the author's original intent - use battery request failure
as a signal that the connection is lost.
2026-01-28 21:34:13 -07:00
Zach 92d2b224e7 fix: address PR review issues
- Fix memory leak by adding dispose() to remove connection listener
- Fix typo: changedNavgation -> _changedNavigation
- Add mounted check before navigation to prevent errors
- Remove overly aggressive _handleDisconnection() call on battery request failure
- Only reset battery flag on error to allow retry without disconnecting
2026-01-28 21:29:18 -07:00
Winston Lowe 34a6b5d895 Added error catching to requestBatteryStatus
to call _handleDisconnection when it fails update.

Updated ScannerScreen to manage navigation state logic on connection.
2026-01-28 20:13:40 -08:00
zjs81 c953a1a798 Merge pull request #105 from erikklavora/main
Updated Slovenian lang
2026-01-28 20:53:49 -07:00
Winston Lowe 42115bf200 Refactor contact handling and enhance UI with new advert options and localized strings 2026-01-28 11:04:34 -08:00
Winston Lowe d0c8fab6fb Add contact import functionality and update UI feedback for import status 2026-01-28 10:19:42 -08:00
Winston Lowe eeb8ff34e8 Implement contact import functionality from clipboard and add relevant UI options 2026-01-26 16:11:21 -08:00
Winston Lowe 641307a316 Added response code for exporting contacts and implement frame listener in contacts_screen.dart 2026-01-26 12:19:45 -08:00
Winston Lowe c37abb63e3 add export and import contact frame builders in meshcore_protocol.dart and implement contact export functionality in contacts_screen.dart 2026-01-26 11:56:42 -08:00
Winston Lowe 898ef1c11c Refactor autofocus logic in login dialogs for better platform handling 2026-01-26 10:40:10 -08:00
Winston Lowe 749f9d4dfd cleaned up. 2026-01-25 12:00:38 -08:00
Winston Lowe 9c1b5899fb Added scroll view to room server login.
Disabled autofocus of password.
2026-01-25 11:55:55 -08:00
Winston Lowe cacb9bc677 Moved all the path tracing logic to the dialog.
refactored repeater hub along with contacts screen to use shortPubKeyHex.
Added localization strings for path tracing, english only.
2026-01-25 10:58:00 -08:00
Winston Lowe 0ebd688787 Added shortPubKeyHex
and added a trace route builder traceRouteBytes
2026-01-25 10:53:28 -08:00
ericz bb18038f60 removed truncation of notification as in Issue #107 2026-01-25 11:40:02 +01:00
Winston Lowe fcf741b20a Got the basic path tracing working. 2026-01-24 20:36:14 -08:00
ericz 88aa104ae5 further translation fixes for german 2026-01-24 18:05:10 +01:00
erikklavora 90f90ad7cf Updated Slovenian lang 2026-01-24 17:05:01 +01:00
Zach 8b0bdd9a46 fix: update PRODUCT_BUNDLE_IDENTIFIER to com.monitormx.meshcoreopen 2026-01-24 01:37:19 -07:00
zjs81 45d914de57 chore: update version to 5.0.0+5 in pubspec.yaml 2026-01-24 01:26:23 -07:00
Zach 2c49534955 feat: add url_launcher_ios dependency and update project configuration 2026-01-24 01:24:56 -07:00
Zach c56cf9c3ed feat: add CocoaPods support for macOS and iOS, including necessary configurations and dependencies 2026-01-24 01:07:18 -07:00
zjs81 fee4cd13be chore: update version to 0.4.5+4 in pubspec.yaml 2026-01-24 00:52:15 -07:00
zjs81 a53d5ccfb6 Merge pull request #69 from spfmoby/better-french-translations2
More french translation updates
2026-01-24 00:50:11 -07:00
zjs81 e5d06b1c7e Merge pull request #102 from zjs81/pr-94
Pr 94
2026-01-24 00:46:48 -07:00
zjs81 e95a55e4f0 feat: add Ukrainian localization support and improve string formatting 2026-01-24 00:45:01 -07:00
zjs81 422ca941c2 Merge remote-tracking branch 'origin/main' into pr-94 2026-01-24 00:42:29 -07:00
zjs81 3098d860e9 Merge pull request #101 from zjs81/anupoh/main
Anupoh/main
2026-01-24 00:32:29 -07:00
zjs81 f0d34f7503 Update Russian localization for improved pluralization and add new chat link handling messages
- Enhanced pluralization rules for "hops" in various contexts to better reflect Russian grammar.
- Added new localization strings for chat link handling, including error messages and confirmation prompts.
- Ensured consistency in the use of plural forms across the application.
2026-01-24 00:27:45 -07:00
zjs81 daa0c3f9c3 Merge branch 'main' into anupoh/main 2026-01-24 00:22:28 -07:00
zjs81 09e1cd2b8d fix: improve BLE scanning reliability and filter out own node from contacts list improve text scaling 2026-01-24 00:17:18 -07:00
zjs81 fa514533eb feat: add ChatScrollController and JumpToBottomButton for improved chat scrolling experience
- Implemented ChatScrollController to manage scroll behavior and visibility of jump-to-bottom button.
- Added functionality to automatically scroll to the bottom when the keyboard opens.
- Created JumpToBottomButton widget that appears when the user scrolls up, allowing quick navigation back to the bottom of the chat.
2026-01-23 17:56:06 -07:00
zjs81 75b8b8af70 Merge pull request #60 from 446564/missing-tooltips
update tooltips
2026-01-23 16:47:31 -07:00
spfmoby 115667a27c More french translation updates6 2026-01-23 17:39:59 +01:00
spfmoby cfb51d96ff More french translation updates6 2026-01-23 17:39:49 +01:00
anupoh 75356fe20d Russian translation for the app
I've prepared the Russian localization files for the app. It would be great if localization were included in the app. Thanx a lot!
2026-01-23 16:58:16 +07:00
Winston Lowe 2089613696 Added the basics for path tracing 2026-01-22 23:42:10 -08:00
megadimich c43df67fac Ukrainian localization files 2026-01-22 15:08:42 +00:00
spfmoby e2b9b58d7d More french translation updates5 2026-01-22 10:25:42 +01:00
spfmoby d6794bc8d7 More french translation updates4 2026-01-22 08:45:54 +01:00
spfmoby 72216e2cf7 More french translation updates3 2026-01-22 08:21:09 +01:00
spfmoby 2a2275ec31 More french translation updates2 2026-01-22 08:16:58 +01:00
spfmoby dff037535d More french translation updates 2026-01-21 18:13:24 +01:00
zjs81 297e609b3e fix: replace RadioListTile with RadioGroup for better state management in community selection 2026-01-20 22:40:42 -07:00
zjs81 20171c491f fix: update iOS platform version and enable sentence capitalization in chat input fields 2026-01-20 22:28:37 -07:00
zjs81 cc43f4d198 Merge pull request #65 from zjs81/fix/message-length-safety-margin
fix: add safety margin to text message overhead calculations
2026-01-20 21:51:53 -07:00
zjs81 537384ea5b fix: add safety margin to text message overhead calculations 2026-01-20 21:50:35 -07:00
zjs81 a0be63b2e7 feat: integrate link handling in chat screen with linkify support
- Added flutter_linkify package to auto-detect and linkify URLs in chat messages.
- Implemented LinkHandler class to manage link tap confirmations and URL launching.
- Updated chat_screen.dart to use Linkify for displaying message text with links.
- Registered url_launcher plugin for handling URL launches across platforms.
- Updated pubspec.yaml and pubspec.lock to include new dependencies.
- Cleaned up untranslated.json by removing unused translations.
2026-01-20 21:42:54 -07:00
zjs81 1cc887e5bb Merge pull request #61 from 446564/remove-rcvd
remove msg notify prefix when preview avail
2026-01-20 21:11:08 -07:00
446564 26d9029538 remove msg notify prefix when preview avail
this removes the 'Received new message: ' prefix from notications
when there is a message preview available
2026-01-20 17:35:14 -08:00
446564 30bcbedf5e update tooltips
add missing tooltip:
- channels, add channel button
- map, filter nodes button
2026-01-20 17:21:44 -08:00
446564 4003519deb add community to hashtag channel name
brings behavior in line with community public channels and prefixes the community name

this allows users to use the same radio with multiple clients and be able to tell which
hashtag channel they are using i.e. Scouts #leaders, where previous it was just a private
chanel named #leaders.
2026-01-20 15:26:41 -08:00
zjs81 3fdd8f5eaf chore: Update version to 0.4.0+4 in pubspec.yaml 2026-01-19 20:58:11 -07:00
zjs81 f4ec732de8 feat: Add community management features with QR code scanning
- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
2026-01-19 20:56:07 -07:00
zjs81 f790604d23 Merge pull request #42 from wel97459/dev-neighbours
Added Neighbors to the repeater hub and a screen to display the Neighbors
2026-01-19 19:17:00 -07:00
zjs81 8e3b563aba revert translate.py 2026-01-19 19:14:48 -07:00
zjs81 ee3b0a3126 Add untranslated messages file and update localization keys
- Added `untranslated.json` to track untranslated messages.
- Updated localization keys in various language files to use camelCase format for consistency.
- Modified `neighbours_screen.dart` to reference updated localization keys.
2026-01-19 19:13:22 -07:00
zjs81 31d633ee0b Merge main into dev-neighbours 2026-01-19 19:09:03 -07:00
zjs81 c269365d81 Merge pull request #48 from wel97459/dev-gps
Added GPS enable and GPS interval settings.
2026-01-19 19:02:13 -07:00
zjs81 9a9f59e53f localization: update GPS settings messages for clarity and consistency across multiple languages 2026-01-19 19:00:30 -07:00
zjs81 9cb667fad0 localization: fix punctuation in GPS interval settings for Spanish and Portuguese 2026-01-19 19:00:24 -07:00
zjs81 3fef594fe5 localization: update GPS settings messages and improve handling of custom variables 2026-01-19 18:56:06 -07:00
zjs81 8387304d2a Merge main into dev-gps
- Resolved localization conflicts by keeping both GPS settings and room management strings
- Merged room management features from main
- Merged map and contacts screen updates from main
2026-01-19 18:51:02 -07:00
zjs81 2acba9eb84 Merge pull request #51 from wel97459/dev-roomManagement
Added room server management
2026-01-19 18:34:51 -07:00
zjs81 30ba1799e1 localization: update room management strings in multiple languages and refactor room login handling 2026-01-19 18:29:53 -07:00
zjs81 13f9c5058a Merge branch 'main' into dev-roomManagement 2026-01-19 18:25:00 -07:00
Winston Lowe 98fc2d6e0a Updated gps setting to follow state of companion. 2026-01-19 16:57:46 -08:00
Winston Lowe 2becbb342c Added buildGetCustomVarsFrame
And added update to refreshDeviceInfo and _requestDeviceInfo.
Added parsing of Custom Vars
2026-01-19 16:55:39 -08:00
zjs81 5b2d5a494c Merge pull request #47 from ericszimmermann/main
Disable Map rotation
2026-01-19 09:26:29 -07:00
Winston Lowe 153736d36e added roomserver management 2026-01-18 21:21:33 -08:00
Winston Lowe 6c8a149e1b fix a few translations and used _neighbourCount 2026-01-18 12:01:57 -08:00
Winston Lowe b41ccee4f9 Merge branch 'main' into dev-neighbours 2026-01-18 11:27:19 -08:00
Winston Lowe 04a713bb76 Added a basic neighbours screen for repeaters 2026-01-18 11:17:47 -08:00
Winston Lowe 714aecd7e6 Added GPS enable and interval settings 2026-01-18 01:05:46 -08:00
Winston Lowe 2e1a5e0fbf added CMD_SET_CUSTOM_VAR to BLE debug 2026-01-18 01:03:45 -08:00
Winston Lowe 1f0b7d8d7b added buildSetCustomVarFrame and setCustomVar 2026-01-18 01:02:48 -08:00
ericszimmermann dffea23ce2 Merge branch 'zjs81:main' into main 2026-01-17 20:47:56 +01:00
zjs81 e0a8fb7ec0 Merge pull request #44 from mtlynch/gh-build
Add a Github Action to build code in CI
2026-01-17 11:39:37 -07:00
zjs81 06fc08c41f Merge pull request #45 from mtlynch/flutter-analyze
Fix issues flagged by flutter analyze
2026-01-17 11:38:08 -07:00
ericz c22bfed680 Merge branch 'disable_map_rotation'
Disable Map Rotation.
2026-01-17 19:30:52 +01:00
zjs81 316c76e5b4 Merge pull request #46 from ericszimmermann/main
German translation V2
2026-01-17 11:20:54 -07:00
ericz 4b215ad574 Disable Map rotation 2026-01-17 17:14:39 +01:00
ericz 09e60cebd9 German translation V2 2026-01-17 17:03:39 +01:00
Michael Lynch 6782347cf4 Fix issues flagged by flutter analyze
This fixes code quality issues that flutter analyze catches and adds a CI step to Github Actions to flag on any future issues.
2026-01-17 11:00:34 -05:00
Michael Lynch 1726119c3e Add a Github Action to build code in CI
This adds a CI workflow in Github Actions to verify that the flutter builds compile for all supported platforms.

I tried adding Windows, but it currently fails, so I excluded it from this initial set.
2026-01-17 10:48:46 -05:00
zjs81 988806dccd Merge pull request #41 from mtlynch/show-error
Show repeater login error in login dialog
2026-01-16 19:10:15 -07:00
zjs81 14ff8250c0 Add support for private and hashtag channels in localization and channel management
- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels.
- Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags.
- Implemented PSK derivation from hashtags using SHA256 in the Channel model.
- Improved the translation script to handle missing keys and translate all locales efficiently.
2026-01-16 19:06:39 -07:00
Michael Lynch 2a04ebb8b6 Show repeater login error in login dialog 2026-01-16 09:35:02 -05:00
zjs81 a14462978d Replace Column with SingleChildScrollView in RepeaterLoginDialog for better layout handling 2026-01-15 21:49:54 -07:00
zjs81 df7fb45683 Merge pull request #38 from wel97459/dev-contactsPubkey
Added public key in contacts list and in the repeater hub
2026-01-15 19:26:53 -07:00
zjs81 f01eff07ff Merge pull request #37 from wel97459/dev-map
Fix map centering
2026-01-15 19:20:20 -07:00
zjs81 7cc7183e0c Refactor map initialization and zoom calculation logic in MapScreen 2026-01-15 19:15:42 -07:00
zjs81 a6b2756d0d Ran flutter format on the file 2026-01-15 19:11:13 -07:00
zjs81 614f3d4601 Add signing configuration support in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 7c33647119 Add key.properties support for signing configuration in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 fde8b686f5 Merge pull request #28 from spfmoby/better-french-translations
Replace Publicité by Annonce in the french translations
2026-01-15 18:30:55 -07:00
zjs81 9bc3a27b53 Merge pull request #30 from dennis1248/main
Update Dutch translations
2026-01-15 18:30:02 -07:00
Winston Lowe a8f387b0da Fix map centering weirdly
When nodes or markers are outside of the main area of interest.
2026-01-14 19:38:01 -08:00
Winston Lowe dd1a73c247 Repeater hub now show public key at the top 2026-01-14 19:34:41 -08:00
Winston Lowe e36f6b7eb9 changed contects list to show public keys of contect 2026-01-14 19:33:07 -08:00
Dennis ten Hoove fcef82be63 Update Dutch translations
This solves many of the most obvious errors and inconsistensies in the initial translation.
2026-01-13 11:53:54 +01:00
spfmoby 6ddb8f1a3d more fr translations / .arb and .dart synced 2026-01-13 08:27:01 +01:00
spfmoby 7a22223756 Replace Publicité by Annonce in the french translations 2026-01-12 10:18:18 +01:00
160 changed files with 45935 additions and 7127 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: flutter pub get
- run: flutter build apk --release --no-pub
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build ios --release --no-codesign --no-pub
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Install Linux build deps
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: flutter pub get
- run: flutter build linux --release --no-pub
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build macos --release --no-pub
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build web --release --no-pub
+31
View File
@@ -0,0 +1,31 @@
name: Flutter and Dart
on:
pull_request:
push:
branches:
- main
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze --fatal-infos --fatal-warnings
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- name: Run tests
run: flutter test -r github
+3
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.flutter-plugins-dependencies
.pub-cache/
.pub/
pubspec.lock
/build/
/coverage/
@@ -65,11 +66,13 @@ secrets.dart
**/ios/Flutter/Flutter.podspec
# Android
.gradle/
**/android/.gradle/
**/android/captures/
**/android/local.properties
**/android/.externalNativeBuild/
*.jks
key.properties
keystore.properties
# Generated files
+1
View File
@@ -0,0 +1 @@
4.0.0
+28 -2
View File
@@ -6,6 +6,10 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities.
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
</a>
## Screenshots
<table>
@@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Features
### Core Functionality
- **Direct Messaging**: Private encrypted conversations with individual contacts
- **Public Channels**: Broadcast messages to channel subscribers on the mesh network
- **Contact Management**: Organize contacts, track last seen times, and manage conversation history
@@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Message Replies**: Thread conversations with inline reply functionality
### Mesh Network
- **Path Visualization**: View routing paths and signal quality for each contact
- **Route Management**: Manual path overriding and automatic route rotation
- **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking
@@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Repeater Support**: Connect to and manage repeater nodes for extended range
### Map & Location
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map
- **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
@@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
### Device Management
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
- **Device Settings**: Configure radio parameters, power settings, and network options
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
### Repeater Hub
- **CLI Access**: Full command-line interface to repeater nodes
- **Settings Management**: Configure repeater behavior, power limits, and network settings
- **Statistics Dashboard**: View repeater traffic, connected clients, and system health
@@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Technical Details
### Architecture
- **Framework**: Flutter 3.38.5 / Dart 3.10.4
- **State Management**: Provider pattern with ChangeNotifier
- **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy
@@ -64,11 +74,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Encryption**: End-to-end encryption for private messages using the MeshCore protocol
### Platform Support
-**Android**: Full support (API 21+)
-**iOS**: Full support (iOS 12+)
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
- 🚧 **Web**: Under construction (Chrome)
### Dependencies
| Package | Purpose |
|---------|---------|
| flutter_blue_plus | Bluetooth Low Energy communication |
@@ -84,6 +97,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Getting Started
### Prerequisites
- Flutter SDK 3.38.5 or later
- Android Studio / Xcode (for mobile development)
- A MeshCore-compatible LoRa device
@@ -91,17 +105,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/zjs81/meshcore-open.git
cd meshcore-open
```
2. **Install dependencies**
```bash
flutter pub get
```
3. **Run the app**
```bash
flutter run
```
@@ -109,11 +126,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Building for Release
**Android APK:**
```bash
flutter build apk --release
```
**iOS:**
```bash
flutter build ios --release
```
@@ -152,25 +171,30 @@ lib/
## BLE Protocol
### Nordic UART Service (NUS)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
### Device Discovery
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
### Message Format
Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions.
## Configuration
### App Settings
- **Theme**: System default, light, or dark mode
- **Notifications**: Configurable for messages, channels, and node advertisements
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
- **Message Retry**: Automatic retry with configurable path clearing
### Device Settings
- **Radio Power**: Transmit power adjustment (10-30 dBm)
- **Frequency**: LoRa frequency configuration
- **Bandwidth**: Channel bandwidth selection
@@ -182,22 +206,24 @@ Messages are transmitted as binary frames using a custom protocol optimized for
This is an open-source project. Contributions are welcome!
### Development Guidelines
- Follow the Flutter style guide
- Use Material 3 design components
- Write clear commit messages
- Test on both Android and iOS before submitting PRs
### Code Style
- Prefer `StatelessWidget` with `Consumer` for reactive UI
- Use `const` constructors where possible
- Keep functions small and focused
- Avoid premature abstractions
- Run dart format on all changes before submitting
## Support
For issues, questions, or feature requests, please open an issue on GitHub:
https://github.com/zjs81/meshcore-open/issues
<https://github.com/zjs81/meshcore-open/issues>
## Donate
+244
View File
@@ -0,0 +1,244 @@
# TestFlight and App Store Deployment Guide
## Prerequisites
- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com)
- [x] Xcode installed
- [x] Apple Transporter app installed
- [x] App icons ready (1024x1024px)
- [x] Bundle ID configured: `com.monitormx.meshcoreopen`
## Step 1: Register Bundle Identifier
1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list)
2. Click the **"+"** button
3. Select **"App IDs"** → Continue
4. Select **"App"** → Continue
5. Fill in:
- **Description**: Meshcore Open
- **Bundle ID**: Explicit - `com.monitormx.meshcoreopen`
- **Capabilities**: Leave defaults (or add as needed)
6. Click **Continue****Register**
## Step 2: Create App in App Store Connect
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
2. Sign in with your Apple ID
3. Click **"My Apps"**
4. Click the **"+"** button → **"New App"**
5. Fill in the form:
- **Platforms**: iOS
- **Name**: Meshcore Open
- **Primary Language**: English (U.S.)
- **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown
- **SKU**: `meshcore-open-001` (or any unique identifier)
- **User Access**: Full Access
6. Click **"Create"**
## Step 3: Build the IPA
Run these commands from the project directory:
```bash
# Add CocoaPods to PATH
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
# Clean previous builds
../flutter/bin/flutter clean
# Build IPA for App Store
../flutter/bin/flutter build ipa
```
The IPA will be created at: `build/ios/ipa/meshcore_open.ipa`
## Step 4: Upload to App Store Connect via Transporter
1. **Open Apple Transporter**
- Launch from Applications folder
- Sign in with your Apple ID
2. **Upload the IPA**
- Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter
- Click **"Deliver"**
- Wait for upload to complete (usually 1-5 minutes)
3. **Processing**
- Apple will process your build (10-30 minutes)
- You'll receive an email when processing is complete
## Step 5: Configure App Store Connect Metadata
### App Information
1. In App Store Connect, go to your app
2. Fill in required information:
- **Subtitle**: Short description (30 chars max)
- **Privacy Policy URL**: Required for Bluetooth apps
- **Category**: Utilities or Productivity
- **Age Rating**: Complete questionnaire
### App Store Listing
1. Go to **App Store** tab
2. Upload **Screenshots** (required):
- iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot
- iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot
- Optional: iPad screenshots
3. Fill in **Description**:
```
Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices.
Features:
- BLE connectivity to MeshCore devices
- Real-time mesh network communication
- Map visualization with OpenStreetMap
- Community management with QR code scanning
- Message tracking and retry system
Connect to your MeshCore LoRa device and start communicating over the mesh network.
```
4. **Keywords**: `lora,mesh,networking,bluetooth,communication`
5. **Support URL**: Your GitHub or website URL
6. **Marketing URL**: (Optional)
### Version Information
1. **What's New in This Version**:
```
Initial release of Meshcore Open
- BLE device connectivity
- Mesh network messaging
- Map integration
- Community features
```
2. **Build**: Select the uploaded build once processing completes
## Step 6: TestFlight Setup
### Internal Testing (No Review Required)
1. Go to **TestFlight** tab in App Store Connect
2. Click **Internal Testing** → **"+"** to create a group
3. Name your group (e.g., "Internal Testers")
4. Add yourself as a tester using your email
5. Select the build you uploaded
6. Testers will receive an email with TestFlight invitation
### External Testing (Requires Beta Review)
1. Click **External Testing** → **"+"** to create a group
2. Add build and testers
3. Fill in **Test Information**:
- **What to Test**: Brief description of features
- **Feedback Email**: Your email address
4. Click **Submit for Review**
5. Beta review typically takes 24-48 hours
## Step 7: App Store Submission
Once you're ready for public release:
1. Go to **App Store** tab
2. Complete all required metadata (if not done)
3. Select your build
4. Fill in **App Review Information**:
- **Contact Information**: Your name, phone, email
- **Demo Account**: If app requires login
- **Notes**: Any special instructions for reviewers
5. Answer **Export Compliance** questions:
- Does your app use encryption? **Yes** (uses TLS/HTTPS)
- Is encryption registration required? **No** (standard encryption)
6. Click **Add for Review**
7. Review summary and click **Submit to App Review**
## Step 8: After Submission
- **App Review**: Typically 24-48 hours
- **Common Rejection Reasons**:
- Missing privacy policy
- Incomplete app information
- Crashes or bugs
- Misleading app description
- **If Approved**: You can release immediately or schedule a release date
- **If Rejected**: Address issues and resubmit
## Updating the App
When you need to release an update:
1. **Update version** in `pubspec.yaml`:
```yaml
version: 0.5.0+6 # Increment version (0.5.0) and build number (+6)
```
2. **Build new IPA**:
```bash
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
../flutter/bin/flutter clean
../flutter/bin/flutter build ipa
```
3. **Upload via Transporter** (same process as above)
4. **Create new version** in App Store Connect:
- Click **"+"** next to versions
- Select version number
- Update "What's New" text
- Select new build
- Submit for review
## macOS Build (Bonus)
To build for macOS:
```bash
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
../flutter/bin/flutter build macos --release
cd build/macos/Build/Products/Release
zip -r meshcore_open-macos.zip meshcore_open.app
```
Distribution:
- Share the zip file directly
- Users unzip and drag to Applications
- First run: Right-click → Open (to bypass Gatekeeper)
## Troubleshooting
### Build Errors
- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin`
- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities)
- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj`
### Upload Errors
- **No profiles found**: Create app in App Store Connect first
- **Bundle ID not registered**: Register in Apple Developer portal
- **Authentication failed**: Use Transporter app instead of CLI
### TestFlight Issues
- **Build not appearing**: Wait 10-30 minutes for processing
- **Can't add testers**: Check you have available slots (100 internal, 10,000 external)
- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators
## Important Files
- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa`
- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app`
- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj`
- **Version Info**: `pubspec.yaml`
## Useful Links
- [App Store Connect](https://appstoreconnect.apple.com)
- [Apple Developer Portal](https://developer.apple.com/account)
- [TestFlight Documentation](https://developer.apple.com/testflight/)
- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios)
## Support
For issues with:
- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/)
- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues)
- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues)
+29 -7
View File
@@ -1,3 +1,5 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,19 +7,25 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
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 {
@@ -40,11 +48,25 @@ android {
// }
}
signingConfigs {
create("release") {
val storeFilePath = keystoreProperties["storeFile"] as String?
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = keystoreProperties["storePassword"] as String?
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
@@ -61,5 +83,5 @@ flutter {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
+12
View File
@@ -16,6 +16,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="meshcore_open"
@@ -64,5 +67,14 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- URL launcher intents for opening links -->
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries>
</manifest>
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+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: 579 KiB

Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770562336,
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+86
View File
@@ -0,0 +1,86 @@
{
description = "MeshCore Flutter Application";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Flutter and Dart
flutter
dart
# Java (required for Android development)
jdk17
# Android development tools
android-tools
gradle
# For the shell hook to set up the environment for Flutter development
gtk3
glib
sysprof
libclang
cmake
ninja
pkg-config
libdatrie
# Additional tools for installing Android SDK if not present
curl
unzip
];
shellHook = ''
echo "MeshCore Flutter Development Environment"
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
# Setup Android SDK in home directory (standard location)
export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
echo "Android SDK: $ANDROID_HOME"
echo ""
# Check if Android SDK exists and offer to download if not
if [ ! -d "$ANDROID_HOME" ]; then
echo "WARNING: Android SDK not found at $ANDROID_HOME"
echo ""
echo "To download and set up the Android SDK, run this command:"
echo ""
cat << 'EOF'
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
unzip -q cmdline-tools.zip && \
mkdir -p cmdline-tools/latest && \
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
rm cmdline-tools.zip && \
cd cmdline-tools/latest/bin && \
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
echo "Android SDK setup complete!"
EOF
echo ""
echo "Then run 'flutter doctor' again to verify."
echo ""
else
echo "Android SDK found at $ANDROID_HOME"
fi
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
'';
};
}
);
}
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+1
View File
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+1 -1
View File
@@ -1,4 +1,4 @@
platform :ios, '12.0'
platform :ios, '15.5'
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+145
View File
@@ -0,0 +1,145 @@
PODS:
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_foreground_task (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- MLImage (1.0.0-beta6)
- MLKitBarcodeScanning (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitCommon (12.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta6)
- MLKitCommon (~> 12.0)
- mobile_scanner (6.0.2):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- PromisesObjC (2.4.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
COCOAPODS: 1.16.2
+74 -6
View File
@@ -14,6 +14,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4268181FCF3E12817B700E9C /* libPods-Runner.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -42,9 +43,13 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
4268181FCF3E12817B700E9C /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -62,6 +67,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -94,6 +100,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
DEE6F094D3B70E76087722E1 /* Pods */,
DAE613E34DF694C2E33B64C7 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -121,6 +129,25 @@
path = Runner;
sourceTree = "<group>";
};
DAE613E34DF694C2E33B64C7 /* Frameworks */ = {
isa = PBXGroup;
children = (
4268181FCF3E12817B700E9C /* libPods-Runner.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
DEE6F094D3B70E76087722E1 /* Pods */ = {
isa = PBXGroup;
children = (
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */,
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */,
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -145,12 +172,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -253,6 +282,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -368,7 +436,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -384,7 +452,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -401,7 +469,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -416,7 +484,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -547,7 +615,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -569,7 +637,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
+3
View File
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+7
View File
@@ -53,5 +53,12 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
</dict>
</plist>
+1
View File
@@ -3,3 +3,4 @@ template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
untranslated-messages-file: untranslated.json
File diff suppressed because it is too large Load Diff
+205 -35
View File
@@ -13,14 +13,29 @@ class BufferReader {
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
);
}
final data = _buffer.sublist(_pointer, _pointer + count);
_pointer += count;
return data;
}
void skipBytes(int count) {
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
);
}
_pointer += count;
}
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) {
final value = <int>[];
@@ -38,13 +53,19 @@ class BufferReader {
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readUInt16LE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() =>
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt32LE() =>
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt24BE() {
var value = (readByte() << 16) | (readByte() << 8) | readByte();
@@ -63,21 +84,25 @@ class BufferWriter {
void writeBytes(Uint8List bytes) => _builder.add(bytes);
void writeUInt16LE(int num) {
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little);
final bytes = Uint8List(2)
..buffer.asByteData().setUint16(0, num, Endian.little);
writeBytes(bytes);
}
void writeUInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setUint32(0, num, Endian.little);
writeBytes(bytes);
}
void writeInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setInt32(0, num, Endian.little);
writeBytes(bytes);
}
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeString(String string) =>
writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeCString(String string, int maxLength) {
final bytes = Uint8List(maxLength);
@@ -87,6 +112,25 @@ class BufferWriter {
}
writeBytes(bytes);
}
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));
}
}
// Command codes (to device)
@@ -116,8 +160,12 @@ const int cmdSendStatusReq = 27;
const int cmdGetContactByKey = 30;
const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
// Text message types
@@ -129,7 +177,7 @@ const int reqTypeGetStatus = 0x01;
const int reqTypeKeepAlive = 0x02;
const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbours = 0x06;
const int reqTypeGetNeighbors = 0x06;
// Repeater response codes
const int respServerLoginOk = 0;
@@ -146,12 +194,14 @@ const int respCodeContactMsgRecv = 7;
const int respCodeChannelMsgRecv = 8;
const int respCodeCurrTime = 9;
const int respCodeNoMoreMessages = 10;
const int respCodeExportContact = 11;
const int respCodeBattAndStorage = 12;
const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -162,17 +212,41 @@ const int pushCodeLoginSuccess = 0x85;
const int pushCodeLoginFail = 0x86;
const int pushCodeStatusResponse = 0x87;
const int pushCodeLogRxData = 0x88;
const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
// Contact/advertisement types
const int advTypeChat = 1;
const int advTypeRepeater = 2;
const int advTypeRoom = 3;
const int advTypeSensor = 4;
// Payload Types
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeRESPONSE =
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeTXTMSG =
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
const int payloadTypeACK = 0x03; // a simple ack
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
const int payloadTypeGRPTXT =
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
const int payloadTypeGRPDATA =
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
const int payloadTypeANONREQ =
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
const int payloadTypePATH =
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
//...
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
// Sizes
const int pubKeySize = 32;
const int maxPathSize = 64;
@@ -182,8 +256,10 @@ const int maxFrameSize = 172;
const int appProtocolVersion = 3;
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
const int maxTextPayloadBytes = 160;
const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1;
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1;
const int _sendTextMsgOverheadBytes =
1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
const int _sendChannelTextMsgOverheadBytes =
1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
int maxContactMessageBytes() {
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
@@ -214,6 +290,7 @@ int _minPositive(int a, int b) {
const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
const int contactPathLenOffset = 35;
const int contactPathOffset = 36;
const int contactNameOffset = 100;
@@ -233,10 +310,7 @@ class ParsedContactText {
final Uint8List senderPrefix;
final String text;
const ParsedContactText({
required this.senderPrefix,
required this.text,
});
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
@@ -265,10 +339,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
return null;
}
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim();
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text =
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim();
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
@@ -362,7 +443,8 @@ Uint8List buildSendTextMsgFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
@@ -444,7 +526,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
// Format: [cmd][name...]
Uint8List buildSetAdvertNameFrame(String name) {
final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
final nameLen = nameBytes.length < maxNameSize
? nameBytes.length
: maxNameSize - 1;
final writer = BufferWriter();
writer.writeByte(cmdSetAdvertName);
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
@@ -461,6 +545,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
return writer.toBytes();
}
Uint8List buildSetCustomVarFrame(String value) {
final writer = BufferWriter();
writer.writeByte(cmdSetCustomVar);
writer.writeString(value);
writer.writeByte(0);
return writer.toBytes();
}
// Build CMD_REBOOT frame
// Format: [cmd]["reboot"]
Uint8List buildRebootFrame() {
@@ -494,18 +586,29 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
}
// Build CMD_SET_RADIO_PARAMS frame
// Format: [cmd][freq x4][bw x4][sf][cr]
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
// freq: frequency in Hz (300000-2500000)
// bw: bandwidth in Hz (7000-500000)
// sf: spreading factor (5-12)
// cr: coding rate (5-8)
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
Uint8List buildSetRadioParamsFrame(
int freqHz,
int bwHz,
int sf,
int cr, {
bool? clientRepeat,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetRadioParams);
writer.writeUInt32LE(freqHz);
writer.writeUInt32LE(bwHz);
writer.writeByte(sf);
writer.writeByte(cr);
if (clientRepeat != null) {
writer.writeByte(clientRepeat ? 1 : 0);
}
return writer.toBytes();
}
@@ -544,7 +647,9 @@ Uint8List buildUpdateContactPathFrame(
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
@@ -575,6 +680,11 @@ Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -598,9 +708,11 @@ int calculateLoRaAirtime({
final crc = 1; // CRC enabled
final de = lowDataRateOptimize ? 1 : 0;
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final numerator =
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final denominator = 4 * (spreadingFactor - 2 * de);
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
var payloadSymbols =
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
if (payloadSymbols < 0) {
payloadSymbols = 8;
@@ -647,7 +759,8 @@ Uint8List buildSendCliCommandFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
@@ -661,10 +774,7 @@ Uint8List buildSendCliCommandFrame(
// Build a telemetry request frame
// Format: [cmd][pub_key x32][payload]
Uint8List buildSendBinaryReq(
Uint8List repeaterPubKey, {
Uint8List? payload,
}) {
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
final writer = BufferWriter();
writer.writeByte(cmdSendBinaryReq);
writer.writeBytes(repeaterPubKey);
@@ -672,4 +782,64 @@ Uint8List buildSendBinaryReq(
writer.writeBytes(payload);
}
return writer.toBytes();
}
}
//Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
final writer = BufferWriter();
writer.writeByte(cmdSendTracePath);
writer.writeUInt32LE(tag);
writer.writeUInt32LE(auth);
writer.writeByte(flag);
if (payload != null && payload.isNotEmpty) {
writer.writeBytes(payload);
}
return writer.toBytes();
}
// Build a export contact frame
// [cmd][pub_key x32 / if empty exports your contact info]
Uint8List buildExportContactFrame(Uint8List pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdExportContact);
writer.writeBytes(pubKey);
return writer.toBytes();
}
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
return writer.toBytes();
}
// Build a export contact frame
// [cmd][pub_key x32]
Uint8List buildZeroHopContact(Uint8List pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdShareContact);
writer.writeBytes(pubKey);
return writer.toBytes();
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
writer.writeByte(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
+179 -163
View File
@@ -1,4 +1,6 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class CayenneLpp {
@@ -26,9 +28,11 @@ class CayenneLpp {
static const int lppUnixTime = 133; // 4 bytes, unsigned
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
static const int lppColour = 135; // 1 byte per RGB Color
static const int lppGps = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
static const int lppGps =
136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
static const int lppSwitch = 142; // 1 byte, 0/1
static const int lppPolyline = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
static const int lppPolyline =
240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
final BufferWriter _writer = BufferWriter();
@@ -82,180 +86,192 @@ class CayenneLpp {
static List<Map<String, dynamic>> parse(Uint8List bytes) {
final buffer = BufferReader(bytes);
final telemetry = <Map<String, dynamic>>[];
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
if (channel == 0 && type == 0) {
break;
}
if (channel == 0 && type == 0) {
break;
}
switch (type) {
case lppGenericSensor:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppLuminosity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPresence:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppTemperature:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 10,
});
break;
case lppRelativeHumidity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8() / 2,
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE() / 10,
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppCurrent:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 1000,
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppConcentration:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPower:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppGps:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'latitude': buffer.readInt24BE() / 10000,
'longitude': buffer.readInt24BE() / 10000,
'altitude': buffer.readInt24BE() / 100,
},
});
break;
default:
return telemetry;
switch (type) {
case lppGenericSensor:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppLuminosity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPresence:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppTemperature:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 10,
});
break;
case lppRelativeHumidity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8() / 2,
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE() / 10,
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppCurrent:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 1000,
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppConcentration:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPower:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppGps:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'latitude': buffer.readInt24BE() / 10000,
'longitude': buffer.readInt24BE() / 10000,
'altitude': buffer.readInt24BE() / 100,
},
});
break;
default:
return telemetry;
}
}
return telemetry;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
// Return any telemetry parsed so far to preserve partial data
return telemetry;
}
return telemetry;
}
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
final buffer = BufferReader(bytes);
final Map<int, Map<String, dynamic>> channels = {};
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
// Optional: stop on padding (00 00)
if (channel == 0 && type == 0) {
break;
}
// Optional: stop on padding (00 00)
if (channel == 0 && type == 0) {
break;
final channelData = channels.putIfAbsent(
channel,
() => {'channel': channel, 'values': <String, dynamic>{}},
);
switch (type) {
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
case lppLuminosity:
channelData['values']['luminosity'] = buffer.readUInt16BE();
break;
case lppPresence:
channelData['values']['presence'] = buffer.readUInt8() != 0;
break;
case lppTemperature:
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
break;
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
case lppConcentration:
channelData['values']['concentration'] = buffer.readUInt16BE();
break;
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
'longitude': buffer.readInt24BE() / 10000.0,
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
default:
//Stopped parsing to avoid misalignment
return channels.values.toList();
}
}
final channelData = channels.putIfAbsent(channel, () => {
'channel': channel,
'values': <String, dynamic>{},
});
switch (type) {
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
case lppLuminosity:
channelData['values']['luminosity'] = buffer.readUInt16BE();
break;
case lppPresence:
channelData['values']['presence'] = buffer.readUInt8() != 0;
break;
case lppTemperature:
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
break;
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
case lppConcentration:
channelData['values']['concentration'] = buffer.readUInt16BE();
break;
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
'longitude': buffer.readInt24BE() / 10000.0,
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
default:
// Unknown type: skip or handle error?
continue;
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
return <
Map<String, dynamic>
>[]; // Return an empty list on error to avoid crashing the app
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
}
}
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class ChatScrollController extends ScrollController {
final ValueNotifier<bool> showJumpToBottom = ValueNotifier(false);
VoidCallback? onScrollNearTop;
static const _bottomThreshold = 100.0;
static const _topThreshold = 50.0;
ChatScrollController() {
addListener(_handleScroll);
}
void _handleScroll() {
if (!hasClients) return;
final pos = position;
// With reverse: true, position 0 is bottom, maxScrollExtent is top
// Show jump button when scrolled away from bottom (position > threshold)
final isAtBottom = pos.pixels <= _bottomThreshold;
if (showJumpToBottom.value == isAtBottom) {
showJumpToBottom.value = !isAtBottom;
}
// Pagination trigger when scrolled near top (maxScrollExtent)
if (pos.pixels >= pos.maxScrollExtent - _topThreshold) {
onScrollNearTop?.call();
}
}
void jumpToBottom() {
if (hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void handleKeyboardOpen() {
// Simple: just scroll to bottom when keyboard opens
if (hasClients) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
void scrollToBottomIfAtBottom() {
// Only scroll if jump button is NOT showing (i.e., already at bottom)
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
showJumpToBottom.dispose();
super.dispose();
}
}
+73
View File
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart';
class LinkHandler {
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_openLink),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.chat_openLinkConfirmation,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
url,
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.chat_open),
),
],
),
);
if (shouldOpen != true) return;
// Launch URL
try {
final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red,
),
);
}
}
}
}
+57 -42
View File
@@ -1,53 +1,68 @@
class ReactionInfo {
final String targetMessageId;
final String emoji;
final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix
import '../widgets/emoji_picker.dart';
ReactionInfo({
required this.targetMessageId,
required this.emoji,
this.reactionKey,
});
class ReactionInfo {
final String targetHash;
final String emoji;
ReactionInfo({required this.targetHash, required this.emoji});
}
class ReactionHelper {
/// Parse reaction format: r:[messageId]:[emoji]
/// Supports both old format (full messageId) and new format (timestamp_senderPrefix)
static List<String>? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order.
/// Order must stay stable for index compatibility.
static List<String> get reactionEmojis {
return _cachedEmojis ??= [
...EmojiPicker.quickEmojis,
...EmojiPicker.smileys,
...EmojiPicker.gestures,
...EmojiPicker.hearts,
...EmojiPicker.objects,
];
}
/// Convert emoji to 2-char hex index. Returns null if emoji not in list.
static String? emojiToIndex(String emoji) {
final idx = reactionEmojis.indexOf(emoji);
if (idx < 0) return null;
return idx.toRadixString(16).padLeft(2, '0');
}
/// Convert 2-char hex index to emoji. Returns null if invalid index.
static String? indexToEmoji(String hexIndex) {
final idx = int.tryParse(hexIndex, radix: 16);
if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null;
return reactionEmojis[idx];
}
/// Compute a 4-char hex hash for a message reaction.
/// Hash input: timestampSeconds + [senderName] + first 5 chars of text
/// For 1:1 chats, senderName can be null (sender is implicit).
static String computeReactionHash(
int timestampSeconds,
String? senderName,
String text,
) {
final first5 = text.length >= 5 ? text.substring(0, 5) : text;
final input = senderName != null
? '$timestampSeconds$senderName$first5'
: '$timestampSeconds$first5';
// Use hashCode and take lower 16 bits, format as 4 hex chars
final hash = input.hashCode & 0xFFFF;
return hash.toRadixString(16).padLeft(4, '0');
}
/// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index)
/// Returns null if text is not a valid reaction format
static ReactionInfo? parseReaction(String text) {
final regex = RegExp(r'^r:([^:]+):(.+)$');
final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$');
final match = regex.firstMatch(text);
if (match == null) return null;
final targetId = match.group(1)!;
final emoji = match.group(2)!;
final emoji = indexToEmoji(match.group(2)!);
if (emoji == null) return null;
// Extract reaction key for deduplication
// If targetId is in new format (timestamp_senderPrefix), use it directly
// Otherwise, extract timestamp from old format (timestamp_nameHash_textHash)
String? reactionKey;
if (targetId.contains('_')) {
final parts = targetId.split('_');
if (parts.length >= 2) {
// New format: timestamp_senderPrefix, or old format with at least timestamp
reactionKey = '${parts[0]}_${parts[1]}';
}
}
return ReactionInfo(
targetMessageId: targetId,
emoji: emoji,
reactionKey: reactionKey,
);
}
/// Generate a lightweight reaction key for a message
/// Format: r:[timestamp]_[senderPrefix]:[emoji]
static String buildReactionText(String timestamp, String senderPrefix, String emoji) {
return 'r:${timestamp}_$senderPrefix:$emoji';
}
/// Extract sender prefix from public key hex (first 8 chars)
static String getSenderPrefix(String senderKeyHex) {
return senderKeyHex.substring(0, 8);
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
}
}
+15 -6
View File
@@ -262,8 +262,9 @@ class Smaz {
".com",
];
static final List<Uint8List> _rcbBytes =
_rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false);
static final List<Uint8List> _rcbBytes = _rcb
.map((s) => Uint8List.fromList(ascii.encode(s)))
.toList(growable: false);
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
return entry.length > maxLen ? entry.length : maxLen;
});
@@ -358,24 +359,32 @@ class Smaz {
final code = input[index];
if (code == _verbatimSingle) {
if (index + 1 >= input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.');
throw const FormatException(
'Invalid SMAZ stream: truncated verbatim byte.',
);
}
out.addByte(input[index + 1]);
index += 2;
} else if (code == _verbatimRun) {
if (index + 1 >= input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim length.');
throw const FormatException(
'Invalid SMAZ stream: truncated verbatim length.',
);
}
final len = input[index + 1] + 1;
final end = index + 2 + len;
if (end > input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim run.');
throw const FormatException(
'Invalid SMAZ stream: truncated verbatim run.',
);
}
out.add(input.sublist(index + 2, end));
index = end;
} else {
if (code >= _rcbBytes.length) {
throw const FormatException('Invalid SMAZ stream: code out of range.');
throw const FormatException(
'Invalid SMAZ stream: code out of range.',
);
}
out.add(_rcbBytes[code]);
index += 1;
+4 -1
View File
@@ -8,7 +8,10 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (maxBytes <= 0) return oldValue;
final bytes = utf8.encode(newValue.text);
if (bytes.length <= maxBytes) return newValue;
+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);
}
}
+470 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Отваряне на връзката?",
"chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?",
"chat_open": "Отвори",
"chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Невалиден формат на връзката",
"map_title": "Карта на възлите",
"map_noNodesWithLocation": "Няма възли с данни за местоположение.",
"map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.",
"common_reload": "Презареди",
"common_clear": "Изчисти",
"path_currentPath": "Текущ път: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Повторители",
"listFilter_roomServers": "Сървъри на стая",
"listFilter_unreadOnly": "Само непрочетените",
"listFilter_newGroup": "Нова група"
"listFilter_newGroup": "Нова група",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbors": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbors": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
"channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.",
"channels_joinPublicChannel": "Присъединете се към Публичния канал",
"channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.",
"channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал",
"channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.",
"channels_scanQrCode": "Сканирайте QR код",
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
"channels_enterHashtag": "Въведете хаштаг",
"channels_hashtagHint": "напр. #отбор",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Слушано преди {time}.",
"neighbors_unknownContact": "Неизвестна {pubkey}",
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Общност",
"common_ok": "Добре",
"community_createDesc": "Създайте нова общност и я споделете чрез QR код.",
"community_create": "Създай общност",
"community_joinTitle": "Присъедини се към общността",
"community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?",
"community_scanQr": "Сканирайте QR кода на общността",
"community_scanInstructions": "Насочете камерата към QR код на общността",
"community_showQr": "Покажи QR код",
"community_publicChannel": "Обществено общност",
"community_hashtagChannel": "Хаштаг на общността",
"community_name": "Име на общността",
"community_enterName": "Въведете име на общността",
"community_created": "Общността \"{name}\" е създадена",
"community_joined": "Присъединено общност \"{name}\"",
"community_qrTitle": "Споделяне в общността",
"community_join": "Присъедини се",
"community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.",
"community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността",
"community_invalidQrCode": "Невалиден QR код на общността",
"community_alreadyMember": "Вече съм член",
"community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".",
"community_addPublicChannel": "Добави публичен общностен канал",
"community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.",
"community_noCommunities": "Няма присъединени общности още.",
"community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.",
"community_manageCommunities": "Управление на общности",
"community_delete": "Напусни общността",
"community_deleteConfirm": "Напускате \"{name}\"?",
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Остави общността \"{name}\"",
"community_addHashtagChannel": "Добави общностен хаштаг",
"community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност",
"community_selectCommunity": "Изберете общност",
"community_regularHashtag": "Обикновен хаштаг",
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"community_forCommunity": "За {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.",
"community_secretRegenerated": "Секретно презареждане за \"{name}\"",
"community_regenerateSecret": "Регенерейрай секрет",
"community_regenerate": "Регенерация",
"community_updateSecret": "Актуализирай тайна",
"community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
"community_secretUpdated": "Секретно обновено за \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Вие",
"pathTrace_notAvailable": "Пътека за проследяване не е достъпна.",
"contacts_pathTrace": "Пътен проследяване",
"pathTrace_refreshTooltip": "Обнови Path Trace.",
"pathTrace_failed": "Пътят за проследяване не успя.",
"contacts_repeaterPing": "Пингване на повторителя",
"contacts_repeaterPathTrace": "Трасировка до повторител",
"contacts_ping": "Пинг",
"contacts_chatTraceRoute": "Трасиране на път",
"contacts_roomPathTrace": "Трасиране на път до съ",
"contacts_roomPing": "Ping на сървъра на стаята",
"contacts_pathTraceTo": "Проследи маршрут към {name}",
"appSettings_languageUk": "Украински",
"contacts_clipboardEmpty": "Клипборда е празна.",
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
"appSettings_languageRu": "Руски",
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
"contacts_contactImported": "Контактът е импортиран.",
"contacts_zeroHopAdvert": "Реклама без скок",
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
"contacts_floodAdvert": "Потопна реклама",
"contacts_addContactFromClipboard": "Добави контакт от клипборда",
"contacts_copyAdvertToClipboard": "Копирай обявата в клипборда",
"contacts_ShareContact": "Копирай контакт в клипборда",
"contacts_ShareContactZeroHop": "Сподели контакт чрез обява",
"contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.",
"contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.",
"contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.",
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
"notification_activityTitle": "Активност на MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}",
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
"notification_newTypeDiscovered": "Открит нов {contactType}",
"notification_receivedNewMessage": "Получено ново съобщение",
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
"settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.",
"settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX",
"settings_gpxExportContacts": "Експортирай спътници към GPX",
"settings_gpxExportSuccess": "Успешно изlexport на файл GPX.",
"settings_gpxExportNoContacts": "Няма контакти за изlexport.",
"settings_gpxExportChat": "Местоположения на спътници",
"settings_gpxExportError": "Възникна грешка при изнасяне.",
"settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята",
"settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС",
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
"map_pathTraceCancelled": "Отменен е следването на пътя.",
"pathTrace_clearTooltip": "Изчисти пътя",
"map_removeLast": "Премахни Последно",
"map_runTrace": "Изпълни Път на Следване",
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
"scanner_bluetoothOff": "Bluetooth е изключен.",
"scanner_enableBluetooth": "Активирайте Bluetooth",
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
"snrIndicator_lastSeen": "Последно видян",
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
"chat_ShowAllPaths": "Покажи всички пътища",
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
"settings_clientRepeat": "Без електричество – повторение",
"settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "единици",
"appSettings_unitsMetric": "Метрика (m / km)",
"appSettings_unitsImperial": "Имперска (ft / mi)",
"map_lineOfSight": "Линия на видимост",
"map_losScreenTitle": "Линия на видимост",
"losSelectStartEnd": "Изберете начални и крайни възли за LOS.",
"losRunFailed": "Проверката на пряката видимост е неуспешна: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Изчистете всички точки",
"losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина",
"losMenuTitle": "LOS меню",
"losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки",
"losShowDisplayNodes": "Показване на възли на дисплея",
"losCustomPoints": "Персонализирани точки",
"losCustomPointLabel": "Персонализирано {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Точка А",
"losPointB": "Точка Б",
"losAntennaA": "Антена A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Антена B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Стартирайте LOS",
"losNoElevationData": "Няма данни за надморска височина",
"losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: проверка...",
"losStatusNoData": "LOS: няма данни",
"losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.",
"losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.",
"losRenameCustomPoint": "Преименувайте персонализирана точка",
"losPointName": "Име на точката",
"losShowPanelTooltip": "Показване на LOS панел",
"losHidePanelTooltip": "Скриване на LOS панела",
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Радиохоризонт",
"losLegendLosBeam": "Линия на видимост",
"losLegendTerrain": "Терен",
"losFrequencyLabel": "Честота",
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Премахване от списъка с любими",
"listFilter_addToFavorites": "Добави към любими",
"listFilter_favorites": "Любими",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchFavorites": "Търсене на {number}{str} любими...",
"contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...",
"contacts_unread": "Непрочетено",
"contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...",
"contacts_searchContactsNoNumber": "Търси контакти...",
"contacts_searchUsers": "Търсене на {number}{str} потребители..."
}
+646 -154
View File
File diff suppressed because it is too large Load Diff
+837 -140
View File
File diff suppressed because it is too large Load Diff
+498 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "¿Abrir enlace?",
"chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?",
"chat_open": "Abrir",
"chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato de enlace no válido",
"map_title": "Mapa de Nodos",
"map_noNodesWithLocation": "No hay nodos con datos de ubicación",
"map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.",
"common_reload": "Recargar",
"common_clear": "Borrar",
"path_currentPath": "Ruta actual: {path}",
@@ -1335,5 +1353,479 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de la sala",
"listFilter_unreadOnly": "Solo sin leer",
"listFilter_newGroup": "Nuevo grupo"
"listFilter_newGroup": "Nuevo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Vecinos",
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
"channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.",
"channels_joinPublicChannel": "Únete al Canal Público",
"channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.",
"channels_joinHashtagChannel": "Únete a un Canal con Hashtag",
"channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.",
"channels_scanQrCode": "Escanear un Código QR",
"channels_scanQrCodeComingSoon": "Próximamente",
"channels_enterHashtag": "Introducir hashtag",
"channels_hashtagHint": "ej. #equipo",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clave pública desconocida {pubkey}",
"neighbors_heardAgo": "Escuchado: {time} hace atrás",
"settings_locationGPSEnable": "Habilitar GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Crear Comunidad",
"community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.",
"community_title": "Comunidad",
"community_join": "Únete",
"community_joinTitle": "Únete a la comunidad",
"community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?",
"community_scanQr": "Escanear Código QR de la Comunidad",
"community_scanInstructions": "Apunte la cámara a un código QR de la comunidad",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidad Pública",
"community_hashtagChannel": "Hashtag de la Comunidad",
"community_name": "Nombre de la comunidad",
"common_ok": "De acuerdo",
"community_enterName": "Introducir nombre de comunidad",
"community_created": "Comunidad \"{name}\" creada",
"community_joined": "Se unió a la comunidad \"{name}\"",
"community_qrTitle": "Compartir Comunidad",
"community_qrInstructions": "Escanear este código QR para unirte a {name}",
"community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad",
"community_invalidQrCode": "Código QR de comunidad no válido",
"community_alreadyMember": "Ya eres Miembro",
"community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".",
"community_addPublicChannel": "Añadir Canal Público de la Comunidad",
"community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.",
"community_noCommunities": "Aún no se han unido comunidades.",
"community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar",
"community_manageCommunities": "Gestionar Comunidades",
"community_delete": "Salir de la Comunidad",
"community_deleteConfirm": "¿Salir de \"{name}\"?",
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
"community_addHashtagChannel": "Añadir Hashtag de la Comunidad",
"community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad",
"community_selectCommunity": "Seleccionar Comunidad",
"community_regularHashtag": "Etiqueta de Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
"community_communityHashtag": "Hashtag de la Comunidad",
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
"community_forCommunity": "Para {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecret": "Regenerar Contraseña Secreta",
"community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.",
"community_secretRegenerated": "Código secreto regenerado para \"{name}\"",
"community_regenerate": "Regenerar",
"community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
"community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
"community_updateSecret": "Actualizar Contraseña",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Tú",
"pathTrace_failed": "El trazado de ruta falló.",
"pathTrace_refreshTooltip": "Actualizar Path Trace",
"contacts_pathTrace": "Rastreo de caminos",
"contacts_repeaterPathTrace": "Rastrear ruta al repetidor",
"contacts_repeaterPing": "Pingar repetidor",
"contacts_ping": "Ping",
"pathTrace_notAvailable": "El trazado de ruta no está disponible.",
"contacts_roomPing": "Pingar servidor de sala",
"contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación",
"contacts_pathTraceTo": "Rastrear ruta a {name}",
"contacts_chatTraceRoute": "Ruta de trazado",
"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.",
"contacts_contactImportFailed": "Contacto no se importó correctamente.",
"contacts_zeroHopAdvert": "Anuncio de Zero Hop",
"contacts_ShareContactZeroHop": "Compartir contacto por anuncio",
"contacts_ShareContact": "Copiar contacto al Portapapeles",
"contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles",
"contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles",
"contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.",
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
"notification_activityTitle": "Actividad de MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
"@notification_messagesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {
"type": "String"
}
}
},
"notification_receivedNewMessage": "Nuevo mensaje recibido",
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
"settings_gpxExportNoContacts": "No hay contactos para exportar.",
"settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO",
"settings_gpxExportError": "Hubo un error al exportar.",
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.",
"settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.",
"settings_gpxExportAll": "Exportar todos los contactos a GPX",
"settings_gpxExportContacts": "Exportar compañeros a GPX",
"settings_gpxExportChat": "Ubicaciones de compañero",
"settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala",
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
"pathTrace_clearTooltip": "Borrar ruta",
"map_runTrace": "Ejecutar Rastreo de Ruta",
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
"map_removeLast": "Eliminar último",
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
"scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.",
"scanner_bluetoothOff": "Bluetooth está desactivado.",
"scanner_enableBluetooth": "Habilitar Bluetooth",
"snrIndicator_nearByRepeaters": "Repetidores cercanos",
"snrIndicator_lastSeen": "Visto por última vez",
"chat_ShowAllPaths": "Mostrar todos los caminos",
"settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.",
"settings_clientRepeat": "Repetir sin conexión",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.",
"settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (pies/millas)",
"map_lineOfSight": "Línea de visión",
"map_losScreenTitle": "Línea de visión",
"losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.",
"losRunFailed": "Error en la comprobación de la línea de visión: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Borrar todos los puntos",
"losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación",
"losMenuTitle": "Menú LOS",
"losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados",
"losShowDisplayNodes": "Mostrar nodos de visualización",
"losCustomPoints": "Puntos personalizados",
"losCustomPointLabel": "Personalizado {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Ejecutar LOS",
"losNoElevationData": "Sin datos de elevación",
"losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: comprobando...",
"losStatusNoData": "LOS: sin datos",
"losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.",
"losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.",
"losRenameCustomPoint": "Cambiar el nombre del punto personalizado",
"losPointName": "Nombre del punto",
"losShowPanelTooltip": "Mostrar panel LOS",
"losHidePanelTooltip": "Ocultar panel LOS",
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte radioeléctrico",
"losLegendLosBeam": "Línea de visión",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frecuencia",
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Favoritos",
"listFilter_removeFromFavorites": "Eliminar de las favoritas",
"listFilter_addToFavorites": "Añadir a favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchContactsNoNumber": "Buscar contactos...",
"contacts_unread": "No leído",
"contacts_searchFavorites": "Buscar {number}{str} Favoritos...",
"contacts_searchUsers": "Buscar {number}{str} Usuarios...",
"contacts_searchRepeaters": "Buscar {number}{str} Repetidores...",
"contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..."
}
+548 -84
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",
@@ -91,12 +99,12 @@
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_privacyMode": "Mode de confidentialité",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les publicités.",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
"settings_privacyModeEnabled": "Mode de confidentialité activé",
"settings_privacyModeDisabled": "Mode de confidentialité désactivé",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Envoyer la publicité",
"settings_sendAdvertisement": "S'annoncer",
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
"settings_advertisementSent": "Annonce envoyée",
"settings_syncTime": "Temps de synchronisation",
@@ -104,7 +112,7 @@
"settings_timeSynchronized": "Synchronisation temporelle",
"settings_refreshContacts": "Rafraîchir les Contacts",
"settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil",
"settings_rebootDevice": "Réinitialiser l'appareil",
"settings_rebootDevice": "Redémarrer l'appareil",
"settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore",
"settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.",
"settings_debug": "Déboguer",
@@ -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": {
@@ -176,7 +179,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Notifications",
"appSettings_enableNotifications": "Activer les Notifications",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les publicités",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces",
"appSettings_notificationPermissionDenied": "Permission de notification refusée",
"appSettings_notificationsEnabled": "Notifications activées",
"appSettings_notificationsDisabled": "Notifications désactivées",
@@ -184,7 +187,7 @@
"appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages",
"appSettings_channelMessageNotifications": "Notifications des Messages de Canal",
"appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal",
"appSettings_advertisementNotifications": "Notifications publicitaires",
"appSettings_advertisementNotifications": "Notifications d'annonces",
"appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds",
"appSettings_messaging": "Messagerie",
"appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry",
@@ -192,7 +195,7 @@
"appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.",
"appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.",
"appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode inondation",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)",
"appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée",
"appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée",
"appSettings_battery": "Batterie",
@@ -210,8 +213,8 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Affichage de la carte",
"appSettings_showRepeaters": "Afficher les répétiteurs",
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte",
"appSettings_showRepeaters": "Afficher les répéteurs",
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte",
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
@@ -266,8 +269,8 @@
}
}
},
"contacts_manageRepeater": "Gérer le répétiteur",
"contacts_roomLogin": "Connexion Salle",
"contacts_manageRepeater": "Gérer le répéteur",
"contacts_roomLogin": "Connexion Room Server",
"contacts_openChat": "Ouverture du Chat",
"contacts_editGroup": "Modifier le groupe",
"contacts_deleteGroup": "Supprimer le groupe",
@@ -279,7 +282,7 @@
}
}
},
"contacts_newGroup": "Nouvelle Groupe",
"contacts_newGroup": "Nouveau Groupe",
"contacts_groupName": "Nom du groupe",
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
@@ -293,8 +296,8 @@
"contacts_filterContacts": "Filtrer les contacts...",
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
"contacts_noMembers": "Aucun membre",
"contacts_lastSeenNow": "Dernière fois vu maintenant",
"contacts_lastSeenMinsAgo": "Dernière fois vu il y a {minutes} minutes.",
"contacts_lastSeenNow": "Vu maintenant",
"contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -302,8 +305,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Dernière fois vu il y a 1 heure.",
"contacts_lastSeenHoursAgo": "Dernière fois vu il y a {hours} heures.",
"contacts_lastSeenHourAgo": "Vu il y a 1 heure",
"contacts_lastSeenHoursAgo": "Vu il y a {hours} heures",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -311,8 +314,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Dernière fois vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Dernière activité il y a {days} jours",
"contacts_lastSeenDayAgo": "Vu il y a 1 jour",
"contacts_lastSeenDaysAgo": "Vu il y a {days} jours",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -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": {
@@ -394,7 +399,7 @@
"channels_sortBy": "Trier par",
"channels_sortManual": "Manuel",
"channels_sortAZ": "A à Z",
"channels_sortLatestMessages": "Dernières messages",
"channels_sortLatestMessages": "Derniers messages",
"channels_sortUnread": "Non lu",
"chat_noMessages": "Aucun message pour le moment.",
"chat_sendMessageToStart": "Envoyer un message pour commencer",
@@ -436,7 +441,7 @@
"chat_messageCopied": "Message copié",
"chat_messageDeleted": "Message supprimé",
"chat_retryingMessage": "Tentative de récupération.",
"chat_retryCount": "Réessayer {current}/{max}",
"chat_retryCount": "Essai {current}/{max}",
"@chat_retryCount": {
"placeholders": {
"current": {
@@ -539,12 +544,12 @@
"chat_pathManagement": "Gestion des chemins",
"chat_routingMode": "Mode de routage",
"chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"chat_forceFloodMode": "Mode Inondation Forcée",
"chat_forceFloodMode": "Mode tout le réseau forcé",
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
"chat_hopSingular": "Sautez",
"chat_hopPlural": "sautez",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"chat_hopSingular": "saut",
"chat_hopPlural": "sauts",
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -562,7 +567,7 @@
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
"chat_floodModeEnabled": "Le mode inondation est activé. Réactiver via l'icône de routage dans la barre d'outils.",
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
"chat_fullPath": "Chemin complet",
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
"chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,7 +588,7 @@
"chat_path": "Chemin",
"chat_publicKey": "Clé Publique",
"chat_compressOutgoingMessages": "Compresser les messages sortants",
"chat_floodForced": "Inondation (forcée)",
"chat_floodForced": "Tout le réseau (forcée)",
"chat_directForced": "Direct (forcé)",
"chat_hopsForced": "{count} sauts (forcés)",
"@chat_hopsForced": {
@@ -593,7 +598,7 @@
}
}
},
"chat_floodAuto": "Inondation (auto)",
"chat_floodAuto": "Tout le réseau (auto)",
"chat_direct": "Afficher",
"chat_poiShared": "Point d'intérêt Partagé",
"chat_unread": "Non lu : {count}",
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Ouvrir le lien ?",
"chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?",
"chat_open": "Ouvrir",
"chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Format de lien invalide",
"map_title": "Carte des nœuds",
"map_noNodesWithLocation": "Aucun nœud avec des données de localisation",
"map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.",
@@ -624,7 +641,7 @@
}
},
"map_chat": "Chat",
"map_repeater": "Répétiteur",
"map_repeater": "Répéteur",
"map_room": "Salle",
"map_sensor": "Capteur",
"map_pinDm": "Clé (DM)",
@@ -665,7 +682,7 @@
"map_lastSeenTime": "Dernière fois vu",
"map_sharedPin": "Clé partagée",
"map_joinRoom": "Rejoindre la salle",
"map_manageRepeater": "Gérer le répétiteur",
"map_manageRepeater": "Gérer le répéteur",
"mapCache_title": "Cache de Carte Hors Ligne",
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
@@ -687,7 +704,7 @@
}
}
},
"mapCache_cachedTilesWithFailed": "Tiles mis en cache ({downloaded}) ({failed} ratés)",
"mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)",
"@mapCache_cachedTilesWithFailed": {
"placeholders": {
"downloaded": {
@@ -734,7 +751,7 @@
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
"@mapCache_boundsLabel": {
"placeholders": {
"north": {
@@ -751,7 +768,7 @@
}
}
},
"time_justNow": "Il y a tout juste maintenant",
"time_justNow": "Maintenant",
"time_minutesAgo": "{minutes} minutes auparavant",
"@time_minutesAgo": {
"placeholders": {
@@ -788,18 +805,18 @@
"time_allTime": "Tout le temps",
"dialog_disconnect": "Déconnecter",
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
"login_repeaterLogin": "Connexion au répétiteur",
"login_roomLogin": "Connexion Salle",
"login_repeaterLogin": "Connexion au répéteur",
"login_roomLogin": "Connexion Room Server",
"login_password": "Mot de passe",
"login_enterPassword": "Entrez votre mot de passe",
"login_savePassword": "Sauvegarder le mot de passe",
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
"login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.",
"login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.",
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
"login_routing": "Redirection",
"login_routingMode": "Mode de routage",
"login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"login_forceFloodMode": "Mode Inondation Forcée",
"login_forceFloodMode": "Mode tout le réseau forcé",
"login_managePaths": "Gérer les chemins",
"login_login": "Connexion",
"login_attempt": "Essayer {current}/{max}",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.",
"common_reload": "Recharger",
"common_clear": "Effacer",
"path_currentPath": "Chemin actuel : {path}",
@@ -858,20 +876,20 @@
},
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
"path_setPath": "Définir le chemin",
"repeater_management": "Gestion des répétiteurs",
"repeater_management": "Gestion des répéteurs",
"repeater_managementTools": "Outils de Gestion",
"repeater_status": "État",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
"repeater_telemetry": "Télémetrie",
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
"repeater_settings": "Paramètres",
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
"repeater_statusTitle": "État du répétiteur",
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
"repeater_statusTitle": "État du répéteur",
"repeater_routingMode": "Mode de routage",
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"repeater_forceFloodMode": "Mode de submersion forcée",
"repeater_forceFloodMode": "Mode tout le réseau forcé",
"repeater_pathManagement": "Gestion des chemins",
"repeater_refresh": "Rafraîchir",
"repeater_statusRequestTimeout": "Demande de statut délai dépassé.",
@@ -898,7 +916,7 @@
"repeater_packetStatistics": "Statistiques des paquets",
"repeater_sent": "Envoyé",
"repeater_received": "Reçu",
"repeater_duplicates": "Dupliques",
"repeater_duplicates": "Doublons",
"repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s",
"@repeater_daysHoursMinsSecs": {
"placeholders": {
@@ -916,7 +934,7 @@
}
}
},
"repeater_packetTxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +948,7 @@
}
}
},
"repeater_packetRxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +962,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Inondation : {flood}, Direct : {direct}",
"repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -963,10 +981,10 @@
}
}
},
"repeater_settingsTitle": "Paramètres du répétiteur",
"repeater_settingsTitle": "Paramètres du répéteur",
"repeater_basicSettings": "Paramètres de base",
"repeater_repeaterName": "Nom du répétiteur",
"repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur",
"repeater_repeaterName": "Nom du répéteur",
"repeater_repeaterNameHelper": "Afficher le nom de ce répéteur",
"repeater_adminPassword": "Mot de passe Administrateur",
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
"repeater_guestPassword": "Mot de passe invité",
@@ -986,13 +1004,13 @@
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
"repeater_features": "Fonctionnalités",
"repeater_packetForwarding": "Transfert de paquets",
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
"repeater_privacyMode": "Mode de confidentialité",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"repeater_advertisementSettings": "Paramètres de Publicité",
"repeater_localAdvertInterval": "Intervalle Publicité Locale",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"repeater_advertisementSettings": "Paramètres d'annonces",
"repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)",
"repeater_localAdvertIntervalMinutes": "{minutes} minutes",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1001,7 +1019,7 @@
}
}
},
"repeater_floodAdvertInterval": "Intervalle de Publicité Inondation",
"repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)",
"repeater_floodAdvertIntervalHours": "{hours} heures",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1010,17 +1028,17 @@
}
}
},
"repeater_encryptedAdvertInterval": "Intervalle publicitaire crypté",
"repeater_dangerZone": "Zone d'alerte",
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone dangereuse",
"repeater_rebootRepeater": "Redémarrer Répéteur",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur",
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?",
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?",
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?",
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur",
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !",
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur",
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !",
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
"repeater_commandSent": "Commande envoyée : {command}",
"@repeater_commandSent": {
@@ -1055,7 +1073,7 @@
"repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets",
"repeater_refreshGuestAccess": "Rafraîchir l'accès invité",
"repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres de la Publicité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces",
"repeater_refreshed": "{label} rafraîchi",
"@repeater_refreshed": {
"placeholders": {
@@ -1072,7 +1090,7 @@
}
}
},
"repeater_cliTitle": "Répétiteur CLI",
"repeater_cliTitle": "Répéteur CLI",
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
"repeater_commandHelp": "Aide",
"repeater_clearHistory": "Effacer l'historique",
@@ -1098,7 +1116,7 @@
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Publier",
"repeater_cliQuickClock": "Horloge",
"repeater_cliHelpAdvert": "Envoie un paquet publicitaire",
"repeater_cliHelpAdvert": "Envoie un paquet d'annonce",
"repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)",
"repeater_cliHelpClock": "Affiche l'heure actuelle par l'horloge de chaque appareil.",
"repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.",
@@ -1106,21 +1124,21 @@
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
"repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
"repeater_cliHelpSetAllowReadOnly": "(Serveur de pièce) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.",
"repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
"repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).",
"repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.",
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
"repeater_cliHelpSetName": "Définit le nom de la publicité.",
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
"repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)",
"repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)",
"repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.",
"repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode inondation pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.",
"repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.",
"repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.",
@@ -1134,9 +1152,9 @@
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des publicités sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations de débordement actuelles.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
"repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"",
"repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.",
@@ -1158,8 +1176,8 @@
"repeater_settingsCategory": "Paramètres",
"repeater_bridge": "Pont",
"repeater_logging": "Journalisation",
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)",
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)",
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)",
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)",
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
"repeater_gpsManagement": "Gestion GPS",
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
@@ -1228,7 +1246,7 @@
"channelPath_title": "Chemin de paquet",
"channelPath_viewMap": "Afficher la carte",
"channelPath_otherObservedPaths": "Autres chemins observés",
"channelPath_repeaterHops": "Sauts du répétiteur",
"channelPath_repeaterHops": "Sauts du répéteur",
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
"channelPath_messageDetails": "Détails du message",
"channelPath_senderLabel": "Expéditeur",
@@ -1271,7 +1289,7 @@
}
},
"channelPath_unknownPath": "Inconnu",
"channelPath_floodPath": "Inondation",
"channelPath_floodPath": "Tout le réseau",
"channelPath_directPath": "Afficher",
"channelPath_observedZeroOf": "0 de {total} sauts",
"@channelPath_observedZeroOf": {
@@ -1293,7 +1311,7 @@
}
},
"channelPath_mapTitle": "Carte du chemin",
"channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.",
"channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.",
"channelPath_primaryPath": "Chemin {index} (Principal)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1326,14 +1344,460 @@
"channelPath_unknownRepeater": "Répéteur Inconnu",
"listFilter_tooltip": "Filtrer et trier",
"listFilter_sortBy": "Trier par",
"listFilter_latestMessages": "Dernières messages",
"listFilter_latestMessages": "Derniers messages",
"listFilter_heardRecently": "Écoute récemment",
"listFilter_az": "A à Z",
"listFilter_filters": "Filtres",
"listFilter_all": "Tout",
"listFilter_users": "Utilisateurs",
"listFilter_repeaters": "Répéteurs",
"listFilter_roomServers": "Serveurs de pièce",
"listFilter_roomServers": "Room servers",
"listFilter_unreadOnly": "Messages non lus seulement",
"listFilter_newGroup": "Nouvelle groupe"
"listFilter_newGroup": "Nouveau groupe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Voisins",
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbors": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
"channels_createPrivateChannel": "Créer un Canal Privé",
"channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.",
"channels_joinPublicChannel": "Rejoindre le canal public",
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Activer le GPS",
"settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS",
"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": "Administrattion Room Server",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Communauté",
"community_create": "Créer une Communauté",
"community_createDesc": "Créer une nouvelle communauté et la partager via QR code.",
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
"community_name": "Nom de la communauté",
"community_enterName": "Entrez le nom de la communauté",
"community_created": "Communauté \"{name}\" créée",
"community_joined": "Rejoint la communauté \"{name}\"",
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté",
"community_selectCommunity": "Sélectionner Communauté",
"community_regularHashtag": "Hashtag régulier",
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
"community_communityHashtag": "Hashtag de la communauté",
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
"community_forCommunity": "Pour {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecret": "Régénérer le secret",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
"community_regenerate": "Régénérer",
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
"community_updateSecret": "Mettre à jour le secret",
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Vous",
"pathTrace_refreshTooltip": "Actualiser Path Trace",
"pathTrace_failed": "Traçage du chemin échoué.",
"pathTrace_notAvailable": "Tracé de chemin non disponible.",
"contacts_pathTrace": "Traçage de chemin",
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
"contacts_repeaterPing": "Pinguer le répéteur",
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
"contacts_chatTraceRoute": "Tracer le chemin",
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
"contacts_ping": "Ping",
"contacts_roomPing": "Pinguer le serveur de la salle",
"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",
"contacts_contactImportFailed": "Échec de l'importation du contact.",
"contacts_zeroHopAdvert": "Annonce Zero saut",
"contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers",
"contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers",
"contacts_ShareContact": "Copier le contact dans le presse-papiers",
"contacts_ShareContactZeroHop": "Partager un contact par annonce",
"contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.",
"contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.",
"contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.",
"contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.",
"notification_activityTitle": "Activité MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}",
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
"notification_receivedNewMessage": "Nouveau message reçu",
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
"settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.",
"settings_gpxExportChat": "Emplacements des compagnons",
"settings_gpxExportSuccess": "Fichier GPX exporté avec succès.",
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
"pathTrace_clearTooltip": "Effacer le chemin",
"map_pathTraceCancelled": "Traçage de chemin annulé",
"map_removeLast": "Supprimer le dernier",
"map_runTrace": "Exécuter la traçage de chemin",
"scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.",
"scanner_bluetoothOff": "Le Bluetooth est désactivé.",
"scanner_enableBluetooth": "Activer le Bluetooth",
"snrIndicator_lastSeen": "Dernière fois vu",
"snrIndicator_nearByRepeaters": "Répéteurs à proximité",
"chat_ShowAllPaths": "Afficher tous les chemins",
"settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.",
"settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.",
"settings_clientRepeat": "Répétition hors réseau",
"settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unités",
"appSettings_unitsMetric": "Métrique (m/km)",
"appSettings_unitsImperial": "Impérial (ft / mi)",
"map_lineOfSight": "Ligne de vue",
"map_losScreenTitle": "Ligne de vue",
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Effacer tous les points",
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
"losCustomPoints": "Points personnalisés",
"losCustomPointLabel": "Personnalisé {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Point A",
"losPointB": "Point B",
"losAntennaA": "Antenne A : {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B : {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Exécuter la LOS",
"losNoElevationData": "Aucune donnée d'altitude",
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS : vérification...",
"losStatusNoData": "LOS : aucune donnée",
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
"losRenameCustomPoint": "Renommer le point personnalisé",
"losPointName": "Nom du point",
"losShowPanelTooltip": "Afficher le panneau LOS",
"losHidePanelTooltip": "Masquer le panneau LOS",
"losElevationAttribution": "Données daltitude : Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizon radio",
"losLegendLosBeam": "Ligne de visée",
"losLegendTerrain": "Terrain",
"losFrequencyLabel": "Fréquence",
"losFrequencyInfoTooltip": "Voir les détails du calcul",
"losFrequencyDialogTitle": "Calcul de lhorizon radio",
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Ajouter à mes favoris",
"listFilter_removeFromFavorites": "Supprimer des favoris",
"listFilter_favorites": "Préférences",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Non lu",
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"contacts_searchContactsNoNumber": "Rechercher des contacts..."
}
+470 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Aprire il link?",
"chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?",
"chat_open": "Apri",
"chat_couldNotOpenLink": "Impossibile aprire il link: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato di link non valido",
"map_title": "Mappa Nodi",
"map_noNodesWithLocation": "Nessun nodo con dati di posizione",
"map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
"common_reload": "Ricaricare",
"common_clear": "Cancella",
"path_currentPath": "Percorso corrente: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Ripetitori",
"listFilter_roomServers": "Server della stanza",
"listFilter_unreadOnly": "Solo non letto",
"listFilter_newGroup": "Nuovo gruppo"
"listFilter_newGroup": "Nuovo gruppo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Vicini",
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Sentito: {time} fa",
"neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}",
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci Server Camera",
"room_management": "Gestione del Server di Camera",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"community_scanQr": "Scansiona il QR Code della Community",
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
"community_showQr": "Mostra il codice QR",
"community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
"community_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"community_deleteConfirm": "Uscire da \"{name}\"?",
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
"community_addHashtagChannel": "Aggiungi Hashtag della Community",
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.",
"community_regenerateSecret": "Ri genera la chiave segreta",
"community_regenerate": "Rigenera",
"community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
"community_updateSecret": "Aggiorna Segreto",
"community_secretUpdated": "Segreto aggiornato per \"{name}\"",
"community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_failed": "Tracciamento del percorso fallito.",
"pathTrace_you": "Tu",
"pathTrace_notAvailable": "Tracciamento del percorso non disponibile.",
"pathTrace_refreshTooltip": "Aggiorna Path Trace.",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Traccia percorso al ripetitore",
"contacts_roomPathTrace": "Traccia del percorso al server della stanza",
"contacts_pathTrace": "Traccia Percorso",
"contacts_repeaterPing": "Ripetitore ping",
"contacts_pathTraceTo": "Traccia percorso verso {name}",
"contacts_roomPing": "Ping al server della stanza",
"contacts_chatTraceRoute": "Traccia percorso path",
"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",
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
"contacts_clipboardEmpty": "La clipboard è vuota.",
"contacts_ShareContact": "Copia contatto negli Appunti",
"contacts_contactImported": "Il contatto è stato importato.",
"contacts_contactImportFailed": "Contatto non importato con successo.",
"contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
"notification_activityTitle": "Attività MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
"settings_gpxExportContacts": "Esporta compagni in GPX",
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
"settings_gpxExportNoContacts": "Nessun contatto da esportare.",
"settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo",
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
"settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.",
"settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
"settings_gpxExportAll": "Esporta tutti i contatti in GPX",
"settings_gpxExportAllSubtitle": "Esporta tutti i contatti con una posizione in un file GPX.",
"settings_gpxExportChat": "Posizioni dei compagni",
"settings_gpxExportRepeatersRoom": "Posizioni del server ripetitore e della stanza",
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
"map_removeLast": "Rimuovi ultimo",
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
"pathTrace_clearTooltip": "Pulisci percorso",
"map_runTrace": "Esegui Path Trace",
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
"scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.",
"scanner_enableBluetooth": "Abilita il Bluetooth",
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
"snrIndicator_lastSeen": "Ultimo accesso",
"chat_ShowAllPaths": "Mostra tutti i percorsi",
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
"settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.",
"settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unità",
"appSettings_unitsMetric": "Metrico (m/km)",
"appSettings_unitsImperial": "Imperiale (ft / mi)",
"map_lineOfSight": "Linea di vista",
"map_losScreenTitle": "Linea di vista",
"losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.",
"losRunFailed": "Controllo della linea di vista fallito: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Cancella tutti i punti",
"losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico",
"losMenuTitle": "Menù LOS",
"losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati",
"losShowDisplayNodes": "Mostra i nodi di visualizzazione",
"losCustomPoints": "Punti personalizzati",
"losCustomPointLabel": "Personalizzato {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punto A",
"losPointB": "Punto B",
"losAntennaA": "Antenna A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenna B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Esegui LOS",
"losNoElevationData": "Nessun dato di elevazione",
"losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controllo...",
"losStatusNoData": "LOS: nessun dato",
"losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.",
"losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.",
"losRenameCustomPoint": "Rinomina punto personalizzato",
"losPointName": "Nome del punto",
"losShowPanelTooltip": "Mostra il pannello LOS",
"losHidePanelTooltip": "Nascondi il pannello LOS",
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Orizzonte radio",
"losLegendLosBeam": "Linea di vista",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza",
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
"losFrequencyDialogTitle": "Calcolo dellorizzonte radio",
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Aggiungi ai preferiti",
"listFilter_removeFromFavorites": "Rimuovi dai preferiti",
"listFilter_favorites": "Preferiti",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchUsers": "Cerca {number}{str} Utenti...",
"contacts_searchContactsNoNumber": "Cerca Contatti...",
"contacts_searchFavorites": "Cerca {number}{str} Preferiti...",
"contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server Room..."
}
File diff suppressed because it is too large Load Diff
+734 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_cancel => 'Отказ';
@override
String get common_ok => 'Добре';
@override
String get common_connect => 'Свържи се';
@@ -140,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_scan => 'Сканирай';
@override
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
@override
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@override
String get device_quickSwitch => 'Бързо превключване';
@@ -201,6 +214,20 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_locationInvalid => 'Невалидна ширина или дължина.';
@override
String get settings_locationGPSEnable => 'Активиране на GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Активирайте автоматичното актуализиране на местоположението чрез GPS.';
@override
String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)';
@override
String get settings_locationIntervalInvalid =>
'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.';
@override
String get settings_latitude => 'Широчина';
@@ -299,6 +326,10 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_aboutDescription =>
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Име';
@@ -323,15 +354,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_presets => 'Предварителни настройки';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Честота (MHz)';
@@ -360,10 +382,15 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
@override
String get settings_longRange => 'Дълъг обхват';
String get settings_clientRepeat => 'Без електричество – повторение';
@override
String get settings_fastSpeed => 'Бърза скорост';
String get settings_clientRepeatSubtitle =>
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
@override
String get settings_clientRepeatFreqWarning =>
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
@override
String settings_error(String message) {
@@ -433,6 +460,20 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Руски';
@override
String get appSettings_languageUk => 'Украински';
@override
String get appSettings_enableMessageTracing =>
'Разрешаване на проследяване на съобщения';
@override
String get appSettings_enableMessageTracingSubtitle =>
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
@override
String get appSettings_notifications => 'Уведомления';
@@ -593,6 +634,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
@override
String get appSettings_unitsTitle => 'единици';
@override
String get appSettings_unitsMetric => 'Метрика (m / km)';
@override
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Няма избрана област';
@@ -631,7 +681,35 @@ class AppLocalizationsBg extends AppLocalizations {
'Контактите ще се появят, когато устройствата рекламират.';
@override
String get contacts_searchContacts => 'Търсене на контакти...';
String get contacts_unread => 'Непрочетено';
@override
String get contacts_searchContactsNoNumber => 'Търси контакти...';
@override
String contacts_searchContacts(int number, String str) {
return 'Търсене на контакти...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Търсене на $number$str любими...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Търсене на $number$str потребители...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Търсене на $number$str повтарящи се...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Търсене на $number$str сървъри в стаята...';
}
@override
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
@@ -650,6 +728,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get contacts_manageRoom => 'Управление на сървър за стая';
@override
String get contacts_roomLogin => 'Вход в стаята';
@@ -753,6 +834,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_editChannel => 'Редактирай канал';
@override
String get channels_muteChannel => 'Заглуши канала';
@override
String get channels_unmuteChannel => 'Включи известията на канала';
@override
String get channels_deleteChannel => 'Изтрий канала';
@@ -761,6 +848,11 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Неуспешно изтриване на канала \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Каналът \"$name\" е изтрит';
@@ -830,6 +922,45 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_sortUnread => 'Непрочетено';
@override
String get channels_createPrivateChannel => 'Създай Частен Канал';
@override
String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.';
@override
String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал';
@override
String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.';
@override
String get channels_joinPublicChannel =>
'Присъединете се към Публичния канал';
@override
String get channels_joinPublicChannelDesc =>
'Всеки може да се присъедини към този канал.';
@override
String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал';
@override
String get channels_joinHashtagChannelDesc =>
'Всеки може да се присъедини към хаштаговите канали.';
@override
String get channels_scanQrCode => 'Сканирайте QR код';
@override
String get channels_scanQrCodeComingSoon => 'Ще излезе скоро';
@override
String get channels_enterHashtag => 'Въведете хаштаг';
@override
String get channels_hashtagHint => 'напр. #отбор';
@override
String get chat_noMessages => 'Няма съобщения.';
@@ -1009,6 +1140,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_pathManagement => 'Управление на пътища';
@override
String get chat_ShowAllPaths => 'Покажи всички пътища';
@override
String get chat_routingMode => 'Режим на маршрутизиране';
@@ -1148,9 +1282,33 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Непрочетени: $count';
}
@override
String get chat_openLink => 'Отваряне на връзката?';
@override
String get chat_openLinkConfirmation =>
'Искате ли да отворите тази връзка в браузъра си?';
@override
String get chat_open => 'Отвори';
@override
String chat_couldNotOpenLink(String url) {
return 'Не можа да се отвори връзката: $url';
}
@override
String get chat_invalidLink => 'Невалиден формат на връзката';
@override
String get map_title => 'Карта на възлите';
@override
String get map_lineOfSight => 'Линия на видимост';
@override
String get map_losScreenTitle => 'Линия на видимост';
@override
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
@@ -1280,6 +1438,19 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get map_tapToAdd =>
'Натиснете върху възлите, за да ги добавите към пътя.';
@override
String get map_runTrace => 'Изпълни Път на Следване';
@override
String get map_removeLast => 'Премахни Последно';
@override
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
@override
String get mapCache_title => 'Кеш на офлайн карти';
@@ -1475,6 +1646,10 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Входът не беше успешен: $error';
}
@override
String get login_failedMessage =>
'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.';
@override
String get common_reload => 'Презареди';
@@ -1544,6 +1719,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_management => 'Управление на повторители';
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_managementTools => 'Инструменти за управление';
@@ -1567,6 +1745,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbors => 'Съседи';
@override
String get repeater_neighborsSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
String get repeater_settings => 'Настройки';
@@ -2252,6 +2437,33 @@ class AppLocalizationsBg extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Получени данни за съседи';
@override
String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.';
@override
String neighbors_errorLoading(String error) {
return 'Грешка при зареждане на съседи: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
@override
String get neighbors_noData => 'Няма налични данни за съседи.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Неизвестна $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Слушано преди $time.';
}
@override
String get channelPath_title => 'Пътеки пъзел';
@@ -2355,6 +2567,174 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Неизвестен повторител';
@override
String get community_title => 'Общност';
@override
String get community_create => 'Създай общност';
@override
String get community_createDesc =>
'Създайте нова общност и я споделете чрез QR код.';
@override
String get community_join => 'Присъедини се';
@override
String get community_joinTitle => 'Присъедини се към общността';
@override
String community_joinConfirmation(String name) {
return 'Искате ли да се присъедините към общността \"$name\"?';
}
@override
String get community_scanQr => 'Сканирайте QR кода на общността';
@override
String get community_scanInstructions =>
'Насочете камерата към QR код на общността';
@override
String get community_showQr => 'Покажи QR код';
@override
String get community_publicChannel => 'Обществено общност';
@override
String get community_hashtagChannel => 'Хаштаг на общността';
@override
String get community_name => 'Име на общността';
@override
String get community_enterName => 'Въведете име на общността';
@override
String community_created(String name) {
return 'Общността \"$name\" е създадена';
}
@override
String community_joined(String name) {
return 'Присъединено общност \"$name\"';
}
@override
String get community_qrTitle => 'Споделяне в общността';
@override
String community_qrInstructions(String name) {
return 'Сканирайте този QR код, за да се присъедините към $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Хаштаг каналите на общността са достъпни само за членове на общността';
@override
String get community_invalidQrCode => 'Невалиден QR код на общността';
@override
String get community_alreadyMember => 'Вече съм член';
@override
String community_alreadyMemberMessage(String name) {
return 'Вие вече сте член на \"$name\".';
}
@override
String get community_addPublicChannel => 'Добави публичен общностен канал';
@override
String get community_addPublicChannelHint =>
'Автоматично добавете публичния канал за тази общност.';
@override
String get community_noCommunities => 'Няма присъединени общности още.';
@override
String get community_scanOrCreate =>
'Сканирайте QR код или създайте общност, за да започнете.';
@override
String get community_manageCommunities => 'Управление на общности';
@override
String get community_delete => 'Напусни общността';
@override
String community_deleteConfirm(String name) {
return 'Напускате \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Това ще изтрие също $count канал(а) и техните съобщения.';
}
@override
String community_deleted(String name) {
return 'Остави общността \"$name\"';
}
@override
String get community_regenerateSecret => 'Регенерейрай секрет';
@override
String community_regenerateSecretConfirm(String name) {
return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
}
@override
String get community_regenerate => 'Регенерация';
@override
String community_secretRegenerated(String name) {
return 'Секретно презареждане за \"$name\"';
}
@override
String get community_updateSecret => 'Актуализирай тайна';
@override
String community_secretUpdated(String name) {
return 'Секретно обновено за \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
}
@override
String get community_addHashtagChannel => 'Добави общностен хаштаг';
@override
String get community_addHashtagChannelDesc =>
'Добавете хаштаг канал за тази общност';
@override
String get community_selectCommunity => 'Изберете общност';
@override
String get community_regularHashtag => 'Обикновен хаштаг';
@override
String get community_regularHashtagDesc =>
'Общ хаштаг (всеки може да се присъедини)';
@override
String get community_communityHashtag => 'Общностен хаштаг';
@override
String get community_communityHashtagDesc => 'Само за членове на общността';
@override
String community_forCommunity(String name) {
return 'За $name';
}
@override
String get listFilter_tooltip => 'Филтрирайте и сортирайте';
@@ -2376,6 +2756,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_all => 'Всички';
@override
String get listFilter_favorites => 'Любими';
@override
String get listFilter_addToFavorites => 'Добави към любими';
@override
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
@override
String get listFilter_users => 'Потребители';
@@ -2390,4 +2779,337 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get listFilter_newGroup => 'Нова група';
@override
String get pathTrace_you => 'Вие';
@override
String get pathTrace_failed => 'Пътят за проследяване не успя.';
@override
String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.';
@override
String get pathTrace_refreshTooltip => 'Обнови Path Trace.';
@override
String get pathTrace_someHopsNoLocation =>
'Един или повече от хмелите липсва местоположение!';
@override
String get pathTrace_clearTooltip => 'Изчисти пътя';
@override
String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.';
@override
String losRunFailed(String error) {
return 'Проверката на пряката видимост е неуспешна: $error';
}
@override
String get losClearAllPoints => 'Изчистете всички точки';
@override
String get losRunToViewElevationProfile =>
'Стартирайте LOS, за да видите профила на надморската височина';
@override
String get losMenuTitle => 'LOS меню';
@override
String get losMenuSubtitle =>
'Докоснете възли или натиснете продължително карта за персонализирани точки';
@override
String get losShowDisplayNodes => 'Показване на възли на дисплея';
@override
String get losCustomPoints => 'Персонализирани точки';
@override
String losCustomPointLabel(int index) {
return 'Персонализирано $index';
}
@override
String get losPointA => 'Точка А';
@override
String get losPointB => 'Точка Б';
@override
String losAntennaA(String value, String unit) {
return 'Антена A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Антена B: $value $unit';
}
@override
String get losRun => 'Стартирайте LOS';
@override
String get losNoElevationData => 'Няма данни за надморска височина';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, блокиран от $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: проверка...';
@override
String get losStatusNoData => 'LOS: няма данни';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно';
}
@override
String get losErrorElevationUnavailable =>
'Няма налични данни за надморска височина за една или повече проби.';
@override
String get losErrorInvalidInput =>
'Невалидни данни за точки/надморска височина за изчисляване на LOS.';
@override
String get losRenameCustomPoint => 'Преименувайте персонализирана точка';
@override
String get losPointName => 'Име на точката';
@override
String get losShowPanelTooltip => 'Показване на LOS панел';
@override
String get losHidePanelTooltip => 'Скриване на LOS панела';
@override
String get losElevationAttribution =>
'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиохоризонт';
@override
String get losLegendLosBeam => 'Линия на видимост';
@override
String get losLegendTerrain => 'Терен';
@override
String get losFrequencyLabel => 'Честота';
@override
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
@override
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
}
@override
String get contacts_pathTrace => 'Пътен проследяване';
@override
String get contacts_ping => 'Пинг';
@override
String get contacts_repeaterPathTrace => 'Трасировка до повторител';
@override
String get contacts_repeaterPing => 'Пингване на повторителя';
@override
String get contacts_roomPathTrace => 'Трасиране на път до съ';
@override
String get contacts_roomPing => 'Ping на сървъра на стаята';
@override
String get contacts_chatTraceRoute => 'Трасиране на път';
@override
String contacts_pathTraceTo(String name) {
return 'Проследи маршрут към $name';
}
@override
String get contacts_clipboardEmpty => 'Клипборда е празна.';
@override
String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт';
@override
String get contacts_contactImported => 'Контактът е импортиран.';
@override
String get contacts_contactImportFailed =>
'Контактът не е успешно импортиран.';
@override
String get contacts_zeroHopAdvert => 'Реклама без скок';
@override
String get contacts_floodAdvert => 'Потопна реклама';
@override
String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда';
@override
String get contacts_addContactFromClipboard => 'Добави контакт от клипборда';
@override
String get contacts_ShareContact => 'Копирай контакт в клипборда';
@override
String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява';
@override
String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Неуспешно изпращане на контакт.';
@override
String get contacts_contactAdvertCopied =>
'Рекламата е копирана в клипборда.';
@override
String get contacts_contactAdvertCopyFailed =>
'Копирането на обявата в клипборда не успя.';
@override
String get notification_activityTitle => 'Активност на MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'съобщения',
one: 'съобщение',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'съобщения в канали',
one: 'съобщение в канал',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'нови възли',
one: 'нов възел',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Открит нов $contactType';
}
@override
String get notification_receivedNewMessage => 'Получено ново съобщение';
@override
String get settings_gpxExportRepeaters =>
'Експортиране на повтарящи се устройства / сървър на стаята до GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Изпраща повторители / roomserver с местоположение в GPX файл.';
@override
String get settings_gpxExportContacts => 'Експортирай спътници към GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Експортира спътници с местоположение в GPX файл.';
@override
String get settings_gpxExportAll => 'Експортирай всички контакти в GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Експортира всички контакти с местоположение в файл GPX.';
@override
String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.';
@override
String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.';
@override
String get settings_gpxExportNotAvailable =>
'Не е поддържан на вашето устройство/ОС';
@override
String get settings_gpxExportError => 'Възникна грешка при изнасяне.';
@override
String get settings_gpxExportRepeatersRoom =>
'Местоположения на повторител и сървър на стаята';
@override
String get settings_gpxExportChat => 'Местоположения на спътници';
@override
String get settings_gpxExportAllContacts =>
'Местоположения на всички контакти';
@override
String get settings_gpxExportShareText =>
'Картинни данни изнесени от meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open износ на данни за карта в формат GPX';
@override
String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства';
@override
String get snrIndicator_lastSeen => 'Последно видян';
}
File diff suppressed because it is too large Load Diff
+726 -15
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_cancel => 'Cancel';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connect';
@@ -139,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is off';
@override
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@override
String get device_quickSwitch => 'Quick switch';
@@ -190,7 +203,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_locationSubtitle => 'GPS coordinates';
@override
String get settings_locationUpdated => 'Location updated';
String get settings_locationUpdated => 'Location and GPS settings updated';
@override
String get settings_locationBothRequired =>
@@ -199,6 +212,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_locationInvalid => 'Invalid latitude or longitude.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
@override
String get settings_latitude => 'Latitude';
@@ -291,6 +318,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_aboutDescription =>
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS elevation data: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Name';
@@ -315,15 +346,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequency (MHz)';
@@ -352,10 +374,15 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
@override
String get settings_longRange => 'Long Range';
String get settings_clientRepeat => 'Off-Grid Repeat';
@override
String get settings_fastSpeed => 'Fast Speed';
String get settings_clientRepeatSubtitle =>
'Allow this device to repeat mesh packets for others';
@override
String get settings_clientRepeatFreqWarning =>
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
@override
String settings_error(String message) {
@@ -425,6 +452,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@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';
@@ -585,6 +625,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Map Cache';
@override
String get appSettings_unitsTitle => 'Units';
@override
String get appSettings_unitsMetric => 'Metric (m / km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
@override
String get appSettings_noAreaSelected => 'No area selected';
@@ -621,7 +670,35 @@ class AppLocalizationsEn extends AppLocalizations {
'Contacts will appear when devices advertise';
@override
String get contacts_searchContacts => 'Search contacts...';
String get contacts_unread => 'Unread';
@override
String get contacts_searchContactsNoNumber => 'Search Contacts...';
@override
String contacts_searchContacts(int number, String str) {
return 'Search $number$str Contacts...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Search $number$str Favorites...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Search $number$str Users...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Search $number$str Repeaters...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Search $number$str Room servers...';
}
@override
String get contacts_noUnreadContacts => 'No unread contacts';
@@ -641,7 +718,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_manageRepeater => 'Manage Repeater';
@override
String get contacts_roomLogin => 'Room Login';
String get contacts_manageRoom => 'Manage Room Server';
@override
String get contacts_roomLogin => 'Room Server Login';
@override
String get contacts_openChat => 'Open Chat';
@@ -742,6 +822,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_editChannel => 'Edit channel';
@override
String get channels_muteChannel => 'Mute channel';
@override
String get channels_unmuteChannel => 'Unmute channel';
@override
String get channels_deleteChannel => 'Delete channel';
@@ -750,6 +836,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Delete \"$name\"? This cannot be undone.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Failed to delete channel \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Channel \"$name\" deleted';
@@ -818,6 +909,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_sortUnread => 'Unread';
@override
String get channels_createPrivateChannel => 'Create a Private Channel';
@override
String get channels_createPrivateChannelDesc => 'Secured with a secret key.';
@override
String get channels_joinPrivateChannel => 'Join a Private Channel';
@override
String get channels_joinPrivateChannelDesc => 'Manually enter a secret key.';
@override
String get channels_joinPublicChannel => 'Join the Public Channel';
@override
String get channels_joinPublicChannelDesc => 'Anyone can join this channel.';
@override
String get channels_joinHashtagChannel => 'Join a Hashtag Channel';
@override
String get channels_joinHashtagChannelDesc =>
'Anyone can join hashtag channels.';
@override
String get channels_scanQrCode => 'Scan a QR Code';
@override
String get channels_scanQrCodeComingSoon => 'Coming soon';
@override
String get channels_enterHashtag => 'Enter hashtag';
@override
String get channels_hashtagHint => 'e.g. #team';
@override
String get chat_noMessages => 'No messages yet';
@@ -996,6 +1124,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_pathManagement => 'Path Management';
@override
String get chat_ShowAllPaths => 'Show all paths';
@override
String get chat_routingMode => 'Routing mode';
@@ -1129,9 +1260,33 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Unread: $count';
}
@override
String get chat_openLink => 'Open Link?';
@override
String get chat_openLinkConfirmation =>
'Do you want to open this link in your browser?';
@override
String get chat_open => 'Open';
@override
String chat_couldNotOpenLink(String url) {
return 'Could not open link: $url';
}
@override
String get chat_invalidLink => 'Invalid link format';
@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';
@@ -1261,6 +1416,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_manageRepeater => 'Manage Repeater';
@override
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
String get map_runTrace => 'Run Path Trace';
@override
String get map_removeLast => 'Remove Last';
@override
String get map_pathTraceCancelled => 'Path trace cancelled.';
@override
String get mapCache_title => 'Offline Map Cache';
@@ -1402,7 +1569,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get login_repeaterLogin => 'Repeater Login';
@override
String get login_roomLogin => 'Room Login';
String get login_roomLogin => 'Room Server Login';
@override
String get login_password => 'Password';
@@ -1453,6 +1620,10 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Login failed: $error';
}
@override
String get login_failedMessage =>
'Login failed. Either the password is incorrect or the repeater is unreachable.';
@override
String get common_reload => 'Reload';
@@ -1520,6 +1691,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_management => 'Repeater Management';
@override
String get room_management => 'Room Server Management';
@override
String get repeater_managementTools => 'Management Tools';
@@ -1543,6 +1717,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbors => 'Neighbors';
@override
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@@ -2216,6 +2396,33 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Received Neighbors Data';
@override
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
@override
String neighbors_errorLoading(String error) {
return 'Error loading neighbors: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
@override
String get neighbors_noData => 'No neighbors data available.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unknown $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Heard: $time ago';
}
@override
String get channelPath_title => 'Packet Path';
@@ -2319,6 +2526,173 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Unknown Repeater';
@override
String get community_title => 'Community';
@override
String get community_create => 'Create Community';
@override
String get community_createDesc =>
'Create a new community and share via QR code.';
@override
String get community_join => 'Join';
@override
String get community_joinTitle => 'Join Community';
@override
String community_joinConfirmation(String name) {
return 'Do you want to join the community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Community QR';
@override
String get community_scanInstructions =>
'Point the camera at a community QR code';
@override
String get community_showQr => 'Show QR Code';
@override
String get community_publicChannel => 'Community Public';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Community Name';
@override
String get community_enterName => 'Enter community name';
@override
String community_created(String name) {
return 'Community \"$name\" created';
}
@override
String community_joined(String name) {
return 'Joined community \"$name\"';
}
@override
String get community_qrTitle => 'Share Community';
@override
String community_qrInstructions(String name) {
return 'Scan this QR code to join \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag channels are only joinable by members of the community';
@override
String get community_invalidQrCode => 'Invalid community QR code';
@override
String get community_alreadyMember => 'Already a Member';
@override
String community_alreadyMemberMessage(String name) {
return 'You are already a member of \"$name\".';
}
@override
String get community_addPublicChannel => 'Add Community Public Channel';
@override
String get community_addPublicChannelHint =>
'Automatically add the public channel for this community';
@override
String get community_noCommunities => 'No communities joined yet';
@override
String get community_scanOrCreate =>
'Scan a QR code or create a community to get started';
@override
String get community_manageCommunities => 'Manage Communities';
@override
String get community_delete => 'Leave Community';
@override
String community_deleteConfirm(String name) {
return 'Leave \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'This will also delete $count channel(s) and their messages.';
}
@override
String community_deleted(String name) {
return 'Left community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Add Community Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Add a hashtag channel for this community';
@override
String get community_selectCommunity => 'Select Community';
@override
String get community_regularHashtag => 'Regular Hashtag';
@override
String get community_regularHashtagDesc => 'Public hashtag (anyone can join)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Private to community members';
@override
String community_forCommunity(String name) {
return 'For $name';
}
@override
String get listFilter_tooltip => 'Filter and sort';
@@ -2340,6 +2714,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_all => 'All';
@override
String get listFilter_favorites => 'Favorites';
@override
String get listFilter_addToFavorites => 'Add to favorites';
@override
String get listFilter_removeFromFavorites => 'Remove from favorites';
@override
String get listFilter_users => 'Users';
@@ -2354,4 +2737,332 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get listFilter_newGroup => 'New group';
@override
String get pathTrace_you => 'You';
@override
String get pathTrace_failed => 'Path trace failed.';
@override
String get pathTrace_notAvailable => 'Path trace not available.';
@override
String get pathTrace_refreshTooltip => 'Refresh Path Trace.';
@override
String get pathTrace_someHopsNoLocation =>
'One or more of the hops is missing a location!';
@override
String get pathTrace_clearTooltip => 'Clear path.';
@override
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
@override
String losRunFailed(String error) {
return 'Line-of-sight check failed: $error';
}
@override
String get losClearAllPoints => 'Clear all points';
@override
String get losRunToViewElevationProfile =>
'Run LOS to view elevation profile';
@override
String get losMenuTitle => 'LOS Menu';
@override
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
@override
String get losShowDisplayNodes => 'Show display nodes';
@override
String get losCustomPoints => 'Custom points';
@override
String losCustomPointLabel(int index) {
return 'Custom $index';
}
@override
String get losPointA => 'Point A';
@override
String get losPointB => 'Point B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Run LOS';
@override
String get losNoElevationData => 'No elevation data';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: checking...';
@override
String get losStatusNoData => 'LOS: no data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
}
@override
String get losErrorElevationUnavailable =>
'Elevation data unavailable for one or more samples.';
@override
String get losErrorInvalidInput =>
'Invalid points/elevation data for LOS calculation.';
@override
String get losRenameCustomPoint => 'Rename custom point';
@override
String get losPointName => 'Point name';
@override
String get losShowPanelTooltip => 'Show LOS panel';
@override
String get losHidePanelTooltip => 'Hide LOS panel';
@override
String get losElevationAttribution =>
'Elevation data: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radio horizon';
@override
String get losLegendLosBeam => 'LOS beam';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Frequency';
@override
String get losFrequencyInfoTooltip => 'View calculation details';
@override
String get losFrequencyDialogTitle => 'Radio horizon calculation';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Path trace to repeater';
@override
String get contacts_repeaterPing => 'Ping repeater';
@override
String get contacts_roomPathTrace => 'Path trace to room server';
@override
String get contacts_roomPing => 'Ping room server';
@override
String get contacts_chatTraceRoute => 'Path trace route';
@override
String contacts_pathTraceTo(String name) {
return 'Trace route to $name';
}
@override
String get contacts_clipboardEmpty => 'Clipboard is empty.';
@override
String get contacts_invalidAdvertFormat => 'Invalid contact data';
@override
String get contacts_contactImported => 'Contact has been imported.';
@override
String get contacts_contactImportFailed => 'Failed to import contact.';
@override
String get contacts_zeroHopAdvert => 'Zero Hop Advert';
@override
String get contacts_floodAdvert => 'Flood Advert';
@override
String get contacts_copyAdvertToClipboard => 'Copy Advert to Clipboard';
@override
String get contacts_addContactFromClipboard => 'Add Contact from Clipboard';
@override
String get contacts_ShareContact => 'Copy contact to Clipboard';
@override
String get contacts_ShareContactZeroHop => 'Share contact by advert';
@override
String get contacts_zeroHopContactAdvertSent => 'Sent contact by advert.';
@override
String get contacts_zeroHopContactAdvertFailed => 'Failed to send contact.';
@override
String get contacts_contactAdvertCopied => 'Advert copied to Clipboard.';
@override
String get contacts_contactAdvertCopyFailed =>
'Copying advert to Clipboard failed.';
@override
String get notification_activityTitle => 'MeshCore Activity';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'messages',
one: 'message',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'channel messages',
one: 'channel message',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'new nodes',
one: 'new node',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'New $contactType discovered';
}
@override
String get notification_receivedNewMessage => 'Received new message';
@override
String get settings_gpxExportRepeaters =>
'Export repeaters / room server to GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exports repeaters / roomserver with a location to GPX file.';
@override
String get settings_gpxExportContacts => 'Export companions to GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exports companions with a location to GPX file.';
@override
String get settings_gpxExportAll => 'Export all contacts to GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exports all contacts with a location to GPX file.';
@override
String get settings_gpxExportSuccess => 'Successfully exported GPX file.';
@override
String get settings_gpxExportNoContacts => 'No contacts to export.';
@override
String get settings_gpxExportNotAvailable =>
'Not supported on your device/OS';
@override
String get settings_gpxExportError => 'There was an error when exporting.';
@override
String get settings_gpxExportRepeatersRoom =>
'Repeater & room server locations';
@override
String get settings_gpxExportChat => 'Companion locations';
@override
String get settings_gpxExportAllContacts => 'All contacts locations';
@override
String get settings_gpxExportShareText =>
'Map data exported from meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX map data export';
@override
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
@override
String get snrIndicator_lastSeen => 'Last seen';
}
+739 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'De acuerdo';
@override
String get common_connect => 'Conectar';
@@ -140,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_scan => 'Escanea';
@override
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@override
String get device_quickSwitch => 'Cambiar rápidamente';
@@ -200,6 +213,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitud o longitud inválidos.';
@override
String get settings_locationGPSEnable => 'Habilitar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override
String get settings_latitude => 'Latitud';
@@ -296,6 +323,10 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_aboutDescription =>
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nombre';
@@ -320,15 +351,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_presets => 'Preajustes';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frecuencia (MHz)';
@@ -357,10 +379,15 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
@override
String get settings_longRange => 'Largo Alcance';
String get settings_clientRepeat => 'Repetir sin conexión';
@override
String get settings_fastSpeed => 'Velocidad Rápida';
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
@override
String get settings_clientRepeatFreqWarning =>
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
@override
String settings_error(String message) {
@@ -430,6 +457,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Ruso';
@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';
@@ -591,6 +632,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
@override
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
@@ -628,7 +678,35 @@ class AppLocalizationsEs extends AppLocalizations {
'Los contactos aparecerán cuando los dispositivos anuncien.';
@override
String get contacts_searchContacts => 'Buscar contactos...';
String get contacts_unread => 'No leído';
@override
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Buscar contactos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Buscar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Buscar $number$str Usuarios...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Buscar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Buscar $number$str servidores de sala...';
}
@override
String get contacts_noUnreadContacts => 'No contactos sin leer';
@@ -648,6 +726,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestionar Repetidor';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override
String get contacts_roomLogin => 'Inicio de Sala';
@@ -751,6 +832,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Activar canal';
@override
String get channels_deleteChannel => 'Eliminar canal';
@@ -759,6 +846,11 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'No se pudo eliminar el canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" eliminado';
@@ -829,6 +921,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_sortUnread => 'Sin leer';
@override
String get channels_createPrivateChannel => 'Crear un Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Cifrado con una clave secreta.';
@override
String get channels_joinPrivateChannel => 'Únete a un Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Introducir manualmente una clave secreta.';
@override
String get channels_joinPublicChannel => 'Únete al Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Cualquiera puede unirse a este canal.';
@override
String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Cualquiera puede unirse a los canales de hashtag.';
@override
String get channels_scanQrCode => 'Escanear un Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Próximamente';
@override
String get channels_enterHashtag => 'Introducir hashtag';
@override
String get channels_hashtagHint => 'ej. #equipo';
@override
String get chat_noMessages => 'Aún no hay mensajes';
@@ -1007,6 +1139,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestión de Rutas';
@override
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
@override
String get chat_routingMode => 'Modo de enrutamiento';
@@ -1144,9 +1279,33 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Sin leer: $count';
}
@override
String get chat_openLink => '¿Abrir enlace?';
@override
String get chat_openLinkConfirmation =>
'¿Quiere abrir este enlace en su navegador?';
@override
String get chat_open => 'Abrir';
@override
String chat_couldNotOpenLink(String url) {
return 'No se pudo abrir el enlace: $url';
}
@override
String get chat_invalidLink => 'Formato de enlace no válido';
@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';
@@ -1276,6 +1435,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestionar Repetidor';
@override
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
@override
String get map_removeLast => 'Eliminar último';
@override
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
@override
String get mapCache_title => 'Caché de Mapa Offline';
@@ -1472,6 +1643,10 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Inicio fallido: $error';
}
@override
String get login_failedMessage =>
'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.';
@override
String get common_reload => 'Recargar';
@@ -1541,6 +1716,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_management => 'Gestión de Repetidores';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@@ -1564,6 +1742,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbors => 'Vecinos';
@override
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@@ -2248,6 +2432,34 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
@override
String get neighbors_requestTimedOut =>
'Los vecinos solicitan que se desconecte.';
@override
String neighbors_errorLoading(String error) {
return 'Error al cargar vecinos: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clave pública desconocida $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Escuchado: $time hace atrás';
}
@override
String get channelPath_title => 'Ruta del Paquete';
@@ -2351,6 +2563,176 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconocido';
@override
String get community_title => 'Comunidad';
@override
String get community_create => 'Crear Comunidad';
@override
String get community_createDesc =>
'Crear una nueva comunidad y compartir a través de código QR.';
@override
String get community_join => 'Únete';
@override
String get community_joinTitle => 'Únete a la comunidad';
@override
String community_joinConfirmation(String name) {
return '¿Quieres unirte a la comunidad \"$name\"?';
}
@override
String get community_scanQr => 'Escanear Código QR de la Comunidad';
@override
String get community_scanInstructions =>
'Apunte la cámara a un código QR de la comunidad';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidad Pública';
@override
String get community_hashtagChannel => 'Hashtag de la Comunidad';
@override
String get community_name => 'Nombre de la comunidad';
@override
String get community_enterName => 'Introducir nombre de comunidad';
@override
String community_created(String name) {
return 'Comunidad \"$name\" creada';
}
@override
String community_joined(String name) {
return 'Se unió a la comunidad \"$name\"';
}
@override
String get community_qrTitle => 'Compartir Comunidad';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para unirte a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad';
@override
String get community_invalidQrCode => 'Código QR de comunidad no válido';
@override
String get community_alreadyMember => 'Ya eres Miembro';
@override
String community_alreadyMemberMessage(String name) {
return 'Ya eres miembro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Añadir Canal Público de la Comunidad';
@override
String get community_addPublicChannelHint =>
'Añade automáticamente el canal público para esta comunidad.';
@override
String get community_noCommunities => 'Aún no se han unido comunidades.';
@override
String get community_scanOrCreate =>
'Escanear un código QR o crear una comunidad para comenzar';
@override
String get community_manageCommunities => 'Gestionar Comunidades';
@override
String get community_delete => 'Salir de la Comunidad';
@override
String community_deleteConfirm(String name) {
return '¿Salir de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Esto también eliminará $count canal(es) y sus mensajes.';
}
@override
String community_deleted(String name) {
return 'Has salido de la comunidad \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.';
}
@override
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Código secreto regenerado para \"$name\"';
}
@override
String get community_updateSecret => 'Actualizar Contraseña';
@override
String community_secretUpdated(String name) {
return 'Confidencialidad actualizada para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
}
@override
String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad';
@override
String get community_addHashtagChannelDesc =>
'Añadir un canal con hashtag para esta comunidad';
@override
String get community_selectCommunity => 'Seleccionar Comunidad';
@override
String get community_regularHashtag => 'Etiqueta de Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (cualquiera puede unirse)';
@override
String get community_communityHashtag => 'Hashtag de la Comunidad';
@override
String get community_communityHashtagDesc =>
'Exclusivo para miembros de la comunidad';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar y ordenar';
@@ -2372,6 +2754,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_all => 'Todas';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Añadir a favoritos';
@override
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
@override
String get listFilter_users => 'Usuarios';
@@ -2386,4 +2777,340 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nuevo grupo';
@override
String get pathTrace_you => '';
@override
String get pathTrace_failed => 'El trazado de ruta falló.';
@override
String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.';
@override
String get pathTrace_refreshTooltip => 'Actualizar Path Trace';
@override
String get pathTrace_someHopsNoLocation =>
'Uno o más de los lúpulos carecen de una ubicación';
@override
String get pathTrace_clearTooltip => 'Borrar ruta';
@override
String get losSelectStartEnd =>
'Seleccione los nodos de inicio y fin para LOS.';
@override
String losRunFailed(String error) {
return 'Error en la comprobación de la línea de visión: $error';
}
@override
String get losClearAllPoints => 'Borrar todos los puntos';
@override
String get losRunToViewElevationProfile =>
'Ejecute LOS para ver el perfil de elevación';
@override
String get losMenuTitle => 'Menú LOS';
@override
String get losMenuSubtitle =>
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
@override
String get losCustomPoints => 'Puntos personalizados';
@override
String losCustomPointLabel(int index) {
return 'Personalizado $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Ejecutar LOS';
@override
String get losNoElevationData => 'Sin datos de elevación';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: comprobando...';
@override
String get losStatusNoData => 'LOS: sin datos';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
}
@override
String get losErrorElevationUnavailable =>
'Datos de elevación no disponibles para una o más muestras.';
@override
String get losErrorInvalidInput =>
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
@override
String get losRenameCustomPoint =>
'Cambiar el nombre del punto personalizado';
@override
String get losPointName => 'Nombre del punto';
@override
String get losShowPanelTooltip => 'Mostrar panel LOS';
@override
String get losHidePanelTooltip => 'Ocultar panel LOS';
@override
String get losElevationAttribution =>
'Datos de elevación: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
@override
String get losLegendLosBeam => 'Línea de visión';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frecuencia';
@override
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
}
@override
String get contacts_pathTrace => 'Rastreo de caminos';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor';
@override
String get contacts_repeaterPing => 'Pingar repetidor';
@override
String get contacts_roomPathTrace =>
'Rastreo de ruta al servidor de la habitación';
@override
String get contacts_roomPing => 'Pingar servidor de sala';
@override
String get contacts_chatTraceRoute => 'Ruta de trazado';
@override
String contacts_pathTraceTo(String name) {
return 'Rastrear ruta a $name';
}
@override
String get contacts_clipboardEmpty => 'El portapapeles está vacío.';
@override
String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos';
@override
String get contacts_contactImported => 'El contacto ha sido importado.';
@override
String get contacts_contactImportFailed =>
'Contacto no se importó correctamente.';
@override
String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop';
@override
String get contacts_floodAdvert => 'Anuncio de inundación';
@override
String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles';
@override
String get contacts_addContactFromClipboard =>
'Agregar contacto desde el portapapeles';
@override
String get contacts_ShareContact => 'Copiar contacto al Portapapeles';
@override
String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio';
@override
String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'No se pudo enviar el contacto.';
@override
String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.';
@override
String get contacts_contactAdvertCopyFailed =>
'Copiar anuncio al Portapapeles ha fallado.';
@override
String get notification_activityTitle => 'Actividad de MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'mensajes',
one: 'mensaje',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'mensajes de canal',
one: 'mensaje de canal',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nuevos nodos',
one: 'nuevo nodo',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nuevo $contactType descubierto';
}
@override
String get notification_receivedNewMessage => 'Nuevo mensaje recibido';
@override
String get settings_gpxExportRepeaters =>
'Exportar repetidores / servidor de sala a GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exporta repetidores o roomserver con una ubicación a un archivo GPX.';
@override
String get settings_gpxExportContacts => 'Exportar compañeros a GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exporta compañeros con una ubicación a archivo GPX.';
@override
String get settings_gpxExportAll => 'Exportar todos los contactos a GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exporta todos los contactos con una ubicación a un archivo GPX.';
@override
String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.';
@override
String get settings_gpxExportNoContacts => 'No hay contactos para exportar.';
@override
String get settings_gpxExportNotAvailable =>
'No compatible con tu dispositivo/SO';
@override
String get settings_gpxExportError => 'Hubo un error al exportar.';
@override
String get settings_gpxExportRepeatersRoom =>
'Ubicaciones del servidor de repetidor y sala';
@override
String get settings_gpxExportChat => 'Ubicaciones de compañero';
@override
String get settings_gpxExportAllContacts =>
'Todas las ubicaciones de contactos';
@override
String get settings_gpxExportShareText =>
'Datos del mapa exportados desde meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportación de datos de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
@override
String get snrIndicator_lastSeen => 'Visto por última vez';
}
File diff suppressed because it is too large Load Diff
+742 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_cancel => 'Annulla';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connetti';
@@ -140,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_scan => 'Scansiona';
@override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@override
String get device_quickSwitch => 'Passa velocemente';
@@ -200,6 +213,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitudine o longitudine non valida.';
@override
String get settings_locationGPSEnable => 'Abilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Abilita l\'aggiornamento automatico della posizione tramite GPS.';
@override
String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)';
@override
String get settings_locationIntervalInvalid =>
'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.';
@override
String get settings_latitude => 'Latitudine';
@@ -295,6 +322,10 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_aboutDescription =>
'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -319,15 +350,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenza (MHz)';
@@ -356,10 +378,15 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
@override
String get settings_longRange => 'Lungo Raggio';
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
@override
String get settings_fastSpeed => 'Velocità Rapida';
String get settings_clientRepeatSubtitle =>
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
@override
String get settings_clientRepeatFreqWarning =>
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
@override
String settings_error(String message) {
@@ -429,6 +456,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russo';
@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';
@@ -590,6 +631,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@override
String get appSettings_unitsTitle => 'Unità';
@override
String get appSettings_unitsMetric => 'Metrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperiale (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
@@ -627,7 +677,35 @@ class AppLocalizationsIt extends AppLocalizations {
'I contatti appariranno quando i dispositivi pubblicizzano.';
@override
String get contacts_searchContacts => 'Cerca contatti...';
String get contacts_unread => 'Non letti';
@override
String get contacts_searchContactsNoNumber => 'Cerca Contatti...';
@override
String contacts_searchContacts(int number, String str) {
return 'Cerca contatti...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Cerca $number$str Preferiti...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Cerca $number$str Utenti...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Cerca $number$str Ripetitori...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Cerca $number$str server Room...';
}
@override
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
@@ -646,6 +724,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestisci Ripetitore';
@override
String get contacts_manageRoom => 'Gestisci Server Camera';
@override
String get contacts_roomLogin => 'Login Camera';
@@ -749,6 +830,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_editChannel => 'Modifica canale';
@override
String get channels_muteChannel => 'Silenzia canale';
@override
String get channels_unmuteChannel => 'Attiva notifiche canale';
@override
String get channels_deleteChannel => 'Elimina canale';
@@ -757,6 +844,11 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Eliminare \"$name\"? Non può essere annullato.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Impossibile eliminare il canale \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canale \"$name\" eliminato';
@@ -827,6 +919,46 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_sortUnread => 'Non letto';
@override
String get channels_createPrivateChannel => 'Crea un Canale Privato';
@override
String get channels_createPrivateChannelDesc =>
'Protetta con una chiave segreta.';
@override
String get channels_joinPrivateChannel => 'Unisciti a un Canale Privato';
@override
String get channels_joinPrivateChannelDesc =>
'Inserire manualmente una chiave segreta.';
@override
String get channels_joinPublicChannel => 'Unisciti al Canale Pubblico';
@override
String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.';
@override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.';
@override
String get channels_scanQrCode => 'Scansiona un codice QR';
@override
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override
String get channels_enterHashtag => 'Inserisci hashtag';
@override
String get channels_hashtagHint => 'es. #team';
@override
String get chat_noMessages => 'Nessun messaggio ancora';
@@ -1005,6 +1137,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gestione Percorsi';
@override
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
@override
String get chat_routingMode => 'Modalità di routing';
@@ -1143,9 +1278,33 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Non letti: $count';
}
@override
String get chat_openLink => 'Aprire il link?';
@override
String get chat_openLinkConfirmation =>
'Vuoi aprire questo link nel tuo browser?';
@override
String get chat_open => 'Apri';
@override
String chat_couldNotOpenLink(String url) {
return 'Impossibile aprire il link: $url';
}
@override
String get chat_invalidLink => 'Formato di link non valido';
@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';
@@ -1275,6 +1434,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gestisci Ripetitore';
@override
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
@override
String get map_runTrace => 'Esegui Path Trace';
@override
String get map_removeLast => 'Rimuovi ultimo';
@override
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
@override
String get mapCache_title => 'Cache Mappa Offline';
@@ -1470,6 +1641,10 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Accesso fallito: $error';
}
@override
String get login_failedMessage =>
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
@override
String get common_reload => 'Ricaricare';
@@ -1539,6 +1714,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_management => 'Gestione Ripetitori';
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@@ -1562,6 +1740,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbors => 'Vicini';
@override
String get repeater_neighborsSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
String get repeater_settings => 'Impostazioni';
@@ -2248,6 +2433,33 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
@override
String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.';
@override
String neighbors_errorLoading(String error) {
return 'Errore nel caricamento dei vicini: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Chiave pubblica sconosciuta $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Sentito: $time fa';
}
@override
String get channelPath_title => 'Percorso Pacchetto';
@@ -2351,6 +2563,176 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override
String get community_title => 'Comunità';
@override
String get community_create => 'Crea Comunità';
@override
String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.';
@override
String get community_join => 'Unisciti';
@override
String get community_joinTitle => 'Unisciti alla Community';
@override
String community_joinConfirmation(String name) {
return 'Vuoi unirti alla community \"$name\"?';
}
@override
String get community_scanQr => 'Scansiona il QR Code della Community';
@override
String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità';
@override
String get community_showQr => 'Mostra il codice QR';
@override
String get community_publicChannel => 'Comunità Pubblica';
@override
String get community_hashtagChannel => 'Hashtag della Comunità';
@override
String get community_name => 'Nome della Comunità';
@override
String get community_enterName => 'Inserisci il nome della comunità';
@override
String community_created(String name) {
return 'Comunità \"$name\" creata';
}
@override
String community_joined(String name) {
return 'Unito alla comunità \"$name\"';
}
@override
String get community_qrTitle => 'Condividi Comunità';
@override
String community_qrInstructions(String name) {
return 'Scansiona questo codice QR per unirti a $name';
}
@override
String get community_hashtagPrivacyHint =>
'I canali hashtag della community sono accessibili solo ai membri della community';
@override
String get community_invalidQrCode => 'Codice QR della community non valido';
@override
String get community_alreadyMember => 'Già membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".';
}
@override
String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità';
@override
String get community_addPublicChannelHint =>
'Aggiungi automaticamente il canale pubblico per questa community';
@override
String get community_noCommunities => 'Nessun gruppo aggiunto finora';
@override
String get community_scanOrCreate =>
'Scansiona un codice QR o crea una community per iniziare.';
@override
String get community_manageCommunities => 'Gestisci Comunità';
@override
String get community_delete => 'Lascia la Comunità';
@override
String community_deleteConfirm(String name) {
return 'Uscire da \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
}
@override
String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"';
}
@override
String get community_regenerateSecret => 'Ri genera la chiave segreta';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.';
}
@override
String get community_regenerate => 'Rigenera';
@override
String community_secretRegenerated(String name) {
return 'Codice segreto rigenerato per \"$name\"';
}
@override
String get community_updateSecret => 'Aggiorna Segreto';
@override
String community_secretUpdated(String name) {
return 'Segreto aggiornato per \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
}
@override
String get community_addHashtagChannel => 'Aggiungi Hashtag della Community';
@override
String get community_addHashtagChannelDesc =>
'Aggiungi un canale con hashtag per questa community';
@override
String get community_selectCommunity => 'Seleziona Comunità';
@override
String get community_regularHashtag => 'Hashtag regolare';
@override
String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)';
@override
String get community_communityHashtag => 'Hashtag della Comunità';
@override
String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità';
@override
String community_forCommunity(String name) {
return 'Per $name';
}
@override
String get listFilter_tooltip => 'Filtra e ordina';
@@ -2372,6 +2754,15 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_all => 'Tutti';
@override
String get listFilter_favorites => 'Preferiti';
@override
String get listFilter_addToFavorites => 'Aggiungi ai preferiti';
@override
String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti';
@override
String get listFilter_users => 'Utenti';
@@ -2386,4 +2777,343 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nuovo gruppo';
@override
String get pathTrace_you => 'Tu';
@override
String get pathTrace_failed => 'Tracciamento del percorso fallito.';
@override
String get pathTrace_notAvailable =>
'Tracciamento del percorso non disponibile.';
@override
String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.';
@override
String get pathTrace_someHopsNoLocation =>
'Uno o più dei luppoli mancano di una posizione!';
@override
String get pathTrace_clearTooltip => 'Pulisci percorso';
@override
String get losSelectStartEnd =>
'Seleziona i nodi iniziali e finali per la LOS.';
@override
String losRunFailed(String error) {
return 'Controllo della linea di vista fallito: $error';
}
@override
String get losClearAllPoints => 'Cancella tutti i punti';
@override
String get losRunToViewElevationProfile =>
'Eseguire LOS per visualizzare il profilo altimetrico';
@override
String get losMenuTitle => 'Menù LOS';
@override
String get losMenuSubtitle =>
'Tocca i nodi o premi a lungo la mappa per punti personalizzati';
@override
String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione';
@override
String get losCustomPoints => 'Punti personalizzati';
@override
String losCustomPointLabel(int index) {
return 'Personalizzato $index';
}
@override
String get losPointA => 'Punto A';
@override
String get losPointB => 'Punto B';
@override
String losAntennaA(String value, String unit) {
return 'Antenna A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenna B: $value $unit';
}
@override
String get losRun => 'Esegui LOS';
@override
String get losNoElevationData => 'Nessun dato di elevazione';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloccato da $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: controllo...';
@override
String get losStatusNoData => 'LOS: nessun dato';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto';
}
@override
String get losErrorElevationUnavailable =>
'Dati di elevazione non disponibili per uno o più campioni.';
@override
String get losErrorInvalidInput =>
'Dati punti/elevazione non validi per il calcolo della LOS.';
@override
String get losRenameCustomPoint => 'Rinomina punto personalizzato';
@override
String get losPointName => 'Nome del punto';
@override
String get losShowPanelTooltip => 'Mostra il pannello LOS';
@override
String get losHidePanelTooltip => 'Nascondi il pannello LOS';
@override
String get losElevationAttribution =>
'Dati di elevazione: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Orizzonte radio';
@override
String get losLegendLosBeam => 'Linea di vista';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequenza';
@override
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
}
@override
String get contacts_pathTrace => 'Traccia Percorso';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Traccia percorso al ripetitore';
@override
String get contacts_repeaterPing => 'Ripetitore ping';
@override
String get contacts_roomPathTrace =>
'Traccia del percorso al server della stanza';
@override
String get contacts_roomPing => 'Ping al server della stanza';
@override
String get contacts_chatTraceRoute => 'Traccia percorso path';
@override
String contacts_pathTraceTo(String name) {
return 'Traccia percorso verso $name';
}
@override
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
@override
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
@override
String get contacts_contactImported => 'Il contatto è stato importato.';
@override
String get contacts_contactImportFailed =>
'Contatto non importato con successo.';
@override
String get contacts_zeroHopAdvert => 'Annuncio Zero Hop';
@override
String get contacts_floodAdvert => 'Annuncio alluvionale';
@override
String get contacts_copyAdvertToClipboard => 'Copia Annuncio negli Appunti';
@override
String get contacts_addContactFromClipboard =>
'Aggiungere contatto dalla clipboard';
@override
String get contacts_ShareContact => 'Copia contatto negli Appunti';
@override
String get contacts_ShareContactZeroHop =>
'Condividi contatto tramite annuncio';
@override
String get contacts_zeroHopContactAdvertSent =>
'Inviato contatto tramite annuncio.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Invio del contatto non riuscito.';
@override
String get contacts_contactAdvertCopied => 'Annuncio copiato negli Appunti.';
@override
String get contacts_contactAdvertCopyFailed =>
'Copia dell\'annuncio nella Clipboard non riuscita.';
@override
String get notification_activityTitle => 'Attività MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'messaggi',
one: 'messaggio',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'messaggi del canale',
one: 'messaggio del canale',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nuovi nodi',
one: 'nuovo nodo',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nuovo $contactType scoperto';
}
@override
String get notification_receivedNewMessage => 'Nuovo messaggio ricevuto';
@override
String get settings_gpxExportRepeaters =>
'Esporta ripetitori / server di stanza in GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Esporta ripetitori / roomserver con una posizione in un file GPX.';
@override
String get settings_gpxExportContacts => 'Esporta compagni in GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Esporta i compagni con una posizione in un file GPX.';
@override
String get settings_gpxExportAll => 'Esporta tutti i contatti in GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Esporta tutti i contatti con una posizione in un file GPX.';
@override
String get settings_gpxExportSuccess =>
'Esportazione del file GPX completata con successo.';
@override
String get settings_gpxExportNoContacts => 'Nessun contatto da esportare.';
@override
String get settings_gpxExportNotAvailable =>
'Non supportato sul tuo dispositivo/Sistema Operativo';
@override
String get settings_gpxExportError =>
'Si è verificato un errore durante l\'esportazione.';
@override
String get settings_gpxExportRepeatersRoom =>
'Posizioni del server ripetitore e della stanza';
@override
String get settings_gpxExportChat => 'Posizioni dei compagni';
@override
String get settings_gpxExportAllContacts => 'Tutte le posizioni dei contatti';
@override
String get settings_gpxExportShareText =>
'Dati mappa esportati da meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open esportazione dati mappa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
@override
String get snrIndicator_lastSeen => 'Ultimo accesso';
}
File diff suppressed because it is too large Load Diff
+744 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_cancel => 'Anuluj';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Połącz';
@@ -140,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_scan => 'Przeskanuj';
@override
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
@override
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_enableBluetooth => 'Włącz Bluetooth';
@override
String get device_quickSwitch => 'Szybka zmiana';
@@ -202,6 +215,20 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_locationInvalid =>
'Nieprawidłowa szerokość geograficzna lub długość geograficzna.';
@override
String get settings_locationGPSEnable => 'Włącz GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.';
@override
String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.';
@override
String get settings_latitude => 'Szerokość';
@@ -296,6 +323,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_aboutDescription =>
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Imię';
@@ -320,15 +351,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Częstotliwość (MHz)';
@@ -358,10 +380,15 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
@override
String get settings_longRange => 'Długi zasięg';
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
@override
String get settings_fastSpeed => 'Szybka prędkość';
String get settings_clientRepeatSubtitle =>
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
@override
String get settings_clientRepeatFreqWarning =>
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
@override
String settings_error(String message) {
@@ -431,6 +458,19 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Rosyjski';
@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';
@@ -592,6 +632,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Bufor Map Offline';
@override
String get appSettings_unitsTitle => 'Jednostki';
@override
String get appSettings_unitsMetric => 'Metryczne (m / km)';
@override
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
@@ -629,7 +678,35 @@ class AppLocalizationsPl extends AppLocalizations {
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
@override
String get contacts_searchContacts => 'Wyszukaj kontakty...';
String get contacts_unread => 'Nieprzeczytane';
@override
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Wyszukaj kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Wyszukaj $number$str ulubione...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Wyszukaj $number$str Użytkowników...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Wyszukaj $number$str powtórników...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Wyszukaj $number$str serwerów Room...';
}
@override
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
@@ -649,6 +726,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@override
String get contacts_roomLogin => 'Logowanie do pokoju';
@@ -752,6 +832,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_editChannel => 'Edytuj kanał';
@override
String get channels_muteChannel => 'Wycisz kanał';
@override
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
@override
String get channels_deleteChannel => 'Usuń kanał';
@@ -760,6 +846,11 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Usuń \"$name\"? Nie można tego cofnąć.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Nie udało się usunąć kanału \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanał \"$name\" usunięto';
@@ -828,6 +919,46 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_sortUnread => 'Niezgłoszone';
@override
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpieczone kluczem szyfrowym.';
@override
String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału';
@override
String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.';
@override
String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.';
@override
String get channels_joinPublicChannelDesc =>
'Każdy może dołączyć do tego kanału.';
@override
String get channels_joinHashtagChannel =>
'Dołącz do kanału oznaczanego hashtagiem';
@override
String get channels_joinHashtagChannelDesc =>
'Każdy może dołączyć do kanałów z hashtagami.';
@override
String get channels_scanQrCode => 'Skanuj kod QR';
@override
String get channels_scanQrCodeComingSoon => 'Wkrótce';
@override
String get channels_enterHashtag => 'Wprowadź hashtag';
@override
String get channels_hashtagHint => 'np. #zespół';
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
@@ -1007,6 +1138,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_pathManagement => 'Zarządzanie ścieżkami';
@override
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
@override
String get chat_routingMode => 'Tryb routingu';
@@ -1145,9 +1279,33 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Niezgłoszone: $count';
}
@override
String get chat_openLink => 'Otworzyć link?';
@override
String get chat_openLinkConfirmation =>
'Czy chcesz otworzyć ten link w przeglądarce?';
@override
String get chat_open => 'Otwórz';
@override
String chat_couldNotOpenLink(String url) {
return 'Nie można otworzyć linku: $url';
}
@override
String get chat_invalidLink => 'Nieprawidłowy format linku';
@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';
@@ -1277,6 +1435,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
@override
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_removeLast => 'Usuń ostatni';
@override
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
@override
String get mapCache_title => 'Bufor Map Offline';
@@ -1474,6 +1644,10 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Zalogowanie się nie powiodło: $error';
}
@override
String get login_failedMessage =>
'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.';
@override
String get common_reload => 'Ponownie załadować';
@@ -1543,6 +1717,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_management => 'Zarządzanie Powtórzami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@@ -1566,6 +1743,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbors => 'Sąsiedzi';
@override
String get repeater_neighborsSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
String get repeater_settings => 'Ustawienia';
@@ -2246,6 +2430,34 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
@override
String get neighbors_requestTimedOut =>
'Sąsiedzi proszą o wyłączenie timingu.';
@override
String neighbors_errorLoading(String error) {
return 'Błąd podczas ładowania sąsiadów: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nieznana $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Usłyszano: $time temu';
}
@override
String get channelPath_title => 'Ścieżka pakietu';
@@ -2349,6 +2561,176 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nieznany Powtarzacz';
@override
String get community_title => 'Społeczność';
@override
String get community_create => 'Utwórz Społeczność';
@override
String get community_createDesc =>
'Utwórz nową społeczność i udostępnij za pomocą kodu QR.';
@override
String get community_join => 'Dołącz';
@override
String get community_joinTitle => 'Dołącz do społeczności';
@override
String community_joinConfirmation(String name) {
return 'Czy chcesz dołączyć do społeczności \"$name\"?';
}
@override
String get community_scanQr => 'Skanuj QR kod społeczności';
@override
String get community_scanInstructions =>
'Skieruj kamerę w kierunku kodu QR społeczności.';
@override
String get community_showQr => 'Pokaż kod QR';
@override
String get community_publicChannel => 'Społeczność Publiczna';
@override
String get community_hashtagChannel => 'Hashtag Społeczności';
@override
String get community_name => 'Nazwa Społeczności';
@override
String get community_enterName => 'Wprowadź nazwę społeczności';
@override
String community_created(String name) {
return 'Społeczność \"$name\" została utworzona';
}
@override
String community_joined(String name) {
return 'Dołączył do społeczności \"$name\"';
}
@override
String get community_qrTitle => 'Dziel się Społecznością';
@override
String community_qrInstructions(String name) {
return 'Skanuj ten kod QR, aby dołączyć $name';
}
@override
String get community_hashtagPrivacyHint =>
'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności';
@override
String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.';
@override
String get community_alreadyMember => 'Już jesteś członkiem.';
@override
String community_alreadyMemberMessage(String name) {
return 'Jesteś już członkiem \"$name\".';
}
@override
String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności';
@override
String get community_addPublicChannelHint =>
'Automatycznie dodaj kanał publiczny dla tej społeczności.';
@override
String get community_noCommunities =>
'Nie dołączono jeszcze żadnych społeczności.';
@override
String get community_scanOrCreate =>
'Skanuj kod QR lub utwórz społeczność, aby zacząć.';
@override
String get community_manageCommunities => 'Zarządzaj Grupami';
@override
String get community_delete => 'Opuszczenie Społeczności';
@override
String community_deleteConfirm(String name) {
return 'Opuścić \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.';
}
@override
String community_deleted(String name) {
return 'Opuszczono społeczność \"$name\"';
}
@override
String get community_regenerateSecret => 'Zregeneruj sekret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
}
@override
String get community_regenerate => 'Zregeneruj';
@override
String community_secretRegenerated(String name) {
return 'Hasło ponownie wygenerowane dla \"$name\"';
}
@override
String get community_updateSecret => 'Zaktualizuj tajny klucz';
@override
String community_secretUpdated(String name) {
return 'Hasło zaktualizowane dla \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj hashtag społeczności';
@override
String get community_addHashtagChannelDesc =>
'Dodaj kanał z hashtagiem dla tej społeczności';
@override
String get community_selectCommunity => 'Wybierz społeczność';
@override
String get community_regularHashtag => 'Hashtag regular';
@override
String get community_regularHashtagDesc =>
'Publiczny hashtag (każdy może dołączyć)';
@override
String get community_communityHashtag => 'Hashtag Społeczności';
@override
String get community_communityHashtagDesc =>
'Dostępne tylko dla członków społeczności';
@override
String community_forCommunity(String name) {
return 'Dla $name';
}
@override
String get listFilter_tooltip => 'Filtruj i sortuj';
@@ -2370,6 +2752,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_all => 'Wszystko';
@override
String get listFilter_favorites => 'Ulubione';
@override
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
@override
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
@override
String get listFilter_users => 'Użytkownicy';
@@ -2384,4 +2775,345 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nowa grupa';
@override
String get pathTrace_you => 'Ty';
@override
String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.';
@override
String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.';
@override
String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.';
@override
String get pathTrace_someHopsNoLocation =>
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
@override
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
@override
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
@override
String losRunFailed(String error) {
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
}
@override
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
@override
String get losRunToViewElevationProfile =>
'Uruchom LOS, aby wyświetlić profil wysokości';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
@override
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
@override
String get losCustomPoints => 'Punkty niestandardowe';
@override
String losCustomPointLabel(int index) {
return 'Niestandardowe $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Uruchom LOS-a';
@override
String get losNoElevationData => 'Brak danych o wysokości';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: sprawdzam...';
@override
String get losStatusNoData => 'LOS: brak danych';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
}
@override
String get losErrorElevationUnavailable =>
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
@override
String get losErrorInvalidInput =>
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
@override
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
@override
String get losPointName => 'Nazwa punktu';
@override
String get losShowPanelTooltip => 'Pokaż panel LOS';
@override
String get losHidePanelTooltip => 'Ukryj panel LOS';
@override
String get losElevationAttribution =>
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horyzont radiowy';
@override
String get losLegendLosBeam => 'Linia widoczności';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Częstotliwość';
@override
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
@override
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
}
@override
String get contacts_pathTrace => 'Śledzenie Ścieżek';
@override
String get contacts_ping => 'Pingować';
@override
String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera';
@override
String get contacts_repeaterPing => 'Repeater pingowy';
@override
String get contacts_roomPathTrace =>
'Śledzenie ścieżki do serwera pokojowego';
@override
String get contacts_roomPing => 'Pinguj serwer pokoju';
@override
String get contacts_chatTraceRoute => 'Śledź trasę promienia';
@override
String contacts_pathTraceTo(String name) {
return 'Śledź trasę do $name';
}
@override
String get contacts_clipboardEmpty => 'Schowek jest pusty.';
@override
String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe';
@override
String get contacts_contactImported => 'Kontakt został zaimportowany.';
@override
String get contacts_contactImportFailed =>
'Kontakt nie został zaimportowany.';
@override
String get contacts_zeroHopAdvert => 'Reklama Zero Hop';
@override
String get contacts_floodAdvert => 'Reklama powodziowa';
@override
String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka';
@override
String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka';
@override
String get contacts_ShareContact => 'Kopiuj kontakt do schowka';
@override
String get contacts_ShareContactZeroHop =>
'Udostępnij kontakt przez ogłoszenie';
@override
String get contacts_zeroHopContactAdvertSent =>
'Wysłano kontakt przez ogłoszenie.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Nie udało się wysłać kontaktu.';
@override
String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.';
@override
String get contacts_contactAdvertCopyFailed =>
'Kopiowanie ogłoszenia do schowka nie powiodło się.';
@override
String get notification_activityTitle => 'Aktywność MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'wiadomości',
many: 'wiadomości',
few: 'wiadomości',
one: 'wiadomość',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'wiadomości kanału',
many: 'wiadomości kanału',
few: 'wiadomości kanału',
one: 'wiadomość kanału',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nowych węzłów',
many: 'nowych węzłów',
few: 'nowe węzły',
one: 'nowy węzeł',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nowy $contactType wykryty';
}
@override
String get notification_receivedNewMessage => 'Otrzymano nową wiadomość';
@override
String get settings_gpxExportRepeaters =>
'Eksportuj powtórki / serwer pokojowy do GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Eksportuje towarzyszy z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.';
@override
String get settings_gpxExportNoContacts =>
'Brak kontaktów do wyeksportowania.';
@override
String get settings_gpxExportNotAvailable =>
'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym';
@override
String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.';
@override
String get settings_gpxExportRepeatersRoom =>
'Lokalizacje serwerów powtarzających i pomieszczeń';
@override
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
@override
String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów';
@override
String get settings_gpxExportShareText =>
'Dane mapy wyeksportowane z meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'Eksport danych mapy GPX meshcore-open';
@override
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
}
+737 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Conectar';
@@ -140,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_scan => 'Digitalizar';
@override
String get scanner_bluetoothOff => 'Bluetooth está desativado';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@override
String get device_quickSwitch => 'Mudar rapidamente';
@@ -201,6 +214,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitude ou longitude inválidos.';
@override
String get settings_locationGPSEnable => 'Ativar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita a atualização automática da localização via GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.';
@override
String get settings_latitude => 'Latitude';
@@ -297,6 +324,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_aboutDescription =>
'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dados de elevação LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Nome';
@@ -321,15 +352,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequência (MHz)';
@@ -358,10 +380,15 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
@override
String get settings_longRange => 'Alcance Longo';
String get settings_clientRepeat => 'Repetição sem rede';
@override
String get settings_fastSpeed => 'Velocidade Rápida';
String get settings_clientRepeatSubtitle =>
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
@override
String get settings_clientRepeatFreqWarning =>
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
@override
String settings_error(String message) {
@@ -431,6 +458,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russo';
@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';
@@ -591,6 +632,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
@override
String get appSettings_unitsTitle => 'Unidades';
@override
String get appSettings_unitsMetric => 'Métrico (m/km)';
@override
String get appSettings_unitsImperial => 'Imperial (ft/mi)';
@override
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
@@ -629,7 +679,35 @@ class AppLocalizationsPt extends AppLocalizations {
'Os contatos serão exibidos quando os dispositivos anunciarem.';
@override
String get contacts_searchContacts => 'Pesquisar contatos...';
String get contacts_unread => 'Não lido';
@override
String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...';
@override
String contacts_searchContacts(int number, String str) {
return 'Pesquisar contatos...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Pesquisar $number$str Favoritos...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Pesquisar $number$str Usuários...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Pesquisar $number$str Repetidores...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Pesquisar $number$str servidores de sala...';
}
@override
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
@@ -649,6 +727,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gerenciar Repetidor';
@override
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
@override
String get contacts_roomLogin => 'Login no Quarto';
@@ -752,6 +833,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_editChannel => 'Editar canal';
@override
String get channels_muteChannel => 'Silenciar canal';
@override
String get channels_unmuteChannel => 'Ativar canal';
@override
String get channels_deleteChannel => 'Excluir canal';
@@ -760,6 +847,11 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Excluir \"$name\"? Não pode ser desfeito.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Falha ao excluir o canal \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Canal \"$name\" excluído';
@@ -829,6 +921,46 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_sortUnread => 'Não lido';
@override
String get channels_createPrivateChannel => 'Criar um Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Protegido com uma chave secreta.';
@override
String get channels_joinPrivateChannel => 'Junte-se a um Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Inserir uma chave secreta manualmente.';
@override
String get channels_joinPublicChannel => 'Junte-se ao Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Qualquer pessoa pode entrar neste canal.';
@override
String get channels_joinHashtagChannel => 'Junte-se a um Canal com Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Qualquer pessoa pode participar de canais com hashtag.';
@override
String get channels_scanQrCode => 'Digitalizar um Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Em breve';
@override
String get channels_enterHashtag => 'Insira hashtag';
@override
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
@@ -1007,6 +1139,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_pathManagement => 'Gerenciamento de Caminhos';
@override
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
@override
String get chat_routingMode => 'Modo de roteamento';
@@ -1144,9 +1279,33 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Não lido: $count';
}
@override
String get chat_openLink => 'Abrir link?';
@override
String get chat_openLinkConfirmation =>
'Deseja abrir este link no seu navegador?';
@override
String get chat_open => 'Abrir';
@override
String chat_couldNotOpenLink(String url) {
return 'Não foi possível abrir o link: $url';
}
@override
String get chat_invalidLink => 'Formato de link inválido';
@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.';
@@ -1277,6 +1436,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_manageRepeater => 'Gerenciar Repetidor';
@override
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
@override
String get map_runTrace => 'Executar Traçado de Caminho';
@override
String get map_removeLast => 'Remover Último';
@override
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
@override
String get mapCache_title => 'Cache de Mapa Offline';
@@ -1472,6 +1643,10 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Login falhou: $error';
}
@override
String get login_failedMessage =>
'Falha no login. A senha está incorreta ou o repetidor está inacessível.';
@override
String get common_reload => 'Recarregar';
@@ -1541,6 +1716,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_management => 'Gerenciamento de Repetidor';
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@@ -1564,6 +1742,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbors => 'Vizinhos';
@override
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@@ -2248,6 +2432,34 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
@override
String get neighbors_requestTimedOut =>
'Vizinhos solicitam tempo limite esgotado.';
@override
String neighbors_errorLoading(String error) {
return 'Erro ao carregar vizinhos: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@override
String neighbors_unknownContact(String pubkey) {
return '$pubkey Desconhecido';
}
@override
String neighbors_heardAgo(String time) {
return 'Ouvido: $time atrás';
}
@override
String get channelPath_title => 'Rótulo de Caminho de Pacote';
@@ -2351,6 +2563,177 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconhecido';
@override
String get community_title => 'Comunidade';
@override
String get community_create => 'Criar Comunidade';
@override
String get community_createDesc =>
'Crie uma nova comunidade e compartilhe via código QR.';
@override
String get community_join => 'Junte-se';
@override
String get community_joinTitle => 'Junte-se à Comunidade';
@override
String community_joinConfirmation(String name) {
return 'Você gostaria de se juntar à comunidade \"$name\"?';
}
@override
String get community_scanQr => 'Digitalizar a QR Code da Comunidade';
@override
String get community_scanInstructions =>
'Aponte a câmera para um código QR da comunidade';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidade Pública';
@override
String get community_hashtagChannel => 'Hashtag da Comunidade';
@override
String get community_name => 'Nome da Comunidade';
@override
String get community_enterName => 'Insira o nome da comunidade';
@override
String community_created(String name) {
return 'Comunidade \"$name\" criada';
}
@override
String community_joined(String name) {
return 'Juntou-se à comunidade \"$name\"';
}
@override
String get community_qrTitle => 'Partilhar Comunidade';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para juntar-se a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade';
@override
String get community_invalidQrCode => 'Código QR da comunidade inválido';
@override
String get community_alreadyMember => 'Já é Membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Você já é membro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Adicionar Canal Público da Comunidade';
@override
String get community_addPublicChannelHint =>
'Adicionar automaticamente o canal público para esta comunidade';
@override
String get community_noCommunities =>
'Ainda não foram adicionadas comunidades.';
@override
String get community_scanOrCreate =>
'Escaneie um código QR ou crie uma comunidade para começar.';
@override
String get community_manageCommunities => 'Gerenciar Comunidades';
@override
String get community_delete => 'Deixar Comunidade';
@override
String community_deleteConfirm(String name) {
return 'Sair de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Isso também excluirá $count canal/canais e suas mensagens.';
}
@override
String community_deleted(String name) {
return 'Saiu da comunidade \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerar Senha Segura';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.';
}
@override
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Senha secreta regenerada para \"$name\"';
}
@override
String get community_updateSecret => 'Atualizar Segredo';
@override
String community_secretUpdated(String name) {
return 'Segredo atualizado para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
}
@override
String get community_addHashtagChannel => 'Adicionar Hashtag da Comunidade';
@override
String get community_addHashtagChannelDesc =>
'Adicionar um canal de hashtag para esta comunidade';
@override
String get community_selectCommunity => 'Selecione Comunidade';
@override
String get community_regularHashtag => 'Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (qualquer pessoa pode participar)';
@override
String get community_communityHashtag => 'Hashtag da Comunidade';
@override
String get community_communityHashtagDesc =>
'Apenas para membros da comunidade';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar e ordenar';
@@ -2372,6 +2755,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_all => 'Tudo';
@override
String get listFilter_favorites => 'Favoritos';
@override
String get listFilter_addToFavorites => 'Adicionar aos favoritos';
@override
String get listFilter_removeFromFavorites => 'Remover da lista de favoritos';
@override
String get listFilter_users => 'Usuários';
@@ -2386,4 +2778,337 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get listFilter_newGroup => 'Novo grupo';
@override
String get pathTrace_you => 'Você';
@override
String get pathTrace_failed => 'Falha no rastreamento de caminho.';
@override
String get pathTrace_notAvailable => 'Traçado de caminho não disponível.';
@override
String get pathTrace_refreshTooltip => 'Atualizar Path Trace.';
@override
String get pathTrace_someHopsNoLocation =>
'Um ou mais dos lúpulos estão sem localização!';
@override
String get pathTrace_clearTooltip => 'Limpar caminho';
@override
String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.';
@override
String losRunFailed(String error) {
return 'Falha na verificação da linha de visão: $error';
}
@override
String get losClearAllPoints => 'Limpe todos os pontos';
@override
String get losRunToViewElevationProfile =>
'Execute o LOS para visualizar o perfil de elevação';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados';
@override
String get losShowDisplayNodes => 'Mostrar nós de exibição';
@override
String get losCustomPoints => 'Pontos personalizados';
@override
String losCustomPointLabel(int index) {
return '$index personalizado';
}
@override
String get losPointA => 'Ponto A';
@override
String get losPointB => 'Ponto B';
@override
String losAntennaA(String value, String unit) {
return 'Antena A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antena B: $value $unit';
}
@override
String get losRun => 'Executar LOS';
@override
String get losNoElevationData => 'Sem dados de elevação';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: verificando...';
@override
String get losStatusNoData => 'LOS: sem dados';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido';
}
@override
String get losErrorElevationUnavailable =>
'Dados de elevação indisponíveis para uma ou mais amostras.';
@override
String get losErrorInvalidInput =>
'Dados de pontos/elevação inválidos para cálculo de LOS.';
@override
String get losRenameCustomPoint => 'Renomear ponto personalizado';
@override
String get losPointName => 'Nome do ponto';
@override
String get losShowPanelTooltip => 'Mostrar painel LOS';
@override
String get losHidePanelTooltip => 'Ocultar painel LOS';
@override
String get losElevationAttribution =>
'Dados de elevação: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte de rádio';
@override
String get losLegendLosBeam => 'Linha de visada';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequência';
@override
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
}
@override
String get contacts_pathTrace => 'Traçado de Caminho';
@override
String get contacts_ping => 'Pingar';
@override
String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor';
@override
String get contacts_repeaterPing => 'Pingar repetidor';
@override
String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala';
@override
String get contacts_roomPing => 'Pingar servidor da sala';
@override
String get contacts_chatTraceRoute => 'Rastrear rota do caminho';
@override
String contacts_pathTraceTo(String name) {
return 'Rastrear rota para $name';
}
@override
String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.';
@override
String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos';
@override
String get contacts_contactImported => 'Contato foi importado.';
@override
String get contacts_contactImportFailed => 'Contato falhou ao ser importado.';
@override
String get contacts_zeroHopAdvert => 'Anúncio Zero Hop';
@override
String get contacts_floodAdvert => 'Anúncio de Inundação';
@override
String get contacts_copyAdvertToClipboard =>
'Copiar Anúncio para Área de Transferência';
@override
String get contacts_addContactFromClipboard =>
'Adicionar Contato da Área de Transferência';
@override
String get contacts_ShareContact =>
'Copiar contato para Área de Transferência';
@override
String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio';
@override
String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.';
@override
String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.';
@override
String get contacts_contactAdvertCopied =>
'Anúncio copiado para a Área de Transferência.';
@override
String get contacts_contactAdvertCopyFailed =>
'Cópia do anúncio para a Área de Transferência falhou.';
@override
String get notification_activityTitle => 'Atividade MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'mensagens',
one: 'mensagem',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'mensagens de canal',
one: 'mensagem de canal',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'novos nós',
one: 'novo nó',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Novo $contactType descoberto';
}
@override
String get notification_receivedNewMessage => 'Nova mensagem recebida';
@override
String get settings_gpxExportRepeaters =>
'Exportar repetidores / servidor de sala para GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exporta repetidores / roomserver com localização para arquivo GPX.';
@override
String get settings_gpxExportContacts => 'Exportar companheiros para GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exporta companheiros com uma localização para um arquivo GPX.';
@override
String get settings_gpxExportAll => 'Exportar todos os contatos para GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exporta todos os contatos com uma localização para um arquivo GPX.';
@override
String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.';
@override
String get settings_gpxExportNoContacts => 'Nenhum contato para exportar.';
@override
String get settings_gpxExportNotAvailable =>
'Não suportado no seu dispositivo/SO';
@override
String get settings_gpxExportError => 'Ocorreu um erro ao exportar.';
@override
String get settings_gpxExportRepeatersRoom =>
'Localizações do servidor de repetidor e sala';
@override
String get settings_gpxExportChat => 'Localizações de companheiros';
@override
String get settings_gpxExportAllContacts => 'Todos os locais de contatos';
@override
String get settings_gpxExportShareText =>
'Dados do mapa exportados do meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exportação de dados de mapa GPX';
@override
String get snrIndicator_nearByRepeaters => 'Repetidores Próximos';
@override
String get snrIndicator_lastSeen => 'Visto pela última vez';
}
File diff suppressed because it is too large Load Diff
+736 -13
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_cancel => 'Zrušiť';
@override
String get common_ok => 'OK\nDobre';
@override
String get common_connect => 'Pripojiť';
@@ -140,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_scan => 'Skončiť';
@override
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
@override
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@override
String get device_quickSwitch => 'Rýchle prepínač';
@@ -200,6 +213,20 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.';
@override
String get settings_locationGPSEnable => 'Aktivovať GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Povolí automatické aktualizovanie polohy pomocou GPS.';
@override
String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.';
@override
String get settings_latitude => 'Súradnica';
@@ -293,6 +320,10 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_aboutDescription =>
'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Meno';
@@ -317,15 +348,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_presets => 'Prednastavenia';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvencia (MHz)';
@@ -354,10 +376,15 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
@override
String get settings_longRange => 'Dlhý dosah';
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
@override
String get settings_fastSpeed => 'Rýchla rýchlosť';
String get settings_clientRepeatSubtitle =>
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
@override
String get settings_clientRepeatFreqWarning =>
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
@override
String settings_error(String message) {
@@ -427,6 +454,19 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Ruština';
@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';
@@ -585,6 +625,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
@override
String get appSettings_unitsTitle => 'Jednotky';
@override
String get appSettings_unitsMetric => 'Metrické (m / km)';
@override
String get appSettings_unitsImperial => 'Imperiálne (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
@@ -622,7 +671,35 @@ class AppLocalizationsSk extends AppLocalizations {
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
@override
String get contacts_searchContacts => 'Vyhľadávajte kontakty...';
String get contacts_unread => 'Neprečítané';
@override
String get contacts_searchContactsNoNumber => 'Hľadať kontakty...';
@override
String contacts_searchContacts(int number, String str) {
return 'Vyhľadávajte kontakty...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Hľadať $number$str obľúbené...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Hľadať $number$str používateľov...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Hľadať $number$str opakovače...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Hľadaj $number$str serverov miestností...';
}
@override
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
@@ -642,6 +719,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
@override
String get contacts_manageRoom => 'Spravovať server miestnosti';
@override
String get contacts_roomLogin => 'Prihlásenie do miestnosti';
@@ -747,6 +827,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_editChannel => 'Upraviť kanál';
@override
String get channels_muteChannel => 'Stlmiť kanál';
@override
String get channels_unmuteChannel => 'Zrušiť stlmenie kanála';
@override
String get channels_deleteChannel => 'Odstrániť kanál';
@@ -755,6 +841,11 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Kanál \"$name\" sa nepodarilo odstrániť';
}
@override
String channels_channelDeleted(String name) {
return 'Kanál \"$name\" bol odstránený';
@@ -824,6 +915,45 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_sortUnread => 'Nezriadené';
@override
String get channels_createPrivateChannel => 'Vytvorte súkromný kanál';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpečené pomocou tajného kľúča.';
@override
String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu';
@override
String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.';
@override
String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu';
@override
String get channels_joinPublicChannelDesc =>
'Któvek sátó na tutó kanalizovát.';
@override
String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu';
@override
String get channels_joinHashtagChannelDesc =>
'Ktoekolikoľvek sa môže pridať do hashtag kanálov.';
@override
String get channels_scanQrCode => 'Skenujte QR kód';
@override
String get channels_scanQrCodeComingSoon => 'Čoskoro';
@override
String get channels_enterHashtag => 'Zadajte hashtag';
@override
String get channels_hashtagHint => 'napr. #tím';
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@@ -1003,6 +1133,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_pathManagement => 'Správa ciest';
@override
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
@override
String get chat_routingMode => 'Režim trasy';
@@ -1061,7 +1194,7 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_clearPathSubtitle =>
'Znovu nájsť vynútene pri nasledujacej pošlite';
'Znovu nájsť vynútene pri nasledujúcej pošlite';
@override
String get chat_pathCleared =>
@@ -1141,9 +1274,33 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Nezriadené: $count';
}
@override
String get chat_openLink => 'Otvoriť odkaz?';
@override
String get chat_openLinkConfirmation =>
'Chcete otvoriť tento odkaz v prehliadači?';
@override
String get chat_open => 'Otvoriť';
@override
String chat_couldNotOpenLink(String url) {
return 'Nepodarilo sa otvoriť odkaz: $url';
}
@override
String get chat_invalidLink => 'Neplatný formát odkazu';
@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';
@@ -1273,6 +1430,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_manageRepeater => 'Spravovať Opakovanie';
@override
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
@override
String get map_removeLast => 'Odstrániť posledný';
@override
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
@override
String get mapCache_title => 'Offline Mapa Pamäť';
@@ -1468,6 +1637,10 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Prihlásenie zlyhalo: $error';
}
@override
String get login_failedMessage =>
'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.';
@override
String get common_reload => 'Načítať';
@@ -1537,6 +1710,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_management => 'Správa opakérov';
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_managementTools => 'Nástroje na správu';
@@ -1560,6 +1736,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbors => 'Súsezný';
@override
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@@ -2237,6 +2419,34 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
@override
String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.';
@override
String neighbors_errorLoading(String error) {
return 'Chyba pri načítaní susedov: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
'Nie je dostupná žiadna informácia o susedoch.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Neznáma $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Počuli sme to: $time dozadu';
}
@override
String get channelPath_title => 'Cesta balíka';
@@ -2340,6 +2550,175 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Neznáme opakovače';
@override
String get community_title => 'Komunita';
@override
String get community_create => 'Vytvoriť komunitu';
@override
String get community_createDesc =>
'Vytvorte novú komunitu a zdieľajte cez QR kód.';
@override
String get community_join => 'Pripojiť';
@override
String get community_joinTitle => 'Pripojiť sa k spoločenstvu';
@override
String community_joinConfirmation(String name) {
return 'Chceš sa pridať do komunity \"$name\"?';
}
@override
String get community_scanQr => 'Skontrolujte komunitný QR kód';
@override
String get community_scanInstructions =>
'Zamerte kameru na komunitný QR kód.';
@override
String get community_showQr => 'Zobraziť QR kód';
@override
String get community_publicChannel => 'Komunita verejná';
@override
String get community_hashtagChannel => 'Komunitný Hashtag';
@override
String get community_name => 'Komunita';
@override
String get community_enterName => 'Zadajte názov komunity';
@override
String community_created(String name) {
return 'Komunita \"$name\" vytvorená';
}
@override
String community_joined(String name) {
return 'Pripojená komunita \"$name\"';
}
@override
String get community_qrTitle => 'Zdieľť komunitu';
@override
String community_qrInstructions(String name) {
return 'Skenejte tento QR kód, aby ste sa pripojili k $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtagové kanály komunity sú prístupné len členom komunity';
@override
String get community_invalidQrCode => 'Neplatná QR kód komunity.';
@override
String get community_alreadyMember => 'Už ste členom.';
@override
String community_alreadyMemberMessage(String name) {
return 'Vy ste už členom \"$name\".';
}
@override
String get community_addPublicChannel => 'Pridať verejný komunikačný kanál';
@override
String get community_addPublicChannelHint =>
'Automaticky prida verejný kanál pre túto komunitu.';
@override
String get community_noCommunities =>
'Zatiaľ ste sa nepripojili k žiadnej komunite';
@override
String get community_scanOrCreate =>
'Skene QR kód alebo vytvor komunitu na začiatok.';
@override
String get community_manageCommunities => 'Spravovať komunity';
@override
String get community_delete => 'Nechajte komunitu';
@override
String community_deleteConfirm(String name) {
return 'Opustiť \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.';
}
@override
String community_deleted(String name) {
return 'Opustená komunita \"$name\"';
}
@override
String get community_regenerateSecret => 'Zobraziť nový tajný kód';
@override
String community_regenerateSecretConfirm(String name) {
return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.';
}
@override
String get community_regenerate => 'Znovu vygenerovať';
@override
String community_secretRegenerated(String name) {
return 'Záznam pre \"$name\" bol regenerovaný tajne';
}
@override
String get community_updateSecret => 'Aktualizovať tajné heslo';
@override
String community_secretUpdated(String name) {
return 'Zmena tajnej slova pre \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
}
@override
String get community_addHashtagChannel => 'Pridať komunitný hashtag';
@override
String get community_addHashtagChannelDesc =>
'Pridajte hashtagový kanál pre túto komunitu.';
@override
String get community_selectCommunity => 'Vyberte komunitu';
@override
String get community_regularHashtag => 'Zvyčajný hashtag';
@override
String get community_regularHashtagDesc =>
'Veľký hashtag (ktočokoľvek sa môže pridať)';
@override
String get community_communityHashtag => 'Komunitný Hashtag';
@override
String get community_communityHashtagDesc => 'Špecifické pre členov komunity';
@override
String community_forCommunity(String name) {
return 'Pre $name';
}
@override
String get listFilter_tooltip => 'Filtrovať a triediť';
@@ -2361,6 +2740,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_all => 'Všetko';
@override
String get listFilter_favorites => 'Obľúbené';
@override
String get listFilter_addToFavorites => 'Pridaj do obľúbených';
@override
String get listFilter_removeFromFavorites => 'Odstrániť z označení';
@override
String get listFilter_users => 'Používatelia';
@@ -2375,4 +2763,339 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nová skupina';
@override
String get pathTrace_you => 'Vy';
@override
String get pathTrace_failed => 'Sledovanie cesty zlyhalo.';
@override
String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.';
@override
String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.';
@override
String get pathTrace_someHopsNoLocation =>
'Jedna alebo viac chmeľov chýba lokalita!';
@override
String get pathTrace_clearTooltip => 'Zmazať cestu';
@override
String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.';
@override
String losRunFailed(String error) {
return 'Kontrola priamej viditeľnosti zlyhala: $error';
}
@override
String get losClearAllPoints => 'Vymazať všetky body';
@override
String get losRunToViewElevationProfile =>
'Ak chcete zobraziť výškový profil, spustite LOS';
@override
String get losMenuTitle => 'Menu LOS';
@override
String get losMenuSubtitle =>
'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body';
@override
String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia';
@override
String get losCustomPoints => 'Vlastné body';
@override
String losCustomPointLabel(int index) {
return 'Vlastné $index';
}
@override
String get losPointA => 'Bod A';
@override
String get losPointB => 'Bod B';
@override
String losAntennaA(String value, String unit) {
return 'Anténa A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Anténa B: $value $unit';
}
@override
String get losRun => 'Spustite LOS';
@override
String get losNoElevationData => 'Žiadne údaje o nadmorskej výške';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blokovaný $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kontrolujem...';
@override
String get losStatusNoData => 'LOS: žiadne údaje';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme';
}
@override
String get losErrorElevationUnavailable =>
'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.';
@override
String get losErrorInvalidInput =>
'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.';
@override
String get losRenameCustomPoint => 'Premenovať vlastný bod';
@override
String get losPointName => 'Názov bodu';
@override
String get losShowPanelTooltip => 'Zobraziť panel LOS';
@override
String get losHidePanelTooltip => 'Skryť panel LOS';
@override
String get losElevationAttribution =>
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Rádiový horizont';
@override
String get losLegendLosBeam => 'Priama viditeľnosť';
@override
String get losLegendTerrain => 'Terén';
@override
String get losFrequencyLabel => 'Frekvencia';
@override
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
@override
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
}
@override
String get contacts_pathTrace => 'Sledovanie lúčov';
@override
String get contacts_ping => 'Pingovať';
@override
String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču';
@override
String get contacts_repeaterPing => 'Pingovať opakovač';
@override
String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti';
@override
String get contacts_roomPing => 'Ping server miestnosti';
@override
String get contacts_chatTraceRoute => 'Sledovať trasu lúča';
@override
String contacts_pathTraceTo(String name) {
return 'Sledovať trasu k $name';
}
@override
String get contacts_clipboardEmpty => 'Schránka je prázdna.';
@override
String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje';
@override
String get contacts_contactImported => 'Kontakt bol importovaný.';
@override
String get contacts_contactImportFailed =>
'Kontakt sa nepodarilo importovať.';
@override
String get contacts_zeroHopAdvert => 'Inzerát Zero Hop';
@override
String get contacts_floodAdvert => 'Inzerát povodní';
@override
String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky';
@override
String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky';
@override
String get contacts_ShareContact => 'Kopírovať kontakt do schránky';
@override
String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát';
@override
String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Zlyhalo odoslanie kontaktu.';
@override
String get contacts_contactAdvertCopied =>
'Inzerát bol skopírovaný do schránky.';
@override
String get contacts_contactAdvertCopyFailed =>
'Kopírovanie inzerátu do schránky zlyhalo.';
@override
String get notification_activityTitle => 'Aktivita MeshCore';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'správ',
few: 'správy',
one: 'správa',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'správ kanálu',
few: 'správy kanálu',
one: 'správa kanálu',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nových uzlov',
few: 'nové uzly',
one: 'nový uzol',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nový $contactType objavený';
}
@override
String get notification_receivedNewMessage => 'Prijatá nová správa';
@override
String get settings_gpxExportRepeaters =>
'Exportovať repeater / server miestnosti do GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exportuje repeater / roomserver s lokalitou do súboru GPX.';
@override
String get settings_gpxExportContacts => 'Export sprievodcov do GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exportuje sprievodcov s umiestnením do súboru GPX.';
@override
String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exportuje všetky kontakty s lokalitou do súboru GPX.';
@override
String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.';
@override
String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.';
@override
String get settings_gpxExportNotAvailable =>
'Nie je podporované na vašom zariadení/operáciomnom systéme';
@override
String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.';
@override
String get settings_gpxExportRepeatersRoom =>
'Umiestnenia opakovačov a serverov miestností';
@override
String get settings_gpxExportChat => 'Lokácie sprievodcov';
@override
String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality';
@override
String get settings_gpxExportShareText =>
'Mapové údaje exportované z meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export dát GPX mapových údajov';
@override
String get snrIndicator_nearByRepeaters => 'Miestne opakovače';
@override
String get snrIndicator_lastSeen => 'Naposledy videný';
}
File diff suppressed because it is too large Load Diff
+730 -12
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_cancel => 'Avbryt';
@override
String get common_ok => 'Okej';
@override
String get common_connect => 'Anslut';
@@ -139,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_scan => 'Skanna';
@override
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
@override
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@override
String get device_quickSwitch => 'Snabb växling';
@@ -199,6 +212,20 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_locationInvalid => 'Ogiltig latitud eller longitud.';
@override
String get settings_locationGPSEnable => 'Aktivera GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.';
@override
String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)';
@override
String get settings_locationIntervalInvalid =>
'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.';
@override
String get settings_latitude => 'Latitud';
@@ -290,6 +317,10 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_aboutDescription =>
'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.';
@override
String get settings_aboutOpenMeteoAttribution =>
'LOS-höjddata: Open-Meteo (CC BY 4.0)';
@override
String get settings_infoName => 'Namn';
@@ -314,15 +345,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_presets => 'Fördefinierade inställningar';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvens (MHz)';
@@ -351,10 +373,15 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
@override
String get settings_longRange => 'Lång räckvidd';
String get settings_clientRepeat => 'Upprepa utan elnät';
@override
String get settings_fastSpeed => 'Snabb hastighet';
String get settings_clientRepeatSubtitle =>
'Låt enheten repetera nätpaket för andra användare.';
@override
String get settings_clientRepeatFreqWarning =>
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
@override
String settings_error(String message) {
@@ -424,6 +451,19 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Ryska';
@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';
@@ -581,6 +621,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get appSettings_offlineMapCache => 'Offline Kartcache';
@override
String get appSettings_unitsTitle => 'Enheter';
@override
String get appSettings_unitsMetric => 'Metriskt (m/km)';
@override
String get appSettings_unitsImperial => 'Imperialt (ft / mi)';
@override
String get appSettings_noAreaSelected => 'Ingen area markerad';
@@ -618,7 +667,35 @@ class AppLocalizationsSv extends AppLocalizations {
'Kontakter kommer att visas när enheter annonserar.';
@override
String get contacts_searchContacts => 'Sök kontakter...';
String get contacts_unread => 'Oläst';
@override
String get contacts_searchContactsNoNumber => 'Sök kontakter...';
@override
String contacts_searchContacts(int number, String str) {
return 'Sök kontakter...';
}
@override
String contacts_searchFavorites(int number, String str) {
return 'Sök $number$str Favoriter...';
}
@override
String contacts_searchUsers(int number, String str) {
return 'Sök $number$str användare...';
}
@override
String contacts_searchRepeaters(int number, String str) {
return 'Sök $number$str upprepningsenheter...';
}
@override
String contacts_searchRoomServers(int number, String str) {
return 'Sök $number$str Room-servrar...';
}
@override
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
@@ -638,6 +715,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Hantera Upprepare';
@override
String get contacts_manageRoom => 'Hantera Rumserver';
@override
String get contacts_roomLogin => 'Rum Inloggning';
@@ -741,6 +821,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_editChannel => 'Redigera kanal';
@override
String get channels_muteChannel => 'Tysta kanal';
@override
String get channels_unmuteChannel => 'Slå på ljud för kanal';
@override
String get channels_deleteChannel => 'Ta bort kanal';
@@ -749,6 +835,11 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Radera \"$name\"? Detta kan inte ångras.';
}
@override
String channels_channelDeleteFailed(String name) {
return 'Det gick inte att ta bort kanalen \"$name\"';
}
@override
String channels_channelDeleted(String name) {
return 'Kanalen \"$name\" raderad';
@@ -817,6 +908,46 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_sortUnread => 'Oläst';
@override
String get channels_createPrivateChannel => 'Skapa en privat kanal';
@override
String get channels_createPrivateChannelDesc =>
'Skyddat med en hemlig nyckel.';
@override
String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal';
@override
String get channels_joinPrivateChannelDesc =>
'Ange en hemlig nyckel manuellt.';
@override
String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen';
@override
String get channels_joinPublicChannelDesc =>
'Vem som helst kan gå med i denna kanal.';
@override
String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal';
@override
String get channels_joinHashtagChannelDesc =>
'Väldigt enkelt att gå med i hashtag-kanaler.';
@override
String get channels_scanQrCode => 'Skanna en QR-kod';
@override
String get channels_scanQrCodeComingSoon => 'Kommer snart';
@override
String get channels_enterHashtag => 'Ange hashtag';
@override
String get channels_hashtagHint => 't.ex. #team';
@override
String get chat_noMessages => 'Inga meddelanden ännu';
@@ -997,6 +1128,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_pathManagement => 'Stigarhantering';
@override
String get chat_ShowAllPaths => 'Visa alla vägar';
@override
String get chat_routingMode => 'Ruttläge';
@@ -1132,9 +1266,33 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Olästa: $count';
}
@override
String get chat_openLink => 'Öppna länk?';
@override
String get chat_openLinkConfirmation =>
'Vill du öppna den här länken i din webbläsare?';
@override
String get chat_open => 'Öppna';
@override
String chat_couldNotOpenLink(String url) {
return 'Kunde inte öppna länken: $url';
}
@override
String get chat_invalidLink => 'Ogiltigt länkformat';
@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';
@@ -1264,6 +1422,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_manageRepeater => 'Hantera Upprepare';
@override
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
@override
String get map_runTrace => 'Kör spårsökning';
@override
String get map_removeLast => 'Ta bort sista';
@override
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
@override
String get mapCache_title => 'Offline Kartcache';
@@ -1457,6 +1627,10 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Inloggning misslyckades: $error';
}
@override
String get login_failedMessage =>
'Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.';
@override
String get common_reload => 'Ladda om';
@@ -1525,6 +1699,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_management => 'Återuppspelarens Hantering';
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@@ -1548,6 +1725,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbors => 'Grannar';
@override
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@@ -2225,6 +2408,33 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Mottagna grannars data';
@override
String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.';
@override
String neighbors_errorLoading(String error) {
return 'Fel vid inläsning av grannar: $error';
}
@override
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Okänd $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hördes: $time sedan';
}
@override
String get channelPath_title => 'Paketväg';
@@ -2328,6 +2538,175 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Okänt Upprepare';
@override
String get community_title => 'Gemenskap';
@override
String get community_create => 'Skapa Gemenskap';
@override
String get community_createDesc =>
'Skapa en ny gemenskap och dela via QR-kod.';
@override
String get community_join => 'Gå med';
@override
String get community_joinTitle => 'Gå med i gemenskapen';
@override
String community_joinConfirmation(String name) {
return 'Vill du gå med i communityn \"$name\"?';
}
@override
String get community_scanQr => 'Skanna Gemenskapens QR';
@override
String get community_scanInstructions =>
'Rikta kameran mot en QR-kod i communityn';
@override
String get community_showQr => 'Visa QR-kod';
@override
String get community_publicChannel => 'Föreningens Offentliga';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Gemenskapens namn';
@override
String get community_enterName => 'Ange communities namn';
@override
String community_created(String name) {
return 'Community \"$name\" har skapats';
}
@override
String community_joined(String name) {
return 'Medlem i communityn \"$name\"';
}
@override
String get community_qrTitle => 'Dela Gemenskap';
@override
String community_qrInstructions(String name) {
return 'Skanna denna QR-kod för att gå med i \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community-hashtagkanaler kan endast nås av medlemmar i communityn';
@override
String get community_invalidQrCode => 'Ogiltig community QR-kod';
@override
String get community_alreadyMember => 'Är redan medlem';
@override
String community_alreadyMemberMessage(String name) {
return 'Du är redan medlem av \"$name\".';
}
@override
String get community_addPublicChannel =>
'Lägg till Gemenskapskanal (Offentlig)';
@override
String get community_addPublicChannelHint =>
'Lägg automatiskt till den offentliga kanalen för denna community';
@override
String get community_noCommunities => 'Inga gemenskaper har anslutats ännu';
@override
String get community_scanOrCreate =>
'Skanna en QR-kod eller skapa en community för att komma igång';
@override
String get community_manageCommunities => 'Hantera Gemenskaper';
@override
String get community_delete => 'Lämna Gemenskap';
@override
String community_deleteConfirm(String name) {
return 'Lämna \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.';
}
@override
String community_deleted(String name) {
return 'Lämnade community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerera hemlig kod';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.';
}
@override
String get community_regenerate => 'Regenerera';
@override
String community_secretRegenerated(String name) {
return 'Lösenord återskapad för \"$name\"';
}
@override
String get community_updateSecret => 'Uppdatera hemlighet';
@override
String community_secretUpdated(String name) {
return 'Hemlighet uppdaterad för \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
}
@override
String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Lägg till en hashtag-kanal för denna community';
@override
String get community_selectCommunity => 'Välj Gemenskap';
@override
String get community_regularHashtag => 'Vanlig Hash Tag';
@override
String get community_regularHashtagDesc =>
'Offentlig hashtag (alla kan gå med)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Endast för medlemmar';
@override
String community_forCommunity(String name) {
return 'För $name';
}
@override
String get listFilter_tooltip => 'Filtrera och sortera';
@@ -2349,6 +2728,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_all => 'Alla';
@override
String get listFilter_favorites => 'Favoriter';
@override
String get listFilter_addToFavorites => 'Lägg till i favoriter';
@override
String get listFilter_removeFromFavorites => 'Ta bort från favoriter';
@override
String get listFilter_users => 'Användare';
@@ -2363,4 +2751,334 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get listFilter_newGroup => 'Ny grupp';
@override
String get pathTrace_you => 'Du';
@override
String get pathTrace_failed => 'Sökvägsföljning misslyckades.';
@override
String get pathTrace_notAvailable => 'Path trace ej tillgänglig.';
@override
String get pathTrace_refreshTooltip => 'Uppdatera Path Trace';
@override
String get pathTrace_someHopsNoLocation =>
'En eller flera av humlen saknar en plats!';
@override
String get pathTrace_clearTooltip => 'Rensa väg';
@override
String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.';
@override
String losRunFailed(String error) {
return 'Synlinjekontroll misslyckades: $error';
}
@override
String get losClearAllPoints => 'Rensa alla punkter';
@override
String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil';
@override
String get losMenuTitle => 'LOS-menyn';
@override
String get losMenuSubtitle =>
'Tryck på noder eller tryck länge på kartan för anpassade punkter';
@override
String get losShowDisplayNodes => 'Visa displaynoder';
@override
String get losCustomPoints => 'Anpassade poäng';
@override
String losCustomPointLabel(int index) {
return 'Anpassad $index';
}
@override
String get losPointA => 'Punkt A';
@override
String get losPointB => 'Punkt B';
@override
String losAntennaA(String value, String unit) {
return 'Antenn A: $value $unit';
}
@override
String losAntennaB(String value, String unit) {
return 'Antenn B: $value $unit';
}
@override
String get losRun => 'Kör LOS';
@override
String get losNoElevationData => 'Inga höjddata';
@override
String losProfileClear(
String distance,
String distanceUnit,
String clearance,
String heightUnit,
) {
return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit';
}
@override
String losProfileBlocked(
String distance,
String distanceUnit,
String obstruction,
String heightUnit,
) {
return '$distance $distanceUnit, blockerad av $obstruction $heightUnit';
}
@override
String get losStatusChecking => 'LOS: kollar...';
@override
String get losStatusNoData => 'LOS: inga data';
@override
String losStatusSummary(int clear, int total, int blocked, int unknown) {
return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd';
}
@override
String get losErrorElevationUnavailable =>
'Höjddata är inte tillgänglig för ett eller flera prover.';
@override
String get losErrorInvalidInput =>
'Ogiltiga poäng/höjddata för LOS-beräkning.';
@override
String get losRenameCustomPoint => 'Byt namn på anpassad punkt';
@override
String get losPointName => 'Punktnamn';
@override
String get losShowPanelTooltip => 'Visa LOS-panelen';
@override
String get losHidePanelTooltip => 'Dölj LOS-panelen';
@override
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorisont';
@override
String get losLegendLosBeam => 'Siktlinje';
@override
String get losLegendTerrain => 'Terräng';
@override
String get losFrequencyLabel => 'Frekvens';
@override
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
@override
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
}
@override
String get contacts_pathTrace => 'Path Trace';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Vägspårning till repeater';
@override
String get contacts_repeaterPing => 'Ping-repeater';
@override
String get contacts_roomPathTrace => 'Vägspårning till rumserver';
@override
String get contacts_roomPing => 'Ping rumsserver';
@override
String get contacts_chatTraceRoute => 'Spåra rutt';
@override
String contacts_pathTraceTo(String name) {
return 'Spåra rutt till $name';
}
@override
String get contacts_clipboardEmpty => 'Urklipp är tomt.';
@override
String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter';
@override
String get contacts_contactImported => 'Kontakt har importerats.';
@override
String get contacts_contactImportFailed => 'Kontakt kunde inte importeras.';
@override
String get contacts_zeroHopAdvert => 'Reklam med nollhopp';
@override
String get contacts_floodAdvert => 'Översvämningsannons';
@override
String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp';
@override
String get contacts_addContactFromClipboard =>
'Lägg till kontakt från urklipp';
@override
String get contacts_ShareContact => 'Kopiera kontakt till Urklipp';
@override
String get contacts_ShareContactZeroHop => 'Dela kontakt via annons';
@override
String get contacts_zeroHopContactAdvertSent => 'Skickat kontakt via annons.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Misslyckades med att skicka kontakt.';
@override
String get contacts_contactAdvertCopied => 'Annons kopierad till Urklipp.';
@override
String get contacts_contactAdvertCopyFailed =>
'Kopiering av annons till Urklipp misslyckades.';
@override
String get notification_activityTitle => 'MeshCore Aktivitet';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'meddelanden',
one: 'meddelande',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'kanalmeddelanden',
one: 'kanalmeddelande',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nya noder',
one: 'ny nod',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Ny $contactType upptäckt';
}
@override
String get notification_receivedNewMessage => 'Nytt meddelande mottaget';
@override
String get settings_gpxExportRepeaters =>
'Exportera repeater / rumsservrar till GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exporterar repeater / roomserver med plats till GPX-fil.';
@override
String get settings_gpxExportContacts => 'Exportera följeslagare till GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exporterar följeslagare med en plats till GPX-fil.';
@override
String get settings_gpxExportAll => 'Exportera alla kontakter till GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exporterar alla kontakter med en plats till GPX-fil.';
@override
String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång';
@override
String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.';
@override
String get settings_gpxExportNotAvailable =>
'Stöds inte på din enhet/operativsystem';
@override
String get settings_gpxExportError =>
'Det uppstod ett fel när data exporterades.';
@override
String get settings_gpxExportRepeatersRoom =>
'Repeater- och rumsserverplatser';
@override
String get settings_gpxExportChat => 'Medhjälparplatser';
@override
String get settings_gpxExportAllContacts => 'Alla kontakters platser';
@override
String get settings_gpxExportShareText =>
'Kartdata exporterad från meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open export av GPX-kartdata';
@override
String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer';
@override
String get snrIndicator_lastSeen => 'Senast sedd';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+557 -93
View File
@@ -1,8 +1,16 @@
{
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "nl",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacten",
"nav_channels": "Kanaal",
"nav_channels": "Kanalen",
"nav_map": "Kaart",
"common_cancel": "Annuleren",
"common_connect": "Verbinden",
@@ -10,12 +18,12 @@
"common_save": "Opslaan",
"common_delete": "Verwijderen",
"common_close": "Sluiten",
"common_edit": "Bewerk",
"common_edit": "Bewerken",
"common_add": "Toevoegen",
"common_settings": "Instellingen",
"common_disconnect": "Verbinden verbreken",
"common_disconnect": "Verbinding verbreken",
"common_connected": "Verbonden",
"common_disconnected": "Ontkoppeld",
"common_disconnected": "Verbinding verbroken",
"common_create": "Maak",
"common_continue": "Doorgaan",
"common_share": "Delen",
@@ -23,10 +31,10 @@
"common_retry": "Nogmaals proberen",
"common_hide": "Verbergen",
"common_remove": "Verwijderen",
"common_enable": "Aktivatie",
"common_enable": "Activeren",
"common_disable": "Uitschakelen",
"common_reboot": "Herstarten",
"common_loading": "Laad...",
"common_loading": "Laden...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
@@ -78,7 +86,7 @@
"settings_nodeSettings": "Node Instellingen",
"settings_nodeName": "Node Naam",
"settings_nodeNameNotSet": "Niet ingesteld",
"settings_nodeNameHint": "Voer knooppuntnaam in",
"settings_nodeNameHint": "Voer nodenaam in",
"settings_nodeNameUpdated": "Naam bijgewerkt",
"settings_radioSettings": "Radio Instellingen",
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
@@ -129,11 +137,8 @@
"settings_infoBattery": "Batterij",
"settings_infoPublicKey": "Openbare Sleutel",
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Kanaal Aantal",
"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": "Lang Bereik",
"settings_fastSpeed": "Snelle Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -157,7 +160,7 @@
"appSettings_appearance": "Uiterlijk",
"appSettings_theme": "Thema",
"appSettings_themeSystem": "Standaardinstelling",
"appSettings_themeLight": "Helder",
"appSettings_themeLight": "Licht",
"appSettings_themeDark": "Donker",
"appSettings_language": "Taal",
"appSettings_languageSystem": "Standaardinstelling",
@@ -185,16 +188,16 @@
"appSettings_channelMessageNotifications": "Kanaal Bericht Meldingen",
"appSettings_channelMessageNotificationsSubtitle": "Toon notificatie bij het ontvangen van kanaalberichten",
"appSettings_advertisementNotifications": "Advertentie-meldingen",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe knooppunten worden ontdekt",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe nodes worden ontdekt",
"appSettings_messaging": "Berichten",
"appSettings_clearPathOnMaxRetry": "Duidelijke Pad op Max Retry",
"appSettings_clearPathOnMaxRetry": "Wis Pad op Max Retry",
"appSettings_clearPathOnMaxRetrySubtitle": "Reset contactpad na 5 mislukte verzendpogingen",
"appSettings_pathsWillBeCleared": "De paden worden na 5 mislukte pogingen leeggehaald.",
"appSettings_pathsWillNotBeCleared": "Padoms worden niet automatisch verwijderd",
"appSettings_autoRouteRotation": "Automatische Route Rotatie",
"appSettings_autoRouteRotationSubtitle": "Wissel tussen de beste paden en floodmodus over.",
"appSettings_autoRouteRotationEnabled": "Automatische routeplanning rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische routeplanning rotatie is uitgeschakeld",
"appSettings_autoRouteRotation": "Route Automatisch Roteren",
"appSettings_autoRouteRotationSubtitle": "Verwissel tussen beste pad en floodmodus.",
"appSettings_autoRouteRotationEnabled": "Automatische route rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische route rotatie is uitgeschakeld",
"appSettings_battery": "Batterij",
"appSettings_batteryChemistry": "Batterijchemie",
"appSettings_batteryChemistryPerDevice": "Instellen per apparaat ({deviceName})",
@@ -210,15 +213,15 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Kaartweergave",
"appSettings_showRepeaters": "Toon Herhalingen",
"appSettings_showRepeatersSubtitle": "Toon herhalende knoopjes op de kaart",
"appSettings_showRepeaters": "Toon Repeaters",
"appSettings_showRepeatersSubtitle": "Toon repeaternodes op de kaart",
"appSettings_showChatNodes": "Chat Nodes tonen",
"appSettings_showChatNodesSubtitle": "Chatnodes weergeven op de kaart",
"appSettings_showOtherNodes": "Toon Andere Knopen",
"appSettings_showOtherNodesSubtitle": "Toon andere knooptypes op de kaart",
"appSettings_showOtherNodes": "Toon Andere Nodes",
"appSettings_showOtherNodesSubtitle": "Toon andere nodetypes op de kaart",
"appSettings_timeFilter": "Filter op tijd",
"appSettings_timeFilterShowAll": "Alle knooppunten tonen",
"appSettings_timeFilterShowLast": "Toon knopen van de laatste {hours} uur",
"appSettings_timeFilterShowAll": "Alle nodes tonen",
"appSettings_timeFilterShowLast": "Toon nodes van de laatste {hours} uur",
"@appSettings_timeFilterShowLast": {
"placeholders": {
"hours": {
@@ -227,8 +230,8 @@
}
},
"appSettings_mapTimeFilter": "Filter tijd op kaart",
"appSettings_showNodesDiscoveredWithin": "Toon knooppunten ontdekt binnen:",
"appSettings_allTime": "Alle tijd",
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
"appSettings_allTime": "Altijd",
"appSettings_lastHour": "Laat uur",
"appSettings_last6Hours": "laatste 6 uur",
"appSettings_last24Hours": "De laatste 24 uur",
@@ -266,7 +269,7 @@
}
}
},
"contacts_manageRepeater": "Beheer Herhaling",
"contacts_manageRepeater": "Beheer Repeater",
"contacts_roomLogin": "Ruimte Inloggen",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Groep bewerken",
@@ -294,7 +297,7 @@
"contacts_noContactsMatchFilter": "Geen contacten matchen met uw filter",
"contacts_noMembers": "Geen leden",
"contacts_lastSeenNow": "Laatste keer gezien nu",
"contacts_lastSeenMinsAgo": "Laast gezien {minutes} minuten geleden",
"contacts_lastSeenMinsAgo": "Laatst gezien {minutes} minuten geleden",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -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": {
@@ -539,7 +544,7 @@
"chat_pathManagement": "Beheer van Paden",
"chat_routingMode": "Routeerwijze",
"chat_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"chat_forceFloodMode": "Dwing Overstromingsmodus",
"chat_forceFloodMode": "Dwing Floodsmodus",
"chat_recentAckPaths": "Recente ACK Paden (tik om te gebruiken):",
"chat_pathHistoryFull": "De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.",
"chat_hopSingular": "Hop",
@@ -562,7 +567,7 @@
"chat_clearPathSubtitle": "Dwing herontdekking bij volgende verzending",
"chat_pathCleared": "Pad is vrijgegeven. Volgende bericht herontdekt route.",
"chat_floodModeSubtitle": "Gebruik de route-schakelaar in de app-balk",
"chat_floodModeEnabled": "Overstromingsmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_floodModeEnabled": "Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_fullPath": "Volledige Pad",
"chat_pathDetailsNotAvailable": "De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.",
"chat_pathSetHops": "Pad ingesteld: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,9 +588,9 @@
"chat_path": "Pad",
"chat_publicKey": "Openbare Sleutel",
"chat_compressOutgoingMessages": "Verzenden van uitgaande berichten comprimeren",
"chat_floodForced": "Overstroming (gedwongen)",
"chat_directForced": "Direct (gedwongen)",
"chat_hopsForced": "{count} sprongen (gedwongen)",
"chat_floodForced": "Flood (afgedwongen)",
"chat_directForced": "Direct (afgedwongen)",
"chat_hopsForced": "{count} hops (afgedwongen)",
"@chat_hopsForced": {
"placeholders": {
"count": {
@@ -593,7 +598,7 @@
}
}
},
"chat_floodAuto": "Overstroming (auto)",
"chat_floodAuto": "Flood (auto)",
"chat_direct": "Direct",
"chat_poiShared": "Gedeelde POI",
"chat_unread": "Nieuw: {count}",
@@ -604,8 +609,20 @@
}
}
},
"chat_openLink": "Link openen?",
"chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?",
"chat_open": "Openen",
"chat_couldNotOpenLink": "Kan link niet openen: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ongeldig linkformaat",
"map_title": "Node Map",
"map_noNodesWithLocation": "Geen knopen met locatiegegevens",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
"map_nodesCount": "Nodes: {count}",
"@map_nodesCount": {
@@ -624,7 +641,7 @@
}
},
"map_chat": "Chat",
"map_repeater": "Herhaling",
"map_repeater": "Repeater",
"map_room": "Ruimte",
"map_sensor": "Sensor",
"map_pinDm": "Verzenden als bericht (DM)",
@@ -652,11 +669,11 @@
}
},
"map_connectToShareMarkers": "Verbind met een apparaat om markers te delen",
"map_filterNodes": "Filter Knopen",
"map_nodeTypes": "Node Types",
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Herhalingen",
"map_otherNodes": "Andere knooppunten",
"map_filterNodes": "Filter Nodes",
"map_nodeTypes": "Nodetypes",
"map_chatNodes": "Chatnodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Andere Nodes",
"map_keyPrefix": "Prefix sleutel",
"map_filterByKeyPrefix": "Filteren op sleutelvoorgemeld",
"map_publicKeyPrefix": "Openbare sleutelvoorgemeld",
@@ -665,7 +682,7 @@
"map_lastSeenTime": "Laatste Bekeken Tijd",
"map_sharedPin": "Gedeelde pin",
"map_joinRoom": "Sluit Kamer",
"map_manageRepeater": "Beheer Herhaling",
"map_manageRepeater": "Beheer Repeater",
"mapCache_title": "Offline Kaarten Cache",
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
"mapCache_noTilesToDownload": "Geen tiles te downloaden voor dit gebied.",
@@ -788,18 +805,18 @@
"time_allTime": "Alle tijd",
"dialog_disconnect": "Verbinden verbreken",
"dialog_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
"login_repeaterLogin": "Herhaalders Inloggen",
"login_repeaterLogin": "Inloggen Repeater",
"login_roomLogin": "Ruimte Inloggen",
"login_password": "Wachtwoord",
"login_enterPassword": "Wachtwoord invoeren",
"login_savePassword": "Wachtwoord opslaan",
"login_savePasswordSubtitle": "Het wachtwoord wordt veilig op dit apparaat opgeslagen.",
"login_repeaterDescription": "Voer het wachtwoord van de herhaling in om instellingen en status te openen.",
"login_repeaterDescription": "Voer het wachtwoord van de repeater in om instellingen en status te openen.",
"login_roomDescription": "Voer het wachtwoord van de kamer in om toegang te krijgen tot instellingen en status.",
"login_routing": "Routing",
"login_routingMode": "Routeerwijze",
"login_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"login_forceFloodMode": "Dwing Overstromingsmodus",
"login_forceFloodMode": "Dwing Floodmodus Af",
"login_managePaths": "Padbeheer",
"login_login": "Inloggen",
"login_attempt": "Poging {current}/{max}",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.",
"common_reload": "Opnieuw laden",
"common_clear": "Schoonmaken",
"path_currentPath": "Huidige pad: {path}",
@@ -846,7 +864,7 @@
"path_labelHexPrefixes": "Pad (hex-voorkeursletters)",
"path_helperMaxHops": "Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)",
"path_selectFromContacts": "Of select contacten:",
"path_noRepeatersFound": "Geen herhalingen of zaalservers gevonden.",
"path_noRepeatersFound": "Geen repeaters of roomservers gevonden.",
"path_customPathsRequire": "Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.",
"path_invalidHexPrefixes": "Ongeldige hex-voorkeursletters: {prefixes}",
"@path_invalidHexPrefixes": {
@@ -858,20 +876,20 @@
},
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
"path_setPath": "Stel Pad in",
"repeater_management": "Beheer Herhalingen",
"repeater_management": "Beheer Repeaters",
"repeater_managementTools": "Beheerinstrumenten",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
"repeater_telemetry": "Telemetry",
"repeater_telemetrySubtitle": "Bekijk telemetrie van sensoren en systeemgegevens",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Verzend commando's naar de herhaaldere",
"repeater_cliSubtitle": "Verzend commando's naar de repeater",
"repeater_settings": "Instellingen",
"repeater_settingsSubtitle": "Configureer repeaterparameters",
"repeater_statusTitle": "Status herhalen",
"repeater_statusTitle": "Status repeater",
"repeater_routingMode": "Routeerwijze",
"repeater_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"repeater_forceFloodMode": "Dwing Overloopmodus",
"repeater_forceFloodMode": "Dwing Floodmodus Af",
"repeater_pathManagement": "Beheer van paden",
"repeater_refresh": "Vernieuwen",
"repeater_statusRequestTimeout": "Statusverzoek is uitgevallen.",
@@ -883,22 +901,22 @@
}
}
},
"repeater_systemInformation": "Systeem Informatie",
"repeater_systemInformation": "Systeeminformatie",
"repeater_battery": "Batterij",
"repeater_clockAtLogin": "Tijd (bij aanmelden)",
"repeater_uptime": "Beschikbaarheid",
"repeater_queueLength": "Wachttijd",
"repeater_debugFlags": "Debug Flags",
"repeater_radioStatistics": "Radio Statistieken",
"repeater_debugFlags": "Debugvlaggen",
"repeater_radioStatistics": "Radiostatistieken",
"repeater_lastRssi": "Laatste RSSI",
"repeater_lastSnr": "Laatste SNR",
"repeater_noiseFloor": "Ruishoordniveau",
"repeater_noiseFloor": "Ruisvloer",
"repeater_txAirtime": "TX Airtime",
"repeater_rxAirtime": "RX Airtime",
"repeater_packetStatistics": "Pakket Statistieken",
"repeater_packetStatistics": "Pakketstatistieken",
"repeater_sent": "Verzonden",
"repeater_received": "Ontvangen",
"repeater_duplicates": "Dubbele",
"repeater_duplicates": "Duplicaat",
"repeater_daysHoursMinsSecs": "{days} dagen {hours} uur {minutes} minuten {seconds} seconden",
"@repeater_daysHoursMinsSecs": {
"placeholders": {
@@ -916,7 +934,7 @@
}
}
},
"repeater_packetTxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetTxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +948,7 @@
}
}
},
"repeater_packetRxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetRxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +962,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Overstroming: {flood}, Direct: {direct}",
"repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -963,10 +981,10 @@
}
}
},
"repeater_settingsTitle": "Herstelinstellingen",
"repeater_settingsTitle": "Repeater Instellingen",
"repeater_basicSettings": "Basisinstellingen",
"repeater_repeaterName": "Herhaalnaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze herhaling",
"repeater_repeaterName": "Repeaternaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
"repeater_adminPassword": "Admin wachtwoord",
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
"repeater_guestPassword": "Wachtwoord Gast",
@@ -977,7 +995,7 @@
"repeater_txPower": "TX Power",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bandbreedte",
"repeater_spreadingFactor": "Spreadsnelheid",
"repeater_spreadingFactor": "Spreidingsfactor",
"repeater_codingRate": "Codeertarief",
"repeater_locationSettings": "Locatie Instellingen",
"repeater_latitude": "Breedtegraad",
@@ -985,11 +1003,11 @@
"repeater_longitude": "Lengtegraad",
"repeater_longitudeHelper": "Graadseconden (bijv. -122.4194)",
"repeater_features": "Kenmerken",
"repeater_packetForwarding": "Pakketdoorstrooming",
"repeater_packetForwardingSubtitle": "Herstel activeren om pakketten door te sturen",
"repeater_packetForwarding": "Pakketdoorvoering",
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
"repeater_guestAccess": "Toegang voor Gasten",
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
"repeater_privacyMode": "Privacy Mode",
"repeater_privacyMode": "Privacy Modus",
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"repeater_advertisementSettings": "Advertentie Instellingen",
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
@@ -1001,7 +1019,7 @@
}
}
},
"repeater_floodAdvertInterval": "Advertentie Interval bij overstroming",
"repeater_floodAdvertInterval": "Flood Advertentie Interval",
"repeater_floodAdvertIntervalHours": "{hours} uur",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1012,15 +1030,15 @@
},
"repeater_encryptedAdvertInterval": "Versleutelde Advertentie Interval",
"repeater_dangerZone": "Gevaarzone",
"repeater_rebootRepeater": "Herstart Herhaalder",
"repeater_rebootRepeaterSubtitle": "De herstart van het herhalerapparaat",
"repeater_rebootRepeater": "Herstart Repeater",
"repeater_rebootRepeaterSubtitle": "Herstart het Repeaterapparaat",
"repeater_rebootRepeaterConfirm": "Ben je er zeker van dat je deze repeater opnieuw wilt opstarten?",
"repeater_regenerateIdentityKey": "Identiteit sleutel opnieuw genereren",
"repeater_regenerateIdentityKeySubtitle": "Nieuwe publieke/private sleutelpaar genereren",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de herhaling. Doorgaan?",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de repeater. Doorgaan?",
"repeater_eraseFileSystem": "Verwijder Besturingssysteem",
"repeater_eraseFileSystemSubtitle": "Formateer het herhalende bestandsysteem",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de herhaling wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
"repeater_commandSent": "Commando verzonden: {command}",
"@repeater_commandSent": {
@@ -1049,7 +1067,7 @@
}
},
"repeater_refreshBasicSettings": "Basisinstellingen vernieuwen",
"repeater_refreshRadioSettings": "Radiozenders Instellingen Bijwerken",
"repeater_refreshRadioSettings": "Radiozender Instellingen Verversen",
"repeater_refreshTxPower": "Nieuw laden TX-vermogen",
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
@@ -1072,7 +1090,7 @@
}
}
},
"repeater_cliTitle": "Herhaling CLI",
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Debug Volgende Commando",
"repeater_commandHelp": "Help",
"repeater_clearHistory": "Verwijder Geschiedenis",
@@ -1106,14 +1124,14 @@
"repeater_cliHelpClearStats": "Reset verschillende statistiek-tellers naar nul.",
"repeater_cliHelpSetAf": "Stelt de luchtvaartfactor in.",
"repeater_cliHelpSetTx": "Stelt LoRa zendvermogen in dBm. (om te wijzigen)",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de herhalerrol voor dit knoop.",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de repeater rol van deze node.",
"repeater_cliHelpSetAllowReadOnly": "(Kamervisie) Als 'aan', dan wordt inloggen met een blanco wachtwoord toegestaan, maar kan niet naar de kamervisie Posten. (alleen lezen mogelijk).",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend overlastpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend floodpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetIntThresh": "Stelt de Interferentiewaarde (in dB) in. Standaardwaarde is 14. Stel in op 0 om het detecteren van kanaalinterferentie uit te schakelen.",
"repeater_cliHelpSetAgcResetInterval": "Stelt het interval in om de Auto Gain Controller te resetten. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'dubbele ACKs'.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'duplicate ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Stelt het timerinterval in minuten in om een lokale (zero-hop) advertentiepakket te versturen. Stel in op 0 om uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een overstromingsadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een floodadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetGuestPassword": "Stelt/past de gastenwacht aan of wijzigt deze. (voor herstelcontacten kunnen gastelogins de \"Get Stats\" verzoek verzenden)",
"repeater_cliHelpSetName": "Stelt de advertentietitel in.",
"repeater_cliHelpSetLat": "Stelt de breedtegraad van de advertentiekaart in. (graadrijssysteem)",
@@ -1136,7 +1154,7 @@
"repeater_cliHelpLogErase": "Verwijdert de pakketlogs uit het bestandssysteem.",
"repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.",
"repeater_cliHelpRegion": "(reeks alleen) Lijst alle gedefinieerde regio's en huidige overstromingsrechten.",
"repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.",
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
"repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"",
"repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.",
@@ -1148,8 +1166,8 @@
"repeater_cliHelpRegionSave": "Bewaar de lijst/kaart van de regio's naar de opslag.",
"repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.",
"repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
"repeater_cliHelpGpsSync": "Synchroniseer knooptime met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van het knoop vast naar GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen",
"repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
"repeater_commandsListTitle": "Commandenlijst",
@@ -1158,8 +1176,8 @@
"repeater_settingsCategory": "Instellingen",
"repeater_bridge": "Bruggen",
"repeater_logging": "Logging",
"repeater_neighborsRepeaterOnly": "Buren (Alleen herhaald)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen voor Repeater)",
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)",
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
"repeater_gpsManagement": "Beheer GPS",
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
@@ -1228,12 +1246,12 @@
"channelPath_title": "Pakketpad",
"channelPath_viewMap": "Kaart bekijken",
"channelPath_otherObservedPaths": "Overige Waargenomen Paden",
"channelPath_repeaterHops": "Herhalingstapjes",
"channelPath_repeaterHops": "Repeater Hops",
"channelPath_noHopDetails": "De details van de pakket zijn niet verstrekt.",
"channelPath_messageDetails": "Details Bericht",
"channelPath_senderLabel": "Afzender",
"channelPath_timeLabel": "Tijd",
"channelPath_repeatsLabel": "Herhalen",
"channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Pad {index}",
"channelPath_observedLabel": "Waargenomen",
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
@@ -1271,7 +1289,7 @@
}
},
"channelPath_unknownPath": "Onbekend",
"channelPath_floodPath": "Overstroming",
"channelPath_floodPath": "Flood",
"channelPath_directPath": "Direct",
"channelPath_observedZeroOf": "0 van {total} sprongen",
"@channelPath_observedZeroOf": {
@@ -1293,7 +1311,7 @@
}
},
"channelPath_mapTitle": "Padkaart",
"channelPath_noRepeaterLocations": "Geen herhaler locaties beschikbaar voor deze route.",
"channelPath_noRepeaterLocations": "Geen repeaters beschikbaar voor deze route.",
"channelPath_primaryPath": "Pad {index} (Hoofdtype)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1323,7 +1341,7 @@
}
},
"channelPath_noHopDetailsAvailable": "Geen details beschikbaar voor dit pakket.",
"channelPath_unknownRepeater": "Onbekend Herhaalaar",
"channelPath_unknownRepeater": "Onbekend Repeater",
"listFilter_tooltip": "Filteren en sorteren",
"listFilter_sortBy": "Sorteren door",
"listFilter_latestMessages": "Recente berichten",
@@ -1332,8 +1350,454 @@
"listFilter_filters": "Filters",
"listFilter_all": "Alles",
"listFilter_users": "Gebruikers",
"listFilter_repeaters": "Herhalingen",
"listFilter_roomServers": "Kamervirtualisatie",
"listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Roomservers",
"listFilter_unreadOnly": "Alleen ongelezen",
"listFilter_newGroup": "Nieuwe groep"
"listFilter_newGroup": "Nieuwe groep",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Buren",
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Onbekende {pubkey}",
"neighbors_heardAgo": "Horen: {time} geleden",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Gemeenschap",
"common_ok": "OK",
"community_createDesc": "Maak een nieuwe community en deel deze via QR-code.",
"community_create": "Maak Gemeenschap",
"community_join": "Sluit aan",
"community_joinTitle": "Worden lid van de community",
"community_joinConfirmation": "Wil je je aansluiten bij de community \"{name}\"?",
"community_scanQr": "Scan Gemeenschap QR",
"community_scanInstructions": "Richt de camera op een gemeenschappelijke QR-code",
"community_showQr": "Toon QR-code",
"community_publicChannel": "Gemeenschap Openbaar",
"community_hashtagChannel": "Gemeenschappelijk Hashtag",
"community_name": "Gemeenschapnaam",
"community_enterName": "Voer de gemeenschapsnaam in",
"community_created": "Gemeenschap \"{name}\" is aangemaakt",
"community_joined": "Gevonden in de community \"{name}\"",
"community_qrTitle": "Deel Gemeenschap",
"community_qrInstructions": "Scan deze QR-code om je aan te sluiten bij {name}",
"community_hashtagPrivacyHint": "Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community",
"community_invalidQrCode": "Ongeldige community QR-code",
"community_alreadyMember": "Alleen al lid",
"community_alreadyMemberMessage": "U bent al lid van \"{name}\".",
"community_addPublicChannel": "Voeg een Openbaar Gemeenschapskanaal toe",
"community_addPublicChannelHint": "Automatisch de publieke kanaal toevoegen voor deze community",
"community_noCommunities": "Nog geen gemeenschappen zijn bijgesloten.",
"community_scanOrCreate": "Scan een QR-code of een community aanmaken om te beginnen",
"community_manageCommunities": "Beheer Gemeenschappen",
"community_delete": "Laat Gemeenschap",
"community_deleteConfirm": "\"{name}\" verlaten?",
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlaten",
"community_addHashtagChannel": "Voeg Community Hashtag toe",
"community_addHashtagChannelDesc": "Voeg een hashtag-kanaal toe aan deze community",
"community_selectCommunity": "Selecteer Gemeenschap",
"community_regularHashtag": "Gewone Hashtag",
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
"community_communityHashtag": "Gemeenschappelijk Hashtag",
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
"community_forCommunity": "Voor {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Geheim hersteld voor \"{name}\"",
"community_regenerateSecret": "Regeneer Geheimwoord",
"community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.",
"community_regenerate": "Regeneer",
"community_updateSecret": "Bijwerken Geheime",
"community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
"community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Jij",
"pathTrace_failed": "Padtrace mislukt.",
"pathTrace_notAvailable": "Padtrace niet beschikbaar.",
"pathTrace_refreshTooltip": "Path Trace vernieuwen.",
"contacts_pathTrace": "Pad Traceren",
"contacts_ping": "Pingen",
"contacts_repeaterPathTrace": "Pad traceren naar repeater",
"contacts_repeaterPing": "Ping repeater",
"contacts_roomPathTrace": "Padtrace naar room server",
"contacts_roomPing": "Ping kamer server",
"contacts_chatTraceRoute": "Route traceren",
"contacts_pathTraceTo": "Trace route to {name}",
"appSettings_languageUk": "Oekraïens",
"contacts_invalidAdvertFormat": "Ongeldige contactgegevens",
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
"contacts_zeroHopAdvert": "Zero Hop Reclame",
"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.",
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"notification_activityTitle": "MeshCore Activiteit",
"notification_messagesCount": "{count} {count, plural, =1{bericht} other{berichten}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{kanaalbericht} other{kanaalberichten}}",
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
"settings_gpxExportError": "Er was een fout bij het exporteren.",
"settings_gpxExportContacts": "Companions exporteren naar GPX",
"settings_gpxExportAll": "Alle contacten exporteren naar GPX",
"settings_gpxExportAllSubtitle": "Exporteert alle contacten met een locatie naar een GPX-bestand.",
"settings_gpxExportContactsSubtitle": "Exporteert metgezellen met een locatie naar een GPX-bestand.",
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
"settings_gpxExportChat": "Locaties van metgezellen",
"settings_gpxExportAllContacts": "Alle contactlocaties",
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
"map_removeLast": "Verwijder Laatste",
"pathTrace_clearTooltip": "Weg wissen",
"map_pathTraceCancelled": "Pad traceren geannuleerd",
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
"map_runTrace": "Padeshulp traceren",
"scanner_enableBluetooth": "Activeer Bluetooth",
"scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.",
"scanner_bluetoothOff": "Bluetooth is uitgeschakeld",
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
"chat_ShowAllPaths": "Toon alle paden",
"settings_clientRepeat": "Herhalen: Afgekoppeld",
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Eenheden",
"appSettings_unitsMetric": "Metrisch (m / km)",
"appSettings_unitsImperial": "Imperiaal (ft / mi)",
"map_lineOfSight": "Zichtlijn",
"map_losScreenTitle": "Zichtlijn",
"losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.",
"losRunFailed": "Zichtlijncontrole mislukt: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wis alle punten",
"losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken",
"losMenuTitle": "LOS-menu",
"losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten",
"losShowDisplayNodes": "Toon weergaveknooppunten",
"losCustomPoints": "Aangepaste punten",
"losCustomPointLabel": "Aangepast {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punt A",
"losPointB": "Punt B",
"losAntennaA": "Antenne A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenne B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Voer LOS uit",
"losNoElevationData": "Geen hoogtegegevens",
"losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: controleren...",
"losStatusNoData": "LOS: geen gegevens",
"losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.",
"losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.",
"losRenameCustomPoint": "Hernoem aangepast punt",
"losPointName": "Puntnaam",
"losShowPanelTooltip": "Toon LOS-paneel",
"losHidePanelTooltip": "LOS-paneel verbergen",
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorizon",
"losLegendLosBeam": "Zichtlijn",
"losLegendTerrain": "Terrein",
"losFrequencyLabel": "Frequentie",
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Verwijderen uit favorieten",
"listFilter_favorites": "Favorieten",
"listFilter_addToFavorites": "Toevoegen aan favorieten",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Ongelezen",
"contacts_searchRepeaters": "Zoek {number}{str} Repeaters...",
"contacts_searchContactsNoNumber": "Zoek contacten...",
"contacts_searchUsers": "Zoek {number}{str} gebruikers...",
"contacts_searchFavorites": "Zoek {number}{str} favorieten...",
"contacts_searchRoomServers": "Zoek {number}{str} Room servers..."
}
+470 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Otworzyć link?",
"chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?",
"chat_open": "Otwórz",
"chat_couldNotOpenLink": "Nie można otworzyć linku: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Nieprawidłowy format linku",
"map_title": "Mapa węzłów",
"map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi",
"map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.",
"common_reload": "Ponownie załadować",
"common_clear": "Wyczyść",
"path_currentPath": "Aktualny ścieżka: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Powtarzacze",
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa"
"listFilter_newGroup": "Nowa grupa",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Sąsiedzi",
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
"channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału",
"channels_joinPublicChannel": "Dołącz do kanału publicznego.",
"channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.",
"channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem",
"channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.",
"channels_scanQrCode": "Skanuj kod QR",
"channels_scanQrCodeComingSoon": "Wkrótce",
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Usłyszano: {time} temu",
"neighbors_unknownContact": "Nieznana {pubkey}",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
"room_management": "Zarządzanie Serwerem Pokoju",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.",
"community_title": "Społeczność",
"community_create": "Utwórz Społeczność",
"common_ok": "OK",
"community_join": "Dołącz",
"community_joinTitle": "Dołącz do społeczności",
"community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?",
"community_scanQr": "Skanuj QR kod społeczności",
"community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.",
"community_showQr": "Pokaż kod QR",
"community_publicChannel": "Społeczność Publiczna",
"community_hashtagChannel": "Hashtag Społeczności",
"community_name": "Nazwa Społeczności",
"community_enterName": "Wprowadź nazwę społeczności",
"community_created": "Społeczność \"{name}\" została utworzona",
"community_joined": "Dołączył do społeczności \"{name}\"",
"community_qrTitle": "Dziel się Społecznością",
"community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}",
"community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności",
"community_invalidQrCode": "Nieprawidłowy kod QR społeczności.",
"community_alreadyMember": "Już jesteś członkiem.",
"community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".",
"community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności",
"community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.",
"community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.",
"community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.",
"community_manageCommunities": "Zarządzaj Grupami",
"community_delete": "Opuszczenie Społeczności",
"community_deleteConfirm": "Opuścić \"{name}\"?",
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
"community_addHashtagChannel": "Dodaj hashtag społeczności",
"community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności",
"community_selectCommunity": "Wybierz społeczność",
"community_regularHashtag": "Hashtag regular",
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
"community_communityHashtag": "Hashtag Społeczności",
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
"community_forCommunity": "Dla {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerate": "Zregeneruj",
"community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"",
"community_regenerateSecret": "Zregeneruj sekret",
"community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.",
"community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"",
"community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
"community_updateSecret": "Zaktualizuj tajny klucz",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Ty",
"pathTrace_failed": "Śledzenie ścieżki nie powiodło się.",
"pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.",
"contacts_pathTrace": "Śledzenie Ścieżek",
"contacts_ping": "Pingować",
"contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera",
"contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego",
"contacts_roomPing": "Pinguj serwer pokoju",
"pathTrace_refreshTooltip": "Odśwież ścieżkę.",
"contacts_repeaterPing": "Repeater pingowy",
"contacts_pathTraceTo": "Śledź trasę do {name}",
"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",
"contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka",
"contacts_clipboardEmpty": "Schowek jest pusty.",
"contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe",
"contacts_addContactFromClipboard": "Dodaj kontakt z schowka",
"contacts_contactImported": "Kontakt został zaimportowany.",
"contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.",
"contacts_contactAdvertCopied": "Reklama skopiowana do schowka.",
"contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.",
"contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie",
"contacts_ShareContact": "Kopiuj kontakt do schowka",
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
"notification_activityTitle": "Aktywność MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}",
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
"settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń",
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
"settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów",
"settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.",
"settings_gpxExportChat": "Lokalizacje towarzyszy",
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
"map_runTrace": "Uruchom ślad ścieżki",
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
"map_removeLast": "Usuń ostatni",
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednostki",
"appSettings_unitsMetric": "Metryczne (m / km)",
"appSettings_unitsImperial": "Imperialne (ft / mi)",
"map_lineOfSight": "Linia wzroku",
"map_losScreenTitle": "Linia wzroku",
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Wyczyść wszystkie punkty",
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
"losCustomPoints": "Punkty niestandardowe",
"losCustomPointLabel": "Niestandardowe {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Uruchom LOS-a",
"losNoElevationData": "Brak danych o wysokości",
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: sprawdzam...",
"losStatusNoData": "LOS: brak danych",
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
"losPointName": "Nazwa punktu",
"losShowPanelTooltip": "Pokaż panel LOS",
"losHidePanelTooltip": "Ukryj panel LOS",
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horyzont radiowy",
"losLegendLosBeam": "Linia widoczności",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Częstotliwość",
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Usuń z ulubionych",
"listFilter_addToFavorites": "Dodaj do ulubionych",
"listFilter_favorites": "Ulubione",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Nieprzeczytane",
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..."
}
+470 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Abrir link?",
"chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?",
"chat_open": "Abrir",
"chat_couldNotOpenLink": "Não foi possível abrir o link: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Formato de link inválido",
"map_title": "Mapa de Nós",
"map_noNodesWithLocation": "Não existem nós com dados de localização.",
"map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.",
"common_reload": "Recarregar",
"common_clear": "Limpar",
"path_currentPath": "Caminho atual: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de sala",
"listFilter_unreadOnly": "Apenas não lido",
"listFilter_newGroup": "Novo grupo"
"listFilter_newGroup": "Novo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbors": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
"channels_createPrivateChannel": "Criar um Canal Privado",
"channels_joinPrivateChannel": "Junte-se a um Canal Privado",
"channels_joinPublicChannel": "Junte-se ao Canal Público",
"channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.",
"channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag",
"channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.",
"channels_scanQrCode": "Digitalizar um Código QR",
"channels_scanQrCodeComingSoon": "Em breve",
"channels_enterHashtag": "Insira hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Ouvido: {time} atrás",
"neighbors_unknownContact": "{pubkey} Desconhecido",
"settings_locationGPSEnable": "Ativar GPS",
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"contacts_manageRoom": "Gerenciar Servidor de Sala",
"room_management": "Gerenciamento de Servidor de Sala",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Comunidade",
"community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.",
"common_ok": "OK",
"community_create": "Criar Comunidade",
"community_join": "Junte-se",
"community_joinTitle": "Junte-se à Comunidade",
"community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?",
"community_scanQr": "Digitalizar a QR Code da Comunidade",
"community_scanInstructions": "Aponte a câmera para um código QR da comunidade",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidade Pública",
"community_hashtagChannel": "Hashtag da Comunidade",
"community_name": "Nome da Comunidade",
"community_enterName": "Insira o nome da comunidade",
"community_created": "Comunidade \"{name}\" criada",
"community_joined": "Juntou-se à comunidade \"{name}\"",
"community_qrTitle": "Partilhar Comunidade",
"community_qrInstructions": "Escanear este código QR para juntar-se a {name}",
"community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade",
"community_invalidQrCode": "Código QR da comunidade inválido",
"community_alreadyMember": "Já é Membro",
"community_alreadyMemberMessage": "Você já é membro de \"{name}\".",
"community_addPublicChannel": "Adicionar Canal Público da Comunidade",
"community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade",
"community_noCommunities": "Ainda não foram adicionadas comunidades.",
"community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.",
"community_manageCommunities": "Gerenciar Comunidades",
"community_delete": "Deixar Comunidade",
"community_deleteConfirm": "Sair de \"{name}\"?",
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
"community_addHashtagChannel": "Adicionar Hashtag da Comunidade",
"community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade",
"community_selectCommunity": "Selecione Comunidade",
"community_regularHashtag": "Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
"community_communityHashtag": "Hashtag da Comunidade",
"community_communityHashtagDesc": "Apenas para membros da comunidade",
"community_forCommunity": "Para {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.",
"community_regenerateSecret": "Regenerar Senha Segura",
"community_secretRegenerated": "Senha secreta regenerada para \"{name}\"",
"community_regenerate": "Regenerar",
"community_secretUpdated": "Segredo atualizado para \"{name}\"",
"community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
"community_updateSecret": "Atualizar Segredo",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Você",
"pathTrace_failed": "Falha no rastreamento de caminho.",
"pathTrace_notAvailable": "Traçado de caminho não disponível.",
"pathTrace_refreshTooltip": "Atualizar Path Trace.",
"contacts_pathTrace": "Traçado de Caminho",
"contacts_ping": "Pingar",
"contacts_repeaterPathTrace": "Traçar caminho para repetidor",
"contacts_repeaterPing": "Pingar repetidor",
"contacts_roomPathTrace": "Traçar caminho para o servidor da sala",
"contacts_roomPing": "Pingar servidor da sala",
"contacts_chatTraceRoute": "Rastrear rota do caminho",
"contacts_pathTraceTo": "Rastrear rota para {name}",
"contacts_invalidAdvertFormat": "Dados de Contato Inválidos",
"contacts_clipboardEmpty": "Área de Transferência Está Vazia.",
"appSettings_languageUk": "Ucraniano",
"contacts_contactImported": "Contato foi importado.",
"contacts_zeroHopAdvert": "Anúncio Zero Hop",
"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.",
"contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.",
"contacts_floodAdvert": "Anúncio de Inundação",
"contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.",
"contacts_ShareContactZeroHop": "Compartilhar contato por anúncio",
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
"notification_activityTitle": "Atividade MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}",
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
"notification_receivedNewMessage": "Nova mensagem recebida",
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
"settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.",
"settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO",
"settings_gpxExportError": "Ocorreu um erro ao exportar.",
"settings_gpxExportAll": "Exportar todos os contatos para GPX",
"settings_gpxExportContacts": "Exportar companheiros para GPX",
"settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.",
"settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala",
"settings_gpxExportChat": "Localizações de companheiros",
"settings_gpxExportNoContacts": "Nenhum contato para exportar.",
"settings_gpxExportAllContacts": "Todos os locais de contatos",
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
"map_runTrace": "Executar Traçado de Caminho",
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
"pathTrace_clearTooltip": "Limpar caminho",
"map_removeLast": "Remover Último",
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
"scanner_enableBluetooth": "Ative o Bluetooth",
"scanner_bluetoothOff": "Bluetooth está desativado",
"scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.",
"snrIndicator_nearByRepeaters": "Repetidores Próximos",
"snrIndicator_lastSeen": "Visto pela última vez",
"chat_ShowAllPaths": "Mostrar todos os caminhos",
"settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.",
"settings_clientRepeat": "Repetição sem rede",
"settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.",
"settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Unidades",
"appSettings_unitsMetric": "Métrico (m/km)",
"appSettings_unitsImperial": "Imperial (ft/mi)",
"map_lineOfSight": "Linha de visão",
"map_losScreenTitle": "Linha de visão",
"losSelectStartEnd": "Selecione nós iniciais e finais para LOS.",
"losRunFailed": "Falha na verificação da linha de visão: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Limpe todos os pontos",
"losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados",
"losShowDisplayNodes": "Mostrar nós de exibição",
"losCustomPoints": "Pontos personalizados",
"losCustomPointLabel": "{index} personalizado",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Ponto A",
"losPointB": "Ponto B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Executar LOS",
"losNoElevationData": "Sem dados de elevação",
"losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: verificando...",
"losStatusNoData": "LOS: sem dados",
"losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.",
"losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.",
"losRenameCustomPoint": "Renomear ponto personalizado",
"losPointName": "Nome do ponto",
"losShowPanelTooltip": "Mostrar painel LOS",
"losHidePanelTooltip": "Ocultar painel LOS",
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Horizonte de rádio",
"losLegendLosBeam": "Linha de visada",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequência",
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_addToFavorites": "Adicionar aos favoritos",
"listFilter_removeFromFavorites": "Remover da lista de favoritos",
"listFilter_favorites": "Favoritos",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...",
"contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...",
"contacts_searchUsers": "Pesquisar {number}{str} Usuários...",
"contacts_searchContactsNoNumber": "Pesquisar Contatos...",
"contacts_unread": "Não lido",
"contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..."
}
+1043
View File
File diff suppressed because it is too large Load Diff
+471 -7
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": {
@@ -559,7 +564,7 @@
"chat_setCustomPath": "Nastaviť vlastnú cestu",
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
"chat_clearPath": "Vyčistiš cestu",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Otvoriť odkaz?",
"chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?",
"chat_open": "Otvoriť",
"chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Neplatný formát odkazu",
"map_title": "Mapa uzlov",
"map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe",
"map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.",
"common_reload": "Načítať",
"common_clear": "Zmazať",
"path_currentPath": "Aktívna cesta: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Opakovadlá",
"listFilter_roomServers": "Servéry miestnosti",
"listFilter_unreadOnly": "Nezaregistrované len",
"listFilter_newGroup": "Nová skupina"
"listFilter_newGroup": "Nová skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbors": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbors": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
"channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.",
"channels_joinPublicChannel": "Pripojte sa k verejnému kanálu",
"channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.",
"channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu",
"channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.",
"channels_scanQrCode": "Skenujte QR kód",
"channels_scanQrCodeComingSoon": "Čoskoro",
"channels_enterHashtag": "Zadajte hashtag",
"channels_hashtagHint": "napr. #tím",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Počuli sme to: {time} dozadu",
"neighbors_unknownContact": "Neznáma {pubkey}",
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
"contacts_manageRoom": "Spravovať server miestnosti",
"room_management": "Správa servera miestnosti",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Vytvoriť komunitu",
"community_title": "Komunita",
"community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.",
"community_join": "Pripojiť",
"community_joinTitle": "Pripojiť sa k spoločenstvu",
"community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?",
"community_scanQr": "Skontrolujte komunitný QR kód",
"community_scanInstructions": "Zamerte kameru na komunitný QR kód.",
"community_showQr": "Zobraziť QR kód",
"common_ok": "OK\nDobre",
"community_publicChannel": "Komunita verejná",
"community_hashtagChannel": "Komunitný Hashtag",
"community_name": "Komunita",
"community_enterName": "Zadajte názov komunity",
"community_created": "Komunita \"{name}\" vytvorená",
"community_joined": "Pripojená komunita \"{name}\"",
"community_qrTitle": "Zdieľť komunitu",
"community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.",
"community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity",
"community_invalidQrCode": "Neplatná QR kód komunity.",
"community_alreadyMember": "Už ste členom.",
"community_alreadyMemberMessage": "Vy ste už členom \"{name}\".",
"community_addPublicChannel": "Pridať verejný komunikačný kanál",
"community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.",
"community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite",
"community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.",
"community_manageCommunities": "Spravovať komunity",
"community_delete": "Nechajte komunitu",
"community_deleteConfirm": "Opustiť \"{name}\"?",
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
"community_addHashtagChannel": "Pridať komunitný hashtag",
"community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.",
"community_selectCommunity": "Vyberte komunitu",
"community_regularHashtag": "Zvyčajný hashtag",
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
"community_communityHashtag": "Komunitný Hashtag",
"community_communityHashtagDesc": "Špecifické pre členov komunity",
"community_forCommunity": "Pre {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne",
"community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.",
"community_regenerate": "Znovu vygenerovať",
"community_regenerateSecret": "Zobraziť nový tajný kód",
"community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
"community_updateSecret": "Aktualizovať tajné heslo",
"community_secretUpdated": "Zmena tajnej slova pre \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Vy",
"pathTrace_failed": "Sledovanie cesty zlyhalo.",
"pathTrace_notAvailable": "Path trace nie je k dispozícii.",
"pathTrace_refreshTooltip": "Obnoviť Path Trace.",
"contacts_pathTrace": "Sledovanie lúčov",
"contacts_ping": "Pingovať",
"contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču",
"contacts_repeaterPing": "Pingovať opakovač",
"contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti",
"contacts_roomPing": "Ping server miestnosti",
"contacts_chatTraceRoute": "Sledovať trasu lúča",
"contacts_pathTraceTo": "Sledovať trasu k {name}",
"contacts_clipboardEmpty": "Schránka je prázdna.",
"appSettings_languageUk": "Ukrajinská",
"contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.",
"contacts_zeroHopAdvert": "Inzerát Zero Hop",
"contacts_floodAdvert": "Inzerát povodní",
"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.",
"contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.",
"contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.",
"contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.",
"contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát",
"contacts_ShareContact": "Kopírovať kontakt do schránky",
"notification_activityTitle": "Aktivita MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}",
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
"notification_newTypeDiscovered": "Nový {contactType} objavený",
"notification_receivedNewMessage": "Prijatá nová správa",
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
"settings_gpxExportContacts": "Export sprievodcov do GPX",
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
"settings_gpxExportNoContacts": "Žiadne kontakty na export.",
"settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme",
"settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností",
"settings_gpxExportError": "Vyskytol sa chyba počas exportu.",
"settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.",
"settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.",
"settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX",
"settings_gpxExportAll": "Exportovať všetky kontakty do GPX",
"settings_gpxExportAllContacts": "Všetky kontaktné lokality",
"settings_gpxExportChat": "Lokácie sprievodcov",
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
"pathTrace_clearTooltip": "Zmazať cestu",
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
"map_removeLast": "Odstrániť posledný",
"map_runTrace": "Spustiť trasovaním cesty",
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
"scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.",
"scanner_bluetoothOff": "Bluetooth je vypnutý",
"scanner_enableBluetooth": "Povolte Bluetooth",
"snrIndicator_lastSeen": "Naposledy videný",
"snrIndicator_nearByRepeaters": "Miestne opakovače",
"chat_ShowAllPaths": "Zobraziť všetky cesty",
"settings_clientRepeat": "Opätovné použitie bez elektrickej siete",
"settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.",
"settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.",
"settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Jednotky",
"appSettings_unitsMetric": "Metrické (m / km)",
"appSettings_unitsImperial": "Imperiálne (ft / mi)",
"map_lineOfSight": "Line of Sight",
"map_losScreenTitle": "Line of Sight",
"losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.",
"losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Vymazať všetky body",
"losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS",
"losMenuTitle": "Menu LOS",
"losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body",
"losShowDisplayNodes": "Zobraziť uzly zobrazenia",
"losCustomPoints": "Vlastné body",
"losCustomPointLabel": "Vlastné {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Bod A",
"losPointB": "Bod B",
"losAntennaA": "Anténa A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Anténa B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Spustite LOS",
"losNoElevationData": "Žiadne údaje o nadmorskej výške",
"losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kontrolujem...",
"losStatusNoData": "LOS: žiadne údaje",
"losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.",
"losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.",
"losRenameCustomPoint": "Premenovať vlastný bod",
"losPointName": "Názov bodu",
"losShowPanelTooltip": "Zobraziť panel LOS",
"losHidePanelTooltip": "Skryť panel LOS",
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Rádiový horizont",
"losLegendLosBeam": "Priama viditeľnosť",
"losLegendTerrain": "Terén",
"losFrequencyLabel": "Frekvencia",
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Odstrániť z označení",
"listFilter_addToFavorites": "Pridaj do obľúbených",
"listFilter_favorites": "Obľúbené",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...",
"contacts_searchFavorites": "Hľadať {number}{str} obľúbené...",
"contacts_searchRepeaters": "Hľadať {number}{str} opakovače...",
"contacts_searchUsers": "Hľadať {number}{str} používateľov...",
"contacts_searchContactsNoNumber": "Hľadať kontakty...",
"contacts_unread": "Neprečítané"
}
+603 -139
View File
@@ -1,7 +1,15 @@
{
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@@locale": "sl",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakti",
"nav_contacts": "Stiki",
"nav_channels": "Kanali",
"nav_map": "Karta",
"common_cancel": "Prekliči",
@@ -69,49 +77,49 @@
},
"scanner_stop": "Prekliči",
"scanner_scan": "Skeniraj",
"device_quickSwitch": "Hitro preklopiti",
"device_quickSwitch": "Hitro preklop",
"device_meshcore": "MeshCore",
"settings_title": "Nastavitve",
"settings_deviceInfo": "Informacije o napravei",
"settings_appSettings": "Nastavitve aplikacije",
"settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.",
"settings_nodeSettings": "Nastavitve časa",
"settings_nodeName": "Ime omrežno mesto",
"settings_nodeNameNotSet": "Nezavedeno",
"settings_nodeNameHint": "Vnesite ime časa",
"settings_nodeSettings": "Nastavitev časa",
"settings_nodeName": "Ime node-a",
"settings_nodeNameNotSet": "Ni nastavljeno",
"settings_nodeNameHint": "Vnesite ime node-a",
"settings_nodeNameUpdated": "Ime posodobljeno",
"settings_radioSettings": "Nastavitve radija",
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširni faktor",
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor",
"settings_radioSettingsUpdated": "Radio nastavitve posodobljene",
"settings_location": "Lokacija",
"settings_locationSubtitle": "GPS koordinate",
"settings_locationUpdated": "Lokacija posodobljena",
"settings_locationBothRequired": "Vnesite širino in dolžino.",
"settings_locationInvalid": "Neveljna zemeljska širina ali dolžina.",
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
"settings_latitude": "Širina",
"settings_longitude": "Dolžina",
"settings_privacyMode": "Mod podjetja",
"settings_privacyMode": "Zasebnost",
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
"settings_privacyModeEnabled": "Privatni režim je omogočen.",
"settings_privacyModeDisabled": "Privatni režim je onemogočen.",
"settings_privacyModeEnabled": "Privatni način je omogočen.",
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
"settings_actions": "Akcije",
"settings_sendAdvertisement": "Pošlji Oglas",
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
"settings_advertisementSent": "Oglas poslan",
"settings_syncTime": "Ugasniti čas",
"settings_syncTimeSubtitle": "Nastavi uro naprave v čas telefona",
"settings_timeSynchronized": "Sinhronizirano po času",
"settings_syncTime": "Nastavi uro",
"settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona",
"settings_timeSynchronized": "Ura sinhronizirana",
"settings_refreshContacts": "Ponovno obišči kontakte",
"settings_refreshContactsSubtitle": "Ponovno naloži seznam kontaktov iz naprave",
"settings_rebootDevice": "Restart Naprave",
"settings_rebootDeviceSubtitle": "Ponovite zažetek naprave MeshCore",
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.",
"settings_debug": "Napravi popravek",
"settings_bleDebugLog": "Logarjev zapis BLE",
"settings_bleDebugLogSubtitle": "Navodila BLE, odgovori in surovo podatkovno",
"settings_appDebugLog": "Log zapiske aplikacije",
"settings_appDebugLogSubtitle": "Prijavni sporočila aplikacije",
"settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi",
"settings_rebootDevice": "Ponovni zagon naprave",
"settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo",
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.",
"settings_debug": "Debug",
"settings_bleDebugLog": "BLE debug log (razhroščevanje)",
"settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki",
"settings_appDebugLog": "Logi aplikacije",
"settings_appDebugLogSubtitle": "Debug sporočila aplikacije",
"settings_about": "Oglejte si",
"settings_aboutVersion": "MeshCore Open v{version}",
"@settings_aboutVersion": {
@@ -121,30 +129,25 @@
}
}
},
"settings_aboutLegalese": "MeshCore Odprtokodni Projekt 2024",
"settings_aboutDescription": "Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.",
"settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024",
"settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.",
"settings_infoName": "Ime",
"settings_infoId": "ID",
"settings_infoStatus": "Status",
"settings_infoBattery": "Baterija",
"settings_infoPublicKey": "Ključ javnega tipa",
"settings_infoContactsCount": "Število kontaktov",
"settings_infoPublicKey": "Javni ključ",
"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": "Neveljčna frekvenca (300-2500 MHz)",
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
"settings_bandwidth": "Pasovna širina",
"settings_spreadingFactor": "Razširitveni faktor",
"settings_codingRate": "Programska hitrost",
"settings_txPower": "TX Moč (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Neveljaven TX moč (0-22 dBm)",
"settings_longRange": "Dolenje območje",
"settings_fastSpeed": "Hitra hitrost",
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
"settings_error": "Napaka: {message}",
"@settings_error": {
"placeholders": {
@@ -156,8 +159,8 @@
"appSettings_title": "Nastavitve aplikacije",
"appSettings_appearance": "Prikaži",
"appSettings_theme": "Tema",
"appSettings_themeSystem": "Predpomnilnik sistema",
"appSettings_themeLight": "Luč",
"appSettings_themeSystem": "Sistemska tema",
"appSettings_themeLight": "Svetlo",
"appSettings_themeDark": "Temno",
"appSettings_language": "Jezik",
"appSettings_languageSystem": "Sistemska privzeta vrednost",
@@ -174,9 +177,9 @@
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih",
"appSettings_notifications": "Obvestila",
"appSettings_enableNotifications": "Omogoči obvestila",
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena",
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
@@ -185,19 +188,19 @@
"appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala",
"appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala",
"appSettings_advertisementNotifications": "Opozorila o oglasih",
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so novi vozlišči odkrivljeni.",
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.",
"appSettings_messaging": "Komuniciranje",
"appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju",
"appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja",
"appSettings_pathsWillBeCleared": "Potnice bodo očiščene po 5 neuspešnih poskusih.",
"appSettings_pathsWillNotBeCleared": "Potniški poti ne bodo samodejno čiščeni.",
"appSettings_autoRouteRotation": "Avtomatsko Občutke in Rotacije",
"appSettings_autoRouteRotationSubtitle": "Med spreminjanjem med najboljšimi potmi in plovilnim načinom",
"appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.",
"appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.",
"appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti",
"appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom",
"appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno",
"appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno",
"appSettings_battery": "Baterija",
"appSettings_batteryChemistry": "Razem z možnostmi",
"appSettings_batteryChemistryPerDevice": "Nastavitve za naprave ({deviceName})",
"appSettings_batteryChemistry": "Kemija baterije",
"appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})",
"@appSettings_batteryChemistryPerDevice": {
"placeholders": {
"deviceName": {
@@ -205,20 +208,20 @@
}
}
},
"appSettings_batteryChemistryConnectFirst": "Povežite se z napravo za izbiro",
"appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo",
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
"appSettings_batteryLifepo4": "LiFePO4 (2,63,65 V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Prikaz zemljevide",
"appSettings_showRepeaters": "Prikaži ponovitve",
"appSettings_showRepeatersSubtitle": "Prikaži ponovljalne notranjosti na zemljeploscu",
"appSettings_showChatNodes": "Prikaži čakalne notranjosti",
"appSettings_showChatNodesSubtitle": "Prikaži pogovorni pike na zemljeploscu",
"appSettings_showOtherNodes": "Pokaži druge vozlišča",
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste notranjih elementov na zemljevalu.",
"appSettings_timeFilter": "Filtri po času",
"appSettings_timeFilterShowAll": "Pokaži vse notranje elemente",
"appSettings_timeFilterShowLast": "Pokaži notranjosti iz zadnjih {hours} ur",
"appSettings_mapDisplay": "Prikaz zemljevida",
"appSettings_showRepeaters": "Prikaži repetitorje",
"appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi",
"appSettings_showChatNodes": "Prikaži naprave za klepet",
"appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu",
"appSettings_showOtherNodes": "Pokaži druge naprave",
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.",
"appSettings_timeFilter": "Filter po času",
"appSettings_timeFilterShowAll": "Pokaži vse naprave",
"appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah",
"@appSettings_timeFilterShowLast": {
"placeholders": {
"hours": {
@@ -226,15 +229,15 @@
}
}
},
"appSettings_mapTimeFilter": "Filtri časa zemljevida",
"appSettings_showNodesDiscoveredWithin": "Pokaži notranje čepke, odkrivene v:",
"appSettings_allTime": "Vse čase",
"appSettings_lastHour": "Minuto nazaj",
"appSettings_mapTimeFilter": "Filter časa na zemljevidu",
"appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:",
"appSettings_allTime": "Brez omejitev",
"appSettings_lastHour": "V zadnji uri",
"appSettings_last6Hours": "Zadnjih 6 ur",
"appSettings_last24Hours": "Zadnjih 24 ur",
"appSettings_lastWeek": "Lepošno",
"appSettings_offlineMapCache": "Omrezni Poudni Arhiv",
"appSettings_noAreaSelected": "Nizkana označena površina",
"appSettings_lastWeek": "Prejšnji teden",
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
"appSettings_noAreaSelected": "Območje ni izbrano",
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
"@appSettings_areaSelectedZoom": {
"placeholders": {
@@ -246,19 +249,19 @@
}
}
},
"appSettings_debugCard": "Napravi popravek",
"appSettings_appDebugLogging": "Programski Log",
"appSettings_appDebugLoggingSubtitle": "Log aplikacijske debug sporočila za odpravljanje težav",
"appSettings_appDebugLoggingEnabled": "Omogočeno zaznamovanje napak v aplikaciji",
"appSettings_appDebugLoggingDisabled": "Programski logi aplikacije so onemogočeni.",
"contacts_title": "Kontakti",
"contacts_noContacts": "Še ni kontaktov.",
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"appSettings_debugCard": "Razhroščevanje",
"appSettings_appDebugLogging": "Programski dnevnik",
"appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav",
"appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno",
"appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.",
"contacts_title": "Stiki",
"contacts_noContacts": "Ni stikov.",
"contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.",
"contacts_searchContacts": "Iskanje stikov...",
"contacts_noUnreadContacts": "Ne prebrani stiki.",
"contacts_noContactsFound": "Stiki niso najdeni.",
"contacts_deleteContact": "Izbriši stik",
"contacts_removeConfirm": "Izbrišem {contactName} iz stikov?",
"@contacts_removeConfirm": {
"placeholders": {
"contactName": {
@@ -266,12 +269,12 @@
}
}
},
"contacts_manageRepeater": "Upravljajte Ponovitve",
"contacts_roomLogin": "Vnos v sobo",
"contacts_openChat": "Odprta kleta",
"contacts_editGroup": "Uredi Skupino",
"contacts_deleteGroup": "Izbrisati Skupino",
"contacts_deleteGroupConfirm": "Odpovedati {groupName}?",
"contacts_manageRepeater": "Upravljaj Ponovitve",
"contacts_roomLogin": "Prijava v sobo",
"contacts_openChat": "Odpri klepet",
"contacts_editGroup": "Uredi skupino",
"contacts_deleteGroup": "Izbriši skupino",
"contacts_deleteGroupConfirm": "Izbriši {groupName}?",
"@contacts_deleteGroupConfirm": {
"placeholders": {
"groupName": {
@@ -279,8 +282,8 @@
}
}
},
"contacts_newGroup": "Novo skupino",
"contacts_groupName": "Skupina imena",
"contacts_newGroup": "Nova skupina",
"contacts_groupName": "Ime skupine",
"contacts_groupNameRequired": "Ime skupine je obvezno.",
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
"@contacts_groupAlreadyExists": {
@@ -290,11 +293,11 @@
}
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Nič članov.",
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
"contacts_filterContacts": "Filtriraj stik\\,...",
"contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Ni članov.",
"contacts_lastSeenNow": "Nazadnje viden zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -302,8 +305,8 @@
}
}
},
"contacts_lastSeenHourAgo": "Zadnjič ogledan pred 1 uro.",
"contacts_lastSeenHoursAgo": "Zadnjič videti {hours} ur nazaj",
"contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.",
"contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -311,8 +314,8 @@
}
}
},
"contacts_lastSeenDayAgo": "Zadnjič ogledan pred 1 dnem",
"contacts_lastSeenDaysAgo": "Zadnjič videti {days} dni nazaj",
"contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem",
"contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -321,10 +324,10 @@
}
},
"channels_title": "Kanali",
"channels_noChannelsConfigured": "Nekonfigurirane kanale",
"channels_addPublicChannel": "Dodaj Objavni Kanal",
"channels_noChannelsConfigured": "Kanali še niso konfigurirani",
"channels_addPublicChannel": "Dodaj javni kanal",
"channels_searchChannels": "Poišči kanale...",
"channels_noChannelsFound": "Niti kanalov najti ni.",
"channels_noChannelsFound": "Ne najdem kanalov.",
"channels_channelIndex": "Kanal {index}",
"@channels_channelIndex": {
"placeholders": {
@@ -334,13 +337,15 @@
}
},
"channels_hashtagChannel": "Hashtag kanal",
"channels_public": "javno",
"channels_private": "Zasebno",
"channels_publicChannel": "Ogljišna skupina",
"channels_privateChannel": "Zatemniščen kanal",
"channels_public": "Javni",
"channels_private": "Zasebni",
"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": "Izbrisati \"{name}\"? To se ne da povrniti.",
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
"@channels_deleteChannelConfirm": {
"placeholders": {
"name": {
@@ -424,8 +429,8 @@
}
}
},
"chat_typeMessage": "Vnesite sporočilo...",
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} bajt).",
"chat_typeMessage": "Vnesi sporočilo...",
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).",
"@chat_messageTooLong": {
"placeholders": {
"maxBytes": {
@@ -433,9 +438,9 @@
}
}
},
"chat_messageCopied": "Pošljeno sporočilo",
"chat_messageDeleted": "Pošiljanje sporočila izbrisano",
"chat_retryingMessage": "Ponovna poskus.",
"chat_messageCopied": "Sporočilo poslano",
"chat_messageDeleted": "Sporočilo izbrisano",
"chat_retryingMessage": "Ponovni poskus.",
"chat_retryCount": "Ponovit {current}/{max}",
"@chat_retryCount": {
"placeholders": {
@@ -448,31 +453,31 @@
}
},
"chat_sendGif": "Pošlji GIF",
"chat_reply": "Odpošlji",
"chat_addReaction": "Dodaj Reakcijo",
"chat_reply": "Odgovori",
"chat_addReaction": "Dodaj reakcijo",
"chat_me": "jaz",
"emojiCategorySmileys": "Emoji",
"emojiCategoryGestures": "Gestikulacije",
"emojiCategoryHearts": "Srce",
"emojiCategoryObjects": "Predmeti",
"gifPicker_title": "Izberi GIF",
"gifPicker_searchHint": "Iskalite GIF-e...",
"gifPicker_poweredBy": "Naprodno z GIPHY",
"gifPicker_noGifsFound": "Niti GIF-jev najti ni.",
"gifPicker_failedLoad": "Neuspešno je naložilo GIF-e",
"gifPicker_failedSearch": "Posodobit neuspešno.",
"gifPicker_searchHint": "Išči GIF-e...",
"gifPicker_poweredBy": "Napredno z GIPHY",
"gifPicker_noGifsFound": "Ne najdem GIF-ov.",
"gifPicker_failedLoad": "Neuspešno nalaganje GIF-a",
"gifPicker_failedSearch": "Iskanje neuspešno.",
"gifPicker_noInternet": "Ni internetne povezave",
"debugLog_appTitle": "Log zapiske aplikacije",
"debugLog_bleTitle": "Logarjev zapis BLE",
"debugLog_copyLog": "Kopiraj zapiske",
"debugLog_clearLog": "Pasters log",
"debugLog_copied": "Kopirana belež poteka.",
"debugLog_bleCopied": "Kopirana beležke iz BLE",
"debugLog_noEntries": "Še ni ustvarjenih debug zapisov.",
"debugLog_enableInSettings": "Omogoči beleženje napak v aplikaciji v nastavitvah",
"debugLog_frames": "Okna",
"debugLog_bleTitle": "Log zapis BLE",
"debugLog_copyLog": "Kopiraj dnevnik",
"debugLog_clearLog": "Briši log",
"debugLog_copied": "Beležka kopirana.",
"debugLog_bleCopied": "Kopirana beležka iz BLE",
"debugLog_noEntries": "Ni ustvarjenih debug zapisov.",
"debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije",
"debugLog_frames": "Okvirji",
"debugLog_rawLogRx": "Svež Log-RX",
"debugLog_noBleActivity": "Šele začnite z aktivnostjo BLE.",
"debugLog_noBleActivity": "Ni BLE aktivnosti.",
"debugFrame_length": "Izhodni rob: {count} bajtov",
"@debugFrame_length": {
"placeholders": {
@@ -542,8 +547,8 @@
"chat_forceFloodMode": "Nasilje obvezati v način",
"chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):",
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
"chat_hopSingular": "skoč",
"chat_hopPlural": "škrabec",
"chat_hopSingular": "skok",
"chat_hopPlural": "skokov",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"@chat_hopsCount": {
"placeholders": {
@@ -554,16 +559,16 @@
},
"chat_successes": "Uspešni",
"chat_removePath": "Izbriši pot",
"chat_noPathHistoryYet": "Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
"chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
"chat_pathActions": "Potni ukazi:",
"chat_setCustomPath": "Nastavi Prilozeno Pot",
"chat_setCustomPathSubtitle": "Ročno določite potniško pot.",
"chat_clearPath": "Čista pot",
"chat_clearPath": "Počisti pot",
"chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.",
"chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.",
"chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.",
"chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.",
"chat_fullPath": "Polni pot",
"chat_fullPath": "Polna pot",
"chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.",
"chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
"@chat_pathSetHops": {
@@ -604,9 +609,21 @@
}
}
},
"chat_openLink": "Odpreti povezavo?",
"chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?",
"chat_open": "Odpri",
"chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Neveljavna oblika povezave",
"map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
"map_nodesCount": "Omize: {count}",
"@map_nodesCount": {
"placeholders": {
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.",
"common_reload": "Ponovno naloži",
"common_clear": "Ponoviti",
"path_currentPath": "Trenutna pot: {path}",
@@ -1091,13 +1109,13 @@
}
}
},
"repeater_cliQuickGetName": "Dobiti ime",
"repeater_cliQuickGetName": "Pridobi ime",
"repeater_cliQuickGetRadio": "Dobiti Radiopravo",
"repeater_cliQuickGetTx": "Dobiti TX",
"repeater_cliQuickGetTx": "Pridobi TX",
"repeater_cliQuickNeighbors": "Sosedi",
"repeater_cliQuickVersion": "Različica",
"repeater_cliQuickAdvertise": "Oglasite",
"repeater_cliQuickClock": "Urnik",
"repeater_cliQuickClock": "Ura",
"repeater_cliHelpAdvert": "Pošlje paket oglasov",
"repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)",
"repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.",
@@ -1129,7 +1147,7 @@
"repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.",
"repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).",
"repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).",
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
"repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow",
"repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.",
"repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.",
@@ -1158,8 +1176,8 @@
"repeater_settingsCategory": "Nastavitve",
"repeater_bridge": "Most",
"repeater_logging": "Logiranje",
"repeater_neighborsRepeaterOnly": "Sosedi (le za ponovitelja)",
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za ponovitve)",
"repeater_neighborsRepeaterOnly": "Sosedi (le za repetitorje)",
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)",
"repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.",
"repeater_gpsManagement": "Upravljanje GPS",
"repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.",
@@ -1231,9 +1249,9 @@
"channelPath_repeaterHops": "Skoki ponovitelja",
"channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.",
"channelPath_messageDetails": "Podrobnosti sporočila",
"channelPath_senderLabel": "Pošiljalec",
"channelPath_timeLabel": "Čas",
"channelPath_repeatsLabel": "Ponovi",
"channelPath_senderLabel": "Pošiljatelj",
"channelPath_timeLabel": "Ura",
"channelPath_repeatsLabel": "Ponovitve",
"channelPath_pathLabel": "Pot {index}",
"channelPath_observedLabel": "Opazovani",
"channelPath_observedPathTitle": "Opazovana pot {index} • {hops}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Ponovitve",
"listFilter_roomServers": "Smeti za prostore",
"listFilter_unreadOnly": "Nezbrani samo",
"listFilter_newGroup": "Nova skupina"
"listFilter_newGroup": "Nova skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbors": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
"channels_createPrivateChannel": "Ustvari zasebno kanal.",
"channels_joinPublicChannel": "Pridružite se javnemu kanalu",
"channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.",
"channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom",
"channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.",
"channels_scanQrCode": "Skeniraj QR kodo",
"channels_scanQrCodeComingSoon": "Prihajajoča",
"channels_enterHashtag": "Vnesite hashtag",
"channels_hashtagHint": "npr. #ekipa",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Nepoznano {pubkey}",
"neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Ustvari novo skupnost in jo deli preko QR kode.",
"community_title": "Skupnost",
"common_ok": "V redu",
"community_create": "Ustvari skupnost",
"community_joinTitle": "Pridružite se skupnosti",
"community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?",
"community_scanQr": "Skeniraj QR kode skupnosti",
"community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.",
"community_showQr": "Pokaži QR kodo",
"community_publicChannel": "Skupnostna javna",
"community_hashtagChannel": "Skupnostni hashtag",
"community_name": "Komunitarne ime",
"community_enterName": "Vnesite ime skupnosti",
"community_join": "Pridružiti se",
"community_created": "Skupnost \"{name}\" je bila ustvarila.",
"community_joined": "Prilojen k skupnosti \"{name}\"",
"community_qrTitle": "Delite skupnost",
"community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.",
"community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti",
"community_invalidQrCode": "Neveljaven QR koden skupnosti",
"community_alreadyMember": "Že član",
"community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.",
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
"community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.",
"community_manageCommunities": "Upravljanje skupnosti",
"community_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapusti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj hashtag kanal",
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
"community_selectCommunity": "Izberi skupnost",
"community_regularHashtag": "Oznaka s hashtagom",
"community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"community_forCommunity": "Za {name}",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno",
"community_regenerateSecret": "Ponovno ustvari geslo",
"community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
"community_regenerate": "Preberi znova",
"community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}",
"community_updateSecret": "Ažuriraj ključ",
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\"",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Ti",
"pathTrace_failed": "Sledenje poti ni uspelo.",
"pathTrace_notAvailable": "Potni sled ni na voljo.",
"pathTrace_refreshTooltip": "Osveži Path Trace.",
"contacts_pathTrace": "Sledenje poti",
"contacts_ping": "Pingati",
"contacts_repeaterPathTrace": "Sledi poti do ponavljalnika",
"contacts_repeaterPing": "Pinguj ponavljalnik",
"contacts_roomPathTrace": "Sledenje poti do strežnika sobe",
"contacts_roomPing": "Ping strežnik sobe",
"contacts_chatTraceRoute": "Slediti poti žarkov",
"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",
"contacts_floodAdvert": "Poplavna oglás",
"contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke",
"contacts_clipboardEmpty": "Odložišče je prazno.",
"contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče",
"contacts_addContactFromClipboard": "Dodaj stik iz odložišča",
"contacts_zeroHopContactAdvertSent": "Poslano po oglasu.",
"contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.",
"contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.",
"contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.",
"contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa",
"contacts_ShareContact": "Kopiraj stik v Odložišče",
"notification_activityTitle": "Aktivnost MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}",
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
"notification_receivedNewMessage": "Prejeto novo sporočilo",
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
"settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX",
"settings_gpxExportError": "Pri izvozu je prišlo do napake.",
"settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe",
"settings_gpxExportChat": "Lokacije spremljevalcev",
"settings_gpxExportAllContacts": "Lokacije vseh stikov",
"settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.",
"settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.",
"settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.",
"settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open",
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
"map_removeLast": "Odstrani Zadnji",
"map_runTrace": "Zaženi sledenje poti",
"pathTrace_clearTooltip": "Počisti pot",
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
"scanner_enableBluetooth": "Omogočite Bluetooth",
"scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.",
"scanner_bluetoothOff": "Bluetooth je izklopljen",
"snrIndicator_lastSeen": "Zadnjič videno",
"snrIndicator_nearByRepeaters": "Bližnji ponovitelji",
"chat_ShowAllPaths": "Prikaži vse poti",
"settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.",
"settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
"settings_clientRepeat": "Neovadno ponavljanje",
"settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enote",
"appSettings_unitsMetric": "Metrična (m/km)",
"appSettings_unitsImperial": "Imperialno (ft / mi)",
"map_lineOfSight": "Linija vida",
"map_losScreenTitle": "Linija vida",
"losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.",
"losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Počisti vse točke",
"losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila",
"losMenuTitle": "LOS meni",
"losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri",
"losShowDisplayNodes": "Pokaži prikazna vozlišča",
"losCustomPoints": "Točke po meri",
"losCustomPointLabel": "Po meri {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Točka A",
"losPointB": "Točka B",
"losAntennaA": "Antena A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antena B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Zaženi LOS",
"losNoElevationData": "Ni podatkov o višini",
"losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: preverjam ...",
"losStatusNoData": "LOS: ni podatkov",
"losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.",
"losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.",
"losRenameCustomPoint": "Preimenujte točko po meri",
"losPointName": "Ime točke",
"losShowPanelTooltip": "Pokaži ploščo LOS",
"losHidePanelTooltip": "Skrij ploščo LOS",
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radijski horizont",
"losLegendLosBeam": "Linija vidnosti",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Frekvenca",
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_favorites": "Priljubljene",
"listFilter_removeFromFavorites": "Odstrani iz priljubljenih",
"listFilter_addToFavorites": "Dodaj v priljubljene",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Neprebrano",
"contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...",
"contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...",
"contacts_searchContactsNoNumber": "Iskanje stikov...",
"contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...",
"contacts_searchUsers": "Išči {number}{str} uporabnikov..."
}
+470 -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": {
@@ -604,6 +609,18 @@
}
}
},
"chat_openLink": "Öppna länk?",
"chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?",
"chat_open": "Öppna",
"chat_couldNotOpenLink": "Kunde inte öppna länken: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ogiltigt länkformat",
"map_title": "Nodkarta",
"map_noNodesWithLocation": "Inga noder med platsinformation",
"map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan",
@@ -821,6 +838,7 @@
}
}
},
"login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.",
"common_reload": "Ladda om",
"common_clear": "Rensa",
"path_currentPath": "Nuvarande sökväg: {path}",
@@ -1335,5 +1353,451 @@
"listFilter_repeaters": "Upprepare",
"listFilter_roomServers": "Rumservrar",
"listFilter_unreadOnly": "Endast oinlästa",
"listFilter_newGroup": "Ny grupp"
"listFilter_newGroup": "Ny grupp",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbors": "Grannar",
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbors": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
"channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.",
"channels_joinPublicChannel": "Gå med i den Offentliga Kanalen",
"channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.",
"channels_joinHashtagChannel": "Gå med i en Hashtagkanal",
"channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.",
"channels_scanQrCode": "Skanna en QR-kod",
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hördes: {time} sedan",
"neighbors_unknownContact": "Okänd {pubkey}",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Skapa Gemenskap",
"community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.",
"common_ok": "Okej",
"community_title": "Gemenskap",
"community_join": "Gå med",
"community_joinTitle": "Gå med i gemenskapen",
"community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?",
"community_scanQr": "Skanna Gemenskapens QR",
"community_scanInstructions": "Rikta kameran mot en QR-kod i communityn",
"community_showQr": "Visa QR-kod",
"community_publicChannel": "Föreningens Offentliga",
"community_name": "Gemenskapens namn",
"community_enterName": "Ange communities namn",
"community_created": "Community \"{name}\" har skapats",
"community_joined": "Medlem i communityn \"{name}\"",
"community_qrTitle": "Dela Gemenskap",
"community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"",
"community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn",
"community_hashtagChannel": "Community Hashtag",
"community_invalidQrCode": "Ogiltig community QR-kod",
"community_alreadyMember": "Är redan medlem",
"community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".",
"community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)",
"community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community",
"community_noCommunities": "Inga gemenskaper har anslutats ännu",
"community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång",
"community_manageCommunities": "Hantera Gemenskaper",
"community_delete": "Lämna Gemenskap",
"community_deleteConfirm": "Lämna \"{name}\"?",
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"community_deleted": "Lämnade community \"{name}\"",
"community_addHashtagChannel": "Lägg till Gemenskapens Hashtag",
"community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community",
"community_selectCommunity": "Välj Gemenskap",
"community_regularHashtag": "Vanlig Hash Tag",
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"community_communityHashtag": "Community Hashtag",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretRegenerated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_secretUpdated": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_regenerate": "Regenerera",
"community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.",
"community_secretRegenerated": "Lösenord återskapad för \"{name}\"",
"community_regenerateSecret": "Regenerera hemlig kod",
"community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
"community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
"community_updateSecret": "Uppdatera hemlighet",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Du",
"pathTrace_failed": "Sökvägsföljning misslyckades.",
"pathTrace_notAvailable": "Path trace ej tillgänglig.",
"pathTrace_refreshTooltip": "Uppdatera Path Trace",
"contacts_pathTrace": "Path Trace",
"contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Vägspårning till repeater",
"contacts_repeaterPing": "Ping-repeater",
"contacts_roomPathTrace": "Vägspårning till rumserver",
"contacts_roomPing": "Ping rumsserver",
"contacts_chatTraceRoute": "Spåra rutt",
"contacts_pathTraceTo": "Spåra rutt till {name}",
"contacts_clipboardEmpty": "Urklipp är tomt.",
"appSettings_languageRu": "Ryska",
"contacts_contactImportFailed": "Kontakt kunde inte importeras.",
"contacts_zeroHopAdvert": "Reklam med nollhopp",
"contacts_floodAdvert": "Översvämningsannons",
"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.",
"contacts_contactAdvertCopied": "Annons kopierad till Urklipp.",
"contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.",
"contacts_ShareContact": "Kopiera kontakt till Urklipp",
"contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.",
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
"notification_activityTitle": "MeshCore Aktivitet",
"notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}",
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
"notification_receivedNewMessage": "Nytt meddelande mottaget",
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
"settings_gpxExportNoContacts": "Inga kontakter att exportera.",
"settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem",
"settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser",
"settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX",
"settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.",
"settings_gpxExportContacts": "Exportera följeslagare till GPX",
"settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.",
"settings_gpxExportChat": "Medhjälparplatser",
"settings_gpxExportError": "Det uppstod ett fel när data exporterades.",
"settings_gpxExportAllContacts": "Alla kontakters platser",
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
"pathTrace_clearTooltip": "Rensa väg",
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
"map_runTrace": "Kör spårsökning",
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
"map_removeLast": "Ta bort sista",
"scanner_enableBluetooth": "Aktivera Bluetooth",
"scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.",
"scanner_bluetoothOff": "Bluetooth är avstängt",
"snrIndicator_lastSeen": "Senast sedd",
"snrIndicator_nearByRepeaters": "Närliggande uppreparstationer",
"chat_ShowAllPaths": "Visa alla vägar",
"settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.",
"settings_clientRepeat": "Upprepa utan elnät",
"settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.",
"settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Enheter",
"appSettings_unitsMetric": "Metriskt (m/km)",
"appSettings_unitsImperial": "Imperialt (ft / mi)",
"map_lineOfSight": "Synlinje",
"map_losScreenTitle": "Synlinje",
"losSelectStartEnd": "Välj start- och slutnoder för LOS.",
"losRunFailed": "Synlinjekontroll misslyckades: {error}",
"@losRunFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"losClearAllPoints": "Rensa alla punkter",
"losRunToViewElevationProfile": "Kör LOS för att se höjdprofil",
"losMenuTitle": "LOS-menyn",
"losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter",
"losShowDisplayNodes": "Visa displaynoder",
"losCustomPoints": "Anpassade poäng",
"losCustomPointLabel": "Anpassad {index}",
"@losCustomPointLabel": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"losPointA": "Punkt A",
"losPointB": "Punkt B",
"losAntennaA": "Antenn A: {value} {unit}",
"@losAntennaA": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losAntennaB": "Antenn B: {value} {unit}",
"@losAntennaB": {
"placeholders": {
"value": {
"type": "String"
},
"unit": {
"type": "String"
}
}
},
"losRun": "Kör LOS",
"losNoElevationData": "Inga höjddata",
"losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}",
"@losProfileClear": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"clearance": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}",
"@losProfileBlocked": {
"placeholders": {
"distance": {
"type": "String"
},
"distanceUnit": {
"type": "String"
},
"obstruction": {
"type": "String"
},
"heightUnit": {
"type": "String"
}
}
},
"losStatusChecking": "LOS: kollar...",
"losStatusNoData": "LOS: inga data",
"losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd",
"@losStatusSummary": {
"placeholders": {
"clear": {
"type": "int"
},
"total": {
"type": "int"
},
"blocked": {
"type": "int"
},
"unknown": {
"type": "int"
}
}
},
"losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.",
"losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.",
"losRenameCustomPoint": "Byt namn på anpassad punkt",
"losPointName": "Punktnamn",
"losShowPanelTooltip": "Visa LOS-panelen",
"losHidePanelTooltip": "Dölj LOS-panelen",
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radiohorisont",
"losLegendLosBeam": "Siktlinje",
"losLegendTerrain": "Terräng",
"losFrequencyLabel": "Frekvens",
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"listFilter_removeFromFavorites": "Ta bort från favoriter",
"listFilter_addToFavorites": "Lägg till i favoriter",
"listFilter_favorites": "Favoriter",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_unread": "Oläst",
"contacts_searchContactsNoNumber": "Sök kontakter...",
"contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...",
"contacts_searchFavorites": "Sök {number}{str} Favoriter...",
"contacts_searchUsers": "Sök {number}{str} användare...",
"contacts_searchRoomServers": "Sök {number}{str} Room-servrar..."
}
+1803
View File
File diff suppressed because it is too large Load Diff
+842 -373
View File
File diff suppressed because it is too large Load Diff
+63 -13
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart';
import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart';
@@ -14,6 +15,7 @@ import 'services/ble_debug_log_service.dart';
import 'services/app_debug_log_service.dart';
import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart';
import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart';
@@ -27,12 +29,13 @@ void main() async {
final storage = StorageService();
final connector = MeshCoreConnector();
final pathHistoryService = PathHistoryService(storage);
final retryService = MessageRetryService(storage);
final retryService = MessageRetryService();
final appSettingsService = AppSettingsService();
final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
// Load settings
await appSettingsService.loadSettings();
@@ -47,6 +50,9 @@ void main() async {
final notificationService = NotificationService();
await notificationService.initialize();
await backgroundService.initialize();
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
// Wire up connector with services
connector.initialize(
@@ -60,21 +66,46 @@ void main() async {
await connector.loadContactCache();
await connector.loadChannelSettings();
await connector.loadCachedChannels();
// Load persisted channel messages
await connector.loadAllChannelMessages();
await connector.loadUnreadState();
runApp(MeshCoreApp(
connector: connector,
retryService: retryService,
pathHistoryService: pathHistoryService,
storage: storage,
appSettingsService: appSettingsService,
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
));
runApp(
MeshCoreApp(
connector: connector,
retryService: retryService,
pathHistoryService: pathHistoryService,
storage: storage,
appSettingsService: appSettingsService,
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 {
@@ -86,6 +117,7 @@ class MeshCoreApp extends StatelessWidget {
final BleDebugLogService bleDebugLogService;
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
const MeshCoreApp({
super.key,
@@ -97,6 +129,7 @@ class MeshCoreApp extends StatelessWidget {
required this.bleDebugLogService,
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
});
@override
@@ -109,6 +142,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
],
@@ -124,10 +158,15 @@ class MeshCoreApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
locale: _localeFromSetting(settingsService.settings.languageOverride),
locale: _localeFromSetting(
settingsService.settings.languageOverride,
),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
@@ -135,8 +174,19 @@ class MeshCoreApp extends StatelessWidget {
brightness: Brightness.dark,
),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
themeMode: _themeModeFromSetting(
settingsService.settings.themeMode,
),
builder: (context, child) {
// Update notification service with resolved locale
final locale = Localizations.localeOf(context);
NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink();
},
home: const ScannerScreen(),
);
},
+74 -12
View File
@@ -1,3 +1,16 @@
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
String get value {
switch (this) {
case UnitSystem.imperial:
return 'imperial';
case UnitSystem.metric:
return 'metric';
}
}
}
class AppSettings {
static const Object _unset = Object();
@@ -9,6 +22,7 @@ class AppSettings {
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
final bool mapShowMarkers;
final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom;
final int mapCacheMaxZoom;
@@ -21,6 +35,9 @@ class AppSettings {
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
final Map<String, String> batteryChemistryByDeviceId;
final Map<String, String> batteryChemistryByRepeaterId;
final UnitSystem unitSystem;
final Set<String> mutedChannels;
AppSettings({
this.clearPathOnMaxRetry = false,
@@ -31,6 +48,7 @@ class AppSettings {
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
this.mapShowMarkers = true,
this.enableMessageTracing = false,
this.mapCacheBounds,
this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15,
@@ -43,7 +61,12 @@ class AppSettings {
this.languageOverride,
this.appDebugLogEnabled = false,
Map<String, String>? batteryChemistryByDeviceId,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
Map<String, String>? batteryChemistryByRepeaterId,
this.unitSystem = UnitSystem.metric,
Set<String>? mutedChannels,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
Map<String, dynamic> toJson() {
return {
@@ -55,6 +78,7 @@ class AppSettings {
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers,
'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom,
@@ -67,22 +91,34 @@ 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,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] as bool? ?? true,
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()),
),
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
),
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
@@ -90,14 +126,27 @@ class AppSettings {
notifyOnNewChannelMessage:
json['notify_on_new_channel_message'] as bool? ?? true,
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false,
autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? false,
themeMode: json['theme_mode'] as String? ?? 'system',
languageOverride: json['language_override'] as String?,
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map(
batteryChemistryByDeviceId:
(json['battery_chemistry_by_device_id'] as Map?)?.map(
(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()) ??
{},
);
}
@@ -110,6 +159,7 @@ class AppSettings {
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
bool? mapShowMarkers,
bool? enableMessageTracing,
Object? mapCacheBounds = _unset,
int? mapCacheMinZoom,
int? mapCacheMaxZoom,
@@ -122,6 +172,9 @@ class AppSettings {
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
Map<String, String>? batteryChemistryByDeviceId,
Map<String, String>? batteryChemistryByRepeaterId,
UnitSystem? unitSystem,
Set<String>? mutedChannels,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -132,8 +185,10 @@ class AppSettings {
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapCacheBounds:
mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map<String, double>?,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
: mapCacheBounds as Map<String, double>?,
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
@@ -141,12 +196,19 @@ class AppSettings {
notifyOnNewChannelMessage:
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
themeMode: themeMode ?? this.themeMode,
languageOverride:
languageOverride == _unset ? this.languageOverride : languageOverride as String?,
languageOverride: languageOverride == _unset
? this.languageOverride
: languageOverride as String?,
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
batteryChemistryByDeviceId:
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
batteryChemistryByRepeaterId:
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
unitSystem: unitSystem ?? this.unitSystem,
mutedChannels: mutedChannels ?? this.mutedChannels,
);
}
}
+44 -5
View File
@@ -1,16 +1,21 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
class Channel {
final int index;
final String name;
final Uint8List psk; // 16 bytes
int unreadCount;
Channel({
required this.index,
required this.name,
required this.psk,
this.unreadCount = 0,
});
String get pskHex => _bytesToHex(psk);
@@ -36,11 +41,7 @@ class Channel {
}
static Channel empty(int index) {
return Channel(
index: index,
name: '',
psk: Uint8List(16),
);
return Channel(index: index, name: '', psk: Uint8List(16));
}
static Channel fromHex(int index, String name, String pskHex) {
@@ -61,6 +62,44 @@ class Channel {
return bytes;
}
/// Derive PSK from hashtag name using SHA256.
/// The hashtag is normalized to include '#' prefix.
/// Returns first 16 bytes of SHA256 hash as PSK.
static Uint8List derivePskFromHashtag(String hashtag) {
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
return Uint8List.fromList(hash.sublist(0, 16));
}
/// Derive PSK for community public channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
///
/// This creates a channel that is "public" only to members who have
/// the community secret. Outsiders see only opaque IDs.
static Uint8List deriveCommunityPublicPsk(Uint8List secret) {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
///
/// Community hashtag channels are deterministic for all members
/// (same name => same id) but impossible to enumerate/guess without K.
static Uint8List deriveCommunityHashtagPsk(Uint8List secret, String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Normalize a hashtag name for consistent community PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
static String formatPskHex(Uint8List psk) {
return _bytesToHex(psk);
}
+23 -16
View File
@@ -59,15 +59,18 @@ class ChannelMessage {
this.replyToSenderName,
this.replyToText,
Map<String, int>? reactions,
}) : messageId = messageId ?? '${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
reactions = reactions ?? {},
pathBytes = pathBytes ?? Uint8List(0),
pathVariants = _mergePathVariants(
pathBytes ?? Uint8List(0),
pathVariants,
);
}) : messageId =
messageId ??
'${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
reactions = reactions ?? {},
pathBytes = pathBytes ?? Uint8List(0),
pathVariants = _mergePathVariants(
pathBytes ?? Uint8List(0),
pathVariants,
);
String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null;
String? get senderKeyHex =>
senderKey != null ? pubKeyToHex(senderKey!) : null;
ChannelMessage copyWith({
ChannelMessageStatus? status,
@@ -125,8 +128,10 @@ class ChannelMessage {
final hasPathBytesFlag = (data[2] & 0x01) != 0;
final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
final hasValidTxtType =
cursor < data.length && (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && canFitPath) {
cursor < data.length &&
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
canFitPath) {
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
cursor += pathLen;
}
@@ -162,7 +167,8 @@ class ChannelMessage {
final potentialSender = text.substring(0, colonIndex);
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
senderName = potentialSender;
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
final offset =
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
? colonIndex + 2
: colonIndex + 1;
actualText = text.substring(offset);
@@ -184,7 +190,11 @@ class ChannelMessage {
);
}
static ChannelMessage outgoing(String text, String senderName, int channelIndex) {
static ChannelMessage outgoing(
String text,
String senderName,
int channelIndex,
) {
return ChannelMessage(
senderKey: null,
senderName: senderName,
@@ -249,8 +259,5 @@ class ReplyInfo {
final String mentionedNode;
final String actualMessage;
ReplyInfo({
required this.mentionedNode,
required this.actualMessage,
});
ReplyInfo({required this.mentionedNode, required this.actualMessage});
}
+239
View File
@@ -0,0 +1,239 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
/// distributed via QR code. Members can create Community Public Channels
/// and Community Hashtag Channels that are opaque to outsiders.
class Community {
/// Unique identifier for local storage
final String id;
/// Display name for the community
final String name;
/// The 32-byte shared secret (K)
final Uint8List secret;
/// Timestamp when the community was created/joined
final DateTime createdAt;
/// List of hashtag channel names (without #) that have been added
final List<String> hashtagChannels;
Community({
required this.id,
required this.name,
required this.secret,
required this.createdAt,
List<String>? hashtagChannels,
}) : hashtagChannels = hashtagChannels ?? [];
/// Generate a new community with a random 32-byte secret
factory Community.create({required String id, required String name}) {
final random = Random.secure();
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: DateTime.now(),
);
}
/// Parse a community from QR code JSON data
factory Community.fromQrData(String id, String qrData) {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') {
throw const FormatException('Invalid QR code type');
}
if (json['v'] != 1) {
throw const FormatException('Unsupported QR code version');
}
final name = json['name'] as String;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) {
throw const FormatException('Invalid secret length');
}
return Community(
id: id,
name: name,
secret: Uint8List.fromList(secret),
createdAt: DateTime.now(),
);
}
/// Parse a community from storage JSON
factory Community.fromJson(Map<String, dynamic> json) {
return Community(
id: json['id'] as String,
name: json['name'] as String,
secret: base64Decode(json['secret'] as String),
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
hashtagChannels:
(json['hashtag_channels'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
/// Convert to JSON for storage
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'secret': base64Encode(secret),
'created_at': createdAt.millisecondsSinceEpoch,
'hashtag_channels': hashtagChannels,
};
}
/// Generate QR code JSON payload for sharing
String toQrJson() {
return jsonEncode({
'v': 1,
'type': 'meshcore_community',
'name': name,
'k': base64Url.encode(secret),
});
}
/// Derive the public Community ID from the secret.
/// This is safe to display/log since it's one-way derived.
/// CID = SHA256("community:v1" || K)
String get communityId {
final data = utf8.encode('community:v1') + secret;
final hash = crypto.sha256.convert(data).bytes;
return _bytesToHex(Uint8List.fromList(hash));
}
/// Short version of community ID for display (first 8 chars)
String get shortCommunityId => communityId.substring(0, 8);
/// Derive PSK for community public channel.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
Uint8List deriveCommunityPublicPsk() {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
Uint8List deriveCommunityHashtagPsk(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Check if QR data is valid community data
static bool isValidQrData(String data) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return false;
if (json['v'] != 1) return false;
if (json['name'] == null || (json['name'] as String).isEmpty) {
return false;
}
if (json['k'] == null) return false;
final secret = base64Url.decode(json['k'] as String);
return secret.length == 32;
} catch (_) {
return false;
}
}
/// Normalize a hashtag name for consistent PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
if (hashtagChannels.contains(normalized)) {
return this;
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: [...hashtagChannels, normalized],
);
}
/// Remove a hashtag channel from this community's list
Community removeHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: hashtagChannels.where((h) => h != normalized).toList(),
);
}
/// Create a copy of this community with a new secret
Community withNewSecret(Uint8List newSecret) {
return Community(
id: id,
name: name,
secret: newSecret,
createdAt: createdAt,
hashtagChannels: hashtagChannels,
);
}
/// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() {
final random = Random.secure();
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
}
/// Extract secret from QR data (for updating existing community)
static Uint8List? extractSecretFromQrData(String qrData) {
try {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return null;
if (json['v'] != 1) return null;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) return null;
return Uint8List.fromList(secret);
} catch (_) {
return null;
}
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Community && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
+98 -38
View File
@@ -5,9 +5,11 @@ 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? pathOverride; // User's path override: -1 = force flood, null = auto
final int?
pathOverride; // User's path override: -1 = force flood, null = auto
final Uint8List? pathOverrideBytes; // User's path override bytes
final double? latitude;
final double? longitude;
@@ -18,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,
@@ -57,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,
@@ -76,10 +81,15 @@ 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 ? null : (pathOverride ?? this.pathOverride),
pathOverrideBytes: clearPathOverride ? null : (pathOverrideBytes ?? this.pathOverrideBytes),
pathOverride: clearPathOverride
? null
: (pathOverride ?? this.pathOverride),
pathOverrideBytes: clearPathOverride
? null
: (pathOverrideBytes ?? this.pathOverrideBytes),
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
lastSeen: lastSeen ?? this.lastSeen,
@@ -93,15 +103,59 @@ class Contact {
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 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(),
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)}>";
}
Uint8List? get traceRouteBytes {
final pathBytes = _pathBytesForDisplay;
Uint8List? traceBytes;
if (pathBytes.isEmpty) {
traceBytes = Uint8List(1);
traceBytes[0] = publicKey[0];
return traceBytes;
}
if (type == advTypeRepeater || type == advTypeRoom) {
final len = (pathBytes.length + pathBytes.length + 1);
traceBytes = Uint8List(len);
traceBytes[pathBytes.length] = publicKey[0];
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
} else {
if (pathBytes.length < 2) {
return pathBytes[0] == 0 ? null : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
}
return traceBytes;
}
Uint8List get _pathBytesForDisplay {
if (pathOverride != null) {
if (pathOverride! < 0) return Uint8List(0);
@@ -111,43 +165,49 @@ class Contact {
}
static Contact? fromFrame(Uint8List data) {
if (data.length < contactFrameSize) return null;
if (data.isEmpty) return null;
if (data[0] != respCodeContact) return null;
try {
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final flags = data[contactFlagsOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
final pubKey = Uint8List.fromList(
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
);
final type = data[contactTypeOffset];
final pathLen = data[contactPathLenOffset].toSigned(8);
final safePathLen = pathLen > 0
? (pathLen > maxPathSize ? maxPathSize : pathLen)
: 0;
final pathBytes = safePathLen > 0
? Uint8List.fromList(
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
)
: Uint8List(0);
final name = readCString(data, contactNameOffset, maxNameSize);
final lastmod = readUint32LE(data, contactLastmodOffset);
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
}
double? lat, lon;
final latRaw = readInt32LE(data, contactLatOffset);
final lonRaw = readInt32LE(data, contactLonOffset);
if (latRaw != 0 || lonRaw != 0) {
lat = latRaw / 1e6;
lon = lonRaw / 1e6;
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
} catch (e) {
// If parsing fails, return null
return null;
}
return Contact(
publicKey: pubKey,
name: name.isEmpty ? 'Unknown' : name,
type: type,
pathLength: pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
);
}
@override
+5 -15
View File
@@ -2,15 +2,9 @@ class ContactGroup {
final String name;
final List<String> memberKeys;
const ContactGroup({
required this.name,
required this.memberKeys,
});
const ContactGroup({required this.name, required this.memberKeys});
ContactGroup copyWith({
String? name,
List<String>? memberKeys,
}) {
ContactGroup copyWith({String? name, List<String>? memberKeys}) {
return ContactGroup(
name: name ?? this.name,
memberKeys: memberKeys ?? List<String>.from(this.memberKeys),
@@ -18,16 +12,12 @@ class ContactGroup {
}
Map<String, dynamic> toJson() {
return {
'name': name,
'members': memberKeys,
};
return {'name': name, 'members': memberKeys};
}
factory ContactGroup.fromJson(Map<String, dynamic> json) {
final members = (json['members'] as List?)
?.map((value) => value.toString())
.toList() ??
final members =
(json['members'] as List?)?.map((value) => value.toString()).toList() ??
<String>[];
return ContactGroup(
name: json['name'] as String? ?? '',
+5 -4
View File
@@ -43,9 +43,9 @@ class Message {
Uint8List? pathBytes,
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
}) : pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {};
}) : pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {};
String get senderKeyHex => pubKeyToHex(senderKey);
@@ -80,7 +80,8 @@ class Message {
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
reactions: reactions ?? this.reactions,
fourByteRoomContactKey: fourByteRoomContactKey ?? this.fourByteRoomContactKey,
fourByteRoomContactKey:
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
);
}
+8 -6
View File
@@ -38,7 +38,8 @@ class PathRecord {
tripTimeMs: json['trip_time_ms'] as int,
timestamp: DateTime.parse(json['timestamp'] as String),
wasFloodDiscovery: json['was_flood'] as bool,
pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
pathBytes:
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
);
@@ -65,14 +66,15 @@ class ContactPathHistory {
}
Map<String, dynamic> toJson() {
return {
'recent_paths': recentPaths.map((p) => p.toJson()).toList(),
};
return {'recent_paths': recentPaths.map((p) => p.toJson()).toList()};
}
factory ContactPathHistory.fromJson(
String contactPubKeyHex, Map<String, dynamic> json) {
final pathsList = (json['recent_paths'] as List?)
String contactPubKeyHex,
Map<String, dynamic> json,
) {
final pathsList =
(json['recent_paths'] as List?)
?.map((p) => PathRecord.fromJson(p as Map<String, dynamic>))
.toList() ??
[];
+183 -29
View File
@@ -59,46 +59,200 @@ class RadioSettings {
required this.txPowerDbm,
});
// Preset configurations
static RadioSettings get preset915MHz => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
// Regional preset configurations
static final List<(String, RadioSettings)> presets = [
(
'Australia',
RadioSettings(
frequencyMHz: 915.8,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Australia (Narrow)',
RadioSettings(
frequencyMHz: 916.575,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get preset868MHz => RadioSettings(
frequencyMHz: 868.0,
bandwidth: LoRaBandwidth.bw125,
),
),
(
'Australia SA, WA, QLD',
RadioSettings(
frequencyMHz: 923.125,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Czech Republic',
RadioSettings(
frequencyMHz: 869.432,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
);
static RadioSettings get preset433MHz => RadioSettings(
),
),
(
'EU 433MHz',
RadioSettings(
frequencyMHz: 433.650,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'EU/UK (Long Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Medium Range)',
RadioSettings(
frequencyMHz: 869.525,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'EU/UK (Narrow)',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'New Zealand',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'New Zealand (Narrow)',
RadioSettings(
frequencyMHz: 917.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 433',
RadioSettings(
frequencyMHz: 433.375,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf9,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Portugal 869',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Switzerland',
RadioSettings(
frequencyMHz: 869.618,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf8,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'USA Arizona',
RadioSettings(
frequencyMHz: 908.205,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf10,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'USA/Canada',
RadioSettings(
frequencyMHz: 910.525,
bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf7,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
(
'Vietnam',
RadioSettings(
frequencyMHz: 920.250,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
// Off-grid repeat presets (valid client_repeat frequencies)
(
'Off-Grid 433',
RadioSettings(
frequencyMHz: 433.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf7,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
static RadioSettings get presetLongRange => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf12,
codingRate: LoRaCodingRate.cr4_8,
txPowerDbm: 20,
);
static RadioSettings get presetFastSpeed => RadioSettings(
frequencyMHz: 915.0,
bandwidth: LoRaBandwidth.bw500,
spreadingFactor: LoRaSpreadingFactor.sf7,
),
),
(
'Off-Grid 869',
RadioSettings(
frequencyMHz: 869.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 14,
),
),
(
'Off-Grid 918',
RadioSettings(
frequencyMHz: 918.0,
bandwidth: LoRaBandwidth.bw250,
spreadingFactor: LoRaSpreadingFactor.sf11,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
);
),
),
];
int get frequencyHz => (frequencyMHz * 1000).round();
int get bandwidthHz => bandwidth.hz;
+33 -10
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(
@@ -26,8 +27,10 @@ class AppDebugLogScreen extends StatelessWidget {
onPressed: hasEntries
? () async {
final text = entries
.map((entry) =>
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}')
.map(
(entry) =>
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}',
)
.join('\n');
await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return;
@@ -53,7 +56,7 @@ class AppDebugLogScreen extends StatelessWidget {
child: hasEntries
? ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final entry = entries[index];
return ListTile(
@@ -61,11 +64,17 @@ class AppDebugLogScreen extends StatelessWidget {
leading: _buildLevelIcon(entry.level),
title: Text(
'[${entry.tag}] ${entry.message}',
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
),
),
subtitle: Text(
entry.formattedTime,
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
);
},
@@ -74,16 +83,26 @@ class AppDebugLogScreen extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bug_report_outlined, size: 64, color: Colors.grey[400]),
Icon(
Icons.bug_report_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
context.l10n.debugLog_noEntries,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
context.l10n.debugLog_enableInSettings,
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
@@ -99,7 +118,11 @@ class AppDebugLogScreen extends StatelessWidget {
case AppDebugLogLevel.info:
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
case AppDebugLogLevel.warning:
return const Icon(Icons.warning_amber_outlined, size: 18, color: Colors.orange);
return const Icon(
Icons.warning_amber_outlined,
size: 18,
color: Colors.orange,
);
case AppDebugLogLevel.error:
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
}
+213 -58
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(
@@ -43,7 +45,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildAppearanceCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildAppearanceCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -58,7 +63,9 @@ class AppSettingsScreen extends StatelessWidget {
ListTile(
leading: const Icon(Icons.brightness_6_outlined),
title: Text(context.l10n.appSettings_theme),
subtitle: Text(_themeModeLabel(context, settingsService.settings.themeMode)),
subtitle: Text(
_themeModeLabel(context, settingsService.settings.themeMode),
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeModeDialog(context, settingsService),
),
@@ -66,16 +73,36 @@ class AppSettingsScreen extends StatelessWidget {
ListTile(
leading: const Icon(Icons.language_outlined),
title: Text(context.l10n.appSettings_language),
subtitle: Text(_languageLabel(context, settingsService.settings.languageOverride)),
subtitle: Text(
_languageLabel(
context,
settingsService.settings.languageOverride,
),
),
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);
},
),
],
),
);
}
Widget _buildNotificationsCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildNotificationsCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -90,17 +117,22 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: const Icon(Icons.notifications_outlined),
title: Text(context.l10n.appSettings_enableNotifications),
subtitle: Text(context.l10n.appSettings_enableNotificationsSubtitle),
subtitle: Text(
context.l10n.appSettings_enableNotificationsSubtitle,
),
value: settingsService.settings.notificationsEnabled,
onChanged: (value) async {
if (value) {
// Request permission when enabling
final granted = await NotificationService().requestPermissions();
final granted = await NotificationService()
.requestPermissions();
if (!granted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.appSettings_notificationPermissionDenied),
content: Text(
context.l10n.appSettings_notificationPermissionDenied,
),
duration: const Duration(seconds: 2),
),
);
@@ -113,9 +145,11 @@ class AppSettingsScreen extends StatelessWidget {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(value
? context.l10n.appSettings_notificationsEnabled
: context.l10n.appSettings_notificationsDisabled),
content: Text(
value
? context.l10n.appSettings_notificationsEnabled
: context.l10n.appSettings_notificationsDisabled,
),
duration: const Duration(seconds: 2),
),
);
@@ -126,18 +160,24 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: Icon(
Icons.message_outlined,
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
title: Text(
context.l10n.appSettings_messageNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
subtitle: Text(
context.l10n.appSettings_messageNotificationsSubtitle,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
value: settingsService.settings.notifyOnNewMessage,
@@ -151,18 +191,24 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: Icon(
Icons.forum_outlined,
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
title: Text(
context.l10n.appSettings_channelMessageNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
subtitle: Text(
context.l10n.appSettings_channelMessageNotificationsSubtitle,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
value: settingsService.settings.notifyOnNewChannelMessage,
@@ -176,18 +222,24 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: Icon(
Icons.cell_tower,
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
title: Text(
context.l10n.appSettings_advertisementNotifications,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
subtitle: Text(
context.l10n.appSettings_advertisementNotificationsSubtitle,
style: TextStyle(
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
color: settingsService.settings.notificationsEnabled
? null
: Colors.grey,
),
),
value: settingsService.settings.notifyOnNewAdvert,
@@ -202,7 +254,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildMessagingCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildMessagingCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -217,15 +272,19 @@ class AppSettingsScreen extends StatelessWidget {
SwitchListTile(
secondary: const Icon(Icons.refresh_outlined),
title: Text(context.l10n.appSettings_clearPathOnMaxRetry),
subtitle: Text(context.l10n.appSettings_clearPathOnMaxRetrySubtitle),
subtitle: Text(
context.l10n.appSettings_clearPathOnMaxRetrySubtitle,
),
value: settingsService.settings.clearPathOnMaxRetry,
onChanged: (value) {
settingsService.setClearPathOnMaxRetry(value);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(value
? context.l10n.appSettings_pathsWillBeCleared
: context.l10n.appSettings_pathsWillNotBeCleared),
content: Text(
value
? context.l10n.appSettings_pathsWillBeCleared
: context.l10n.appSettings_pathsWillNotBeCleared,
),
duration: const Duration(seconds: 2),
),
);
@@ -241,9 +300,11 @@ class AppSettingsScreen extends StatelessWidget {
settingsService.setAutoRouteRotationEnabled(value);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(value
? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled),
content: Text(
value
? context.l10n.appSettings_autoRouteRotationEnabled
: context.l10n.appSettings_autoRouteRotationDisabled,
),
duration: const Duration(seconds: 2),
),
);
@@ -254,7 +315,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildMapSettingsCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildMapSettingsCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -302,12 +366,26 @@ class AppSettingsScreen extends StatelessWidget {
subtitle: Text(
settingsService.settings.mapTimeFilterHours == 0
? context.l10n.appSettings_timeFilterShowAll
: context.l10n.appSettings_timeFilterShowLast(settingsService.settings.mapTimeFilterHours.toInt()),
: context.l10n.appSettings_timeFilterShowLast(
settingsService.settings.mapTimeFilterHours.toInt(),
),
),
trailing: const Icon(Icons.chevron_right),
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),
@@ -332,6 +410,7 @@ class AppSettingsScreen extends StatelessWidget {
);
}
// Fixed rendering issues
Widget _buildBatteryCard(
BuildContext context,
AppSettingsService settingsService,
@@ -339,13 +418,15 @@ class AppSettingsScreen extends StatelessWidget {
) {
final deviceId = connector.deviceId;
final isConnected = connector.isConnected && deviceId != null;
final selection =
isConnected ? settingsService.batteryChemistryForDevice(deviceId) : 'nmc';
final selection = isConnected
? settingsService.batteryChemistryForDevice(deviceId)
: 'nmc';
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
@@ -353,20 +434,38 @@ class AppSettingsScreen extends StatelessWidget {
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
// Main tile (icon + text only)
ListTile(
leading: const Icon(Icons.battery_full),
title: Text(context.l10n.appSettings_batteryChemistry),
subtitle: Text(
isConnected
? context.l10n.appSettings_batteryChemistryPerDevice(connector.deviceDisplayName)
? context.l10n.appSettings_batteryChemistryPerDevice(
connector.deviceDisplayName,
)
: context.l10n.appSettings_batteryChemistryConnectFirst,
),
trailing: DropdownButton<String>(
value: selection,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
// Dropdown (separate full-width row)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: DropdownButtonFormField<String>(
initialValue: selection,
isExpanded: true,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
isDense: true,
),
onChanged: isConnected
? (value) {
if (value != null) {
settingsService.setBatteryChemistryForDevice(deviceId, value);
settingsService.setBatteryChemistryForDevice(
deviceId,
value,
);
}
}
: null,
@@ -391,7 +490,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showThemeModeDialog(BuildContext context, AppSettingsService settingsService) {
void _showThemeModeDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -471,12 +573,19 @@ class AppSettingsScreen extends StatelessWidget {
return context.l10n.appSettings_languageSk;
case 'bg':
return context.l10n.appSettings_languageBg;
case 'ru':
return context.l10n.appSettings_languageRu;
case 'uk':
return context.l10n.appSettings_languageUk;
default:
return context.l10n.appSettings_languageSystem;
}
}
void _showLanguageDialog(BuildContext context, AppSettingsService settingsService) {
void _showLanguageDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -547,6 +656,14 @@ class AppSettingsScreen extends StatelessWidget {
title: Text(context.l10n.appSettings_languageBg),
value: 'bg',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageRu),
value: 'ru',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageUk),
value: 'uk',
),
],
),
),
@@ -561,7 +678,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showTimeFilterDialog(BuildContext context, AppSettingsService settingsService) {
void _showTimeFilterDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -581,33 +701,23 @@ class AppSettingsScreen extends StatelessWidget {
const SizedBox(height: 16),
ListTile(
title: Text(context.l10n.appSettings_allTime),
leading: Radio<double>(
value: 0,
),
leading: Radio<double>(value: 0),
),
ListTile(
title: Text(context.l10n.appSettings_lastHour),
leading: Radio<double>(
value: 1,
),
leading: Radio<double>(value: 1),
),
ListTile(
title: Text(context.l10n.appSettings_last6Hours),
leading: Radio<double>(
value: 6,
),
leading: Radio<double>(value: 6),
),
ListTile(
title: Text(context.l10n.appSettings_last24Hours),
leading: Radio<double>(
value: 24,
),
leading: Radio<double>(value: 24),
),
ListTile(
title: Text(context.l10n.appSettings_lastWeek),
leading: Radio<double>(
value: 168,
),
leading: Radio<double>(value: 168),
),
],
),
@@ -622,7 +732,50 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildDebugCard(BuildContext context, AppSettingsService settingsService) {
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,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -644,9 +797,11 @@ class AppSettingsScreen extends StatelessWidget {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(value
? context.l10n.appSettings_appDebugLoggingEnabled
: context.l10n.appSettings_appDebugLoggingDisabled),
content: Text(
value
? context.l10n.appSettings_appDebugLoggingEnabled
: context.l10n.appSettings_appDebugLoggingDisabled,
),
duration: const Duration(seconds: 2),
),
);
+48 -19
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 }
@@ -24,10 +25,12 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
final entries = logService.entries.reversed.toList();
final rawEntries = logService.rawLogRxEntries.reversed.toList();
final showingFrames = _view == _BleLogView.frames;
final hasEntries = showingFrames ? entries.isNotEmpty : rawEntries.isNotEmpty;
final hasEntries = showingFrames
? entries.isNotEmpty
: 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,
@@ -36,15 +39,23 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
? () async {
final text = showingFrames
? entries
.map((entry) => '${entry.description}\n${entry.hexPreview}\n')
.join('\n')
.map(
(entry) =>
'${entry.description}\n${entry.hexPreview}\n',
)
.join('\n')
: rawEntries
.map((entry) => 'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n')
.join('\n');
.map(
(entry) =>
'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n',
)
.join('\n');
await Clipboard.setData(ClipboardData(text: text));
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.debugLog_bleCopied)),
SnackBar(
content: Text(context.l10n.debugLog_bleCopied),
),
);
}
: null,
@@ -68,8 +79,14 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: SegmentedButton<_BleLogView>(
segments: [
ButtonSegment(value: _BleLogView.frames, label: Text(context.l10n.debugLog_frames)),
ButtonSegment(value: _BleLogView.rawLogRx, label: Text(context.l10n.debugLog_rawLogRx)),
ButtonSegment(
value: _BleLogView.frames,
label: Text(context.l10n.debugLog_frames),
),
ButtonSegment(
value: _BleLogView.rawLogRx,
label: Text(context.l10n.debugLog_rawLogRx),
),
],
selected: {_view},
onSelectionChanged: (selection) {
@@ -81,8 +98,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
Expanded(
child: hasEntries
? ListView.separated(
itemCount: showingFrames ? entries.length : rawEntries.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemCount: showingFrames
? entries.length
: rawEntries.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
if (showingFrames) {
final entry = entries[index];
@@ -94,7 +113,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
subtitle: Text('${entry.hexPreview}\n$time'),
isThreeLine: true,
leading: Icon(
entry.outgoing ? Icons.upload : Icons.download,
entry.outgoing
? Icons.upload
: Icons.download,
size: 18,
),
);
@@ -131,9 +152,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
context: context,
builder: (context) => AlertDialog(
title: Text(info.title),
content: SingleChildScrollView(
child: SelectableText(info.rawHex),
),
content: SingleChildScrollView(child: SelectableText(info.rawHex)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
@@ -195,11 +214,18 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
}
final payload = raw.sublist(index);
final title = 'RX ${_payloadTypeLabel(payloadType)}${_routeLabel(routeType)} • v$payloadVer';
final title =
'RX ${_payloadTypeLabel(payloadType)}${_routeLabel(routeType)} • v$payloadVer';
final summary = _decodePayloadSummary(payloadType, payload);
final pathSummary = pathLen > 0 ? 'Path=${_bytesToHex(pathBytes)}' : 'Path=none';
final pathSummary = pathLen > 0
? 'Path=${_bytesToHex(pathBytes)}'
: 'Path=none';
final detail = '$summary$pathSummary • len=${raw.length}';
return _RawPacketInfo(title: title, summary: detail, rawHex: _bytesToHex(raw));
return _RawPacketInfo(
title: title,
summary: detail,
rawHex: _bytesToHex(raw),
);
}
String _decodePayloadSummary(int payloadType, Uint8List payload) {
@@ -245,7 +271,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
return 'ADVERT (short)';
}
var offset = 0;
final pubKey = _bytesToHex(payload.sublist(offset, offset + 32), spaced: false);
final pubKey = _bytesToHex(
payload.sublist(offset, offset + 32),
spaced: false,
);
offset += 32;
final timestamp = readUint32LE(payload, offset);
offset += 4;
File diff suppressed because it is too large Load Diff
+303 -87
View File
@@ -4,22 +4,27 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package: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;
final bool channelMessage;
const ChannelMessagePathScreen({
super.key,
required this.message,
this.channelMessage = false,
});
@override
@@ -27,7 +32,15 @@ class ChannelMessagePathScreen extends StatelessWidget {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final l10n = context.l10n;
final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants);
final primaryPathTmp = _selectPrimaryPath(
message.pathBytes,
message.pathVariants,
);
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
@@ -36,17 +49,31 @@ class ChannelMessagePathScreen extends StatelessWidget {
l10n,
);
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
return Scaffold(
appBar: AppBar(
title: Text(l10n.channelPath_title),
title: AdaptiveAppBarTitle(l10n.channelPath_title),
actions: [
IconButton(
icon: const Icon(Icons.radar_outlined),
tooltip: l10n.channelPath_viewMap,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath,
flipPathRound: true,
reversePathRound: !message.isOutgoing && !channelMessage,
),
),
),
),
IconButton(
icon: const Icon(Icons.map_outlined),
tooltip: l10n.channelPath_viewMap,
onPressed: hasHopDetails
? () {
_openPathMap(context);
_openPathMap(context, channelMessage: channelMessage);
}
: null,
),
@@ -88,10 +115,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
);
}
Widget _buildSummaryCard(
BuildContext context, {
String? observedLabel,
}) {
Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) {
final l10n = context.l10n;
return Card(
child: Padding(
@@ -105,21 +129,28 @@ class ChannelMessagePathScreen extends StatelessWidget {
),
const SizedBox(height: 8),
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
_buildDetailRow(l10n.channelPath_timeLabel, _formatTime(message.timestamp, l10n)),
_buildDetailRow(
l10n.channelPath_timeLabel,
_formatTime(message.timestamp, l10n),
),
if (message.repeatCount > 0)
_buildDetailRow(l10n.channelPath_repeatsLabel, message.repeatCount.toString()),
_buildDetailRow(l10n.channelPath_pathLabelTitle, _formatPathLabel(message.pathLength, l10n)),
if (observedLabel != null) _buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
_buildDetailRow(
l10n.channelPath_repeatsLabel,
message.repeatCount.toString(),
),
_buildDetailRow(
l10n.channelPath_pathLabelTitle,
_formatPathLabel(message.pathLength, l10n),
),
if (observedLabel != null)
_buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
],
),
),
);
}
Widget _buildPathVariants(
BuildContext context,
List<Uint8List> variants,
) {
Widget _buildPathVariants(BuildContext context, List<Uint8List> variants) {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -137,7 +168,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
),
subtitle: Text(_formatPathPrefixes(variants[i])),
trailing: const Icon(Icons.map_outlined, size: 20),
onTap: () => _openPathMap(context, initialPath: variants[i]),
onTap: () => _openPathMap(
context,
initialPath: variants[i],
channelMessage: channelMessage,
),
),
),
],
@@ -163,7 +198,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
subtitle: Text(
hop.hasLocation
? '${hop.position!.latitude.toStringAsFixed(5)}, '
'${hop.position!.longitude.toStringAsFixed(5)}'
'${hop.position!.longitude.toStringAsFixed(5)}'
: l10n.channelPath_noLocationData,
),
),
@@ -228,28 +263,34 @@ class ChannelMessagePathScreen extends StatelessWidget {
);
}
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
void _openPathMap(
BuildContext context, {
Uint8List? initialPath,
bool channelMessage = false,
}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChannelMessagePathMapScreen(
message: message,
initialPath: initialPath,
channelMessage: channelMessage,
),
),
);
}
}
class ChannelMessagePathMapScreen extends StatefulWidget {
final ChannelMessage message;
final Uint8List? initialPath;
final bool channelMessage;
const ChannelMessagePathMapScreen({
super.key,
required this.message,
this.initialPath,
this.channelMessage = false,
});
@override
@@ -257,8 +298,14 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
_ChannelMessagePathMapScreenState();
}
class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScreen> {
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() {
@@ -270,32 +317,77 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
void didUpdateWidget(ChannelMessagePathMapScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.message != widget.message ||
!_pathsEqual(oldWidget.initialPath ?? Uint8List(0),
widget.initialPath ?? Uint8List(0))) {
!_pathsEqual(
oldWidget.initialPath ?? Uint8List(0),
widget.initialPath ?? Uint8List(0),
)) {
_selectedPath = widget.initialPath;
}
}
double _getPathDistance(List<LatLng> points) {
double totalDistance = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < points.length - 1; i++) {
totalDistance += distanceCalculator(points[i], points[i + 1]);
}
return totalDistance;
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final primaryPath =
_selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants);
final observedPaths =
_buildObservedPaths(primaryPath, widget.message.pathVariants);
final selectedPath = _resolveSelectedPath(
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
widget.message.pathVariants,
);
final observedPaths = _buildObservedPaths(
primaryPath,
widget.message.pathVariants,
);
final selectedPathTmp = _resolveSelectedPath(
_selectedPath,
observedPaths,
primaryPath,
);
final selectedPath =
((!widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage))
? Uint8List.fromList(selectedPathTmp.reversed.toList())
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
final hops = _buildPathHops(selectedPath, connector.contacts, context.l10n);
final points = hops
.where((hop) => hop.hasLocation)
.map((hop) => hop.position!)
.toList();
final hops = _buildPathHops(
selectedPath,
connector.contacts,
context.l10n,
);
final points = <LatLng>[];
if ((widget.message.isOutgoing && !widget.channelMessage) ||
(widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
for (final hop in hops) {
if (hop.hasLocation) {
points.add(hop.position!);
}
}
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
(!widget.message.isOutgoing && widget.channelMessage)) {
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
}
final polylines = points.length > 1
? [
Polyline(
@@ -306,15 +398,24 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
]
: <Polyline>[];
final initialCenter =
points.isNotEmpty ? points.first : const LatLng(0, 0);
final initialCenter = points.isNotEmpty
? points.first
: const LatLng(0, 0);
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null;
final mapKey = ValueKey(_formatPathPrefixes(selectedPath));
if (!_didReceivePositionUpdate) {
_showNodeLabels = initialZoom >= _labelZoomThreshold;
}
final bounds = points.length > 1
? LatLngBounds.fromPoints(points)
: null;
final mapKey = ValueKey(
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}',
);
_pathDistance = _getPathDistance(points);
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.channelPath_mapTitle),
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
),
body: SafeArea(
top: false,
@@ -334,6 +435,20 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
),
minZoom: 2.0,
maxZoom: 18.0,
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(
@@ -343,34 +458,37 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
if (polylines.isNotEmpty)
PolylineLayer(polylines: polylines),
MarkerLayer(
markers: _buildHopMarkers(hops),
markers: _buildHopMarkers(
hops,
showLabels: _showNodeLabels,
),
),
],
),
if (observedPaths.length > 1)
_buildPathSelector(
context,
observedPaths,
selectedIndex,
(index) {
setState(() {
_selectedPath = observedPaths[index].pathBytes;
});
},
),
_buildPathSelector(context, observedPaths, selectedIndex, (
index,
) {
setState(() {
_selectedPath = observedPaths[index].pathBytes;
});
}),
if (points.isEmpty)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
child: Padding(
padding: EdgeInsets.all(12),
child: Text(context.l10n.channelPath_noRepeaterLocations),
child: Text(
context.l10n.channelPath_noRepeaterLocations,
),
),
),
),
_buildLegendCard(context, hops),
_buildLegendCard(context, hops, isImperial),
],
),
),
@@ -442,42 +560,141 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
);
}
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
return [
for (final hop in hops)
if (hop.hasLocation)
Marker(
point: hop.position!,
width: 40,
height: 40,
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 (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(
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.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),
),
],
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
hop.index.toString(),
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
];
),
),
);
}
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
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);
@@ -496,7 +713,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
Padding(
padding: const EdgeInsets.all(12),
child: Text(
l10n.channelPath_repeaterHops,
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -509,7 +726,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: hops.length,
separatorBuilder: (_, __) => const Divider(height: 1),
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
return ListTile(
@@ -525,7 +742,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
subtitle: Text(
hop.hasLocation
? '${hop.position!.latitude.toStringAsFixed(5)}, '
'${hop.position!.longitude.toStringAsFixed(5)}'
'${hop.position!.longitude.toStringAsFixed(5)}'
: l10n.channelPath_noLocationData,
),
);
@@ -567,10 +784,7 @@ class _ObservedPath {
final Uint8List pathBytes;
final bool isPrimary;
const _ObservedPath({
required this.pathBytes,
required this.isPrimary,
});
const _ObservedPath({required this.pathBytes, required this.isPrimary});
}
List<_PathHop> _buildPathHops(
@@ -597,10 +811,12 @@ List<_PathHop> _buildPathHops(
Contact? _matchContactForPrefix(List<Contact> contacts, int prefix) {
final matches = contacts
.where((contact) =>
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
contact.publicKey.isNotEmpty &&
contact.publicKey[0] == prefix)
.where(
(contact) =>
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
contact.publicKey.isNotEmpty &&
contact.publicKey[0] == prefix,
)
.toList();
if (matches.isEmpty) return null;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
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.
///
/// After successful scan, the user can:
/// 1. Join the community (saves to local storage)
/// 2. Optionally add the Community Public Channel to the device
class CommunityQrScannerScreen extends StatefulWidget {
const CommunityQrScannerScreen({super.key});
@override
State<CommunityQrScannerScreen> createState() =>
_CommunityQrScannerScreenState();
}
class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
final CommunityStore _communityStore = CommunityStore();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
? const Center(child: CircularProgressIndicator())
: QrScannerWidget(
onScanned: (data) => _handleScannedData(context, data),
validator: Community.isValidQrData,
onValidationFailed: (_) => _showInvalidQrError(context),
instructions: context.l10n.community_scanInstructions,
),
);
}
Future<void> _handleScannedData(BuildContext context, String data) async {
if (_isProcessing) return;
setState(() {
_isProcessing = true;
});
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
// Check if this community already exists
final existing = await _communityStore.findByCommunityId(
community.communityId,
);
if (existing != null) {
if (context.mounted) {
_showAlreadyMemberDialog(context, existing);
}
return;
}
// Show confirmation dialog
if (context.mounted) {
await _showJoinConfirmationDialog(context, community);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
);
}
void _showAlreadyMemberDialog(BuildContext context, Community community) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.community_alreadyMember),
content: Text(
context.l10n.community_alreadyMemberMessage(community.name),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: Text(context.l10n.common_ok),
),
],
),
);
}
Future<void> _showJoinConfirmationDialog(
BuildContext context,
Community community,
) async {
bool addPublicChannel = true;
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(context.l10n.community_joinTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.community_joinConfirmation(community.name)),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.groups,
color: Theme.of(dialogContext).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
community.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'ID: ${community.shortCommunityId}...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
CheckboxListTile(
value: addPublicChannel,
onChanged: (value) {
setDialogState(() {
addPublicChannel = value ?? true;
});
},
title: Text(context.l10n.community_addPublicChannel),
subtitle: Text(context.l10n.community_addPublicChannelHint),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(context.l10n.community_join),
),
],
),
),
);
if (result == true && context.mounted) {
await _joinCommunity(context, community, addPublicChannel);
} else if (context.mounted) {
// User cancelled - go back
Navigator.pop(context);
}
}
Future<void> _joinCommunity(
BuildContext context,
Community community,
bool addPublicChannel,
) async {
// Save community to local storage
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
if (addPublicChannel && context.mounted) {
final connector = context.read<MeshCoreConnector>();
final nextIndex = _findNextAvailableChannelIndex(connector);
if (nextIndex != null) {
final psk = community.deriveCommunityPublicPsk();
final channelName = '${community.name} Public';
connector.setChannel(nextIndex, channelName, psk);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
),
);
// Return to previous screen
Navigator.pop(context, community);
}
}
int? _findNextAvailableChannelIndex(MeshCoreConnector connector) {
final usedIndices = connector.channels.map((c) => c.index).toSet();
for (int i = 0; i < connector.maxChannels; i++) {
if (!usedIndices.contains(i)) return i;
}
return null;
}
}
File diff suppressed because it is too large Load Diff
+5 -18
View File
@@ -127,9 +127,7 @@ class _DeviceScreenState extends State<DeviceScreen>
return Card(
elevation: 0,
color: colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -207,7 +205,6 @@ class _DeviceScreenState extends State<DeviceScreen>
);
}
Widget _buildBatteryIndicator(
MeshCoreConnector connector,
BuildContext context,
@@ -224,11 +221,7 @@ class _DeviceScreenState extends State<DeviceScreen>
final icon = _batteryIcon(percent);
return ActionChip(
avatar: Icon(
icon,
size: 16,
color: colorScheme.onSecondaryContainer,
),
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
label: Text(displayLabel),
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
@@ -260,25 +253,19 @@ class _DeviceScreenState extends State<DeviceScreen>
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ContactsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const MapScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
File diff suppressed because it is too large Load Diff
+32 -20
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});
@@ -56,10 +57,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
_updateEstimate();
if (bounds != null) {
_mapController.fitCamera(
CameraFit.bounds(
bounds: bounds,
padding: const EdgeInsets.all(48),
),
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
);
}
}
@@ -72,8 +70,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return;
}
final cacheService = context.read<MapTileCacheService>();
final count =
cacheService.estimateTileCount(_selectedBounds!, _minZoom, _maxZoom);
final count = cacheService.estimateTileCount(
_selectedBounds!,
_minZoom,
_maxZoom,
);
setState(() {
_estimatedTiles = count;
});
@@ -181,9 +182,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed,
)
: context.l10n.mapCache_cachedTiles(result.downloaded);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _clearCache() async {
@@ -225,7 +226,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return Scaffold(
appBar: AppBar(
title: Text(l10n.mapCache_title),
title: AdaptiveAppBarTitle(l10n.mapCache_title),
centerTitle: true,
),
body: Column(
@@ -290,7 +291,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
children: [
Text(
l10n.mapCache_cacheArea,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Row(
@@ -304,8 +308,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
),
const SizedBox(width: 12),
TextButton(
onPressed:
_isDownloading || selectedBounds == null ? null : _clearBounds,
onPressed: _isDownloading || selectedBounds == null
? null
: _clearBounds,
child: Text(l10n.common_clear),
),
],
@@ -313,11 +318,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
const SizedBox(height: 12),
Text(
l10n.mapCache_zoomRange,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
RangeSlider(
values:
RangeValues(_minZoom.toDouble(), _maxZoom.toDouble()),
values: RangeValues(
_minZoom.toDouble(),
_maxZoom.toDouble(),
),
min: 3,
max: 18,
divisions: 15,
@@ -341,10 +351,12 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
const SizedBox(height: 8),
LinearProgressIndicator(value: progressValue),
const SizedBox(height: 4),
Text(l10n.mapCache_downloadedTiles(
_completedTiles,
_estimatedTiles,
)),
Text(
l10n.mapCache_downloadedTiles(
_completedTiles,
_estimatedTiles,
),
),
],
const SizedBox(height: 12),
Row(
File diff suppressed because it is too large Load Diff
+474
View File
@@ -0,0 +1,474 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighborsScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighborsScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighborsScreen> createState() => _NeighborsScreenState();
}
class _NeighborsScreenState extends State<NeighborsScreen> {
static const int _reqNeighborsKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighborCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
bool _hasData = false;
Timer? _statusTimeout;
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbors;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbors();
_hasData = false;
}
void _setupMessageListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighborsResponse(connector, frame.sublist(6));
}
});
}
String fmtDuration(double seconds) {
if (seconds < 60) {
return '${seconds.toStringAsFixed(1)}s';
}
final int m = (seconds ~/ 60).toInt();
final double s = seconds - (60 * m);
if (m < 60) {
return '${m}m ${s.toStringAsFixed(0)}s';
}
final int h = m ~/ 60;
final int m2 = m % 60;
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighborsData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbors = {};
try {
for (var i = 0; i < resultsCount; i++) {
final neighborData = neighbors.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
neighborData['lastHeard'] = buffer.readUInt32LE();
neighborData['snr'] = buffer.readInt8() / 4.0;
}
return neighbors.values.toList();
} catch (e) {
appLogger.error(
'Error parsing neighbors data: $e',
tag: 'NeighborsScreen',
);
return [];
}
}
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighborData in parsedNeighbors) {
final publicKey = neighborData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighborsKeyLen),
publicKey,
)) {
neighborData['contact'] = repeater;
}
}
});
setState(() {
_parsedNeighbors = parsedNeighbors;
_neighborCount = neighborCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
} catch (e) {
appLogger.error('Error handling neighbors response: $e');
}
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbors() async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbors][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbors,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighborsKeyLen,
]),
);
await connector.sendFrame(frame);
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
final messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
),
);
_recordStatusResult(false);
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
void _recordStatusResult(bool success) {
final selection = _pendingStatusSelection;
if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
connector.recordRepeaterPathResult(repeater, selection, success, null);
_pendingStatusSelection = null;
}
@override
void dispose() {
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbors,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbors,
tooltip: l10n.repeater_refresh,
),
],
),
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbors,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
_buildNeighborsInfoCard(
"${l10n.repeater_neighbors} - $_neighborCount",
),
],
),
),
),
);
}
Widget _buildNeighborsInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
for (final entry in _parsedNeighbors!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
: context.l10n.neighbors_unknownContact(
"<${pubKeyToHex(entry.value['publicKey'])}>",
),
context.l10n.neighbors_heardAgo(
fmtDuration(entry.value['lastHeard'] + 0.0),
),
entry.value['snr'],
connector.currentSf!,
),
],
),
),
);
}
Widget _buildInfoRow(
String label,
String value,
double snr,
int spreadingFactor,
) {
final snrUi = snrUiFromSNR(snr, spreadingFactor);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
Text(
snrUi.text,
style: TextStyle(fontSize: 10, color: snrUi.color),
),
],
),
),
),
],
),
);
}
}
+710
View File
@@ -0,0 +1,710 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
import 'package:meshcore_open/services/map_tile_cache_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:provider/provider.dart';
double getPathDistanceMeters(List<LatLng> points) {
if (points.length <= 1) return 0.0;
double distanceMeters = 0.0;
final distanceCalculator = Distance();
for (int i = 0; i < points.length - 1; i++) {
distanceMeters += distanceCalculator(points[i], points[i + 1]);
}
return distanceMeters;
}
String formatDistance(double distanceMeters, {required bool isImperial}) {
if (isImperial) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
}
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
}
class PathTraceData {
final Uint8List pathData;
final List<double> snrData;
final Map<int, Contact> pathContacts;
PathTraceData({
required this.pathData,
required this.snrData,
required this.pathContacts,
});
}
class PathTraceMapScreen extends StatefulWidget {
final String title;
final Uint8List path;
final int? repeaterId;
final bool flipPathRound;
final bool reversePathRound;
const PathTraceMapScreen({
super.key,
required this.title,
required this.path,
this.repeaterId,
this.flipPathRound = false,
this.reversePathRound = false,
});
@override
State<PathTraceMapScreen> createState() => _PathTraceMapScreenState();
}
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
bool _isLoading = false;
bool _failed2Loaded = false;
bool _hasData = false;
PathTraceData? _traceData;
List<LatLng> _points = <LatLng>[];
List<Polyline> _polylines = [];
LatLng? _initialCenter = LatLng(0, 0);
double _initialZoom = 2.0;
LatLngBounds? _bounds;
ValueKey<String> _mapKey = const ValueKey('initial');
double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true;
String _formatPathPrefixes(Uint8List pathBytes) {
return pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
}
@override
void initState() {
super.initState();
_setupFrameListener();
_doPathTrace();
}
@override
void dispose() {
_frameSubscription?.cancel();
_timeoutTimer?.cancel();
super.dispose();
}
Uint8List addReturnPath(Uint8List pathBytes) {
Uint8List? traceBytes;
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
}
return traceBytes;
}
Future<void> _doPathTrace() async {
if (mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
});
}
final Uint8List path;
Uint8List pathTmp = widget.reversePathRound
? Uint8List.fromList(widget.path.reversed.toList())
: widget.path;
if (widget.flipPathRound) {
path = addReturnPath(pathTmp);
} else {
path = pathTmp;
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
0, //flags
0, //auth
payload: path,
);
connector.sendFrame(frame);
}
void _setupFrameListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
Uint8List tagData = Uint8List(4);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
final frameBuffer = BufferReader(frame);
try {
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutMilliseconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(
Duration(milliseconds: timeoutMilliseconds),
() {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
},
);
}
if (code == respCodeErr) {
_timeoutTimer?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
}
// Check if it's a binary response
if (frame.length > 8 &&
code == pushCodeTraceData &&
listEquals(frame.sublist(4, 8), tagData)) {
_timeoutTimer?.cancel();
if (!mounted) return;
frameBuffer.skipBytes(3); //reserved + path length + flag
if (listEquals(frameBuffer.readBytes(4), tagData)) {
_handleTraceResponse(frame);
}
}
} catch (e) {
_timeoutTimer?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
// Handle any parsing errors gracefully
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
}
});
}
Future<void> _handleTraceResponse(Uint8List frame) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
try {
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
List<double> snrData = buffer
.readRemainingBytes()
.map((snr) => snr.toSigned(8).toDouble() / 4)
.toList();
Map<int, Contact> pathContacts = {};
connector.contacts.where((c) => c.type != advTypeChat).forEach((
repeater,
) {
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
}
}
});
setState(() {
_isLoading = false;
_hasData = true;
_traceData = PathTraceData(
pathData: pathData,
snrData: snrData,
pathContacts: pathContacts,
);
_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) {
_points.add(LatLng(contact.latitude!, contact.longitude!));
}
}
_polylines = _points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[];
_initialCenter = _points.isNotEmpty
? _points.first
: const LatLng(0, 0);
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
_mapKey = ValueKey(
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
);
_pathDistanceMeters = getPathDistanceMeters(_points);
});
} catch (e) {
appLogger.error(
'Error handling trace response: $e',
tag: 'PathTraceMapScreen',
);
if (mounted) {
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
}
}
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
widget.title,
style: const TextStyle(fontSize: 24),
),
),
],
),
centerTitle: false,
actions: [
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _doPathTrace,
tooltip: context.l10n.pathTrace_refreshTooltip,
),
],
),
body: SafeArea(
top: false,
child: Stack(
children: [
if (!_hasData)
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isLoading) const CircularProgressIndicator(),
const SizedBox(height: 16),
if (!_isLoading && _failed2Loaded)
Text(context.l10n.pathTrace_notAvailable),
],
),
),
if (_hasData) _buildMapPathTrace(context, tileCache),
if (_points.isEmpty &&
!_hasData &&
!_isLoading &&
!_failed2Loaded)
Center(
child: Card(
color: Colors.white.withValues(alpha: 0.9),
child: Padding(
padding: EdgeInsets.all(12),
child: Text(
context.l10n.channelPath_noRepeaterLocations,
),
),
),
),
if (_hasData)
_buildLegendCard(context, _traceData!, isImperial),
],
),
),
);
},
);
}
List<Marker> _buildHopMarkers(
List<int> pathData, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop];
if (contact == null || !contact.hasLocation) continue;
final point = LatLng(contact.latitude!, contact.longitude!);
markers.add(
Marker(
point: point,
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(
contact.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 (showLabels) {
markers.add(_buildNodeLabelMarker(point: point, label: contact.name));
}
}
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
if (selfLat != null && selfLon != null) {
final selfPoint = LatLng(selfLat, selfLon);
markers.add(
Marker(
point: selfPoint,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.blue,
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(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: selfPoint,
label: context.l10n.pathTrace_you,
),
);
}
}
return markers;
}
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
return Marker(
point: point,
width: 120,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -20),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
String formatDirectionText(PathTraceData pathTraceData, int index) {
if (index == 0 || index == pathTraceData.snrData.length - 1) {
if (index == 0) {
return context.l10n.pathTrace_you;
} else {
final contactName = pathTraceData
.pathContacts[pathTraceData.pathData[pathTraceData.pathData.length -
1]]
?.name;
final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1]
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
} else {
final contactName =
pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name;
final hex = pathTraceData.pathData[index - 1]
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
}
String formatDirectionSubText(PathTraceData pathTraceData, int index) {
if (index == 0 || index == pathTraceData.snrData.length - 1) {
if (index == 0) {
final contactName =
pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name;
final hex = pathTraceData.pathData[0]
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
} else {
return context.l10n.pathTrace_you;
}
} else {
final contactName =
pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name;
final hex = pathTraceData.pathData[index]
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null
? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
}
}
Widget _buildMapPathTrace(
BuildContext context,
MapTileCacheService tileCache,
) {
return FlutterMap(
key: _mapKey,
options: MapOptions(
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
initialCenter: _initialCenter!,
initialZoom: _initialZoom,
initialCameraFit: _bounds == null
? null
: CameraFit.bounds(
bounds: _bounds!,
padding: const EdgeInsets.all(64),
maxZoom: 16,
),
minZoom: 2.0,
maxZoom: 18.0,
onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) {
setState(() {
_showNodeLabels = shouldShow;
});
}
},
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
if (_traceData!.pathData.isNotEmpty)
MarkerLayer(
markers: _buildHopMarkers(
_traceData!.pathData,
showLabels: _showNodeLabels,
),
),
],
);
}
Widget _buildLegendCard(
BuildContext context,
PathTraceData pathTraceData,
bool isImperial,
) {
final l10n = context.l10n;
final maxHeight = MediaQuery.of(context).size.height * 0.35;
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
final cardHeight = max(96.0, min(maxHeight, estimatedHeight));
return Positioned(
left: 16,
right: 16,
bottom: 16,
child: SizedBox(
height: cardHeight,
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
const Divider(height: 1),
Expanded(
child: pathTraceData.pathData.isEmpty
? Center(
child: Text(l10n.channelPath_noHopDetailsAvailable),
)
: Scrollbar(
child: ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: pathTraceData.pathData.length + 1,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final snrUi = snrUiFromSNR(
index < pathTraceData.snrData.length
? pathTraceData.snrData[index]
: null,
context.read<MeshCoreConnector>().currentSf,
);
return Column(
children: [
ListTile(
leading:
index >= pathTraceData.snrData.length / 2
? Icon(Icons.call_received)
: Icon(Icons.call_made),
title: Text(
formatDirectionText(pathTraceData, index),
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
formatDirectionSubText(
pathTraceData,
index,
),
style: const TextStyle(fontSize: 14),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
snrUi.icon,
color: snrUi.color,
size: 18.0,
),
Text(
snrUi.text,
style: TextStyle(
fontSize: 10,
color: snrUi.color,
),
),
],
),
onTap: () {
// Handle item tap
},
),
],
);
},
),
),
),
],
),
),
),
);
}
}
+62 -25
View File
@@ -119,14 +119,24 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
// Show debug info if requested
if (showDebug && mounted) {
final frame = buildSendCliCommandFrame(widget.repeater.publicKey, command);
DebugFrameViewer.showFrameDebug(context, frame, context.l10n.repeater_cliCommandFrameTitle);
final frame = buildSendCliCommandFrame(
widget.repeater.publicKey,
command,
);
DebugFrameViewer.showFrameDebug(
context,
frame,
context.l10n.repeater_cliCommandFrameTitle,
);
}
// Send CLI command to repeater with retry
try {
if (_commandService != null) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final connector = Provider.of<MeshCoreConnector>(
context,
listen: false,
);
final repeater = _resolveRepeater(connector);
final response = await _commandService!.sendCommand(
repeater,
@@ -158,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
_commandController.clear();
_historyIndex = -1;
_commandFocusNode.requestFocus();
// Auto-scroll to bottom
Future.delayed(const Duration(milliseconds: 100), () {
@@ -230,7 +241,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
Text(l10n.repeater_cliTitle),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -251,12 +265,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
value: 'auto',
child: Row(
children: [
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -266,12 +288,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
value: 'flood',
child: Row(
children: [
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -282,7 +312,8 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () => PathManagementDialog.show(context, contact: repeater),
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: const Icon(Icons.bug_report),
@@ -473,7 +504,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
decoration: InputDecoration(
hintText: l10n.repeater_enterCommandHint,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
prefixText: '> ',
),
style: const TextStyle(fontFamily: 'monospace'),
@@ -718,10 +752,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
];
final gpsCommands = [
_CommandHelpEntry(
command: 'gps',
description: l10n.repeater_cliHelpGps,
),
_CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps),
_CommandHelpEntry(
command: 'gps {on|off}',
description: l10n.repeater_cliHelpGpsOnOff,
@@ -758,13 +789,25 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_general, generalCommands),
_buildHelpSection(
context,
l10n.repeater_general,
generalCommands,
),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands),
_buildHelpSection(
context,
l10n.repeater_settingsCategory,
settingsCommands,
),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_bridge, bridgeCommands),
const SizedBox(height: 16),
_buildHelpSection(context, l10n.repeater_logging, loggingCommands),
_buildHelpSection(
context,
l10n.repeater_logging,
loggingCommands,
),
const SizedBox(height: 16),
_buildHelpSection(
context,
@@ -813,10 +856,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
),
if (note != null) ...[
const SizedBox(height: 6),
Text(
note,
style: const TextStyle(fontSize: 12),
),
Text(note, style: const TextStyle(fontSize: 12)),
],
const SizedBox(height: 8),
...commands.map((entry) => _buildHelpCommandCard(context, entry)),
@@ -871,8 +911,5 @@ class _CommandHelpEntry {
final String command;
final String description;
const _CommandHelpEntry({
required this.command,
required this.description,
});
const _CommandHelpEntry({required this.command, required this.description});
}
+221 -118
View File
@@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbors_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@@ -19,16 +23,27 @@ 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(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_management),
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -39,130 +54,221 @@ class RepeaterHubScreen extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white),
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.shortPubKeyHex,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
],
),
],
),
],
),
],
),
],
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
),
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: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TelemetryScreen(
repeater: repeater,
password: password,
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: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(repeater: repeater, password: password),
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
);
},
),
),
);
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbors,
subtitle: l10n.repeater_neighborsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
NeighborsScreen(repeater: repeater, password: password),
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
);
},
),
],
),
),
@@ -209,10 +315,7 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
+186 -68
View File
@@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Basic settings
final TextEditingController _nameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _guestPasswordController = TextEditingController();
final TextEditingController _guestPasswordController =
TextEditingController();
// Radio settings
final TextEditingController _freqController = TextEditingController();
@@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
bool _privacyMode = false;
// Advertisement settings
bool _advertEnable = true;
int _advertInterval = 120; // minutes/2
bool _floodAdvertEnable = true;
int _floodAdvertInterval = 12; // hours
int _privAdvertInterval = 60; // minutes
@@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (_fetchedSettings.isEmpty) return;
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings');
appLog.info(
'Updating UI with keys: ${_fetchedSettings.keys.toList()}',
tag: 'RadioSettings',
);
setState(() {
// Update name
@@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
final radioStr = _fetchedSettings['radio']!;
appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings');
final parts = radioStr.split(',');
appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings');
appLog.info(
'Split into ${parts.length} parts: $parts',
tag: 'RadioSettings',
);
if (parts.isNotEmpty) {
final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim();
@@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
appLog.info('CR text: "$crText"', tag: 'RadioSettings');
_codingRate = int.tryParse(crText) ?? _codingRate;
}
appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings');
appLog.info(
'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate',
tag: 'RadioSettings',
);
}
if (_fetchedSettings.containsKey('tx')) {
@@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
}
if (_fetchedSettings.containsKey('lat')) {
appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings');
appLog.info(
'Setting lat to: "${_fetchedSettings['lat']}"',
tag: 'RadioSettings',
);
_latController.text = _fetchedSettings['lat']!;
}
if (_fetchedSettings.containsKey('lon')) {
appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings');
appLog.info(
'Setting lon to: "${_fetchedSettings['lon']}"',
tag: 'RadioSettings',
);
_lonController.text = _fetchedSettings['lon']!;
}
@@ -230,12 +248,14 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_fetchedSettings['advert.interval']!,
_advertInterval,
);
_advertEnable = _advertInterval > 0;
}
if (_fetchedSettings.containsKey('flood.advert.interval')) {
_floodAdvertInterval = _parseIntWithFallback(
_fetchedSettings['flood.advert.interval']!,
_floodAdvertInterval,
);
_floodAdvertEnable = _floodAdvertInterval > 0;
}
if (_fetchedSettings.containsKey('priv.advert.interval')) {
_privAdvertInterval = _parseIntWithFallback(
@@ -268,7 +288,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
void _applySettingResponse(String command, String response) {
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings');
appLog.info(
'Command: "$command", Raw response: "$response"',
tag: 'RadioSettings',
);
final value = _extractCliValue(response);
appLog.info('Extracted value: "$value"', tag: 'RadioSettings');
if (value == null) return;
@@ -280,7 +303,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Validate response content matches expected format for the command
// This prevents mismatched responses over LoRa where order isn't guaranteed
if (!_validateResponseForCommand(key, value)) {
appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings');
appLog.warn(
'Response "$value" does not match expected format for "$key", ignoring',
tag: 'RadioSettings',
);
return;
}
@@ -311,7 +337,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Must have at least 3 commas and start with a frequency-like number
final parts = value.split(',');
if (parts.length < 4) return false;
final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), ''));
final freq = double.tryParse(
parts[0].replaceAll(RegExp(r'[^0-9.]'), ''),
);
// Frequency should be in reasonable LoRa range (300-2500 MHz)
return freq != null && freq >= 300 && freq <= 2500;
@@ -339,22 +367,33 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
case 'privacy':
// Boolean values: on/off/true/false/1/0/enabled/disabled
final lower = value.toLowerCase().trim();
return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower);
return [
'on',
'off',
'true',
'false',
'1',
'0',
'enabled',
'disabled',
].contains(lower);
case 'advert.interval':
case 'flood.advert.interval':
case 'priv.advert.interval':
// Interval: positive integer
// Interval: non-negative integer (0 means disabled)
if (value.contains(',')) return false;
final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), ''));
return interval != null && interval > 0;
return interval != null && interval >= 0;
case 'name':
// Name: any non-empty string, but should NOT look like radio settings
if (value.isEmpty) return false;
// If it has 3+ commas and looks like numbers, probably radio data
final commaCount = ','.allMatches(value).length;
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false;
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) {
return false;
}
return true;
default:
@@ -551,7 +590,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
final freqMHz = double.tryParse(_freqController.text);
if (freqMHz != null) {
final bwKHz = _bandwidth! / 1000;
commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate');
commands.add(
'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate',
);
}
}
@@ -590,7 +631,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
timestampSeconds: timestampSeconds,
);
await connector.sendFrame(frame);
await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands
await Future.delayed(
const Duration(milliseconds: 200),
); // Delay between commands
}
setState(() {
@@ -614,7 +657,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.repeater_errorSavingSettings(e.toString())),
content: Text(
context.l10n.repeater_errorSavingSettings(e.toString()),
),
backgroundColor: Colors.red,
),
);
@@ -699,7 +744,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
Text(l10n.repeater_settingsTitle),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -723,12 +771,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
value: 'auto',
child: Row(
children: [
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -738,12 +794,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
value: 'flood',
child: Row(
children: [
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -754,7 +818,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () => PathManagementDialog.show(context, contact: repeater),
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
if (_hasChanges)
TextButton.icon(
@@ -865,7 +930,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
border: const OutlineInputBorder(),
suffixText: 'MHz',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
onChanged: (_) => _markChanged(),
),
const SizedBox(height: 16),
@@ -895,7 +962,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _bandwidth,
initialValue: _bandwidth,
decoration: InputDecoration(
labelText: l10n.repeater_bandwidth,
border: const OutlineInputBorder(),
@@ -917,16 +984,13 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _spreadingFactor,
initialValue: _spreadingFactor,
decoration: InputDecoration(
labelText: l10n.repeater_spreadingFactor,
border: const OutlineInputBorder(),
),
items: _spreadingFactorOptions.map((sf) {
return DropdownMenuItem(
value: sf,
child: Text('SF$sf'),
);
return DropdownMenuItem(value: sf, child: Text('SF$sf'));
}).toList(),
onChanged: (value) {
if (value != null) {
@@ -939,16 +1003,13 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _codingRate,
initialValue: _codingRate,
decoration: InputDecoration(
labelText: l10n.repeater_codingRate,
border: const OutlineInputBorder(),
),
items: _codingRateOptions.map((cr) {
return DropdownMenuItem(
value: cr,
child: Text('4/$cr'),
);
return DropdownMenuItem(value: cr, child: Text('4/$cr'));
}).toList(),
onChanged: (value) {
if (value != null) {
@@ -988,7 +1049,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
helperText: l10n.repeater_latitudeHelper,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
onChanged: (_) => _markChanged(),
),
const SizedBox(height: 16),
@@ -999,7 +1063,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
helperText: l10n.repeater_longitudeHelper,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
onChanged: (_) => _markChanged(),
),
],
@@ -1018,11 +1085,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
children: [
Row(
children: [
Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color),
Icon(
Icons.toggle_on,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.repeater_features,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
@@ -1102,7 +1175,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
)
: const Icon(Icons.refresh, size: 20),
onPressed: isRefreshing ? null : onRefresh,
tooltip: refreshTooltip,
@@ -1130,40 +1203,72 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
const Divider(),
ListTile(
title: Text(l10n.repeater_localAdvertInterval),
subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
subtitle: Text(
l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
),
trailing: Switch(
value: _advertEnable,
onChanged: (value) {
setState(() {
_advertInterval = value ? 60 : 0;
_advertEnable = value;
});
_markChanged();
},
),
),
Slider(
value: _advertInterval.toDouble(),
value: _advertInterval == 0
? 60.toDouble()
: _advertInterval.toDouble(),
min: 60,
max: 240,
divisions: 18,
label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
onChanged: (value) {
setState(() {
_advertInterval = value.toInt();
});
_markChanged();
},
onChanged: _advertEnable
? (value) {
setState(() {
_advertInterval = value.toInt();
});
_markChanged();
}
: null,
),
const SizedBox(height: 16),
ListTile(
title: Text(l10n.repeater_floodAdvertInterval),
subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
subtitle: Text(
l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
),
trailing: Switch(
value: _floodAdvertEnable,
onChanged: (value) {
setState(() {
_floodAdvertInterval = value ? 3 : 0;
_floodAdvertEnable = value;
});
_markChanged();
},
),
),
Slider(
value: _floodAdvertInterval.toDouble(),
value: _floodAdvertInterval == 0
? 3.toDouble()
: _floodAdvertInterval.toDouble(),
min: 3,
max: 48,
divisions: 45,
label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
onChanged: (value) {
setState(() {
_floodAdvertInterval = value.toInt();
});
_markChanged();
},
max: 168,
divisions: 165,
label: l10n.repeater_floodAdvertIntervalHours(
_floodAdvertInterval,
),
onChanged: _floodAdvertEnable
? (value) {
setState(() {
_floodAdvertInterval = value.toInt();
});
_markChanged();
}
: null,
),
// Encrypted advertisement interval - hidden until privacy mode is implemented
// if (_privacyMode) ...[
@@ -1220,10 +1325,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
const Divider(),
ListTile(
leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer),
title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)),
title: Text(
l10n.repeater_rebootRepeater,
style: TextStyle(color: colorScheme.onErrorContainer),
),
subtitle: Text(
l10n.repeater_rebootRepeaterSubtitle,
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
style: TextStyle(
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
),
),
onTap: () => _confirmAction(
l10n.repeater_rebootRepeater,
@@ -1246,11 +1356,19 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// ),
// ),
ListTile(
leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer),
title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)),
leading: Icon(
Icons.delete_forever,
color: colorScheme.onErrorContainer,
),
title: Text(
l10n.repeater_eraseFileSystem,
style: TextStyle(color: colorScheme.onErrorContainer),
),
subtitle: Text(
l10n.repeater_eraseFileSystemSubtitle,
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
style: TextStyle(
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
),
),
onTap: () => _confirmAction(
l10n.repeater_eraseFileSystem,
@@ -1272,9 +1390,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
if (command == 'erase') {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.repeater_eraseSerialOnly)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
}
return;
}
+103 -27
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 {
@@ -28,7 +30,8 @@ class RepeaterStatusScreen extends StatefulWidget {
class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
bool _isLoading = false;
StreamSubscription<Uint8List>? _frameSubscription;
@@ -178,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);
}
@@ -200,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']);
@@ -293,7 +314,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
content: Text(
context.l10n.repeater_errorLoadingStatus(e.toString()),
),
backgroundColor: Colors.red,
),
);
@@ -327,7 +350,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
Text(l10n.repeater_statusTitle),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -348,12 +374,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
value: 'auto',
child: Row(
children: [
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -363,12 +397,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
value: 'flood',
child: Row(
children: [
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
@@ -379,7 +421,8 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () => PathManagementDialog.show(context, contact: repeater),
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
@@ -423,11 +466,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
children: [
Row(
children: [
Icon(Icons.info_outline, color: Theme.of(context).textTheme.headlineSmall?.color),
Icon(
Icons.info_outline,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.repeater_systemInformation,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
@@ -453,18 +502,30 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
children: [
Row(
children: [
Icon(Icons.radio, color: Theme.of(context).textTheme.headlineSmall?.color),
Icon(
Icons.radio,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.repeater_radioStatistics,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
_buildInfoRow(l10n.repeater_lastRssi, _formatValue(_lastRssi, suffix: ' dB')),
_buildInfoRow(
l10n.repeater_lastRssi,
_formatValue(_lastRssi, suffix: ' dB'),
),
_buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)),
_buildInfoRow(l10n.repeater_noiseFloor, _formatValue(_noiseFloor, suffix: ' dB')),
_buildInfoRow(
l10n.repeater_noiseFloor,
_formatValue(_noiseFloor, suffix: ' dB'),
),
_buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)),
_buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)),
],
@@ -483,11 +544,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
children: [
Row(
children: [
Icon(Icons.analytics, color: Theme.of(context).textTheme.headlineSmall?.color),
Icon(
Icons.analytics,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.repeater_packetStatistics,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
@@ -543,25 +610,32 @@ 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() {
if (_statusRequestedAt == null) return '';
final dt = _statusRequestedAt!;
final date = '${dt.day}/${dt.month}/${dt.year}';
final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
final time =
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
return '$date $time';
}
@@ -598,7 +672,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
final direct = _formatValue(_dupDirect);
return l10n.repeater_duplicatesFloodDirect(flood, direct);
}
if (_packetsRecv == null || _floodRx == null || _directRx == null) return '';
if (_packetsRecv == null || _floodRx == null || _directRx == null) {
return '';
}
final dupTotal = _packetsRecv! - _floodRx! - _directRx!;
if (dupTotal < 0) return '';
return l10n.repeater_duplicatesTotal(dupTotal);
+128 -35
View File
@@ -1,21 +1,77 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
import 'contacts_screen.dart';
/// Screen for scanning and connecting to MeshCore devices
class ScannerScreen extends StatelessWidget {
class ScannerScreen extends StatefulWidget {
const ScannerScreen({super.key});
@override
State<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen> {
bool _changedNavigation = false;
late final VoidCallback _connectionListener;
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_connectionListener = () {
if (connector.state == MeshCoreConnectionState.disconnected) {
_changedNavigation = false;
} else if (connector.state == MeshCoreConnectionState.connected &&
!_changedNavigation) {
_changedNavigation = true;
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const ContactsScreen()),
);
}
}
};
connector.addListener(_connectionListener);
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
if (mounted) {
setState(() {
_bluetoothState = state;
});
// Cancel scan if Bluetooth turns off while scanning
if (state != BluetoothAdapterState.on) {
unawaited(connector.stopScan());
}
}
});
}
@override
void dispose() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
connector.removeListener(_connectionListener);
unawaited(_bluetoothStateSubscription.cancel());
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.scanner_title),
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
centerTitle: true,
automaticallyImplyLeading: false,
),
@@ -25,13 +81,15 @@ class ScannerScreen extends StatelessWidget {
builder: (context, connector, child) {
return Column(
children: [
// Bluetooth off warning
if (_bluetoothState == BluetoothAdapterState.off)
_bluetoothOffWarning(context),
// Status bar
_buildStatusBar(context, connector),
// Device list
Expanded(
child: _buildDeviceList(context, connector),
),
Expanded(child: _buildDeviceList(context, connector)),
],
);
},
@@ -39,17 +97,21 @@ class ScannerScreen extends StatelessWidget {
),
floatingActionButton: Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
final isScanning = connector.state == MeshCoreConnectionState.scanning;
final isScanning =
connector.state == MeshCoreConnectionState.scanning;
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
return FloatingActionButton.extended(
onPressed: () {
if (isScanning) {
connector.stopScan();
} else {
connector.startScan();
}
},
icon: isScanning
onPressed: isBluetoothOff
? null
: () {
if (isScanning) {
connector.stopScan();
} else {
connector.startScan();
}
},
icon: isScanning
? const SizedBox(
width: 20,
height: 20,
@@ -59,7 +121,11 @@ class ScannerScreen extends StatelessWidget {
),
)
: const Icon(Icons.bluetooth_searching),
label: Text(isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan),
label: Text(
isScanning
? context.l10n.scanner_stop
: context.l10n.scanner_scan,
),
);
},
),
@@ -70,7 +136,7 @@ class ScannerScreen extends StatelessWidget {
String statusText;
Color statusColor;
final l10n = context.l10n;
final l10n = context.l10n;
switch (connector.state) {
case MeshCoreConnectionState.scanning:
statusText = l10n.scanner_scanning;
@@ -117,20 +183,13 @@ final l10n = context.l10n;
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth,
size: 64,
color: Colors.grey[400],
),
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
connector.state == MeshCoreConnectionState.scanning
? context.l10n.scanner_searchingDevices
: context.l10n.scanner_tapToScan,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
@@ -161,15 +220,6 @@ final l10n = context.l10n;
? result.device.platformName
: result.advertisementData.advName;
await connector.connect(result.device, displayName: name);
if (context.mounted && connector.isConnected) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ContactsScreen(),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -181,4 +231,47 @@ final l10n = context.l10n;
}
}
}
Widget _bluetoothOffWarning(BuildContext context) {
final errorColor = Theme.of(context).colorScheme.error;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
color: errorColor.withValues(alpha: 0.15),
child: Row(
children: [
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.scanner_bluetoothOff,
style: TextStyle(
color: errorColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
context.l10n.scanner_bluetoothOffMessage,
style: TextStyle(
color: errorColor.withValues(alpha: 0.85),
fontSize: 12,
),
),
],
),
),
if (Platform.isAndroid)
TextButton(
onPressed: () => FlutterBluePlus.turnOn(),
child: Text(context.l10n.scanner_enableBluetooth),
),
],
),
);
}
}
+466 -181
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/gpx_export.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -6,6 +8,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
@@ -19,6 +22,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
bool _showBatteryVoltage = false;
bool _deviceInfoExpanded = false;
String _appVersion = '';
@override
@@ -39,7 +43,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings_title),
title: AdaptiveAppBarTitle(l10n.settings_title),
centerTitle: true,
),
body: SafeArea(
@@ -59,6 +63,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
_buildDebugCard(context),
const SizedBox(height: 16),
_buildExportCard(connector),
const SizedBox(height: 16),
_buildAboutCard(context),
],
);
@@ -68,36 +74,97 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildDeviceInfoCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.settings_deviceInfo,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
setState(() {
_deviceInfoExpanded = !_deviceInfoExpanded;
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Expanded(
child: Text(
l10n.settings_deviceInfo,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
AnimatedRotation(
turns: _deviceInfoExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(Icons.expand_more),
),
],
),
),
const SizedBox(height: 16),
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected),
_buildBatteryInfoRow(context, connector),
if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
if (connector.selfPublicKey != null)
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'),
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'),
],
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(
l10n.settings_infoName,
connector.deviceDisplayName,
),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(
l10n.settings_infoStatus,
connector.isConnected
? l10n.common_connected
: l10n.common_disconnected,
),
_buildBatteryInfoRow(context, connector),
if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
if (connector.selfPublicKey != null)
_buildInfoRow(
l10n.settings_infoPublicKey,
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
),
_buildInfoRow(
l10n.settings_infoContactsCount,
'${connector.contacts.length}',
),
_buildInfoRow(
l10n.settings_infoChannelCount,
'${connector.channels.length}',
),
],
),
),
crossFadeState: _deviceInfoExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
);
}
Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) {
Widget _buildBatteryInfoRow(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
@@ -167,7 +234,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildNodeSettingsCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Column(
@@ -298,7 +368,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const BleDebugLogScreen(),
),
);
},
),
@@ -311,7 +383,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const AppDebugLogScreen(),
),
);
},
),
@@ -327,28 +401,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
Color? valueColor,
VoidCallback? onTap,
}) {
final theme = Theme.of(context);
final row = Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (leading != null) ...[
leading,
const SizedBox(width: 8),
],
Text(label, style: TextStyle(color: Colors.grey[600])),
if (leading != null) ...[leading, const SizedBox(width: 8)],
Expanded(
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
Flexible(
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w500,
color: valueColor,
),
overflow: TextOverflow.ellipsis,
const SizedBox(height: 4),
Text(
value,
style: theme.textTheme.bodyLarge?.copyWith(
color: valueColor,
fontWeight: FontWeight.w500,
),
),
],
@@ -357,11 +436,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (onTap != null) {
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: row,
);
}
return row;
}
@@ -413,75 +493,155 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n;
final latController = TextEditingController();
final lonController = TextEditingController();
final intervalController = TextEditingController();
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
// Safe access to custom vars - may be null before device responds
final customVars = connector.currentCustomVars ?? {};
final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval =
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
if (hasGPS) ...[
const SizedBox(height: 16),
TextField(
controller: intervalController,
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: false,
),
),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_locationGPSEnable,
subtitle: l10n.settings_locationGPSEnableSubtitle,
value: isGPSEnabled,
onChanged: (value) async {
setDialogState(() => isGPSEnabled = value);
if (value) {
await connector.setCustomVar("gps:1");
} else {
await connector.setCustomVar("gps:0");
}
},
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
TextButton(
onPressed: () async {
Navigator.pop(context);
if (hasGPS) {
final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) {
return;
}
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.settings_locationIntervalInvalid),
),
);
return;
}
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
}
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty
? double.tryParse(latText)
: currentLat;
final lon = lonText.isNotEmpty
? double.tryParse(lonText)
: currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
);
}
@@ -530,17 +690,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_advertisementSent)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
}
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_timeSynchronized)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -560,7 +720,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context);
connector.rebootDevice();
},
child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)),
child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
),
),
],
),
@@ -572,7 +735,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
showAboutDialog(
context: context,
applicationName: l10n.appTitle,
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion,
applicationVersion: _appVersion.isEmpty
? l10n.common_loading
: _appVersion,
applicationLegalese: l10n.settings_aboutLegalese,
children: [
const SizedBox(height: 16),
@@ -580,6 +745,110 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
);
}
Future<void> _gpxExport(
GpxExport exporter,
String name,
String description,
String filename,
String shareText,
String subject,
) async {
final l10n = context.l10n;
final result = await exporter.exportGPX(
name,
description,
filename,
shareText,
subject,
);
if (!mounted) return;
switch (result) {
case gpxExportSuccess:
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess)));
case gpxExportNoContacts:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)),
);
break;
case gpxExportNotAvailable:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)),
);
break;
case gpxExportFailed:
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError)));
break;
}
}
Widget _buildExportCard(MeshCoreConnector connector) {
final l10n = context.l10n;
return Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(l10n.settings_gpxExportRepeaters),
subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final exporter = GpxExport(connector);
exporter.addRepeaters();
_gpxExport(
exporter,
l10n.map_repeater,
l10n.settings_gpxExportRepeatersRoom,
"meshcore_repeaters_",
l10n.settings_gpxExportShareText,
l10n.settings_gpxExportShareSubject,
);
},
),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(l10n.settings_gpxExportContacts),
subtitle: Text(l10n.settings_gpxExportContactsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final exporter = GpxExport(connector);
exporter.addContacts();
_gpxExport(
exporter,
l10n.map_repeater,
l10n.settings_gpxExportChat,
"meshcore_contacts_",
l10n.settings_gpxExportShareText,
l10n.settings_gpxExportShareSubject,
);
},
),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(l10n.settings_gpxExportAll),
subtitle: Text(l10n.settings_gpxExportAllSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final exporter = GpxExport(connector);
exporter.addAll();
_gpxExport(
exporter,
l10n.map_repeater,
l10n.settings_gpxExportAllContacts,
"meshcore_all_",
l10n.settings_gpxExportShareText,
l10n.settings_gpxExportShareSubject,
);
},
),
],
),
);
}
}
class _RadioSettingsDialog extends StatefulWidget {
@@ -597,6 +866,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false;
@override
void initState() {
@@ -604,7 +874,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
// Populate with current settings if available
if (widget.connector.currentFreqHz != null) {
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3);
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
.toStringAsFixed(3);
} else {
_frequencyController.text = '915.0';
}
@@ -645,6 +916,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (widget.connector.currentTxPower != null) {
_txPowerController.text = widget.connector.currentTxPower.toString();
}
_clientRepeat = widget.connector.clientRepeat ?? false;
}
@override
@@ -670,15 +943,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_frequencyInvalid)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
return;
}
if (txPower == null || txPower < 0 || txPower > 22) {
final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_txPowerInvalid)),
SnackBar(
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
);
return;
}
@@ -686,10 +962,35 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final freqHz = (freqMHz * 1000).round();
final bwHz = _bandwidth.hz;
final sf = _spreadingFactor.value;
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr);
final cr = _toDeviceCodingRate(
_codingRate.value,
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));
await widget.connector.sendFrame(
buildSetRadioParamsFrame(
freqHz,
bwHz,
sf,
cr,
clientRepeat: knownRepeat ? _clientRepeat : null,
),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -727,34 +1028,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(
@@ -762,7 +1054,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
const SizedBox(height: 16),
DropdownButtonFormField<LoRaBandwidth>(
@@ -772,10 +1066,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaBandwidth.values
.map((bw) => DropdownMenuItem(
value: bw,
child: Text(bw.label),
))
.map(
(bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _bandwidth = value);
@@ -789,10 +1082,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaSpreadingFactor.values
.map((sf) => DropdownMenuItem(
value: sf,
child: Text(sf.label),
))
.map(
(sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value);
@@ -806,10 +1098,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaCodingRate.values
.map((cr) => DropdownMenuItem(
value: cr,
child: Text(cr.label),
))
.map(
(cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _codingRate = value);
@@ -821,10 +1112,22 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
decoration: InputDecoration(
labelText: l10n.settings_txPower,
border: const OutlineInputBorder(),
helperText: l10n.settings_txPowerHelper,
helperText: widget.connector.maxTxPower != null
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
: l10n.settings_txPowerHelper,
),
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,
),
],
],
),
),
@@ -833,26 +1136,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: _saveSettings,
child: Text(l10n.common_save),
),
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
],
);
}
}
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,
);
}
}
+53 -24
View File
@@ -1,18 +1,18 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
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;
@@ -34,7 +34,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _timeEstment = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -64,20 +63,31 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleStatusResponse(context, frame.sublist(6));
if (!mounted) return;
_handleStatusResponse(frame.sublist(6));
}
});
}
void _handleStatusResponse(BuildContext context, Uint8List frame) {
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(
@@ -184,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;
@@ -310,6 +322,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
entry['values'],
l10n.telemetry_channelTitle(entry['channel']),
entry['channel'],
isImperialUnits,
),
],
),
@@ -322,6 +335,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
Map<String, dynamic> channelData,
String title,
int channel,
bool isImperialUnits,
) {
final l10n = context.l10n;
return Card(
@@ -361,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(
@@ -408,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';
}
}
+6 -6
View File
@@ -1,10 +1,6 @@
import 'package:flutter/foundation.dart';
enum AppDebugLogLevel {
info,
warning,
error,
}
enum AppDebugLogLevel { info, warning, error }
class AppDebugLogEntry {
final DateTime timestamp;
@@ -51,7 +47,11 @@ class AppDebugLogService extends ChangeNotifier {
notifyListeners();
}
void log(String message, {String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info}) {
void log(
String message, {
String tag = 'App',
AppDebugLogLevel level = AppDebugLogLevel.info,
}) {
if (!_enabled) return;
_entries.add(
+53 -7
View File
@@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier {
return stored ?? 'nmc';
}
String batteryChemistryForRepeater(String repeaterPubKeyHex) {
final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex];
if (stored == 'liion') return 'nmc';
return stored ?? 'nmc';
}
Future<void> loadSettings() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_settingsKey);
@@ -74,6 +80,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowMarkers: value));
}
Future<void> setEnableMessageTracing(bool value) async {
await updateSettings(_settings.copyWith(enableMessageTracing: value));
}
Future<void> setMapCacheBounds(Map<String, double>? value) async {
await updateSettings(_settings.copyWith(mapCacheBounds: value));
}
@@ -82,10 +92,7 @@ class AppSettingsService extends ChangeNotifier {
final safeMin = minZoom <= maxZoom ? minZoom : maxZoom;
final safeMax = minZoom <= maxZoom ? maxZoom : minZoom;
await updateSettings(
_settings.copyWith(
mapCacheMinZoom: safeMin,
mapCacheMaxZoom: safeMax,
),
_settings.copyWith(mapCacheMinZoom: safeMin, mapCacheMaxZoom: safeMax),
);
}
@@ -123,9 +130,48 @@ class AppSettingsService extends ChangeNotifier {
appLogger.setEnabled(value);
}
Future<void> setBatteryChemistryForDevice(String deviceId, String chemistry) async {
final updated = Map<String, String>.from(_settings.batteryChemistryByDeviceId);
Future<void> setBatteryChemistryForDevice(
String deviceId,
String chemistry,
) async {
final updated = Map<String, String>.from(
_settings.batteryChemistryByDeviceId,
);
updated[deviceId] = chemistry;
await updateSettings(_settings.copyWith(batteryChemistryByDeviceId: updated));
await updateSettings(
_settings.copyWith(batteryChemistryByDeviceId: updated),
);
}
Future<void> setBatteryChemistryForRepeater(
String repeaterPubKeyHex,
String chemistry,
) async {
final updated = Map<String, String>.from(
_settings.batteryChemistryByRepeaterId,
);
updated[repeaterPubKeyHex] = chemistry;
await updateSettings(
_settings.copyWith(batteryChemistryByRepeaterId: updated),
);
}
Future<void> setUnitSystem(UnitSystem value) async {
await updateSettings(_settings.copyWith(unitSystem: value));
}
bool isChannelMuted(String channelName) {
return _settings.mutedChannels.contains(channelName);
}
Future<void> muteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)..add(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
Future<void> unmuteChannel(String channelName) async {
final updated = Set<String>.from(_settings.mutedChannels)
..remove(channelName);
await updateSettings(_settings.copyWith(mutedChannels: updated));
}
}
+5 -12
View File
@@ -1,4 +1,3 @@
import 'dart:isolate';
import 'dart:io';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
@@ -15,20 +14,14 @@ class BackgroundService {
channelDescription: 'Keeps MeshCore running in the background.',
channelImportance: NotificationChannelImportance.LOW,
priority: NotificationPriority.LOW,
iconData: const NotificationIconData(
resType: ResourceType.mipmap,
resPrefix: ResourcePrefix.ic,
name: 'launcher',
),
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: const ForegroundTaskOptions(
interval: 5000,
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
autoRunOnBoot: false,
allowWakeLock: true,
allowWifiLock: false,
),
);
@@ -64,13 +57,13 @@ void startCallback() {
class _MeshCoreTaskHandler extends TaskHandler {
@override
void onStart(DateTime timestamp, SendPort? sendPort) {}
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
@override
void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {}
void onRepeatEvent(DateTime timestamp) {}
@override
void onDestroy(DateTime timestamp, SendPort? sendPort) {}
Future<void> onDestroy(DateTime timestamp, bool isTimeout) async {}
@override
void onNotificationButtonPressed(String id) {}
+72 -13
View File
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import '../connector/meshcore_protocol.dart';
class BleDebugLogEntry {
@@ -16,7 +17,9 @@ class BleDebugLogEntry {
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -26,14 +29,13 @@ class BleRawLogRxEntry {
final DateTime timestamp;
final Uint8List payload;
BleRawLogRxEntry({
required this.timestamp,
required this.payload,
});
BleRawLogRxEntry({required this.timestamp, required this.payload});
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -43,9 +45,11 @@ 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 => List.unmodifiable(_rawLogRxEntries);
List<BleRawLogRxEntry> get rawLogRxEntries =>
List.unmodifiable(_rawLogRxEntries);
void logFrame(Uint8List frame, {required bool outgoing, String? note}) {
if (frame.isEmpty) return;
@@ -76,24 +80,59 @@ class BleDebugLogService extends ChangeNotifier {
}
}
notifyListeners();
_notifyListenersSafely();
}
void clear() {
_entries.clear();
_rawLogRxEntries.clear();
notifyListeners();
_notifyListenersSafely();
}
String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) {
final label = _codeLabel(code);
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(
int code,
Uint8List frame,
bool outgoing,
String? note,
) {
final label = _codeLabel(code, outgoing: outgoing);
final prefix = outgoing ? 'TX' : 'RX';
final extra = _frameDetail(code, frame);
final noteText = note != null ? '$note' : '';
return '$prefix $label$extra$noteText';
}
String _codeLabel(int code) {
String _codeLabel(int code, {required bool outgoing}) {
if (outgoing) {
return _commandLabel(code) ?? 'CODE_$code';
}
final pushLabel = _pushLabel(code);
if (pushLabel != null) return pushLabel;
final responseLabel = _responseLabel(code);
if (responseLabel != null) return responseLabel;
return 'CODE_$code';
}
String? _commandLabel(int code) {
switch (code) {
case cmdAppStart:
return 'CMD_APP_START';
@@ -135,6 +174,17 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_SET_CHANNEL';
case cmdGetRadioSettings:
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
case cmdSendTracePath:
return 'CMD_SEND_TRACE_PATH';
default:
return null;
}
}
String? _responseLabel(int code) {
switch (code) {
case respCodeOk:
return 'RESP_CODE_OK';
case respCodeErr:
@@ -167,6 +217,15 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_INFO';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
case pushCodeTraceData:
return 'PUSH_CODE_TRACE_DATA';
default:
return null;
}
}
String? _pushLabel(int code) {
switch (code) {
case pushCodeAdvert:
return 'PUSH_CODE_ADVERT';
case pushCodePathUpdated:
@@ -184,7 +243,7 @@ class BleDebugLogService extends ChangeNotifier {
case pushCodeNewAdvert:
return 'PUSH_CODE_NEW_ADVERT';
default:
return 'CODE_$code';
return null;
}
}
+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});
}

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