Compare commits

...

148 Commits

Author SHA1 Message Date
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
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
137 changed files with 24324 additions and 5393 deletions
+2
View File
@@ -2,6 +2,8 @@ name: Build
on:
push:
branches:
- main
pull_request:
jobs:
@@ -1,8 +1,10 @@
name: Flutter Analyze
name: Flutter and Dart
on:
pull_request:
push:
branches:
- main
jobs:
analyze:
@@ -19,5 +21,11 @@ jobs:
- name: Install dependencies
run: flutter pub get
- name: Analyze
- 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
+1
View File
@@ -65,6 +65,7 @@ secrets.dart
**/ios/Flutter/Flutter.podspec
# Android
.gradle/
**/android/.gradle/
**/android/captures/
**/android/local.properties
+27 -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,13 @@ 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)
### Dependencies
| Package | Purpose |
|---------|---------|
| flutter_blue_plus | Bluetooth Low Energy communication |
@@ -84,6 +96,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 +104,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 +125,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 +170,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 +205,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)
+1 -1
View File
@@ -83,5 +83,5 @@ flutter {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
+9
View File
@@ -67,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

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>
+5
View File
@@ -55,5 +55,10 @@
<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>
+317 -185
View File
@@ -5,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto;
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
@@ -24,6 +23,7 @@ import '../services/notification_service.dart';
import '../storage/channel_message_store.dart';
import '../storage/channel_order_store.dart';
import '../storage/channel_settings_store.dart';
import '../storage/channel_store.dart';
import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart';
import '../storage/message_store.dart';
@@ -67,9 +67,9 @@ class MeshCoreConnector extends ChangeNotifier {
final Map<int, List<ChannelMessage>> _channelMessages = {};
final Set<String> _loadedConversationKeys = {};
final Map<int, Set<String>> _processedChannelReactions =
{}; // channelIndex -> Set of "reactionKey_emoji"
{}; // channelIndex -> Set of "targetHash_emoji"
final Map<String, Set<String>> _processedContactReactions =
{}; // contactPubKeyHex -> Set of "reactionKey_emoji"
{}; // contactPubKeyHex -> Set of "targetHash_emoji"
StreamSubscription<List<ScanResult>>? _scanSubscription;
StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
@@ -90,11 +90,14 @@ class MeshCoreConnector extends ChangeNotifier {
int? _currentBwHz;
int? _currentSf;
int? _currentCr;
bool? _clientRepeat;
int? _firmwareVerCode;
int? _batteryMillivolts;
double? _selfLatitude;
double? _selfLongitude;
bool _isLoadingContacts = false;
bool _isLoadingChannels = false;
bool _hasLoadedChannels = false;
bool _batteryRequested = false;
bool _awaitingSelfInfo = false;
bool _preserveContactsOnRefresh = false;
@@ -122,7 +125,7 @@ class MeshCoreConnector extends ChangeNotifier {
List<Channel> _previousChannelsCache = [];
static const int _maxChannelSyncRetries = 3;
static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel
static const Duration _batteryPollInterval = Duration(seconds: 30);
static const Duration _batteryPollInterval = Duration(seconds: 120);
// Services
MessageRetryService? _retryService;
@@ -138,14 +141,16 @@ class MeshCoreConnector extends ChangeNotifier {
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
final ContactStore _contactStore = ContactStore();
final ChannelStore _channelStore = ChannelStore();
final UnreadStore _unreadStore = UnreadStore();
List<Channel> _cachedChannels = [];
final Map<int, bool> _channelSmazEnabled = {};
bool _lastSentWasCliCommand =
false; // Track if last sent message was a CLI command
final Map<String, bool> _contactSmazEnabled = {};
final Set<String> _knownContactKeys = {};
final Map<String, int> _contactLastReadMs = {};
final Map<int, int> _channelLastReadMs = {};
final Map<String, int> _contactUnreadCount = {};
bool _unreadStateLoaded = false;
final Map<String, _RepeaterAckContext> _pendingRepeaterAcks = {};
String? _activeContactKey;
int? _activeChannelIndex;
@@ -197,6 +202,8 @@ class MeshCoreConnector extends ChangeNotifier {
int? get currentBwHz => _currentBwHz;
int? get currentSf => _currentSf;
int? get currentCr => _currentCr;
bool? get clientRepeat => _clientRepeat;
int? get firmwareVerCode => _firmwareVerCode;
Map<String, String>? get currentCustomVars => _currentCustomVars;
int? get batteryMillivolts => _batteryMillivolts;
int get maxContacts => _maxContacts;
@@ -317,18 +324,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getUnreadCountForContactKey(String contactKeyHex) {
if (!_unreadStateLoaded) return 0;
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0;
final messages = _conversations[contactKeyHex];
if (messages == null || messages.isEmpty) return 0;
final lastReadMs = _contactLastReadMs[contactKeyHex] ?? 0;
var count = 0;
for (final message in messages) {
if (message.isOutgoing || message.isCli) continue;
if (message.timestamp.millisecondsSinceEpoch > lastReadMs) {
count++;
}
}
return count;
return _contactUnreadCount[contactKeyHex] ?? 0;
}
int getUnreadCountForChannel(Channel channel) {
@@ -336,20 +334,12 @@ class MeshCoreConnector extends ChangeNotifier {
}
int getUnreadCountForChannelIndex(int channelIndex) {
final messages = _channelMessages[channelIndex];
if (messages == null || messages.isEmpty) return 0;
final lastReadMs = _channelLastReadMs[channelIndex] ?? 0;
var count = 0;
for (final message in messages) {
if (message.isOutgoing) continue;
if (message.timestamp.millisecondsSinceEpoch > lastReadMs) {
count++;
}
}
return count;
if (!_unreadStateLoaded) return 0;
return _findChannelByIndex(channelIndex)?.unreadCount ?? 0;
}
int getTotalUnreadCount() {
if (!_unreadStateLoaded) return 0;
var total = 0;
// Count unread contact messages
for (final contact in _contacts) {
@@ -375,15 +365,17 @@ class MeshCoreConnector extends ChangeNotifier {
}
Future<void> loadUnreadState() async {
_contactLastReadMs
_contactUnreadCount
..clear()
..addAll(await _unreadStore.loadContactLastRead());
_channelLastReadMs
..clear()
..addAll(await _unreadStore.loadChannelLastRead());
..addAll(await _unreadStore.loadContactUnreadCount());
_unreadStateLoaded = true;
notifyListeners();
}
Future<void> loadCachedChannels() async {
_cachedChannels = await _channelStore.loadChannels();
}
void setActiveContact(String? contactKeyHex) {
if (contactKeyHex != null &&
!_shouldTrackUnreadForContactKey(contactKeyHex)) {
@@ -405,17 +397,36 @@ class MeshCoreConnector extends ChangeNotifier {
void markContactRead(String contactKeyHex) {
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
final markMs = _calculateReadTimestampMs(
_conversations[contactKeyHex]?.map((m) => m.timestamp),
);
_setContactLastReadMs(contactKeyHex, markMs);
final previousCount = _contactUnreadCount[contactKeyHex] ?? 0;
if (previousCount > 0) {
_contactUnreadCount[contactKeyHex] = 0;
_appDebugLogService?.info(
'Contact $contactKeyHex marked as read (was $previousCount unread)',
tag: 'Unread',
);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
);
notifyListeners();
}
}
void markChannelRead(int channelIndex) {
final markMs = _calculateReadTimestampMs(
_channelMessages[channelIndex]?.map((m) => m.timestamp),
);
_setChannelLastReadMs(channelIndex, markMs);
final channel = _findChannelByIndex(channelIndex);
if (channel != null && channel.unreadCount > 0) {
final previousCount = channel.unreadCount;
channel.unreadCount = 0;
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)',
tag: 'Unread',
);
unawaited(
_channelStore.saveChannels(
_channels.isNotEmpty ? _channels : _cachedChannels,
),
);
notifyListeners();
}
}
Future<void> setChannelSmazEnabled(int channelIndex, bool enabled) async {
@@ -620,11 +631,38 @@ class MeshCoreConnector extends ChangeNotifier {
_scanResults.clear();
_setState(MeshCoreConnectionState.scanning);
// Ensure any previous scan is fully stopped
await FlutterBluePlus.stopScan();
await _scanSubscription?.cancel();
// On iOS/macOS, wait for Bluetooth to be powered on before scanning
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
// Wait for adapter state to be powered on
final adapterState = await FlutterBluePlus.adapterState.first;
if (adapterState != BluetoothAdapterState.on) {
// Wait for the adapter to turn on, with timeout
await FlutterBluePlus.adapterState
.firstWhere((state) => state == BluetoothAdapterState.on)
.timeout(
const Duration(seconds: 5),
onTimeout: () {
_setState(MeshCoreConnectionState.disconnected);
throw Exception('Bluetooth adapter not available');
},
);
}
// Add a small delay to allow BLE stack to fully initialize
await Future.delayed(const Duration(milliseconds: 300));
}
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults.clear();
for (var result in results) {
if (result.device.platformName.startsWith("MeshCore-") ||
result.advertisementData.advName.startsWith("MeshCore-")) {
result.advertisementData.advName.startsWith("MeshCore-") ||
result.advertisementData.advName.startsWith("Whisper-")) {
_scanResults.add(result);
}
}
@@ -675,7 +713,7 @@ class MeshCoreConnector extends ChangeNotifier {
try {
_connectionSubscription = device.connectionState.listen((state) {
if (state == BluetoothConnectionState.disconnected) {
if (state == BluetoothConnectionState.disconnected && isConnected) {
_handleDisconnection();
}
});
@@ -741,9 +779,6 @@ class MeshCoreConnector extends ChangeNotifier {
_setState(MeshCoreConnectionState.connected);
// Enable wake lock to prevent BLE disconnection when screen turns off
await WakelockPlus.enable();
await _requestDeviceInfo();
_startBatteryPolling();
final gotSelfInfo = await _waitForSelfInfo(
@@ -756,6 +791,9 @@ class MeshCoreConnector extends ChangeNotifier {
// Keep device clock aligned on every connection.
await syncTime();
// Fetch channels so we can track unread counts for incoming messages
unawaited(getChannels());
} catch (e) {
debugPrint("Connection error: $e");
await disconnect(manual: false);
@@ -849,9 +887,6 @@ class MeshCoreConnector extends ChangeNotifier {
_setState(MeshCoreConnectionState.disconnecting);
_stopBatteryPolling();
// Disable wake lock when disconnecting
await WakelockPlus.disable();
await _notifySubscription?.cancel();
_notifySubscription = null;
@@ -885,6 +920,8 @@ class MeshCoreConnector extends ChangeNotifier {
_selfName = null;
_selfLatitude = null;
_selfLongitude = null;
_clientRepeat = null;
_firmwareVerCode = null;
_batteryMillivolts = null;
_batteryRequested = false;
_awaitingSelfInfo = false;
@@ -896,6 +933,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingQueueSync = false;
_isSyncingChannels = false;
_channelSyncInFlight = false;
_hasLoadedChannels = false;
_setState(MeshCoreConnectionState.disconnected);
if (!manual) {
@@ -959,6 +997,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
Future<void> _requestDeviceInfo() async {
if (!isConnected || _awaitingSelfInfo) return;
_awaitingSelfInfo = true;
await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame());
@@ -1252,15 +1291,12 @@ class MeshCoreConnector extends ChangeNotifier {
if (reactionInfo != null) {
// Check if we've already processed this reaction
_processedChannelReactions.putIfAbsent(channel.index, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier =
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
if (reactionIdentifier != null &&
_processedChannelReactions[channel.index]!.contains(
reactionIdentifier,
)) {
if (_processedChannelReactions[channel.index]!.contains(
reactionIdentifier,
)) {
// Already processed, don't process again
return;
}
@@ -1274,9 +1310,7 @@ class MeshCoreConnector extends ChangeNotifier {
await _channelMessageStore.saveChannelMessages(channel.index, messages);
// Mark this reaction as processed
if (reactionIdentifier != null) {
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
}
_processedChannelReactions[channel.index]!.add(reactionIdentifier);
notifyListeners();
@@ -1312,8 +1346,10 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistContacts());
_conversations.remove(contact.publicKeyHex);
_loadedConversationKeys.remove(contact.publicKeyHex);
_contactLastReadMs.remove(contact.publicKeyHex);
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
);
_messageStore.clearMessages(contact.publicKeyHex);
notifyListeners();
}
@@ -1469,13 +1505,21 @@ class MeshCoreConnector extends ChangeNotifier {
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
}
Future<void> getChannels({int? maxChannels}) async {
Future<void> getChannels({int? maxChannels, bool force = false}) async {
if (!isConnected) return;
if (_isSyncingChannels) {
debugPrint('[ChannelSync] Already syncing channels, ignoring request');
return;
}
// Skip fetching if already loaded and not forced
if (_hasLoadedChannels && !force) {
debugPrint(
'[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)',
);
return;
}
_isLoadingChannels = true;
_isSyncingChannels = true;
_previousChannelsCache = List<Channel>.from(_channels);
@@ -1580,6 +1624,10 @@ class MeshCoreConnector extends ChangeNotifier {
_cleanupChannelSync(completed: true);
// Cache channels for offline use
_cachedChannels = List<Channel>.from(_channels);
unawaited(_channelStore.saveChannels(_channels));
// Apply ordering and notify UI
_applyChannelOrder();
notifyListeners();
@@ -1595,6 +1643,7 @@ class MeshCoreConnector extends ChangeNotifier {
_totalChannelsToRequest = 0;
if (completed) {
_hasLoadedChannels = true;
_previousChannelsCache.clear();
}
// Keep cache on failure/disconnection for future attempts
@@ -1605,7 +1654,7 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildSetChannelFrame(index, name, psk));
// Refresh channels after setting
await getChannels();
await getChannels(force: true);
}
Future<void> deleteChannel(int index) async {
@@ -1613,14 +1662,12 @@ class MeshCoreConnector extends ChangeNotifier {
// Delete by setting empty name and zero PSK
await sendFrame(buildSetChannelFrame(index, '', Uint8List(16)));
_channelLastReadMs.remove(index);
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
// Clear stored messages for this channel
await _channelMessageStore.clearChannelMessages(index);
// Clear in-memory messages for this channel
_channelMessages.remove(index);
// Refresh channels after deleting
await getChannels();
await getChannels(force: true);
}
void _handleFrame(List<int> data) {
@@ -1779,6 +1826,13 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleDeviceInfo(Uint8List frame) {
if (frame.length < 4) return;
_firmwareVerCode = frame[1];
// Parse client_repeat from firmware v9+ (byte 80)
if (frame.length >= 81) {
_clientRepeat = frame[80] != 0;
}
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
final reportedContacts = frame[2];
final reportedChannels = frame[3];
@@ -1799,8 +1853,8 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(getChannels(maxChannels: nextMaxChannels));
}
}
notifyListeners();
}
notifyListeners();
}
void _handleNoMoreMessages() {
@@ -1892,9 +1946,9 @@ class MeshCoreConnector extends ChangeNotifier {
final contact = Contact.fromFrame(frame);
if (contact != null) {
if (contact.type == advTypeRepeater) {
_contactLastReadMs.remove(contact.publicKeyHex);
_unreadStore.saveContactLastRead(
Map<String, int>.from(_contactLastReadMs),
_contactUnreadCount.remove(contact.publicKeyHex);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
);
}
// Check if this is a new contact
@@ -2081,6 +2135,15 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (message != null) {
// Ignore messages from self (device hearing its own broadcast)
// BUT allow repeated messages (pathLength indicates it went through repeater)
if (_selfPublicKey != null &&
message.senderKeyHex == pubKeyToHex(_selfPublicKey!) &&
(message.pathLength == null || message.pathLength == 0)) {
debugPrint('Ignoring direct message from self');
return;
}
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c?.publicKeyHex == message!.senderKeyHex,
orElse: () => null,
@@ -2110,7 +2173,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
_addMessage(message.senderKeyHex, message);
_maybeMarkActiveContactRead(message);
_maybeIncrementContactUnread(message);
notifyListeners();
// Show notification for new incoming message
@@ -2301,7 +2364,7 @@ class MeshCoreConnector extends ChangeNotifier {
pathBytes: message.pathBytes,
);
final isNew = _addChannelMessage(message.channelIndex!, message);
_maybeMarkActiveChannelRead(message);
_maybeIncrementChannelUnread(message, isNew: isNew);
notifyListeners();
if (isNew) {
_maybeNotifyChannelMessage(message);
@@ -2323,7 +2386,9 @@ class MeshCoreConnector extends ChangeNotifier {
final channelHash = payload[0];
final encrypted = Uint8List.fromList(payload.sublist(1));
for (final channel in _channels) {
// Use cached channels as fallback if live channels not yet loaded
final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels;
for (final channel in channelsToSearch) {
if (channel.isEmpty) continue;
final hash = _computeChannelHash(channel.psk);
if (hash != channelHash) continue;
@@ -2362,7 +2427,7 @@ class MeshCoreConnector extends ChangeNotifier {
pathBytes: message.pathBytes,
);
final isNew = _addChannelMessage(channel.index, message);
_maybeMarkActiveChannelRead(message);
_maybeIncrementChannelUnread(message, isNew: isNew);
notifyListeners();
if (isNew) {
final label = channel.name.isEmpty
@@ -2492,6 +2557,15 @@ class MeshCoreConnector extends ChangeNotifier {
'[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}',
);
// Preserve unread count from cached channel
final cachedChannel = _cachedChannels.cast<Channel?>().firstWhere(
(c) => c?.index == channel.index,
orElse: () => null,
);
if (cachedChannel != null) {
channel.unreadCount = cachedChannel.unreadCount;
}
// If we're syncing and this is the channel we're waiting for
if (_isSyncingChannels && _channelSyncInFlight) {
if (channel.index == _nextChannelIndexToRequest) {
@@ -2531,6 +2605,8 @@ class MeshCoreConnector extends ChangeNotifier {
(c) => c.index == channel.index,
);
if (existingIndex >= 0) {
// Preserve unread count from existing channel
channel.unreadCount = _channels[existingIndex].unreadCount;
_channels[existingIndex] = channel;
} else {
_channels.add(channel);
@@ -2581,67 +2657,98 @@ class MeshCoreConnector extends ChangeNotifier {
return contact.type != advTypeRepeater;
}
int _calculateReadTimestampMs(Iterable<DateTime>? timestamps) {
var latestMs = 0;
if (timestamps != null) {
for (final timestamp in timestamps) {
final ms = timestamp.millisecondsSinceEpoch;
if (ms > latestMs) {
latestMs = ms;
}
}
}
return latestMs;
Channel? _findChannelByIndex(int index) {
return _channels.cast<Channel?>().firstWhere(
(c) => c?.index == index,
orElse: () => null,
) ??
_cachedChannels.cast<Channel?>().firstWhere(
(c) => c?.index == index,
orElse: () => null,
);
}
void _setContactLastReadMs(
String contactKeyHex,
int timestampMs, {
bool notify = true,
void _maybeIncrementChannelUnread(
ChannelMessage message, {
required bool isNew,
}) {
if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return;
final existing = _contactLastReadMs[contactKeyHex] ?? 0;
if (timestampMs <= existing) return;
_contactLastReadMs[contactKeyHex] = timestampMs;
_unreadStore.saveContactLastRead(Map<String, int>.from(_contactLastReadMs));
if (notify) {
notifyListeners();
if (!isNew || message.isOutgoing) {
_appDebugLogService?.info(
'Skip unread increment: isNew=$isNew, isOutgoing=${message.isOutgoing}',
tag: 'Unread',
);
return;
}
}
void _setChannelLastReadMs(
int channelIndex,
int timestampMs, {
bool notify = true,
}) {
final existing = _channelLastReadMs[channelIndex] ?? 0;
if (timestampMs <= existing) return;
_channelLastReadMs[channelIndex] = timestampMs;
_unreadStore.saveChannelLastRead(Map<int, int>.from(_channelLastReadMs));
if (notify) {
notifyListeners();
}
}
void _maybeMarkActiveContactRead(Message message) {
if (message.isOutgoing || message.isCli) return;
if (_activeContactKey != message.senderKeyHex) return;
if (!_shouldTrackUnreadForContactKey(message.senderKeyHex)) return;
_setContactLastReadMs(
message.senderKeyHex,
message.timestamp.millisecondsSinceEpoch,
notify: false,
);
}
void _maybeMarkActiveChannelRead(ChannelMessage message) {
if (message.isOutgoing) return;
final channelIndex = message.channelIndex;
if (channelIndex == null || _activeChannelIndex != channelIndex) return;
_setChannelLastReadMs(
channelIndex,
message.timestamp.millisecondsSinceEpoch,
notify: false,
if (channelIndex == null) {
_appDebugLogService?.info(
'Skip unread increment: channelIndex is null',
tag: 'Unread',
);
return;
}
// Don't increment if user is viewing this channel
if (_activeChannelIndex == channelIndex) {
_appDebugLogService?.info(
'Skip unread increment: channel $channelIndex is active',
tag: 'Unread',
);
return;
}
final channel = _findChannelByIndex(channelIndex);
if (channel != null) {
channel.unreadCount++;
_appDebugLogService?.info(
'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}',
tag: 'Unread',
);
unawaited(
_channelStore.saveChannels(
_channels.isNotEmpty ? _channels : _cachedChannels,
),
);
} else {
_appDebugLogService?.info(
'Channel $channelIndex not found in _channels (${_channels.length}) or _cachedChannels (${_cachedChannels.length})',
tag: 'Unread',
);
}
}
void _maybeIncrementContactUnread(Message message) {
if (message.isOutgoing || message.isCli) {
_appDebugLogService?.info(
'Skip contact unread increment: isOutgoing=${message.isOutgoing}, isCli=${message.isCli}',
tag: 'Unread',
);
return;
}
final contactKey = message.senderKeyHex;
if (!_shouldTrackUnreadForContactKey(contactKey)) {
_appDebugLogService?.info(
'Skip contact unread increment: should not track for $contactKey',
tag: 'Unread',
);
return;
}
// Don't increment if user is viewing this contact
if (_activeContactKey == contactKey) {
_appDebugLogService?.info(
'Skip contact unread increment: contact $contactKey is active',
tag: 'Unread',
);
return;
}
final currentCount = _contactUnreadCount[contactKey] ?? 0;
_contactUnreadCount[contactKey] = currentCount + 1;
_appDebugLogService?.info(
'Contact $contactKey unread count incremented to ${currentCount + 1}',
tag: 'Unread',
);
_unreadStore.saveContactUnreadCount(
Map<String, int>.from(_contactUnreadCount),
);
}
@@ -2652,26 +2759,22 @@ class MeshCoreConnector extends ChangeNotifier {
// Parse reaction info
final reactionInfo = Message.parseReaction(message.text);
if (reactionInfo != null) {
// Check if we've already processed this exact reaction using lightweight key
// Check if we've already processed this exact reaction
_processedContactReactions.putIfAbsent(pubKeyHex, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier =
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
final isDuplicate =
reactionIdentifier != null &&
_processedContactReactions[pubKeyHex]!.contains(reactionIdentifier);
final isDuplicate = _processedContactReactions[pubKeyHex]!.contains(
reactionIdentifier,
);
if (!isDuplicate) {
// New reaction - process it
_processContactReaction(messages, reactionInfo);
_processContactReaction(messages, reactionInfo, pubKeyHex);
_messageStore.saveMessages(pubKeyHex, messages);
// Mark as processed
if (reactionIdentifier != null) {
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
}
_processedContactReactions[pubKeyHex]!.add(reactionIdentifier);
notifyListeners();
}
@@ -2686,15 +2789,53 @@ class MeshCoreConnector extends ChangeNotifier {
void _processContactReaction(
List<Message> messages,
ReactionInfo reactionInfo,
String contactPubKeyHex,
) {
// Find target message by messageId
for (int i = 0; i < messages.length; i++) {
if (messages[i].messageId == reactionInfo.targetMessageId) {
final currentReactions = Map<String, int>.from(messages[i].reactions);
// Find target message by computing hash and comparing
final targetHash = reactionInfo.targetHash;
final contact = _contacts.cast<Contact?>().firstWhere(
(c) => c?.publicKeyHex == contactPubKeyHex,
orElse: () => null,
);
final isRoomServer = contact?.type == advTypeRoom;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
// For 1:1 chats: contact reacts to my outgoing messages only
// For room servers: any message can be reacted to (multi-user)
if (!isRoomServer && !msg.isOutgoing) continue;
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
// For room servers, include sender name (resolve from fourByteRoomContactKey)
// For 1:1 chats, sender is implicit (null)
String? senderName;
if (isRoomServer && !msg.isOutgoing) {
// Resolve sender from the message's fourByteRoomContactKey
final senderContact = _contacts.cast<Contact?>().firstWhere(
(c) =>
c != null &&
_matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
orElse: () => null,
);
senderName = senderContact?.name;
} else if (isRoomServer && msg.isOutgoing) {
senderName = selfName;
}
// For 1:1, senderName stays null
final msgHash = ReactionHelper.computeReactionHash(
timestampSecs,
senderName,
msg.text,
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(msg.reactions);
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
messages[i] = messages[i].copyWith(reactions: currentReactions);
messages[i] = msg.copyWith(reactions: currentReactions);
break;
}
}
@@ -2845,18 +2986,14 @@ class MeshCoreConnector extends ChangeNotifier {
// Parse reaction info
final reactionInfo = ChannelMessage.parseReaction(message.text);
if (reactionInfo != null) {
// Check if we've already processed this exact reaction using lightweight key
// Check if we've already processed this exact reaction
_processedChannelReactions.putIfAbsent(channelIndex, () => {});
final reactionKey = reactionInfo.reactionKey;
final reactionIdentifier = reactionKey != null
? '${reactionKey}_${reactionInfo.emoji}'
: null;
final reactionIdentifier =
'${reactionInfo.targetHash}_${reactionInfo.emoji}';
final isDuplicate =
reactionIdentifier != null &&
_processedChannelReactions[channelIndex]!.contains(
reactionIdentifier,
);
final isDuplicate = _processedChannelReactions[channelIndex]!.contains(
reactionIdentifier,
);
if (!isDuplicate) {
// New reaction - process it
@@ -2865,9 +3002,7 @@ class MeshCoreConnector extends ChangeNotifier {
_channelMessageStore.saveChannelMessages(channelIndex, messages);
// Mark as processed
if (reactionIdentifier != null) {
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
}
_processedChannelReactions[channelIndex]!.add(reactionIdentifier);
}
return false; // Don't add reaction as a visible message
}
@@ -2963,14 +3098,22 @@ class MeshCoreConnector extends ChangeNotifier {
List<ChannelMessage> messages,
ReactionInfo reactionInfo,
) {
// Find target message by messageId
for (int i = 0; i < messages.length; i++) {
if (messages[i].messageId == reactionInfo.targetMessageId) {
final currentReactions = Map<String, int>.from(messages[i].reactions);
// Find target message by computing hash and comparing
final targetHash = reactionInfo.targetHash;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
final msgHash = ReactionHelper.computeReactionHash(
timestampSecs,
msg.senderName,
msg.text,
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(msg.reactions);
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
messages[i] = messages[i].copyWith(reactions: currentReactions);
messages[i] = msg.copyWith(reactions: currentReactions);
notifyListeners();
break;
}
@@ -3012,28 +3155,19 @@ class MeshCoreConnector extends ChangeNotifier {
}
bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) {
final selfKey = _selfPublicKey;
if (selfKey == null) return false;
if (pathBytes.length < pathHashSize) return false;
final trimmed = senderName.trim();
if (trimmed.isEmpty) return false;
final selfName = _selfName?.trim();
if (selfName == null || selfName.isEmpty) return false;
// If sender name doesn't match, keep the message
if (trimmed != selfName) return false;
final prefix = selfKey.sublist(0, pathHashSize);
for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
var match = true;
for (int j = 0; j < pathHashSize; j++) {
if (pathBytes[i + j] != prefix[j]) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
// Name matches - this is from self
// Drop only if pathBytes is empty (direct broadcast)
// Keep if pathBytes has data (repeated through another node)
return pathBytes.isEmpty;
}
Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) {
@@ -3087,8 +3221,6 @@ class MeshCoreConnector extends ChangeNotifier {
}
void _handleDisconnection() {
// Disable wake lock when connection is lost
WakelockPlus.disable();
_stopBatteryPolling();
for (final entry in _pendingRepeaterAcks.values) {
+84 -4
View File
@@ -18,6 +18,10 @@ class BufferReader {
return data;
}
void skipBytes(int count) {
_pointer += count;
}
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() =>
@@ -98,6 +102,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)
@@ -127,6 +150,7 @@ const int cmdSendStatusReq = 27;
const int cmdGetContactByKey = 30;
const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
@@ -159,6 +183,7 @@ 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;
@@ -176,6 +201,7 @@ 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;
@@ -195,8 +221,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;
@@ -522,18 +550,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();
}
@@ -708,3 +747,44 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? 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();
}
+11 -9
View File
@@ -26,9 +26,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();
@@ -201,10 +203,10 @@ class CayenneLpp {
break;
}
final channelData = channels.putIfAbsent(channel, () => {
'channel': channel,
'values': <String, dynamic>{},
});
final channelData = channels.putIfAbsent(
channel,
() => {'channel': channel, 'values': <String, dynamic>{}},
);
switch (type) {
case lppGenericSensor:
@@ -254,8 +256,8 @@ class CayenneLpp {
}
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
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;
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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за да се появят на картата.",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Остави общността \"{name}\"",
@@ -1484,5 +1493,108 @@
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"community_forCommunity": "За {name}"
"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": "Руски",
"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, за да сканирате за устройства.",
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
"settings_clientRepeat": "Без електричество – повторение"
}
+178 -38
View File
@@ -74,7 +74,7 @@
"settings_title": "Einstellungen",
"settings_deviceInfo": "Geräteinformationen",
"settings_appSettings": "App-Einstellungen",
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmungen",
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung",
"settings_nodeSettings": "Knoten-Einstellungen",
"settings_nodeName": "Knotenname",
"settings_nodeNameNotSet": "Nicht festgelegt",
@@ -96,14 +96,14 @@
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
"settings_actions": "Aktionen",
"settings_sendAdvertisement": "Sende eine Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
"settings_sendAdvertisement": "Sende Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
"settings_advertisementSent": "Ankündigung gesendet",
"settings_syncTime": "Zeitsynchronisierung",
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
"settings_timeSynchronized": "Zeit synchronisiert",
"settings_refreshContacts": "Kontakte aktualisieren",
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
"settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden",
"settings_rebootDevice": "Gerät neu starten",
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
@@ -131,9 +131,6 @@
"settings_infoContactsCount": "Anzahl Kontakte",
"settings_infoChannelCount": "Anzahl Kanäle",
"settings_presets": "Voreinstellungen",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequenz (MHz)",
"settings_frequencyHelper": "300,00 - 2.500,00",
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
@@ -143,8 +140,6 @@
"settings_txPower": "TX-Leistung (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
"settings_longRange": "Grosse Reichweite",
"settings_fastSpeed": "Schnelle Geschwindigkeit",
"settings_error": "Fehler: {message}",
"@settings_error": {
"placeholders": {
@@ -266,7 +261,7 @@
}
}
},
"contacts_manageRepeater": "Wiederholungen verwalten",
"contacts_manageRepeater": "Repeater verwalten",
"contacts_roomLogin": "Raum-Login",
"contacts_openChat": "Öffne Chat",
"contacts_editGroup": "Gruppe bearbeiten",
@@ -360,7 +355,7 @@
"channels_channelIndexLabel": "Kanalindex",
"channels_channelName": "Kanalname",
"channels_usePublicChannel": "Verwende öffentlichen Kanal",
"channels_standardPublicPsk": "Standard-Öffentliche PSK",
"channels_standardPublicPsk": "Öffentliche Standard PSK",
"channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Zufällige PSK generieren",
"channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.",
@@ -489,8 +484,8 @@
}
}
},
"debugFrame_textMessageHeader": "Textnachricht-Frame:",
"debugFrame_destinationPubKey": "- Ziel-Pub-Schlüssel: {pubKey}",
"debugFrame_textMessageHeader": "Textnachrichten Frame:",
"debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}",
"@debugFrame_destinationPubKey": {
"placeholders": {
"pubKey": {
@@ -540,7 +535,7 @@
"chat_routingMode": "Routenmodus",
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"chat_forceFloodMode": "Flut-Modus erzwingen",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):",
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
"chat_hopSingular": "Sprung",
"chat_hopPlural": "Sprünge",
@@ -554,7 +549,7 @@
},
"chat_successes": "Erfolgreich",
"chat_removePath": "Pfad entfernen",
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_pathActions": "Pfadaktionen:",
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
@@ -604,6 +599,18 @@
}
}
},
"chat_openLink": "Link öffnen?",
"chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?",
"chat_open": "Öffnen",
"chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"chat_invalidLink": "Ungültiges Link-Format",
"map_title": "Karte",
"map_noNodesWithLocation": "Keine Knoten mit Standortdaten",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
@@ -705,7 +712,7 @@
"mapCache_cacheArea": "Zwischenspeicherbereich",
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
"mapCache_zoomRange": "Zoom Bereich",
"mapCache_estimatedTiles": "Geschätzte Fliesen: {count}",
"mapCache_estimatedTiles": "Geschätzte Kacheln: {count}",
"@mapCache_estimatedTiles": {
"placeholders": {
"count": {
@@ -842,7 +849,7 @@
},
"path_enterCustomPath": "Gebe Pfad ein",
"path_currentPathLabel": "Aktueller Pfad",
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.",
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
@@ -875,7 +882,7 @@
"repeater_forceFloodMode": "Flut-Modus erzwingen",
"repeater_pathManagement": "Pfadverwaltung",
"repeater_refresh": "Aktualisieren",
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
"repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.",
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
"@repeater_errorLoadingStatus": {
"placeholders": {
@@ -945,7 +952,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}",
"repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -971,7 +978,7 @@
"repeater_adminPassword": "Admin-Passwort",
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
"repeater_guestPassword": "Gast-Passwort",
"repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort",
"repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort",
"repeater_radioSettings": "Funk Einstellungen",
"repeater_frequencyMhz": "Frequenz (MHz)",
"repeater_frequencyHelper": "300-2500 MHz",
@@ -1014,7 +1021,7 @@
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
"repeater_dangerZone": "Gefahrenzone",
"repeater_rebootRepeater": "Neustart Repeater",
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
"repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.",
"repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?",
"repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung",
"repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren",
@@ -1074,11 +1081,11 @@
}
},
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
"repeater_debugNextCommand": "Fehlersuche des nächsten Befehls",
"repeater_commandHelp": "Hilfe",
"repeater_clearHistory": "Löschen der Historie",
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
"repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle",
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
"repeater_previousCommand": "Vorhergehende Aktion",
"repeater_nextCommand": "Nächste Aktion",
@@ -1120,7 +1127,7 @@
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
@@ -1131,14 +1138,14 @@
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.",
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.",
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingeckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.",
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
@@ -1231,7 +1238,7 @@
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
"channelPath_repeaterHops": "Repeater-Sprünge",
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
"channelPath_messageDetails": "Nachrichtsdetails",
"channelPath_messageDetails": "Nachrichtendetails",
"channelPath_senderLabel": "Sender",
"channelPath_timeLabel": "Zeit",
"channelPath_repeatsLabel": "Wiederholungen",
@@ -1335,7 +1342,7 @@
"listFilter_users": "Benutzer",
"listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_unreadOnly": "Nicht gelesen",
"listFilter_newGroup": "Neue Gruppe",
"@neighbors_errorLoading": {
"placeholders": {
@@ -1346,11 +1353,11 @@
},
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"neighbors_receivedData": "Empfangene Nachbarsdaten",
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"neighbors_repeatersNeighbours": "Nachbarn",
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
@@ -1377,8 +1384,8 @@
}
}
},
"neighbors_heardAgo": "Hörte: {time} vor her.",
"neighbors_unknownContact": "Unbekannte {pubkey}",
"neighbors_heardAgo": "Gehört vor: {time}",
"neighbors_unknownContact": "Unbekannt {pubkey}",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
@@ -1473,16 +1480,149 @@
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlassen",
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
"community_selectCommunity": "Wählen Sie Community",
"community_selectCommunity": "Wählen Sie eine Community",
"community_regularHashtag": "Regulärer Hashtag",
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
"community_communityHashtag": "Community Hashtag"
"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": "Neu generieren",
"community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich",
"community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
"community_regenerateSecret": "Neugenerierung des Schlüssels",
"community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert",
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
"community_updateSecret": "Aktualisieren Sie den Schlüssel",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_refreshTooltip": "Path Trace aktualisieren.",
"pathTrace_you": "Du",
"pathTrace_failed": "Pfadverfolgung fehlgeschlagen.",
"pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.",
"contacts_pathTrace": "Pfadverfolgung",
"contacts_ping": "Pingen",
"contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater",
"contacts_repeaterPing": "Repeater pingen",
"contacts_roomPathTrace": "Pfadverfolgung zum Raumserver",
"contacts_roomPing": "Raumserver anpingen",
"contacts_pathTraceTo": "Route nach {name} verfolgen",
"contacts_chatTraceRoute": "Pfadverfolgungsroute",
"appSettings_languageRu": "Russisch",
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
"appSettings_languageUk": "Ukrainisch",
"contacts_contactImported": "Kontakt wurde importiert.",
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
"contacts_floodAdvert": "Flut-Ankündigung",
"contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen",
"contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen",
"contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren",
"contacts_ShareContact": "Kontakt in die Zwischenablage kopieren",
"contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.",
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
"notification_activityTitle": "MeshCore Aktivität",
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
"@notification_messagesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
"@notification_channelMessagesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
"@notification_newNodesCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
"@notification_newTypeDiscovered": {
"placeholders": {
"contactType": {
"type": "String"
}
}
},
"notification_receivedNewMessage": "Neue Nachricht empfangen",
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
"settings_gpxExportContacts": "Kontakte als GPX exportieren",
"settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.",
"settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.",
"settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte",
"settings_gpxExportChat": "Kontaktstandorte",
"settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.",
"settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.",
"settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt",
"settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.",
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
"map_removeLast": "Letztes Entfernen",
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
"map_runTrace": "Pfadverlauf ausführen",
"pathTrace_clearTooltip": "Pfad löschen",
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
"scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.",
"scanner_bluetoothOff": "Bluetooth ist deaktiviert.",
"scanner_enableBluetooth": "Bluetooth aktivieren",
"settings_clientRepeat": "Wiederholung, ohne Stromanschluss",
"settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.",
"settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen."
}
+482 -156
View File
File diff suppressed because it is too large Load Diff
+147 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
@@ -1484,5 +1493,136 @@
"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_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",
"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",
"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."
}
+177 -65
View File
@@ -104,7 +104,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 +131,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 +140,6 @@
"settings_txPower": "TX Puissance (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
"settings_longRange": "Portée Longue",
"settings_fastSpeed": "Vitesse Rapide",
"settings_error": "Erreur : {message}",
"@settings_error": {
"placeholders": {
@@ -210,8 +205,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,7 +261,7 @@
}
}
},
"contacts_manageRepeater": "Gérer le répétiteur",
"contacts_manageRepeater": "Gérer le répéteur",
"contacts_roomLogin": "Connexion Salle",
"contacts_openChat": "Ouverture du Chat",
"contacts_editGroup": "Modifier le groupe",
@@ -279,7 +274,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 +288,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 +297,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 +306,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": {
@@ -394,7 +389,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 +431,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": {
@@ -542,9 +537,9 @@
"chat_forceFloodMode": "Mode tout le réseau forcé",
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
"chat_hopSingular": "Sautez",
"chat_hopPlural": "sautez",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"chat_hopSingular": "saut",
"chat_hopPlural": "sauts",
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -604,6 +599,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 +631,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 +672,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 +694,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 +741,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 +758,7 @@
}
}
},
"time_justNow": "Il y a tout juste maintenant",
"time_justNow": "Maintenant",
"time_minutesAgo": "{minutes} minutes auparavant",
"@time_minutesAgo": {
"placeholders": {
@@ -788,13 +795,13 @@
"time_allTime": "Tout le temps",
"dialog_disconnect": "Déconnecter",
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
"login_repeaterLogin": "Connexion au répétiteur",
"login_repeaterLogin": "Connexion au répéteur",
"login_roomLogin": "Connexion Salle",
"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",
@@ -859,17 +866,17 @@
},
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
"path_setPath": "Définir le chemin",
"repeater_management": "Gestion des répétiteurs",
"repeater_management": "Gestion des répéteurs",
"repeater_managementTools": "Outils de Gestion",
"repeater_status": "État",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
"repeater_telemetry": "Télémetrie",
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
"repeater_settings": "Paramètres",
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
"repeater_statusTitle": "État du répétiteur",
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
"repeater_statusTitle": "État du répéteur",
"repeater_routingMode": "Mode de routage",
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"repeater_forceFloodMode": "Mode tout le réseau forcé",
@@ -899,7 +906,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": {
@@ -964,10 +971,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é",
@@ -987,7 +994,7 @@
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
"repeater_features": "Fonctionnalités",
"repeater_packetForwarding": "Transfert de paquets",
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
"repeater_privacyMode": "Mode de confidentialité",
@@ -1012,16 +1019,16 @@
}
},
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone d'alerte",
"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": {
@@ -1073,7 +1080,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",
@@ -1107,13 +1114,13 @@
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
"repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
"repeater_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 l'annonce.",
@@ -1135,7 +1142,7 @@
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
@@ -1159,8 +1166,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.",
@@ -1229,7 +1236,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",
@@ -1294,7 +1301,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": {
@@ -1327,16 +1334,16 @@
"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": {
@@ -1362,7 +1369,7 @@
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
@@ -1379,11 +1386,11 @@
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour 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",
"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": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
@@ -1473,16 +1480,121 @@
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette 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_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",
"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",
"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"
}
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
@@ -1484,5 +1493,108 @@
"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_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",
"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",
"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."
}
+400 -24
View File
@@ -14,9 +14,11 @@ import 'app_localizations_it.dart';
import 'app_localizations_nl.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_sk.dart';
import 'app_localizations_sl.dart';
import 'app_localizations_sv.dart';
import 'app_localizations_uk.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
@@ -114,9 +116,11 @@ abstract class AppLocalizations {
Locale('nl'),
Locale('pl'),
Locale('pt'),
Locale('ru'),
Locale('sk'),
Locale('sl'),
Locale('sv'),
Locale('uk'),
Locale('zh'),
];
@@ -372,6 +376,24 @@ abstract class AppLocalizations {
/// **'Scan'**
String get scanner_scan;
/// No description provided for @scanner_bluetoothOff.
///
/// In en, this message translates to:
/// **'Bluetooth is off'**
String get scanner_bluetoothOff;
/// No description provided for @scanner_bluetoothOffMessage.
///
/// In en, this message translates to:
/// **'Please turn on Bluetooth to scan for devices'**
String get scanner_bluetoothOffMessage;
/// No description provided for @scanner_enableBluetooth.
///
/// In en, this message translates to:
/// **'Enable Bluetooth'**
String get scanner_enableBluetooth;
/// No description provided for @device_quickSwitch.
///
/// In en, this message translates to:
@@ -726,24 +748,6 @@ abstract class AppLocalizations {
/// **'Presets'**
String get settings_presets;
/// No description provided for @settings_preset915Mhz.
///
/// In en, this message translates to:
/// **'915 MHz'**
String get settings_preset915Mhz;
/// No description provided for @settings_preset868Mhz.
///
/// In en, this message translates to:
/// **'868 MHz'**
String get settings_preset868Mhz;
/// No description provided for @settings_preset433Mhz.
///
/// In en, this message translates to:
/// **'433 MHz'**
String get settings_preset433Mhz;
/// No description provided for @settings_frequency.
///
/// In en, this message translates to:
@@ -798,17 +802,23 @@ abstract class AppLocalizations {
/// **'Invalid TX power (0-22 dBm)'**
String get settings_txPowerInvalid;
/// No description provided for @settings_longRange.
/// No description provided for @settings_clientRepeat.
///
/// In en, this message translates to:
/// **'Long Range'**
String get settings_longRange;
/// **'Off-Grid Repeat'**
String get settings_clientRepeat;
/// No description provided for @settings_fastSpeed.
/// No description provided for @settings_clientRepeatSubtitle.
///
/// In en, this message translates to:
/// **'Fast Speed'**
String get settings_fastSpeed;
/// **'Allow this device to repeat mesh packets for others'**
String get settings_clientRepeatSubtitle;
/// No description provided for @settings_clientRepeatFreqWarning.
///
/// In en, this message translates to:
/// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'**
String get settings_clientRepeatFreqWarning;
/// No description provided for @settings_error.
///
@@ -942,6 +952,18 @@ abstract class AppLocalizations {
/// **'Български'**
String get appSettings_languageBg;
/// No description provided for @appSettings_languageRu.
///
/// In en, this message translates to:
/// **'Русский'**
String get appSettings_languageRu;
/// No description provided for @appSettings_languageUk.
///
/// In en, this message translates to:
/// **'Українська'**
String get appSettings_languageUk;
/// No description provided for @appSettings_notifications.
///
/// In en, this message translates to:
@@ -2226,6 +2248,36 @@ abstract class AppLocalizations {
/// **'Unread: {count}'**
String chat_unread(int count);
/// No description provided for @chat_openLink.
///
/// In en, this message translates to:
/// **'Open Link?'**
String get chat_openLink;
/// No description provided for @chat_openLinkConfirmation.
///
/// In en, this message translates to:
/// **'Do you want to open this link in your browser?'**
String get chat_openLinkConfirmation;
/// No description provided for @chat_open.
///
/// In en, this message translates to:
/// **'Open'**
String get chat_open;
/// No description provided for @chat_couldNotOpenLink.
///
/// In en, this message translates to:
/// **'Could not open link: {url}'**
String chat_couldNotOpenLink(String url);
/// No description provided for @chat_invalidLink.
///
/// In en, this message translates to:
/// **'Invalid link format'**
String get chat_invalidLink;
/// No description provided for @map_title.
///
/// In en, this message translates to:
@@ -2472,6 +2524,30 @@ abstract class AppLocalizations {
/// **'Manage Repeater'**
String get map_manageRepeater;
/// No description provided for @map_tapToAdd.
///
/// In en, this message translates to:
/// **'Tap on nodes to add them to the path.'**
String get map_tapToAdd;
/// No description provided for @map_runTrace.
///
/// In en, this message translates to:
/// **'Run Path Trace'**
String get map_runTrace;
/// No description provided for @map_removeLast.
///
/// In en, this message translates to:
/// **'Remove Last'**
String get map_removeLast;
/// No description provided for @map_pathTraceCancelled.
///
/// In en, this message translates to:
/// **'Path trace cancelled.'**
String get map_pathTraceCancelled;
/// No description provided for @mapCache_title.
///
/// In en, this message translates to:
@@ -4653,6 +4729,300 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'New group'**
String get listFilter_newGroup;
/// No description provided for @pathTrace_you.
///
/// In en, this message translates to:
/// **'You'**
String get pathTrace_you;
/// No description provided for @pathTrace_failed.
///
/// In en, this message translates to:
/// **'Path trace failed.'**
String get pathTrace_failed;
/// No description provided for @pathTrace_notAvailable.
///
/// In en, this message translates to:
/// **'Path trace not available.'**
String get pathTrace_notAvailable;
/// No description provided for @pathTrace_refreshTooltip.
///
/// In en, this message translates to:
/// **'Refresh Path Trace.'**
String get pathTrace_refreshTooltip;
/// No description provided for @pathTrace_someHopsNoLocation.
///
/// In en, this message translates to:
/// **'One or more of the hops is missing a location!'**
String get pathTrace_someHopsNoLocation;
/// No description provided for @pathTrace_clearTooltip.
///
/// In en, this message translates to:
/// **'Clear path.'**
String get pathTrace_clearTooltip;
/// No description provided for @contacts_pathTrace.
///
/// In en, this message translates to:
/// **'Path Trace'**
String get contacts_pathTrace;
/// No description provided for @contacts_ping.
///
/// In en, this message translates to:
/// **'Ping'**
String get contacts_ping;
/// No description provided for @contacts_repeaterPathTrace.
///
/// In en, this message translates to:
/// **'Path trace to repeater'**
String get contacts_repeaterPathTrace;
/// No description provided for @contacts_repeaterPing.
///
/// In en, this message translates to:
/// **'Ping repeater'**
String get contacts_repeaterPing;
/// No description provided for @contacts_roomPathTrace.
///
/// In en, this message translates to:
/// **'Path trace to room server'**
String get contacts_roomPathTrace;
/// No description provided for @contacts_roomPing.
///
/// In en, this message translates to:
/// **'Ping room server'**
String get contacts_roomPing;
/// No description provided for @contacts_chatTraceRoute.
///
/// In en, this message translates to:
/// **'Path trace route'**
String get contacts_chatTraceRoute;
/// No description provided for @contacts_pathTraceTo.
///
/// In en, this message translates to:
/// **'Trace route to {name}'**
String contacts_pathTraceTo(String name);
/// No description provided for @contacts_clipboardEmpty.
///
/// In en, this message translates to:
/// **'Clipboard is empty.'**
String get contacts_clipboardEmpty;
/// No description provided for @contacts_invalidAdvertFormat.
///
/// In en, this message translates to:
/// **'Invalid contact data'**
String get contacts_invalidAdvertFormat;
/// No description provided for @contacts_contactImported.
///
/// In en, this message translates to:
/// **'Contact has been imported.'**
String get contacts_contactImported;
/// No description provided for @contacts_contactImportFailed.
///
/// In en, this message translates to:
/// **'Failed to import contact.'**
String get contacts_contactImportFailed;
/// No description provided for @contacts_zeroHopAdvert.
///
/// In en, this message translates to:
/// **'Zero Hop Advert'**
String get contacts_zeroHopAdvert;
/// No description provided for @contacts_floodAdvert.
///
/// In en, this message translates to:
/// **'Flood Advert'**
String get contacts_floodAdvert;
/// No description provided for @contacts_copyAdvertToClipboard.
///
/// In en, this message translates to:
/// **'Copy Advert to Clipboard'**
String get contacts_copyAdvertToClipboard;
/// No description provided for @contacts_addContactFromClipboard.
///
/// In en, this message translates to:
/// **'Add Contact from Clipboard'**
String get contacts_addContactFromClipboard;
/// No description provided for @contacts_ShareContact.
///
/// In en, this message translates to:
/// **'Copy contact to Clipboard'**
String get contacts_ShareContact;
/// No description provided for @contacts_ShareContactZeroHop.
///
/// In en, this message translates to:
/// **'Share contact by advert'**
String get contacts_ShareContactZeroHop;
/// No description provided for @contacts_zeroHopContactAdvertSent.
///
/// In en, this message translates to:
/// **'Sent contact by advert.'**
String get contacts_zeroHopContactAdvertSent;
/// No description provided for @contacts_zeroHopContactAdvertFailed.
///
/// In en, this message translates to:
/// **'Failed to send contact.'**
String get contacts_zeroHopContactAdvertFailed;
/// No description provided for @contacts_contactAdvertCopied.
///
/// In en, this message translates to:
/// **'Advert copied to Clipboard.'**
String get contacts_contactAdvertCopied;
/// No description provided for @contacts_contactAdvertCopyFailed.
///
/// In en, this message translates to:
/// **'Copying advert to Clipboard failed.'**
String get contacts_contactAdvertCopyFailed;
/// No description provided for @notification_activityTitle.
///
/// In en, this message translates to:
/// **'MeshCore Activity'**
String get notification_activityTitle;
/// No description provided for @notification_messagesCount.
///
/// In en, this message translates to:
/// **'{count} {count, plural, =1{message} other{messages}}'**
String notification_messagesCount(int count);
/// No description provided for @notification_channelMessagesCount.
///
/// In en, this message translates to:
/// **'{count} {count, plural, =1{channel message} other{channel messages}}'**
String notification_channelMessagesCount(int count);
/// No description provided for @notification_newNodesCount.
///
/// In en, this message translates to:
/// **'{count} {count, plural, =1{new node} other{new nodes}}'**
String notification_newNodesCount(int count);
/// No description provided for @notification_newTypeDiscovered.
///
/// In en, this message translates to:
/// **'New {contactType} discovered'**
String notification_newTypeDiscovered(String contactType);
/// No description provided for @notification_receivedNewMessage.
///
/// In en, this message translates to:
/// **'Received new message'**
String get notification_receivedNewMessage;
/// No description provided for @settings_gpxExportRepeaters.
///
/// In en, this message translates to:
/// **'Export repeaters / room server to GPX'**
String get settings_gpxExportRepeaters;
/// No description provided for @settings_gpxExportRepeatersSubtitle.
///
/// In en, this message translates to:
/// **'Exports repeaters / roomserver with a location to GPX file.'**
String get settings_gpxExportRepeatersSubtitle;
/// No description provided for @settings_gpxExportContacts.
///
/// In en, this message translates to:
/// **'Export companions to GPX'**
String get settings_gpxExportContacts;
/// No description provided for @settings_gpxExportContactsSubtitle.
///
/// In en, this message translates to:
/// **'Exports companions with a location to GPX file.'**
String get settings_gpxExportContactsSubtitle;
/// No description provided for @settings_gpxExportAll.
///
/// In en, this message translates to:
/// **'Export all contacts to GPX'**
String get settings_gpxExportAll;
/// No description provided for @settings_gpxExportAllSubtitle.
///
/// In en, this message translates to:
/// **'Exports all contacts with a location to GPX file.'**
String get settings_gpxExportAllSubtitle;
/// No description provided for @settings_gpxExportSuccess.
///
/// In en, this message translates to:
/// **'Successfully exported GPX file.'**
String get settings_gpxExportSuccess;
/// No description provided for @settings_gpxExportNoContacts.
///
/// In en, this message translates to:
/// **'No contacts to export.'**
String get settings_gpxExportNoContacts;
/// No description provided for @settings_gpxExportNotAvailable.
///
/// In en, this message translates to:
/// **'Not supported on your device/OS'**
String get settings_gpxExportNotAvailable;
/// No description provided for @settings_gpxExportError.
///
/// In en, this message translates to:
/// **'There was an error when exporting.'**
String get settings_gpxExportError;
/// No description provided for @settings_gpxExportRepeatersRoom.
///
/// In en, this message translates to:
/// **'Repeater & room server locations'**
String get settings_gpxExportRepeatersRoom;
/// No description provided for @settings_gpxExportChat.
///
/// In en, this message translates to:
/// **'Companion locations'**
String get settings_gpxExportChat;
/// No description provided for @settings_gpxExportAllContacts.
///
/// In en, this message translates to:
/// **'All contacts locations'**
String get settings_gpxExportAllContacts;
/// No description provided for @settings_gpxExportShareText.
///
/// In en, this message translates to:
/// **'Map data exported from meshcore-open'**
String get settings_gpxExportShareText;
/// No description provided for @settings_gpxExportShareSubject.
///
/// In en, this message translates to:
/// **'meshcore-open GPX map data export'**
String get settings_gpxExportShareSubject;
}
class _AppLocalizationsDelegate
@@ -4675,9 +5045,11 @@ class _AppLocalizationsDelegate
'nl',
'pl',
'pt',
'ru',
'sk',
'sl',
'sv',
'uk',
'zh',
].contains(locale.languageCode);
@@ -4706,12 +5078,16 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsPl();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'sk':
return AppLocalizationsSk();
case 'sl':
return AppLocalizationsSl();
case 'sv':
return AppLocalizationsSv();
case 'uk':
return AppLocalizationsUk();
case 'zh':
return AppLocalizationsZh();
}
+250 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get scanner_scan => 'Сканирай';
@override
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
@override
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
@override
String get device_quickSwitch => 'Бързо превключване';
@@ -340,15 +350,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get settings_presets => 'Предварителни настройки';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Честота (MHz)';
@@ -377,10 +378,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) {
@@ -450,6 +456,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Руски';
@override
String get appSettings_languageUk => 'Украински';
@override
String get appSettings_notifications => 'Уведомления';
@@ -1207,6 +1219,24 @@ 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 => 'Карта на възлите';
@@ -1339,6 +1369,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 => 'Кеш на офлайн карти';
@@ -2567,32 +2610,32 @@ class AppLocalizationsBg extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Регенерейрай секрет';
@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.';
return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Регенерация';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Секретно презареждане за \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Актуализирай тайна';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Секретно обновено за \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
}
@override
@@ -2658,4 +2701,193 @@ 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 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';
}
+285 -50
View File
@@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get scanner_scan => 'Scannen';
@override
String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.';
@override
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_enableBluetooth => 'Bluetooth aktivieren';
@override
String get device_quickSwitch => 'Schnelles Umschalten';
@@ -160,7 +170,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_appSettingsSubtitle =>
'Benachrichtigungen, Messaging und Kartenwahrnehmungen';
'Benachrichtigungen, Messaging und Kartenwahrnehmung';
@override
String get settings_nodeSettings => 'Knoten-Einstellungen';
@@ -244,10 +254,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_actions => 'Aktionen';
@override
String get settings_sendAdvertisement => 'Sende eine Ankündigung';
String get settings_sendAdvertisement => 'Sende Ankündigung';
@override
String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung';
String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung';
@override
String get settings_advertisementSent => 'Ankündigung gesendet';
@@ -267,7 +277,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_refreshContactsSubtitle =>
'Kontakte-Liste vom Gerät neu laden';
'Kontakt-Liste vom Gerät neu laden';
@override
String get settings_rebootDevice => 'Gerät neu starten';
@@ -334,15 +344,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_presets => 'Voreinstellungen';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenz (MHz)';
@@ -371,10 +372,15 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)';
@override
String get settings_longRange => 'Grosse Reichweite';
String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss';
@override
String get settings_fastSpeed => 'Schnelle Geschwindigkeit';
String get settings_clientRepeatSubtitle =>
'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.';
@override
String get settings_clientRepeatFreqWarning =>
'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.';
@override
String settings_error(String message) {
@@ -444,6 +450,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russisch';
@override
String get appSettings_languageUk => 'Ukrainisch';
@override
String get appSettings_notifications => 'Benachrichtigungen';
@@ -662,7 +674,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Wiederholungen verwalten';
String get contacts_manageRepeater => 'Repeater verwalten';
@override
String get contacts_manageRoom => 'Raum-Server verwalten';
@@ -796,7 +808,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get channels_usePublicChannel => 'Verwende öffentlichen Kanal';
@override
String get channels_standardPublicPsk => 'Standard-Öffentliche PSK';
String get channels_standardPublicPsk => 'Öffentliche Standard PSK';
@override
String get channels_pskHex => 'PSK (Hex)';
@@ -1029,11 +1041,11 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get debugFrame_textMessageHeader => 'Textnachricht-Frame:';
String get debugFrame_textMessageHeader => 'Textnachrichten Frame:';
@override
String debugFrame_destinationPubKey(String pubKey) {
return '- Ziel-Pub-Schlüssel: $pubKey';
return '- Ziel-Public-Schlüssel: $pubKey';
}
@override
@@ -1080,7 +1092,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_recentAckPaths =>
'Aktuelle ACK-Pfade (tasten, um zu verwenden):';
'Aktuelle ACK-Pfade (antippen, um zu verwenden):';
@override
String get chat_pathHistoryFull =>
@@ -1111,7 +1123,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_noPathHistoryYet =>
'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.';
@override
String get chat_pathActions => 'Pfadaktionen:';
@@ -1206,6 +1218,24 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Ungelesen: $count';
}
@override
String get chat_openLink => 'Link öffnen?';
@override
String get chat_openLinkConfirmation =>
'Möchten Sie diesen Link in Ihrem Browser öffnen?';
@override
String get chat_open => 'Öffnen';
@override
String chat_couldNotOpenLink(String url) {
return 'Link konnte nicht geöffnet werden: $url';
}
@override
String get chat_invalidLink => 'Ungültiges Link-Format';
@override
String get map_title => 'Karte';
@@ -1338,6 +1368,19 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_manageRepeater => 'Repeater verwalten';
@override
String get map_tapToAdd =>
'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.';
@override
String get map_runTrace => 'Pfadverlauf ausführen';
@override
String get map_removeLast => 'Letztes Entfernen';
@override
String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.';
@override
String get mapCache_title => 'Offline-Karten-Cache';
@@ -1394,7 +1437,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String mapCache_estimatedTiles(int count) {
return 'Geschätzte Fliesen: $count';
return 'Geschätzte Kacheln: $count';
}
@override
@@ -1568,7 +1611,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get path_hexPrefixInstructions =>
'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.';
'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.';
@override
String get path_hexPrefixExample =>
@@ -1665,7 +1708,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_statusRequestTimeout =>
'Statusanfrage zeitweise fehlgeschlagen.';
'Statusanfrage durch Timeout fehlgeschlagen.';
@override
String repeater_errorLoadingStatus(String error) {
@@ -1742,7 +1785,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Überflut: $flood, Direkt: $direct';
return 'Flut: $flood, Direkt: $direct';
}
@override
@@ -1773,7 +1816,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_guestPasswordHelper =>
'Schreibgeschützter Zugriffspasswort';
'Schreibgeschütztes Zugriffspasswort';
@override
String get repeater_radioSettings => 'Funk Einstellungen';
@@ -1870,8 +1913,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_rebootRepeater => 'Neustart Repeater';
@override
String get repeater_rebootRepeaterSubtitle =>
'Wiederholen Sie das Repeater-Gerät.';
String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.';
@override
String get repeater_rebootRepeaterConfirm =>
@@ -1969,7 +2011,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_cliTitle => 'Repeater CLI';
@override
String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl';
String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls';
@override
String get repeater_commandHelp => 'Hilfe';
@@ -1982,7 +2024,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_typeCommandOrUseQuick =>
'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle';
'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle';
@override
String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...';
@@ -2108,7 +2150,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpSetRxDelay =>
'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.';
@override
String get repeater_cliHelpSetTxDelay =>
@@ -2152,7 +2194,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpGetBridgeType =>
'Ruft Brückentyp none, rs232, espnow ab.';
'Ruft Brückentyp: none, rs232, espnow ab.';
@override
String get repeater_cliHelpLogStart =>
@@ -2179,7 +2221,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliHelpRegionLoad =>
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.';
'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingeckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.';
@override
String get repeater_cliHelpRegionGet =>
@@ -2328,10 +2370,11 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_receivedData => 'Empfangene Nachbarendaten';
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
@override
String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.';
String get neighbors_requestTimedOut =>
'Anfrage durch Timeout fehlgeschlagen.';
@override
String neighbors_errorLoading(String error) {
@@ -2339,19 +2382,19 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn';
String get neighbors_repeatersNeighbours => 'Nachbarn';
@override
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unbekannte $pubkey';
return 'Unbekannt $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hörte: $time vor her.';
return 'Gehört vor: $time';
}
@override
@@ -2371,7 +2414,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Die Detailangaben für dieses Paket sind nicht verfügbar.';
@override
String get channelPath_messageDetails => 'Nachrichtsdetails';
String get channelPath_messageDetails => 'Nachrichtendetails';
@override
String get channelPath_senderLabel => 'Sender';
@@ -2570,32 +2613,32 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Neugenerierung des Schlüssels';
@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.';
return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Neu generieren';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aktualisieren Sie den Schlüssel';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Schlüssel für \"$name\" aktualisiert';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.';
}
@override
@@ -2607,14 +2650,14 @@ class AppLocalizationsDe extends AppLocalizations {
'Füge einen Hashtag-Kanal für diese Community hinzu';
@override
String get community_selectCommunity => 'Wählen Sie Community';
String get community_selectCommunity => 'Wählen Sie eine Community';
@override
String get community_regularHashtag => 'Regulärer Hashtag';
@override
String get community_regularHashtagDesc =>
'Öffentliches Hashtag (jeder kann teilnehmen)';
'Öffentlicher Hashtag (jeder kann teilnehmen)';
@override
String get community_communityHashtag => 'Community Hashtag';
@@ -2659,8 +2702,200 @@ class AppLocalizationsDe extends AppLocalizations {
String get listFilter_roomServers => 'Raumserver';
@override
String get listFilter_unreadOnly => 'Nur nicht gelesen';
String get listFilter_unreadOnly => 'Nicht gelesen';
@override
String get listFilter_newGroup => 'Neue Gruppe';
@override
String get pathTrace_you => 'Du';
@override
String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.';
@override
String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.';
@override
String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.';
@override
String get pathTrace_someHopsNoLocation =>
'Bei einer oder mehreren Knoten fehlt der Standort!';
@override
String get pathTrace_clearTooltip => 'Pfad löschen';
@override
String get contacts_pathTrace => 'Pfadverfolgung';
@override
String get contacts_ping => 'Pingen';
@override
String get contacts_repeaterPathTrace => 'Pfadverfolgung zum Repeater';
@override
String get contacts_repeaterPing => 'Repeater pingen';
@override
String get contacts_roomPathTrace => 'Pfadverfolgung zum Raumserver';
@override
String get contacts_roomPing => 'Raumserver anpingen';
@override
String get contacts_chatTraceRoute => 'Pfadverfolgungsroute';
@override
String contacts_pathTraceTo(String name) {
return 'Route nach $name verfolgen';
}
@override
String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.';
@override
String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten';
@override
String get contacts_contactImported => 'Kontakt wurde importiert.';
@override
String get contacts_contactImportFailed =>
'Kontakt konnte nicht importiert werden';
@override
String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung';
@override
String get contacts_floodAdvert => 'Flut-Ankündigung';
@override
String get contacts_copyAdvertToClipboard =>
'Ankündigung in die Zwischenablage kopieren';
@override
String get contacts_addContactFromClipboard =>
'Kontakt aus Zwischenablage hinzufügen';
@override
String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren';
@override
String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen';
@override
String get contacts_zeroHopContactAdvertSent =>
'Kontakt über Anzeige gesendet';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Kontakt konnte nicht gesendet werden.';
@override
String get contacts_contactAdvertCopied =>
'Anzeige in die Zwischenablage kopiert.';
@override
String get contacts_contactAdvertCopyFailed =>
'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.';
@override
String get notification_activityTitle => 'MeshCore Aktivität';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Nachrichten',
one: 'Nachricht',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Kanalnachrichten',
one: 'Kanalnachricht',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'neue Knoten',
one: 'neuer Knoten',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Neuer $contactType entdeckt';
}
@override
String get notification_receivedNewMessage => 'Neue Nachricht empfangen';
@override
String get settings_gpxExportRepeaters =>
'Repeater und Raumserver als GPX exportieren';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.';
@override
String get settings_gpxExportContacts => 'Kontakte als GPX exportieren';
@override
String get settings_gpxExportContactsSubtitle =>
'Exportiert Kontakte mit einem Ort in eine GPX-Datei.';
@override
String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren';
@override
String get settings_gpxExportAllSubtitle =>
'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.';
@override
String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.';
@override
String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.';
@override
String get settings_gpxExportNotAvailable =>
'Nicht auf Ihrem Gerät/Betriebssystem unterstützt';
@override
String get settings_gpxExportError =>
'Beim Export ist ein Fehler aufgetreten.';
@override
String get settings_gpxExportRepeatersRoom =>
'Repeater- und Raumserver-Standorte';
@override
String get settings_gpxExportChat => 'Kontaktstandorte';
@override
String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte';
@override
String get settings_gpxExportShareText =>
'GPX-Kartendaten aus meshcore-open exportiert';
@override
String get settings_gpxExportShareSubject =>
'GPX-Kartendaten aus meshcore-open exportieren';
}
+238 -11
View File
@@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is off';
@override
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_enableBluetooth => 'Enable Bluetooth';
@override
String get device_quickSwitch => 'Quick switch';
@@ -332,15 +342,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequency (MHz)';
@@ -369,10 +370,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) {
@@ -442,6 +448,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Русский';
@override
String get appSettings_languageUk => 'Українська';
@override
String get appSettings_notifications => 'Notifications';
@@ -1186,6 +1198,24 @@ 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';
@@ -1318,6 +1348,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';
@@ -2618,4 +2660,189 @@ 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 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';
}
+250 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get scanner_scan => 'Escanea';
@override
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
@override
String get device_quickSwitch => 'Cambiar rápidamente';
@@ -337,15 +347,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_presets => 'Preajustes';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frecuencia (MHz)';
@@ -374,10 +375,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) {
@@ -447,6 +453,12 @@ 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_notifications => 'Notificaciones';
@@ -1204,6 +1216,24 @@ 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';
@@ -1336,6 +1366,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';
@@ -2565,32 +2607,32 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
@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.';
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 => 'Regenerate';
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Código secreto regenerado para \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Actualizar Contraseña';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Confidencialidad actualizada para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
}
@override
@@ -2657,4 +2699,194 @@ 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 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';
}
+318 -78
View File
@@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get scanner_scan => 'Scanner';
@override
String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.';
@override
String get scanner_bluetoothOffMessage =>
'Veuillez activer le Bluetooth pour rechercher des appareils.';
@override
String get scanner_enableBluetooth => 'Activer le Bluetooth';
@override
String get device_quickSwitch => 'Basculement rapide';
@@ -204,18 +214,19 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_locationInvalid => 'Latitude ou longitude invalide.';
@override
String get settings_locationGPSEnable => 'Habilita GPS';
String get settings_locationGPSEnable => 'Activer le GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
'Activer la mise à jour automatique de la position via GPS';
@override
String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)';
String get settings_locationIntervalSec =>
'Intervalle de mise-à-jour du GPS (Secondes)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
'L\'intervalle doit être compris entre 60 et 86400 secondes.';
@override
String get settings_latitude => 'Latitude';
@@ -272,7 +283,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Recharger la liste des contacts depuis l\'appareil';
@override
String get settings_rebootDevice => 'Réinitialiser l\'appareil';
String get settings_rebootDevice => 'Redémarrer l\'appareil';
@override
String get settings_rebootDeviceSubtitle => 'Redémarrer l\'appareil MeshCore';
@@ -337,15 +348,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_presets => 'Préréglages';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Fréquence (MHz)';
@@ -374,10 +376,15 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)';
@override
String get settings_longRange => 'Portée Longue';
String get settings_clientRepeat => 'Répétition hors réseau';
@override
String get settings_fastSpeed => 'Vitesse Rapide';
String get settings_clientRepeatSubtitle =>
'Permettez à cet appareil de répéter les paquets de données pour les autres.';
@override
String get settings_clientRepeatFreqWarning =>
'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.';
@override
String settings_error(String message) {
@@ -447,6 +454,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russe';
@override
String get appSettings_languageUk => 'Ukrainien';
@override
String get appSettings_notifications => 'Notifications';
@@ -553,11 +566,11 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_mapDisplay => 'Affichage de la carte';
@override
String get appSettings_showRepeaters => 'Afficher les répétiteurs';
String get appSettings_showRepeaters => 'Afficher les répéteurs';
@override
String get appSettings_showRepeatersSubtitle =>
'Afficher les nœuds répétiteurs sur la carte';
'Afficher les nœuds répéteurs sur la carte';
@override
String get appSettings_showChatNodes => 'Afficher les nœuds de discussion';
@@ -664,10 +677,10 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Gérer le répétiteur';
String get contacts_manageRepeater => 'Gérer le répéteur';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
String get contacts_manageRoom => 'Gérer le Room Server';
@override
String get contacts_roomLogin => 'Connexion Salle';
@@ -687,7 +700,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get contacts_newGroup => 'Nouvelle Groupe';
String get contacts_newGroup => 'Nouveau Groupe';
@override
String get contacts_groupName => 'Nom du groupe';
@@ -711,27 +724,27 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_noMembers => 'Aucun membre';
@override
String get contacts_lastSeenNow => 'Dernière fois vu maintenant';
String get contacts_lastSeenNow => 'Vu maintenant';
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Dernière fois vu il y a $minutes minutes.';
return 'Vu il y a $minutes minutes';
}
@override
String get contacts_lastSeenHourAgo => 'Dernière fois vu il y a 1 heure.';
String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure';
@override
String contacts_lastSeenHoursAgo(int hours) {
return 'Dernière fois vu il y a $hours heures.';
return 'Vu il y a $hours heures';
}
@override
String get contacts_lastSeenDayAgo => 'Dernière fois vu il y a 1 jour';
String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour';
@override
String contacts_lastSeenDaysAgo(int days) {
return 'Dernière activité il y a $days jours';
return 'Vu il y a $days jours';
}
@override
@@ -845,7 +858,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channels_sortAZ => 'A à Z';
@override
String get channels_sortLatestMessages => 'Dernières messages';
String get channels_sortLatestMessages => 'Derniers messages';
@override
String get channels_sortUnread => 'Non lu';
@@ -888,7 +901,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channels_enterHashtag => 'Entrez le hashtag';
@override
String get channels_hashtagHint => 'ex. #équipe';
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@@ -936,7 +949,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String chat_retryCount(int current, int max) {
return 'Réessayer $current/$max';
return 'Essai $current/$max';
}
@override
@@ -1087,18 +1100,18 @@ class AppLocalizationsFr extends AppLocalizations {
'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.';
@override
String get chat_hopSingular => 'Sautez';
String get chat_hopSingular => 'saut';
@override
String get chat_hopPlural => 'sautez';
String get chat_hopPlural => 'sauts';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'hops',
one: 'hop',
other: 'sauts',
one: 'saut',
);
return '$count $_temp0';
}
@@ -1209,6 +1222,24 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Non lu : $count';
}
@override
String get chat_openLink => 'Ouvrir le lien ?';
@override
String get chat_openLinkConfirmation =>
'Voulez-vous ouvrir ce lien dans votre navigateur ?';
@override
String get chat_open => 'Ouvrir';
@override
String chat_couldNotOpenLink(String url) {
return 'Impossible d\'ouvrir le lien : $url';
}
@override
String get chat_invalidLink => 'Format de lien invalide';
@override
String get map_title => 'Carte des nœuds';
@@ -1234,7 +1265,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_chat => 'Chat';
@override
String get map_repeater => 'Répétiteur';
String get map_repeater => 'Répéteur';
@override
String get map_room => 'Salle';
@@ -1340,7 +1371,20 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_joinRoom => 'Rejoindre la salle';
@override
String get map_manageRepeater => 'Gérer le répétiteur';
String get map_manageRepeater => 'Gérer le répéteur';
@override
String get map_tapToAdd =>
'Appuyez sur les nœuds pour les ajouter au chemin.';
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
@override
String get map_removeLast => 'Supprimer le dernier';
@override
String get map_pathTraceCancelled => 'Traçage de chemin annulé';
@override
String get mapCache_title => 'Cache de Carte Hors Ligne';
@@ -1371,7 +1415,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
return 'Tiles mis en cache ($downloaded) ($failed ratés)';
return 'Tuiles mis en cache ($downloaded) ($failed ratés)';
}
@override
@@ -1425,11 +1469,11 @@ class AppLocalizationsFr extends AppLocalizations {
String east,
String west,
) {
return 'N $north, S $south, E $east, W $west';
return 'N $north, S $south, E $east, O $west';
}
@override
String get time_justNow => 'Il y a tout juste maintenant';
String get time_justNow => 'Maintenant';
@override
String time_minutesAgo(int minutes) {
@@ -1484,7 +1528,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?';
@override
String get login_repeaterLogin => 'Connexion au répétiteur';
String get login_repeaterLogin => 'Connexion au répéteur';
@override
String get login_roomLogin => 'Connexion Salle';
@@ -1504,7 +1548,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.';
'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.';
@override
String get login_roomDescription =>
@@ -1609,7 +1653,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get path_setPath => 'Définir le chemin';
@override
String get repeater_management => 'Gestion des répétiteurs';
String get repeater_management => 'Gestion des répéteurs';
@override
String get room_management => 'Administración del Servidor de Habitación';
@@ -1622,7 +1666,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_statusSubtitle =>
'Afficher l\'état, les statistiques et les voisins du répétiteur';
'Afficher l\'état, les statistiques et les voisins du répéteur';
@override
String get repeater_telemetry => 'Télémetrie';
@@ -1635,7 +1679,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_cli => 'CLI';
@override
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur';
@override
String get repeater_neighbours => 'Voisins';
@@ -1649,10 +1693,10 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_settingsSubtitle =>
'Configurer les paramètres du répétiteur';
'Configurer les paramètres du répéteur';
@override
String get repeater_statusTitle => 'État du répétiteur';
String get repeater_statusTitle => 'État du répéteur';
@override
String get repeater_routingMode => 'Mode de routage';
@@ -1725,7 +1769,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_received => 'Reçu';
@override
String get repeater_duplicates => 'Dupliques';
String get repeater_duplicates => 'Doublons';
@override
String repeater_daysHoursMinsSecs(
@@ -1758,16 +1802,16 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Paramètres du répétiteur';
String get repeater_settingsTitle => 'Paramètres du répéteur';
@override
String get repeater_basicSettings => 'Paramètres de base';
@override
String get repeater_repeaterName => 'Nom du répétiteur';
String get repeater_repeaterName => 'Nom du répéteur';
@override
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur';
String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur';
@override
String get repeater_adminPassword => 'Mot de passe Administrateur';
@@ -1831,7 +1875,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_packetForwardingSubtitle =>
'Activer le répétiteur pour transmettre des paquets';
'Activer le répéteur pour transmettre des paquets';
@override
String get repeater_guestAccess => 'Accès Invité';
@@ -1873,18 +1917,18 @@ class AppLocalizationsFr extends AppLocalizations {
'Intervalle d\'annonces cryptées';
@override
String get repeater_dangerZone => 'Zone d\'alerte';
String get repeater_dangerZone => 'Zone dangereuse';
@override
String get repeater_rebootRepeater => 'Redémarrer Répéteur';
@override
String get repeater_rebootRepeaterSubtitle =>
'Réinitialiser l\'appareil répétiteur';
'Réinitialiser l\'appareil répéteur';
@override
String get repeater_rebootRepeaterConfirm =>
'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?';
'Êtes-vous sûr de vouloir redémarrer ce répéteur ?';
@override
String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité';
@@ -1895,18 +1939,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
'Cela générera une nouvelle identité pour le répétiteur. Continuer ?';
'Cela générera une nouvelle identité pour le répéteur. Continuer ?';
@override
String get repeater_eraseFileSystem => 'Supprimer le système de fichiers';
@override
String get repeater_eraseFileSystemSubtitle =>
'Formater le système de fichiers du répétiteur';
'Formater le système de fichiers du répéteur';
@override
String get repeater_eraseFileSystemConfirm =>
'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !';
'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !';
@override
String get repeater_eraseSerialOnly =>
@@ -1974,7 +2018,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get repeater_cliTitle => 'Répétiteur CLI';
String get repeater_cliTitle => 'Répéteur CLI';
@override
String get repeater_debugNextCommand => 'Déboguer Prochaine Commande';
@@ -2066,11 +2110,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetRepeat =>
'Active ou désactive le rôle du répétiteur pour ce nœud.';
'Active ou désactive le rôle du répéteur pour ce nœud.';
@override
String get repeater_cliHelpSetAllowReadOnly =>
'(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)';
'(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)';
@override
String get repeater_cliHelpSetFloodMax =>
@@ -2090,7 +2134,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetAdvertInterval =>
'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.';
'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.';
@override
String get repeater_cliHelpSetFloodAdvertInterval =>
@@ -2176,7 +2220,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpNeighbors =>
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
@override
String get repeater_cliHelpNeighborRemove =>
@@ -2264,12 +2308,11 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_logging => 'Journalisation';
@override
String get repeater_neighborsRepeaterOnly =>
'Voisins (Uniquement répétiteur)';
String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)';
@override
String get repeater_regionManagementRepeaterOnly =>
'Gestion des régions (uniquement pour le répétiteur)';
'Gestion des régions (uniquement pour le répéteur)';
@override
String get repeater_regionNote =>
@@ -2374,7 +2417,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channelPath_otherObservedPaths => 'Autres chemins observés';
@override
String get channelPath_repeaterHops => 'Sauts du répétiteur';
String get channelPath_repeaterHops => 'Sauts du répéteur';
@override
String get channelPath_noHopDetails =>
@@ -2442,7 +2485,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channelPath_noRepeaterLocations =>
'Aucune position de répétiteur disponible pour ce chemin.';
'Aucune position de répéteur disponible pour ce chemin.';
@override
String channelPath_primaryPath(int index) {
@@ -2581,32 +2624,32 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Régénérer le 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.';
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Régénérer';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Mot de passe secret régénéré pour \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Mettre à jour le secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Modification secrète mise à jour pour \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
}
@override
@@ -2614,7 +2657,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get community_addHashtagChannelDesc =>
'Ajouter un canal hachage pour cette communauté';
'Ajouter un canal hashtag pour cette communauté';
@override
String get community_selectCommunity => 'Sélectionner Communauté';
@@ -2645,7 +2688,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get listFilter_sortBy => 'Trier par';
@override
String get listFilter_latestMessages => 'Dernières messages';
String get listFilter_latestMessages => 'Derniers messages';
@override
String get listFilter_heardRecently => 'Écoute récemment';
@@ -2666,11 +2709,208 @@ class AppLocalizationsFr extends AppLocalizations {
String get listFilter_repeaters => 'Répéteurs';
@override
String get listFilter_roomServers => 'Serveurs de pièce';
String get listFilter_roomServers => 'Room servers';
@override
String get listFilter_unreadOnly => 'Messages non lus seulement';
@override
String get listFilter_newGroup => 'Nouvelle groupe';
String get listFilter_newGroup => 'Nouveau groupe';
@override
String get pathTrace_you => 'Vous';
@override
String get pathTrace_failed => 'Traçage du chemin échoué.';
@override
String get pathTrace_notAvailable => 'Tracé de chemin non disponible.';
@override
String get pathTrace_refreshTooltip => 'Actualiser Path Trace';
@override
String get pathTrace_someHopsNoLocation =>
'Un ou plusieurs des sauts manquent d\'une localisation !';
@override
String get pathTrace_clearTooltip => 'Effacer le chemin';
@override
String get contacts_pathTrace => 'Traçage de chemin';
@override
String get contacts_ping => 'Ping';
@override
String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur';
@override
String get contacts_repeaterPing => 'Pinguer le répéteur';
@override
String get contacts_roomPathTrace =>
'Traçage du chemin vers le serveur de la salle';
@override
String get contacts_roomPing => 'Pinguer le serveur de la salle';
@override
String get contacts_chatTraceRoute => 'Tracer le chemin';
@override
String contacts_pathTraceTo(String name) {
return 'Tracer l\'itinéraire vers $name';
}
@override
String get contacts_clipboardEmpty => 'Le presse-papiers est vide.';
@override
String get contacts_invalidAdvertFormat => 'Données de contact non valides';
@override
String get contacts_contactImported => 'Le contact a été importé.';
@override
String get contacts_contactImportFailed =>
'Échec de l\'importation du contact.';
@override
String get contacts_zeroHopAdvert => 'Annonce Zero saut';
@override
String get contacts_floodAdvert => 'Annonce à tout le réseau';
@override
String get contacts_copyAdvertToClipboard =>
'Copier l\'annonce dans le presse-papiers';
@override
String get contacts_addContactFromClipboard =>
'Ajouter un contact depuis le presse-papiers';
@override
String get contacts_ShareContact =>
'Copier le contact dans le presse-papiers';
@override
String get contacts_ShareContactZeroHop => 'Partager un contact par annonce';
@override
String get contacts_zeroHopContactAdvertSent =>
'Envoyer un contact par annonce.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Échec de l\'envoi du contact.';
@override
String get contacts_contactAdvertCopied =>
'Annonce copiée dans le presse-papiers.';
@override
String get contacts_contactAdvertCopyFailed =>
'La copie de l\'annonce vers le presse-papiers a échoué.';
@override
String get notification_activityTitle => 'Activité MeshCore';
@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: 'messages de canal',
one: 'message de canal',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nouveaux nœuds',
one: 'nouveau nœud',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nouveau $contactType découvert';
}
@override
String get notification_receivedNewMessage => 'Nouveau message reçu';
@override
String get settings_gpxExportRepeaters =>
'Exporter les répéteurs / serveur de salle au format GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.';
@override
String get settings_gpxExportContacts =>
'Exporter les compagnons au format GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exporte les compagnons avec un emplacement vers un fichier GPX.';
@override
String get settings_gpxExportAll =>
'Exporter tous les contacts au format GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exporte tous les contacts avec une localisation vers un fichier GPX.';
@override
String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.';
@override
String get settings_gpxExportNoContacts => 'Aucun contact à exporter.';
@override
String get settings_gpxExportNotAvailable =>
'Non pris en charge sur votre appareil/Système d\'exploitation';
@override
String get settings_gpxExportError =>
'Une erreur s\'est produite lors de l\'exportation.';
@override
String get settings_gpxExportRepeatersRoom =>
'Emplacements des serveurs de répéteur et de salle';
@override
String get settings_gpxExportChat => 'Emplacements des compagnons';
@override
String get settings_gpxExportAllContacts =>
'Tous les emplacements des contacts';
@override
String get settings_gpxExportShareText =>
'Données de carte exportées à partir de meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open exporter les données de carte GPX';
}
+254 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get scanner_scan => 'Scansiona';
@override
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
@override
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
@override
String get device_quickSwitch => 'Passa velocemente';
@@ -336,15 +346,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequenza (MHz)';
@@ -373,10 +374,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) {
@@ -446,6 +452,12 @@ 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_notifications => 'Notifiche';
@@ -1203,6 +1215,24 @@ 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';
@@ -1335,6 +1365,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';
@@ -2565,32 +2607,32 @@ class AppLocalizationsIt extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Ri genera la chiave segreta';
@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.';
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 => 'Regenerate';
String get community_regenerate => 'Rigenera';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Codice segreto rigenerato per \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aggiorna Segreto';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Segreto aggiornato per \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
}
@override
@@ -2657,4 +2699,198 @@ 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 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';
}
+251 -18
View File
@@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get scanner_scan => 'Scan';
@override
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
@override
String get scanner_bluetoothOffMessage =>
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
@override
String get scanner_enableBluetooth => 'Activeer Bluetooth';
@override
String get device_quickSwitch => 'Snelle overschakeling';
@@ -334,15 +344,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequentie (MHz)';
@@ -371,10 +372,15 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_longRange => 'Lange Afstand';
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
@override
String get settings_fastSpeed => 'Hoge Snelheid';
String get settings_clientRepeatSubtitle =>
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
@override
String get settings_clientRepeatFreqWarning =>
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
@override
String settings_error(String message) {
@@ -444,6 +450,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_languageBg => 'Български';
@override
String get appSettings_languageRu => 'Russisch';
@override
String get appSettings_languageUk => 'Oekraïens';
@override
String get appSettings_notifications => 'Notificaties';
@@ -1199,6 +1211,24 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Nieuw: $count';
}
@override
String get chat_openLink => 'Link openen?';
@override
String get chat_openLinkConfirmation =>
'Wilt u deze link in uw browser openen?';
@override
String get chat_open => 'Openen';
@override
String chat_couldNotOpenLink(String url) {
return 'Kan link niet openen: $url';
}
@override
String get chat_invalidLink => 'Ongeldig linkformaat';
@override
String get map_title => 'Node Map';
@@ -1331,6 +1361,19 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_manageRepeater => 'Beheer Repeater';
@override
String get map_tapToAdd =>
'Tik op knooppunten om ze toe te voegen aan het pad';
@override
String get map_runTrace => 'Padeshulp traceren';
@override
String get map_removeLast => 'Verwijder Laatste';
@override
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
@override
String get mapCache_title => 'Offline Kaarten Cache';
@@ -2556,32 +2599,32 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regeneer Geheimwoord';
@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.';
return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Regeneer';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Geheim hersteld voor \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Bijwerken Geheime';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Geheim gewijzigd voor \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken';
}
@override
@@ -2648,4 +2691,194 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get listFilter_newGroup => 'Nieuwe groep';
@override
String get pathTrace_you => 'Jij';
@override
String get pathTrace_failed => 'Padtrace mislukt.';
@override
String get pathTrace_notAvailable => 'Padtrace niet beschikbaar.';
@override
String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.';
@override
String get pathTrace_someHopsNoLocation =>
'Een of meer van de hops ontbreken een locatie!';
@override
String get pathTrace_clearTooltip => 'Weg wissen';
@override
String get contacts_pathTrace => 'Pad Traceren';
@override
String get contacts_ping => 'Pingen';
@override
String get contacts_repeaterPathTrace => 'Pad traceren naar repeater';
@override
String get contacts_repeaterPing => 'Ping repeater';
@override
String get contacts_roomPathTrace => 'Padtrace naar room server';
@override
String get contacts_roomPing => 'Ping kamer server';
@override
String get contacts_chatTraceRoute => 'Route traceren';
@override
String contacts_pathTraceTo(String name) {
return 'Trace route to $name';
}
@override
String get contacts_clipboardEmpty => 'Knipbord is leeg.';
@override
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
@override
String get contacts_contactImported => 'Contact is geïmporteerd.';
@override
String get contacts_contactImportFailed =>
'Contact kon niet geïmporteerd worden.';
@override
String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
@override
String get contacts_floodAdvert => 'Overstromingsadvertentie';
@override
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
@override
String get contacts_addContactFromClipboard =>
'Contact uit klembord toevoegen';
@override
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
@override
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
@override
String get contacts_zeroHopContactAdvertSent =>
'Contact verzonden via advertentie';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Mislukt om contact te verzenden';
@override
String get contacts_contactAdvertCopied =>
'Reclame gekopieerd naar Klembord.';
@override
String get contacts_contactAdvertCopyFailed =>
'Kopiëren van advertentie naar Clipboard is mislukt.';
@override
String get notification_activityTitle => 'MeshCore Activiteit';
@override
String notification_messagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'berichten',
one: 'bericht',
);
return '$count $_temp0';
}
@override
String notification_channelMessagesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'kanaalberichten',
one: 'kanaalbericht',
);
return '$count $_temp0';
}
@override
String notification_newNodesCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'nieuwe knooppunten',
one: 'nieuw knooppunt',
);
return '$count $_temp0';
}
@override
String notification_newTypeDiscovered(String contactType) {
return 'Nieuw $contactType ontdekt';
}
@override
String get notification_receivedNewMessage => 'Nieuw bericht ontvangen';
@override
String get settings_gpxExportRepeaters =>
'Exporteer repeaters / roomserver naar GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Exporteert repeaters / roomserver met een locatie naar GPX-bestand.';
@override
String get settings_gpxExportContacts => 'Companions exporteren naar GPX';
@override
String get settings_gpxExportContactsSubtitle =>
'Exporteert metgezellen met een locatie naar een GPX-bestand.';
@override
String get settings_gpxExportAll => 'Alle contacten exporteren naar GPX';
@override
String get settings_gpxExportAllSubtitle =>
'Exporteert alle contacten met een locatie naar een GPX-bestand.';
@override
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.';
@override
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
@override
String get settings_gpxExportNotAvailable =>
'Niet ondersteund op uw apparaat/besturingssysteem';
@override
String get settings_gpxExportError => 'Er was een fout bij het exporteren.';
@override
String get settings_gpxExportRepeatersRoom =>
'Repeater- en kamer servers locaties';
@override
String get settings_gpxExportChat => 'Locaties van metgezellen';
@override
String get settings_gpxExportAllContacts => 'Alle contactlocaties';
@override
String get settings_gpxExportShareText =>
'Kaartgegevens geëxporteerd uit meshcore-open';
@override
String get settings_gpxExportShareSubject =>
'meshcore-open GPX kaartgegevens exporteren';
}
+257 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get scanner_scan => 'Przeskanuj';
@override
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
@override
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_enableBluetooth => 'Włącz Bluetooth';
@override
String get device_quickSwitch => 'Szybka zmiana';
@@ -337,15 +347,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_presets => 'Preset';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Częstotliwość (MHz)';
@@ -375,10 +376,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) {
@@ -448,6 +454,12 @@ 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_notifications => 'Powiadomienia';
@@ -1205,6 +1217,24 @@ 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';
@@ -1337,6 +1367,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';
@@ -2564,32 +2606,32 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Zregeneruj sekret';
@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.';
return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
}
@override
String get community_regenerate => 'Regenerate';
String get community_regenerate => 'Zregeneruj';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Hasło ponownie wygenerowane dla \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Zaktualizuj tajny klucz';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Hasło zaktualizowane dla \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
}
@override
@@ -2656,4 +2698,201 @@ 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 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';
}
+249 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get scanner_scan => 'Digitalizar';
@override
String get scanner_bluetoothOff => 'Bluetooth está desativado';
@override
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_enableBluetooth => 'Ative o Bluetooth';
@override
String get device_quickSwitch => 'Mudar rapidamente';
@@ -338,15 +348,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_presets => 'Presets';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frequência (MHz)';
@@ -375,10 +376,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) {
@@ -448,6 +454,12 @@ 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_notifications => 'Notificações';
@@ -1204,6 +1216,24 @@ 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';
@@ -1337,6 +1367,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';
@@ -2567,32 +2609,32 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerar Senha Segura';
@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.';
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 => 'Regenerate';
String get community_regenerate => 'Regenerar';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Senha secreta regenerada para \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Atualizar Segredo';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Segredo atualizado para \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
}
@override
@@ -2659,4 +2701,193 @@ 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 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';
}
File diff suppressed because it is too large Load Diff
+251 -18
View File
@@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get scanner_scan => 'Skončiť';
@override
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
@override
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_enableBluetooth => 'Povolte Bluetooth';
@override
String get device_quickSwitch => 'Rýchle prepínač';
@@ -334,15 +344,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_presets => 'Prednastavenia';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvencia (MHz)';
@@ -371,10 +372,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) {
@@ -444,6 +450,12 @@ 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_notifications => 'Upozornenia';
@@ -1200,6 +1212,24 @@ 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';
@@ -1332,6 +1362,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äť';
@@ -2553,32 +2595,32 @@ class AppLocalizationsSk extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Zobraziť nový tajný kód';
@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.';
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 => 'Regenerate';
String get community_regenerate => 'Znovu vygenerovať';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Záznam pre \"$name\" bol regenerovaný tajne';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Aktualizovať tajné heslo';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Zmena tajnej slova pre \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
}
@override
@@ -2644,4 +2686,195 @@ 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 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';
}
File diff suppressed because it is too large Load Diff
+248 -18
View File
@@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get scanner_scan => 'Skanna';
@override
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
@override
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
@override
String get device_quickSwitch => 'Snabb växling';
@@ -331,15 +341,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_presets => 'Fördefinierade inställningar';
@override
String get settings_preset915Mhz => '915 MHz';
@override
String get settings_preset868Mhz => '868 MHz';
@override
String get settings_preset433Mhz => '433 MHz';
@override
String get settings_frequency => 'Frekvens (MHz)';
@@ -368,10 +369,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) {
@@ -441,6 +447,12 @@ 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_notifications => 'Meddelanden';
@@ -1192,6 +1204,24 @@ 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';
@@ -1324,6 +1354,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';
@@ -2541,32 +2583,32 @@ class AppLocalizationsSv extends AppLocalizations {
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
String get community_regenerateSecret => 'Regenerera hemlig kod';
@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.';
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 => 'Regenerate';
String get community_regenerate => 'Regenerera';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
return 'Lösenord återskapad för \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
String get community_updateSecret => 'Uppdatera hemlighet';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
return 'Hemlighet uppdaterad för \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
}
@override
@@ -2632,4 +2674,192 @@ 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 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';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+119 -7
View File
@@ -131,9 +131,6 @@
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Aantal Kanalen",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
"settings_preset433Mhz": "433 MHz",
"settings_frequency": "Frequentie (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
@@ -143,8 +140,6 @@
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_longRange": "Lange Afstand",
"settings_fastSpeed": "Hoge Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -604,6 +599,18 @@
}
}
},
"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 nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Community \"{name}\" verlaten",
@@ -1484,5 +1493,108 @@
"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_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",
"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",
"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."
}
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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.",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
@@ -1484,5 +1493,108 @@
"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_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",
"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",
"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."
}
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
@@ -1484,5 +1493,108 @@
"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_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",
"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.",
"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."
}
+840
View File
@@ -0,0 +1,840 @@
{
"@@locale": "ru",
"appTitle": "MeshCore Open",
"nav_contacts": "Контакты",
"nav_channels": "Каналы",
"nav_map": "Карта",
"common_cancel": "Отмена",
"common_ok": "OK",
"common_connect": "Коннект",
"common_unknownDevice": "Неизвестное устройство",
"common_save": "Сохранить",
"common_delete": "Удалить",
"common_close": "Закрыть",
"common_edit": "Изменить",
"common_add": "Добавить",
"common_settings": "Настройки",
"common_disconnect": "Отключить",
"common_connected": "Подключено",
"common_disconnected": "Отключено",
"common_create": "Создать",
"common_continue": "Продолжить",
"common_share": "Поделиться",
"common_copy": "Копировать",
"common_retry": "Повторить",
"common_hide": "Скрыть",
"common_remove": "Убрать",
"common_enable": "Включить",
"common_disable": "Выключить",
"common_reboot": "Перезагрузить",
"common_loading": "Загрузка...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} В",
"common_percentValue": "{percent}%",
"scanner_title": "MeshCore Open",
"scanner_scanning": "Поиск устройств...",
"scanner_connecting": "Подключение...",
"scanner_disconnecting": "Отключение...",
"scanner_notConnected": "Не подключено",
"scanner_connectedTo": "Подключено к {deviceName}",
"scanner_searchingDevices": "Поиск устройств MeshCore...",
"scanner_tapToScan": "Нажмите для поиска MeshCore устройств",
"scanner_connectionFailed": "Подключение не удалось: {error}",
"scanner_stop": "Стоп",
"scanner_scan": "Сканирование",
"device_quickSwitch": "Быстрое переключение",
"device_meshcore": "MeshCore",
"settings_title": "Настройки",
"settings_deviceInfo": "Информация об устройстве",
"settings_appSettings": "Настройки приложения",
"settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты",
"settings_nodeSettings": "Настройки ноды",
"settings_nodeName": "Имя ноды",
"settings_nodeNameNotSet": "Не установлено",
"settings_nodeNameHint": "Введите имя ноды",
"settings_nodeNameUpdated": "Имя обновлено",
"settings_radioSettings": "Настройки радио",
"settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения",
"settings_radioSettingsUpdated": "Настройки радио обновлены",
"settings_location": "Позиция",
"settings_locationSubtitle": "Координаты GPS",
"settings_locationUpdated": "Позиция и настройки GPS обновлены",
"settings_locationBothRequired": "Введите широту и долготу.",
"settings_locationInvalid": "Неверная широта или долгота.",
"settings_locationGPSEnable": "Включить GPS",
"settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.",
"settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)",
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
"settings_latitude": "Широта",
"settings_longitude": "Долгота",
"settings_privacyMode": "Режим конфиденциальности",
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
"settings_privacyModeEnabled": "Режим конфиденциальности включен",
"settings_privacyModeDisabled": "Режим конфиденциальности выключен",
"settings_actions": "Действия",
"settings_sendAdvertisement": "Отправить анонсирование",
"settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас",
"settings_advertisementSent": "Анонсирование отправлено",
"settings_syncTime": "Синхронизация времени",
"settings_syncTimeSubtitle": "Синхронизировать время с телефоном",
"settings_timeSynchronized": "Время синхронизировано",
"settings_refreshContacts": "Обновить контакты",
"settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства",
"settings_rebootDevice": "Перезагрузить устройство",
"settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore",
"settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.",
"settings_debug": "Отладка",
"settings_bleDebugLog": "Журнал отладки BLE",
"settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные",
"settings_appDebugLog": "Журнал отладки приложения",
"settings_appDebugLogSubtitle": "Сообщения отладки приложения",
"settings_about": "О программе",
"settings_aboutVersion": "MeshCore Open v{version}",
"settings_aboutLegalese": "2026 MeshCore Open Source Project",
"settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.",
"settings_infoName": "Имя",
"settings_infoId": "ID",
"settings_infoStatus": "Статус",
"settings_infoBattery": "Батарея",
"settings_infoPublicKey": "Публичный ключ",
"settings_infoContactsCount": "Количество контактов",
"settings_infoChannelCount": "Количество каналов",
"settings_presets": "Пресеты",
"settings_frequency": "Частота (МГц)",
"settings_frequencyHelper": "300.0 2500.0",
"settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)",
"settings_bandwidth": "Полоса пропускания",
"settings_spreadingFactor": "Коэффициент расширения",
"settings_codingRate": "Коэффициент кодирования",
"settings_txPower": "Мощность передачи (дБм)",
"settings_txPowerHelper": "0 22",
"settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)",
"settings_error": "Ошибка: {message}",
"appSettings_title": "Настройки приложения",
"appSettings_appearance": "Внешний вид",
"appSettings_theme": "Тема",
"appSettings_themeSystem": "Как в системе",
"appSettings_themeLight": "Светлая",
"appSettings_themeDark": "Тёмная",
"appSettings_language": "Язык",
"appSettings_languageSystem": "Как в системе",
"appSettings_languageEn": "Английский",
"appSettings_languageFr": "Французский",
"appSettings_languageEs": "Испанский",
"appSettings_languageDe": "Немецкий",
"appSettings_languagePl": "Польский",
"appSettings_languageSl": "Словенский",
"appSettings_languagePt": "Португальский",
"appSettings_languageIt": "Итальянский",
"appSettings_languageZh": "Китайский",
"appSettings_languageSv": "Шведский",
"appSettings_languageNl": "Нидерландский",
"appSettings_languageSk": "Словацкий",
"appSettings_languageBg": "Болгарский",
"appSettings_languageRu": "Русский",
"appSettings_notifications": "Уведомления",
"appSettings_enableNotifications": "Включить уведомления",
"appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях",
"appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено",
"appSettings_notificationsEnabled": "Уведомления включены",
"appSettings_notificationsDisabled": "Уведомления отключены",
"appSettings_messageNotifications": "Уведомления о сообщениях",
"appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений",
"appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах",
"appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах",
"appSettings_advertisementNotifications": "Уведомления об анонсированиях",
"appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод",
"appSettings_messaging": "Обмен сообщениями",
"appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток",
"appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки",
"appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток",
"appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться",
"appSettings_autoRouteRotation": "Автоматическое переключение маршрутов",
"appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки",
"appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено",
"appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено",
"appSettings_battery": "Батарея",
"appSettings_batteryChemistry": "Химия батареи",
"appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})",
"appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать",
"appSettings_batteryNmc": "18650 NMC (3.04.2 В)",
"appSettings_batteryLifepo4": "LiFePO4 (2.63.65 В)",
"appSettings_batteryLipo": "LiPo (3.04.2 В)",
"appSettings_mapDisplay": "Отображение карты",
"appSettings_showRepeaters": "Показывать репитеры",
"appSettings_showRepeatersSubtitle": "Отображать репитеры на карте",
"appSettings_showChatNodes": "Показывать чат-ноды",
"appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте",
"appSettings_showOtherNodes": "Показывать другие ноды",
"appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте",
"appSettings_timeFilter": "Фильтр по времени",
"appSettings_timeFilterShowAll": "Показывать все ноды",
"appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч",
"appSettings_mapTimeFilter": "Временной фильтр карты",
"appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:",
"appSettings_allTime": "Всё время",
"appSettings_lastHour": "Последний час",
"appSettings_last6Hours": "Последние 6 часов",
"appSettings_last24Hours": "Последние 24 часа",
"appSettings_lastWeek": "Последнюю неделю",
"appSettings_offlineMapCache": "Кэш офлайн-карты",
"appSettings_noAreaSelected": "Область не выбрана",
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})",
"appSettings_debugCard": "Отладка",
"appSettings_appDebugLogging": "Журнал отладки приложения",
"appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики",
"appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён",
"appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён",
"contacts_title": "Контакты",
"contacts_noContacts": "Контактов пока нет",
"contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения",
"contacts_searchContacts": "Поиск контактов...",
"contacts_noUnreadContacts": "Нет непрочитанных контактов",
"contacts_noContactsFound": "Контакты или группы не найдены",
"contacts_deleteContact": "Удалить контакт",
"contacts_removeConfirm": "Удалить {contactName} из контактов?",
"contacts_manageRepeater": "Управление репитером",
"contacts_manageRoom": "Управление сервером комнат",
"contacts_roomLogin": "Вход на сервер комнат",
"contacts_openChat": "Открыть чат",
"contacts_editGroup": "Изменить группу",
"contacts_deleteGroup": "Удалить группу",
"contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?",
"contacts_newGroup": "Новая группа",
"contacts_groupName": "Имя группы",
"contacts_groupNameRequired": "Имя группы обязательно",
"contacts_groupAlreadyExists": "Группа \"{name}\" уже существует",
"contacts_filterContacts": "Фильтр контактов...",
"contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру",
"contacts_noMembers": "Нет участников",
"contacts_lastSeenNow": "Видели только что",
"contacts_lastSeenMinsAgo": "Видели {minutes} мин назад",
"contacts_lastSeenHourAgo": "Видели 1 час назад",
"contacts_lastSeenHoursAgo": "Видели {hours} ч назад",
"contacts_lastSeenDayAgo": "Видели 1 день назад",
"contacts_lastSeenDaysAgo": "Видели {days} дн. назад",
"channels_title": "Каналы",
"channels_noChannelsConfigured": "Каналы не настроены",
"channels_addPublicChannel": "Добавить публичный канал",
"channels_searchChannels": "Поиск каналов...",
"channels_noChannelsFound": "Каналы не найдены",
"channels_channelIndex": "Канал {index}",
"channels_hashtagChannel": "Хэштег-канал",
"channels_public": "Публичный",
"channels_private": "Приватный",
"channels_publicChannel": "Публичный канал",
"channels_privateChannel": "Приватный канал",
"channels_editChannel": "Изменить канал",
"channels_deleteChannel": "Удалить канал",
"channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.",
"channels_channelDeleted": "Канал \"{name}\" удалён",
"channels_addChannel": "Добавить канал",
"channels_channelIndexLabel": "Индекс канала",
"channels_channelName": "Имя канала",
"channels_usePublicChannel": "Использовать публичный канал",
"channels_standardPublicPsk": "Стандартный публичный PSK",
"channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Сгенерировать случайный PSK",
"channels_enterChannelName": "Введите имя канала",
"channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа",
"channels_channelAdded": "Канал \"{name}\" добавлен",
"channels_editChannelTitle": "Изменить канал {index}",
"channels_smazCompression": "Сжатие SMAZ",
"channels_channelUpdated": "Канал \"{name}\" обновлён",
"channels_publicChannelAdded": "Публичный канал добавлен",
"channels_sortBy": "Сортировка",
"channels_sortManual": "Вручную",
"channels_sortAZ": "По алфавиту",
"channels_sortLatestMessages": "По последним сообщениям",
"channels_sortUnread": "По непрочитанным",
"channels_createPrivateChannel": "Создать приватный канал",
"channels_createPrivateChannelDesc": "Защищён секретным ключом.",
"channels_joinPrivateChannel": "Присоединиться к приватному каналу",
"channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.",
"channels_joinPublicChannel": "Присоединиться к публичному каналу",
"channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.",
"channels_joinHashtagChannel": "Присоединиться к хэштег-каналу",
"channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.",
"channels_scanQrCode": "Сканировать QR-код",
"channels_scanQrCodeComingSoon": "Скоро будет",
"channels_enterHashtag": "Введите хэштег",
"channels_hashtagHint": "например, #команда",
"chat_noMessages": "Сообщений пока нет",
"chat_sendMessageToStart": "Отправьте сообщение, чтобы начать",
"chat_originalMessageNotFound": "Исходное сообщение не найдено",
"chat_replyingTo": "Ответ для {name}",
"chat_replyTo": "Ответить {name}",
"chat_location": "Местоположение",
"chat_sendMessageTo": "Отправить сообщение {contactName}",
"chat_typeMessage": "Напишите сообщение...",
"chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).",
"chat_messageCopied": "Сообщение скопировано",
"chat_messageDeleted": "Сообщение удалено",
"chat_retryingMessage": "Повтор отправки сообщения",
"chat_retryCount": "Попытка {current}/{max}",
"chat_sendGif": "Отправить GIF",
"chat_reply": "Ответить",
"chat_addReaction": "Добавить реакцию",
"chat_me": "Я",
"emojiCategorySmileys": "Смайлы",
"emojiCategoryGestures": "Жесты",
"emojiCategoryHearts": "Сердечки",
"emojiCategoryObjects": "Предметы",
"gifPicker_title": "Выберите GIF",
"gifPicker_searchHint": "Поиск GIF...",
"gifPicker_poweredBy": "Работает на GIPHY",
"gifPicker_noGifsFound": "GIF не найдены",
"gifPicker_failedLoad": "Не удалось загрузить GIF",
"gifPicker_failedSearch": "Не удалось выполнить поиск GIF",
"gifPicker_noInternet": "Нет подключения к интернету",
"debugLog_appTitle": "Журнал отладки приложения",
"debugLog_bleTitle": "Журнал отладки BLE",
"debugLog_copyLog": "Копировать журнал",
"debugLog_clearLog": "Очистить журнал",
"debugLog_copied": "Журнал отладки скопирован",
"debugLog_bleCopied": "Журнал BLE скопирован",
"debugLog_noEntries": "Журнал отладки пока пуст",
"debugLog_enableInSettings": "Включите запись журнала отладки в настройках",
"debugLog_frames": "Фреймы",
"debugLog_rawLogRx": "Сырой журнал приёма",
"debugLog_noBleActivity": "Активность BLE пока отсутствует",
"debugFrame_length": "Длина фрейма: {count} байт",
"debugFrame_command": "Команда: 0x{value}",
"debugFrame_textMessageHeader": "Фрейм текстового сообщения:",
"debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}",
"debugFrame_timestamp": "- Временная метка: {timestamp}",
"debugFrame_flags": "- Флаги: 0x{value}",
"debugFrame_textType": "- Тип текста: {type} ({label})",
"debugFrame_textTypeCli": "CLI",
"debugFrame_textTypePlain": "Обычный",
"debugFrame_text": "- Текст: \"{text}\"",
"debugFrame_hexDump": "Шестнадцатеричный дамп:",
"chat_pathManagement": "Управление маршрутами",
"chat_routingMode": "Режим маршрутизации",
"chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"chat_forceFloodMode": "Принудительный режим рассылки",
"chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):",
"chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.",
"chat_hopSingular": "хоп",
"chat_hopPlural": "хопов",
"chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
"chat_successes": "успешно",
"chat_removePath": "Удалить маршрут",
"chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.",
"chat_pathActions": "Действия с маршрутом:",
"chat_setCustomPath": "Указать маршрут вручную",
"chat_setCustomPathSubtitle": "Вручную задать маршрут передачи",
"chat_clearPath": "Очистить маршрут",
"chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке",
"chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.",
"chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения",
"chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.",
"chat_fullPath": "Полный маршрут",
"chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.",
"chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}",
"chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.",
"chat_pathDeviceConfirmed": "Подтверждено устройством.",
"chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.",
"chat_type": "Тип",
"chat_path": "Маршрут",
"chat_publicKey": "Публичный ключ",
"chat_compressOutgoingMessages": "Сжимать исходящие сообщения",
"chat_floodForced": "Рассылка (принудительно)",
"chat_directForced": "Прямой (принудительно)",
"chat_hopsForced": "{count} хоп(ов) (принудительно)",
"chat_floodAuto": "Рассылка (авто)",
"chat_direct": "Прямой",
"chat_poiShared": "Точка интереса отправлена",
"chat_unread": "Непрочитанных: {count}",
"map_title": "Карта нод",
"map_noNodesWithLocation": "Нет нод с данными о местоположении",
"map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте",
"map_nodesCount": "Нод: {count}",
"map_pinsCount": "Меток: {count}",
"map_chat": "Чат",
"map_repeater": "Репитер",
"map_room": "Комната",
"map_sensor": "Сенсор",
"map_pinDm": "Метка (ЛС)",
"map_pinPrivate": "Метка (Приватная)",
"map_pinPublic": "Метка (Публичная)",
"map_lastSeen": "Последнее появление",
"map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
"map_from": "От",
"map_source": "Источник",
"map_flags": "Флаги",
"map_shareMarkerHere": "Поделиться меткой здесь",
"map_pinLabel": "Метка",
"map_label": "Подпись",
"map_pointOfInterest": "Точка интереса",
"map_sendToContact": "Отправить контакту",
"map_sendToChannel": "Отправить в канал",
"map_noChannelsAvailable": "Нет доступных каналов",
"map_publicLocationShare": "Публичная передача местоположения",
"map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.",
"map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками",
"map_filterNodes": "Фильтр нод",
"map_nodeTypes": "Типы нод",
"map_chatNodes": "Чат-ноды",
"map_repeaters": "Репитеры",
"map_otherNodes": "Другие ноды",
"map_keyPrefix": "Префикс ключа",
"map_filterByKeyPrefix": "Фильтр по префиксу ключа",
"map_publicKeyPrefix": "Префикс публичного ключа",
"map_markers": "Метки",
"map_showSharedMarkers": "Показывать общие метки",
"map_lastSeenTime": "Время последнего появления",
"map_sharedPin": "Общая метка",
"map_joinRoom": "Присоединиться к комнате",
"map_manageRepeater": "Управление репитером",
"mapCache_title": "Кэш офлайн-карты",
"mapCache_selectAreaFirst": "Сначала выберите область для кэширования",
"mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области",
"mapCache_downloadTilesTitle": "Загрузить плитки",
"mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?",
"mapCache_downloadAction": "Загрузить",
"mapCache_cachedTiles": "Закэшировано {count} плиток",
"mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)",
"mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш",
"mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?",
"mapCache_offlineCacheCleared": "Офлайн-кэш очищен",
"mapCache_noAreaSelected": "Область не выбрана",
"mapCache_cacheArea": "Область кэширования",
"mapCache_useCurrentView": "Использовать текущий вид",
"mapCache_zoomRange": "Диапазон масштаба",
"mapCache_estimatedTiles": "Оценочное количество плиток: {count}",
"mapCache_downloadedTiles": "Загружено {completed} из {total}",
"mapCache_downloadTilesButton": "Загрузить плитки",
"mapCache_clearCacheButton": "Очистить кэш",
"mapCache_failedDownloads": "Неудачных загрузок: {count}",
"mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
"time_justNow": "Только что",
"time_minutesAgo": "{minutes} мин назад",
"time_hoursAgo": "{hours} ч назад",
"time_daysAgo": "{days} дн. назад",
"time_hour": "час",
"time_hours": "часов",
"time_day": "день",
"time_days": "дней",
"time_week": "неделя",
"time_weeks": "недель",
"time_month": "месяц",
"time_months": "месяцев",
"time_minutes": "минут",
"time_allTime": "Всё время",
"dialog_disconnect": "Отключиться",
"dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?",
"login_repeaterLogin": "Вход в репитер",
"login_roomLogin": "Вход на сервер комнат",
"login_password": "Пароль",
"login_enterPassword": "Введите пароль",
"login_savePassword": "Сохранить пароль",
"login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве",
"login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.",
"login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.",
"login_routing": "Маршрутизация",
"login_routingMode": "Режим маршрутизации",
"login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"login_forceFloodMode": "Принудительный режим рассылки",
"login_managePaths": "Управление маршрутами",
"login_login": "Войти",
"login_attempt": "Попытка {current}/{max}",
"login_failed": "Ошибка входа: {error}",
"login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.",
"common_reload": "Обновить",
"common_clear": "Очистить",
"path_currentPath": "Текущий маршрут: {path}",
"path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}",
"path_enterCustomPath": "Введите маршрут вручную",
"path_currentPathLabel": "Текущий маршрут",
"path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.",
"path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)",
"path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)",
"path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)",
"path_selectFromContacts": "Или выберите из контактов:",
"path_noRepeatersFound": "Репитеры или серверы комнат не найдены.",
"path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.",
"path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}",
"path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.",
"path_setPath": "Установить маршрут",
"repeater_management": "Управление репитером",
"room_management": "Управление сервером комнат",
"repeater_managementTools": "Инструменты управления",
"repeater_status": "Статус",
"repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера",
"repeater_telemetry": "Телеметрия",
"repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Отправка команд репитеру",
"repeater_neighbours": "Соседи",
"repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.",
"repeater_settings": "Настройки",
"repeater_settingsSubtitle": "Настройка параметров репитера",
"repeater_statusTitle": "Статус репитера",
"repeater_routingMode": "Режим маршрутизации",
"repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)",
"repeater_forceFloodMode": "Принудительный режим рассылки",
"repeater_pathManagement": "Управление маршрутами",
"repeater_refresh": "Обновить",
"repeater_statusRequestTimeout": "Время ожидания статуса истекло.",
"repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}",
"repeater_systemInformation": "Системная информация",
"repeater_battery": "Батарея",
"repeater_clockAtLogin": "Время (при входе)",
"repeater_uptime": "Время работы",
"repeater_queueLength": "Длина очереди",
"repeater_debugFlags": "Флаги отладки",
"repeater_radioStatistics": "Радиостатистика",
"repeater_lastRssi": "Последний RSSI",
"repeater_lastSnr": "Последний SNR",
"repeater_noiseFloor": "Уровень шума",
"repeater_txAirtime": "Время эфира (передача)",
"repeater_rxAirtime": "Время эфира (приём)",
"repeater_packetStatistics": "Статистика пакетов",
"repeater_sent": "Отправлено",
"repeater_received": "Получено",
"repeater_duplicates": "Дубликаты",
"repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с",
"repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}",
"repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}",
"repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}",
"repeater_duplicatesTotal": "Всего: {total}",
"repeater_settingsTitle": "Настройки репитера",
"repeater_basicSettings": "Основные настройки",
"repeater_repeaterName": "Имя репитера",
"repeater_repeaterNameHelper": "Отображаемое имя этого репитера",
"repeater_adminPassword": "Пароль администратора",
"repeater_adminPasswordHelper": "Пароль с полным доступом",
"repeater_guestPassword": "Гостевой пароль",
"repeater_guestPasswordHelper": "Пароль для доступа только для чтения",
"repeater_radioSettings": "Настройки радио",
"repeater_frequencyMhz": "Частота (МГц)",
"repeater_frequencyHelper": "3002500 МГц",
"repeater_txPower": "Мощность передачи",
"repeater_txPowerHelper": "130 дБм",
"repeater_bandwidth": "Полоса пропускания",
"repeater_spreadingFactor": "Коэффициент расширения",
"repeater_codingRate": "Коэффициент кодирования",
"repeater_locationSettings": "Настройки местоположения",
"repeater_latitude": "Широта",
"repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)",
"repeater_longitude": "Долгота",
"repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)",
"repeater_features": "Функции",
"repeater_packetForwarding": "Пересылка пакетов",
"repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты",
"repeater_guestAccess": "Гостевой доступ",
"repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения",
"repeater_privacyMode": "Режим конфиденциальности",
"repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях",
"repeater_advertisementSettings": "Настройки анонсирования",
"repeater_localAdvertInterval": "Интервал локальных анонсирований",
"repeater_localAdvertIntervalMinutes": "{minutes} минут",
"repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)",
"repeater_floodAdvertIntervalHours": "{hours} часов",
"repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований",
"repeater_dangerZone": "Опасная зона",
"repeater_rebootRepeater": "Перезагрузить репитер",
"repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера",
"repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?",
"repeater_regenerateIdentityKey": "Пересоздать ключ идентификации",
"repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей",
"repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?",
"repeater_eraseFileSystem": "Стереть файловую систему",
"repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера",
"repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!",
"repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.",
"repeater_commandSent": "Команда отправлена: {command}",
"repeater_errorSendingCommand": "Ошибка отправки команды: {error}",
"repeater_confirm": "Подтвердить",
"repeater_settingsSaved": "Настройки успешно сохранены",
"repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}",
"repeater_refreshBasicSettings": "Обновить основные настройки",
"repeater_refreshRadioSettings": "Обновить настройки радио",
"repeater_refreshTxPower": "Обновить мощность передачи",
"repeater_refreshLocationSettings": "Обновить настройки местоположения",
"repeater_refreshPacketForwarding": "Обновить пересылку пакетов",
"repeater_refreshGuestAccess": "Обновить гостевой доступ",
"repeater_refreshPrivacyMode": "Обновить режим конфиденциальности",
"repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований",
"repeater_refreshed": "{label} обновлён",
"repeater_errorRefreshing": "Ошибка обновления {label}",
"repeater_cliTitle": "CLI репитера",
"repeater_debugNextCommand": "Отладка следующей команды",
"repeater_commandHelp": "Справка по командам",
"repeater_clearHistory": "Очистить историю",
"repeater_noCommandsSent": "Команды ещё не отправлялись",
"repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды",
"repeater_enterCommandHint": "Введите команду...",
"repeater_previousCommand": "Предыдущая команда",
"repeater_nextCommand": "Следующая команда",
"repeater_enterCommandFirst": "Сначала введите команду",
"repeater_cliCommandFrameTitle": "Фрейм CLI-команды",
"repeater_cliCommandError": "Ошибка: {error}",
"repeater_cliQuickGetName": "Получить имя",
"repeater_cliQuickGetRadio": "Получить радио",
"repeater_cliQuickGetTx": "Получить TX",
"repeater_cliQuickNeighbors": "Соседи",
"repeater_cliQuickVersion": "Версия",
"repeater_cliQuickAdvertise": "Анонсировать",
"repeater_cliQuickClock": "Время",
"repeater_cliHelpAdvert": "Отправляет пакет анонсирования",
"repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)",
"repeater_cliHelpClock": "Показывает текущее время по часам устройства.",
"repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.",
"repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.",
"repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.",
"repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.",
"repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)",
"repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.",
"repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)",
"repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)",
"repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.",
"repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.",
"repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».",
"repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.",
"repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.",
"repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)",
"repeater_cliHelpSetName": "Устанавливает имя в оповещениях.",
"repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)",
"repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)",
"repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.",
"repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.",
"repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).",
"repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.",
"repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.",
"repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.",
"repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.",
"repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.",
"repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.",
"repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).",
"repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).",
"repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)",
"repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow",
"repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.",
"repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.",
"repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.",
"repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4",
"repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.",
"repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.",
"repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.",
"repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»",
"repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.",
"repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)",
"repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)",
"repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)",
"repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)",
"repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.",
"repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.",
"repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.",
"repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.",
"repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.",
"repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.",
"repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек",
"repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.",
"repeater_commandsListTitle": "Список команд",
"repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».",
"repeater_general": "Общие",
"repeater_settingsCategory": "Настройки",
"repeater_bridge": "Мост",
"repeater_logging": "Журналирование",
"repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)",
"repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)",
"repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.",
"repeater_gpsManagement": "Управление GPS",
"repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.",
"telemetry_receivedData": "Полученные телеметрические данные",
"telemetry_requestTimeout": "Время ожидания телеметрии истекло.",
"telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}",
"telemetry_noData": "Данные телеметрии недоступны.",
"telemetry_channelTitle": "Канал {channel}",
"telemetry_batteryLabel": "Батарея",
"telemetry_voltageLabel": "Напряжение",
"telemetry_mcuTemperatureLabel": "Температура МК",
"telemetry_temperatureLabel": "Температура",
"telemetry_currentLabel": "Ток",
"telemetry_batteryValue": "{percent}% / {volts}В",
"telemetry_voltageValue": "{volts}В",
"telemetry_currentValue": "{amps}А",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
"neighbors_repeatersNeighbours": "Соседи репитеров",
"neighbors_noData": "Данные о соседях недоступны.",
"neighbors_unknownContact": "Неизвестный {pubkey}",
"neighbors_heardA ago": "Слышали: {time} назад",
"channelPath_title": "Путь пакета",
"channelPath_viewMap": "Посмотреть на карте",
"channelPath_otherObservedPaths": "Другие наблюдаемые пути",
"channelPath_repeaterHops": "Хопы через репитеры",
"channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.",
"channelPath_messageDetails": "Детали сообщения",
"channelPath_senderLabel": "Отправитель",
"channelPath_timeLabel": "Время",
"channelPath_repeatsLabel": "Повторы",
"channelPath_pathLabel": "Путь {index}",
"channelPath_observedLabel": "Наблюдаемый",
"channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}",
"channelPath_noLocationData": "Нет данных о местоположении",
"channelPath_timeWithDate": "{day}/{month} {time}",
"channelPath_timeOnly": "{time}",
"channelPath_unknownPath": "Неизвестный",
"channelPath_floodPath": "Рассылка",
"channelPath_directPath": "Прямой",
"channelPath_observedZeroOf": "0 из {total} хопов",
"channelPath_observedSomeOf": "{observed} из {total} хопов",
"channelPath_mapTitle": "Карта пути",
"channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.",
"channelPath_primaryPath": "Путь {index} (Основной)",
"channelPath_pathLabelTitle": "Путь",
"channelPath_observedPathHeader": "Наблюдаемый путь",
"channelPath_selectedPathLabel": "{label} • {prefixes}",
"channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.",
"channelPath_unknownRepeater": "Неизвестный репитер",
"community_title": "Сообщество",
"community_create": "Создать сообщество",
"community_createDesc": "Создать новое сообщество и поделиться через QR-код.",
"community_join": "Присоединиться",
"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_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_deleted": "Покинули сообщество \"{name}\"",
"community_regenerateSecret": "Пересоздать секрет",
"community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.",
"community_regenerate": "Пересоздать",
"community_secretRegenerated": "Секрет пересоздан для \"{name}\"",
"community_updateSecret": "Обновить секрет",
"community_secretUpdated": "Секрет обновлён для \"{name}\"",
"community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"",
"community_addHashtagChannel": "Добавить хэштег-канал сообщества",
"community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества",
"community_selectCommunity": "Выбрать сообщество",
"community_regularHashtag": "Обычный хэштег",
"community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)",
"community_communityHashtag": "Хэштег сообщества",
"community_communityHashtagDesc": "Доступен только участникам сообщества",
"community_forCommunity": "Для {name}",
"listFilter_tooltip": "Фильтр и сортировка",
"listFilter_sortBy": "Сортировка по",
"listFilter_latestMessages": "Последние сообщения",
"listFilter_heardRecently": "Слышали недавно",
"listFilter_az": "По алфавиту",
"listFilter_filters": "Фильтры",
"listFilter_all": "Все",
"listFilter_users": "Пользователи",
"listFilter_repeaters": "Репитеры",
"listFilter_roomServers": "Серверы комнат",
"listFilter_unreadOnly": "Только непрочитанные",
"listFilter_newGroup": "Новая группа",
"@chat_couldNotOpenLink": {
"placeholders": {
"url": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"chat_open": "Открыть",
"chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}",
"chat_openLink": "Открыть ссылку?",
"chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?",
"neighbors_heardAgo": "Слушал(а): {time} назад",
"chat_invalidLink": "Неправильный формат ссылки",
"@contacts_pathTraceTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"pathTrace_you": "Вы",
"pathTrace_failed": "Путь трассировки не выполнен.",
"pathTrace_notAvailable": "Трассировка пути недоступна.",
"pathTrace_refreshTooltip": "Обновить Path Trace",
"contacts_pathTrace": "Трассировка пути",
"contacts_ping": "Пинговать",
"contacts_repeaterPathTrace": "Отследить путь к ретранслятору",
"contacts_repeaterPing": "Пинговать повторитель",
"contacts_roomPathTrace": "Трассировка пути к серверу комнаты",
"contacts_roomPing": "Пинговать сервер комнаты",
"contacts_chatTraceRoute": "Трассировка маршрута",
"contacts_pathTraceTo": "Показать маршрут к {name}",
"contacts_contactImported": "Контакт был импортирован",
"contacts_contactImportFailed": "Контакт не удалось импортировать",
"contacts_invalidAdvertFormat": "Недействительные контактные данные",
"contacts_zeroHopAdvert": "Реклама Zero Hop",
"appSettings_languageUk": "Українська",
"contacts_floodAdvert": "Рекламный поток",
"contacts_clipboardEmpty": "Буфер обмена пуст.",
"contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена",
"contacts_ShareContact": "Копировать контакт в буфер обмена",
"contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.",
"contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.",
"contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.",
"contacts_addContactFromClipboard": "Добавить контакт из буфера обмена",
"contacts_ShareContactZeroHop": "Поделиться контактом по объявлению",
"contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.",
"notification_activityTitle": "Активность MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}",
"notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}",
"notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}",
"notification_newTypeDiscovered": "Обнаружен новый {contactType}",
"notification_receivedNewMessage": "Получено новое сообщение",
"settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX",
"settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.",
"settings_gpxExportContacts": "Экспортировать спутников в GPX",
"settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС",
"settings_gpxExportError": "Произошла ошибка при экспорте.",
"settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат",
"settings_gpxExportChat": "Местоположения спутников",
"settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.",
"settings_gpxExportAll": "Экспортировать все контакты в GPX",
"settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.",
"settings_gpxExportAllContacts": "Все местоположения контактов",
"settings_gpxExportSuccess": "Успешно экспортирован файл GPX.",
"settings_gpxExportNoContacts": "Нет контактов для экспорта.",
"settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open",
"settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX",
"pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!",
"map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.",
"map_removeLast": "Удалить последний",
"map_pathTraceCancelled": "Отмена трассировки пути",
"pathTrace_clearTooltip": "Очистить путь",
"map_runTrace": "Запустить трассировку пути",
"scanner_enableBluetooth": "Включите Bluetooth",
"scanner_bluetoothOff": "Bluetooth выключен",
"scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.",
"settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.",
"settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.",
"settings_clientRepeat": "Повторение \"вне сети\""
}
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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.",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
@@ -1484,5 +1493,108 @@
"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_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",
"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",
"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."
}
+255 -143
View File
@@ -1,7 +1,7 @@
{
"@@locale": "sl",
"appTitle": "MeshCore Open",
"nav_contacts": "Kontakti",
"nav_contacts": "Stiki",
"nav_channels": "Kanali",
"nav_map": "Karta",
"common_cancel": "Prekliči",
@@ -69,49 +69,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 +121,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 +151,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,8 +169,8 @@
"appSettings_languageNl": "Nederlands",
"appSettings_languageSk": "Slovenčina",
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"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",
@@ -185,19 +180,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 +200,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 +221,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 +241,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 oseba 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 +261,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 +274,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 +285,11 @@
}
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena oseba 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 +297,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 +306,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 +316,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 +329,13 @@
}
},
"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_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 +419,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 +428,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 +443,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 +537,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 +549,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,6 +599,18 @@
}
}
},
"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 prikazao na zemljeobrazniku.",
@@ -1092,13 +1099,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.",
@@ -1130,7 +1137,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.",
@@ -1159,8 +1166,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.",
@@ -1232,9 +1239,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}",
@@ -1466,23 +1473,128 @@
"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": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
"community_manageCommunities": "Upravljajte skupnosti",
"community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.",
"community_manageCommunities": "Upravljanje skupnosti",
"community_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapustiti \"{name}\"?",
"community_deleteConfirm": "Zapusti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
"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 (kateri koli lahko sodelujejo)",
"community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"community_forCommunity": "Za {name}"
"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",
"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",
"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"
}
+119 -7
View File
@@ -131,9 +131,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 +140,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": {
@@ -604,6 +599,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",
@@ -1473,7 +1480,9 @@
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
"count": {
"type": "int"
}
}
},
"community_deleted": "Lämnade community \"{name}\"",
@@ -1484,5 +1493,108 @@
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"community_communityHashtag": "Community Hashtag"
"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",
"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",
"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."
}
+1600
View File
File diff suppressed because it is too large Load Diff
+602 -490
View File
File diff suppressed because it is too large Load Diff
+31 -12
View File
@@ -60,21 +60,24 @@ 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,
),
);
}
class MeshCoreApp extends StatelessWidget {
@@ -124,10 +127,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 +143,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(),
);
},
+18 -11
View File
@@ -76,13 +76,14 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
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,
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,11 +91,13 @@ 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()),
) ??
{},
@@ -132,8 +135,9 @@ class AppSettings {
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapCacheBounds:
mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map<String, double>?,
mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds
: mapCacheBounds as Map<String, double>?,
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
@@ -141,12 +145,15 @@ 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,
);
}
}
+3 -5
View File
@@ -9,11 +9,13 @@ 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);
@@ -39,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) {
+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});
}
+4 -8
View File
@@ -34,10 +34,7 @@ class Community {
}) : hashtagChannels = hashtagChannels ?? [];
/// Generate a new community with a random 32-byte secret
factory Community.create({
required String id,
required String name,
}) {
factory Community.create({required String id, required String name}) {
final random = Random.secure();
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
@@ -84,7 +81,8 @@ class Community {
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>?)
hashtagChannels:
(json['hashtag_channels'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
@@ -234,9 +232,7 @@ class Community {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Community &&
runtimeType == other.runtimeType &&
id == other.id;
other is Community && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
+54 -5
View File
@@ -7,7 +7,8 @@ class Contact {
final int type;
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;
@@ -78,8 +79,12 @@ class Contact {
type: type ?? this.type,
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 +98,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 (pathLength <= 0) {
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);
+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;
+31 -9
View File
@@ -26,8 +26,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 +55,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 +63,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 +82,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 +117,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);
}
+146 -57
View File
@@ -43,7 +43,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 +61,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,7 +71,12 @@ 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),
),
@@ -75,7 +85,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildNotificationsCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildNotificationsCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -90,17 +103,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 +131,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 +146,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 +177,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 +208,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 +240,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 +258,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 +286,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 +301,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,7 +352,9 @@ 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),
@@ -332,6 +384,7 @@ class AppSettingsScreen extends StatelessWidget {
);
}
// Fixed rendering issues
Widget _buildBatteryCard(
BuildContext context,
AppSettingsService settingsService,
@@ -339,13 +392,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 +408,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 +464,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 +547,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 +630,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 +652,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 +675,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 +706,10 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildDebugCard(BuildContext context, AppSettingsService settingsService) {
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -644,9 +731,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),
),
);
+46 -18
View File
@@ -24,7 +24,9 @@ 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),
@@ -36,15 +38,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 +78,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 +97,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 +112,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 +151,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 +213,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 +270,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;
+365 -243
View File
@@ -3,11 +3,15 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/link_handler.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
@@ -15,6 +19,7 @@ import '../models/channel_message.dart';
import '../utils/emoji_utils.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -22,10 +27,7 @@ import 'map_screen.dart';
class ChannelChatScreen extends StatefulWidget {
final Channel channel;
const ChannelChatScreen({
super.key,
required this.channel,
});
const ChannelChatScreen({super.key, required this.channel});
@override
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
@@ -33,42 +35,54 @@ class ChannelChatScreen extends StatefulWidget {
class _ChannelChatScreenState extends State<ChannelChatScreen> {
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final ChatScrollController _scrollController = ChatScrollController();
final FocusNode _textFieldFocusNode = FocusNode();
ChannelMessage? _replyingToMessage;
final Map<String, GlobalKey> _messageKeys = {};
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
@override
void initState() {
super.initState();
_textFieldFocusNode.addListener(_onTextFieldFocusChange);
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<MeshCoreConnector>().setActiveChannel(widget.channel.index);
// Scroll to bottom when opening channel chat - use SchedulerBinding for next frame
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
_connector = context.read<MeshCoreConnector>();
_connector?.setActiveChannel(widget.channel.index);
});
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
}
}
Future<void> _loadOlderMessages() async {
if (_isLoadingOlder) return;
setState(() => _isLoadingOlder = true);
final connector = context.read<MeshCoreConnector>();
await connector.loadOlderChannelMessages(widget.channel.index);
if (mounted) {
setState(() => _isLoadingOlder = false);
}
}
@override
void dispose() {
context.read<MeshCoreConnector>().setActiveChannel(null);
_connector?.setActiveChannel(null);
_textFieldFocusNode.removeListener(_onTextFieldFocusChange);
_textFieldFocusNode.dispose();
_textController.dispose();
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void _setReplyingTo(ChannelMessage message) {
setState(() {
_replyingToMessage = message;
@@ -121,15 +135,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [
Text(
widget.channel.name.isEmpty
? context.l10n.channels_channelIndex(widget.channel.index)
? context.l10n.channels_channelIndex(
widget.channel.index,
)
: widget.channel.name,
style: const TextStyle(fontSize: 16),
),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final unreadCount =
connector.getUnreadCountForChannelIndex(widget.channel.index);
final privacy = widget.channel.isPublicChannel ? context.l10n.channels_public : context.l10n.channels_private;
final unreadCount = connector
.getUnreadCountForChannelIndex(widget.channel.index);
final privacy = widget.channel.isPublicChannel
? context.l10n.channels_public
: context.l10n.channels_private;
return Text(
'$privacy${context.l10n.chat_unread(unreadCount)}',
overflow: TextOverflow.ellipsis,
@@ -153,10 +171,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
builder: (context, connector, child) {
final messages = connector.getChannelMessages(widget.channel);
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
if (messages.isEmpty) {
return Center(
child: Column(
@@ -190,20 +204,52 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
// Reverse messages so newest appear at bottom with reverse: true
final reversedMessages = messages.reversed.toList();
final itemCount =
reversedMessages.length + (_isLoadingOlder ? 1 : 0);
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.scrollToBottomIfAtBottom();
});
return Stack(
children: [
ListView.builder(
reverse: true, // List grows from bottom up
controller: _scrollController,
padding: const EdgeInsets.all(8),
itemCount: itemCount,
itemBuilder: (context, index) {
// Loading indicator now appears at end (bottom) of reversed list
if (_isLoadingOlder && index == itemCount - 1) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
),
JumpToBottomButton(scrollController: _scrollController),
],
);
},
),
@@ -221,15 +267,21 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final poi = _parsePoiMessage(message.text);
final displayPath = message.pathBytes.isNotEmpty
? message.pathBytes
: (message.pathVariants.isNotEmpty ? message.pathVariants.first : Uint8List(0));
: (message.pathVariants.isNotEmpty
? message.pathVariants.first
: Uint8List(0));
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Column(
crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start,
crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
@@ -241,97 +293,160 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
onTap: () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isOutgoing) ...[
Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
else if (gifId != null)
GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
fallbackTextColor: isOutgoing
? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
)
else
Text(
message.text,
style: const TextStyle(fontSize: 14),
),
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'via ${_formatPathPrefixes(displayPath)}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
padding: gifId != null
? const EdgeInsets.all(4)
: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.65,
),
decoration: BoxDecoration(
color: isOutgoing
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
if (!isOutgoing) ...[
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
top: 4,
bottom: 4,
)
: EdgeInsets.zero,
child: Text(
message.senderName,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
),
if (gifId == null) const SizedBox(height: 4),
],
if (message.replyToMessageId != null) ...[
_buildReplyPreview(message),
const SizedBox(height: 8),
],
if (poi != null)
_buildPoiMessage(context, poi, isOutgoing)
else if (gifId != null)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Colors.transparent,
fallbackTextColor: isOutgoing
? Theme.of(context)
.colorScheme
.onPrimaryContainer
.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.6),
),
)
else
Linkify(
text: message.text,
style: const TextStyle(fontSize: 14),
linkStyle: const TextStyle(
fontSize: 14,
color: Colors.green,
decoration: TextDecoration.underline,
),
options: const LinkifyOptions(
humanize: false,
defaultToHttps: false,
),
linkifiers: const [UrlLinkifier()],
onOpen: (link) =>
LinkHandler.handleLinkTap(context, link.url),
),
if (displayPath.isNotEmpty) ...[
const SizedBox(height: 4),
Padding(
padding: gifId != null
? const EdgeInsets.symmetric(horizontal: 8)
: EdgeInsets.zero,
child: Text(
'via ${_formatPathPrefixes(displayPath)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
),
],
const SizedBox(height: 4),
Padding(
padding: gifId != null
? const EdgeInsets.only(
left: 8,
right: 8,
bottom: 4,
)
: EdgeInsets.zero,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
if (message.repeatCount > 0) ...[
const SizedBox(width: 6),
Icon(
Icons.repeat,
size: 12,
color: Colors.grey[600],
),
const SizedBox(width: 2),
Text(
'${message.repeatCount}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
if (isOutgoing) ...[
const SizedBox(width: 4),
Icon(
message.status == ChannelMessageStatus.sent
? Icons.check
: message.status ==
ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color:
message.status ==
ChannelMessageStatus.failed
? Colors.red
: Colors.grey[600],
),
],
],
),
),
if (message.repeatCount > 0) ...[
const SizedBox(width: 6),
Icon(Icons.repeat, size: 12, color: Colors.grey[600]),
const SizedBox(width: 2),
Text(
'${message.repeatCount}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
if (isOutgoing) ...[
const SizedBox(width: 4),
Icon(
message.status == ChannelMessageStatus.sent
? Icons.check
: message.status == ChannelMessageStatus.pending
? Icons.schedule
: Icons.error_outline,
size: 14,
color: message.status == ChannelMessageStatus.failed
? Colors.red
: Colors.grey[600],
),
],
],
),
],
),
),
),
),
),
],
),
if (message.reactions.isNotEmpty) ...[
@@ -364,8 +479,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: colorScheme.surfaceContainerHighest,
fallbackTextColor: previewTextColor,
width: 120,
height: 80,
maxSize: 80,
),
);
} else if (poi != null) {
@@ -373,7 +487,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [
Icon(Icons.location_on_outlined, size: 14, color: previewTextColor),
const SizedBox(width: 4),
Text(context.l10n.chat_location, style: TextStyle(fontSize: 12, color: previewTextColor)),
Text(
context.l10n.chat_location,
style: TextStyle(fontSize: 12, color: previewTextColor),
),
],
);
} else {
@@ -397,10 +514,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border(
left: BorderSide(
color: colorScheme.primary,
width: 3,
),
left: BorderSide(color: colorScheme.primary, width: 3),
),
),
child: Column(
@@ -438,17 +552,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
color: Theme.of(
context,
).colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
emoji,
style: const TextStyle(fontSize: 16),
),
Text(emoji, style: const TextStyle(fontSize: 16)),
if (count > 1) ...[
const SizedBox(width: 4),
Text(
@@ -475,7 +588,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim();
final match = RegExp(r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|').firstMatch(trimmed);
final match = RegExp(
r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|',
).firstMatch(trimmed);
if (match == null) return null;
final lat = double.tryParse(match.group(1) ?? '');
final lon = double.tryParse(match.group(2) ?? '');
@@ -486,10 +601,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
final colorScheme = Theme.of(context).colorScheme;
final textColor =
isOutgoing ? colorScheme.onPrimaryContainer : colorScheme.onSurface;
final textColor = isOutgoing
? colorScheme.onPrimaryContainer
: colorScheme.onSurface;
final metaColor = textColor.withValues(alpha: 0.7);
final channelColor = widget.channel.isPublicChannel ? Colors.orange : Colors.blue;
final channelColor = widget.channel.isPublicChannel
? Colors.orange
: Colors.blue;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -517,18 +635,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [
Text(
context.l10n.chat_poiShared,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
),
if (poi.label.isNotEmpty)
Text(
poi.label,
style: TextStyle(
color: metaColor,
fontSize: 12,
),
style: TextStyle(color: metaColor, fontSize: 12),
),
],
),
@@ -605,10 +717,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1),
),
),
child: Row(
@@ -637,7 +746,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
),
),
],
@@ -675,69 +786,76 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
],
),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.gif_box),
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Row(
children: [
Expanded(
child: GifMessage(
url: 'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
fallbackTextColor:
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
width: 160,
height: 110,
children: [
IconButton(
icon: const Icon(Icons.gif_box),
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
builder: (context, value, child) {
final gifId = _parseGifId(value.text);
if (gifId != null) {
return Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GifMessage(
url:
'https://media.giphy.com/media/$gifId/giphy.gif',
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
fallbackTextColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
maxSize: 160,
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
),
],
);
}
return TextField(
controller: _textController,
focusNode: _textFieldFocusNode,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => _textController.clear(),
),
],
);
}
return TextField(
controller: _textController,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(maxBytes),
],
decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
);
},
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
color: Theme.of(context).colorScheme.primary,
),
],
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
);
},
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
onPressed: _sendMessage,
color: Theme.of(context).colorScheme.primary,
),
],
),
),
],
@@ -803,14 +921,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_setReplyingTo(message);
},
),
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message);
},
),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
leading: const Icon(Icons.add_reaction_outlined),
title: Text(context.l10n.chat_addReaction),
onTap: () {
Navigator.pop(sheetContext);
_showEmojiPicker(message);
},
),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.common_copy),
@@ -852,25 +972,31 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
void _sendReaction(ChannelMessage message, String emoji) {
final connector = context.read<MeshCoreConnector>();
// Send reaction with full messageId to find target, but parser will extract
// lightweight reactionKey (timestamp_senderPrefix) for deduplication
final reactionText = 'r:${message.messageId}:$emoji';
final emojiIndex = ReactionHelper.emojiToIndex(emoji);
if (emojiIndex == null) return; // Unknown emoji, skip
final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000;
final hash = ReactionHelper.computeReactionHash(
timestampSecs,
message.senderName,
message.text,
);
final reactionText = 'r:$hash:$emojiIndex';
connector.sendChannelMessage(widget.channel, reactionText);
}
void _copyMessageText(String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageCopied)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied)));
}
Future<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().deleteChannelMessage(message);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageDeleted)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted)));
}
String _formatPathPrefixes(Uint8List pathBytes) {
@@ -885,9 +1011,5 @@ class _PoiInfo {
final double lon;
final String label;
const _PoiInfo({
required this.lat,
required this.lon,
required this.label,
});
const _PoiInfo({required this.lat, required this.lon, required this.label});
}
+150 -66
View File
@@ -4,6 +4,7 @@ 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';
@@ -17,17 +18,17 @@ import '../models/contact.dart';
class ChannelMessagePathScreen extends StatelessWidget {
final ChannelMessage message;
const ChannelMessagePathScreen({
super.key,
required this.message,
});
const ChannelMessagePathScreen({super.key, required this.message});
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final l10n = context.l10n;
final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants);
final primaryPath = _selectPrimaryPath(
message.pathBytes,
message.pathVariants,
);
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
@@ -41,6 +42,21 @@ class ChannelMessagePathScreen extends StatelessWidget {
appBar: AppBar(
title: Text(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: Uint8List.fromList(primaryPath),
flipPathRound: true,
reversePathRound: true,
),
),
),
),
IconButton(
icon: const Icon(Icons.map_outlined),
tooltip: l10n.channelPath_viewMap,
@@ -88,10 +104,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 +118,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,
@@ -163,7 +183,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,
),
),
@@ -239,7 +259,6 @@ class ChannelMessagePathScreen extends StatelessWidget {
),
);
}
}
class ChannelMessagePathMapScreen extends StatefulWidget {
@@ -257,8 +276,10 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
_ChannelMessagePathMapScreenState();
}
class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScreen> {
class _ChannelMessagePathMapScreenState
extends State<ChannelMessagePathMapScreen> {
Uint8List? _selectedPath;
double _pathDistance = 0.0;
@override
void initState() {
@@ -270,32 +291,58 @@ 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 tileCache = context.read<MapTileCacheService>();
final primaryPath =
_selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants);
final observedPaths =
_buildObservedPaths(primaryPath, widget.message.pathVariants);
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
widget.message.pathVariants,
);
final observedPaths = _buildObservedPaths(
primaryPath,
widget.message.pathVariants,
);
final selectedPath = _resolveSelectedPath(
_selectedPath,
observedPaths,
primaryPath,
);
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>[];
for (final hop in hops) {
if (hop.hasLocation) {
points.add(hop.position!);
}
}
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
final polylines = points.length > 1
? [
Polyline(
@@ -306,16 +353,20 @@ 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));
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),
),
appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)),
body: SafeArea(
top: false,
child: Stack(
@@ -334,6 +385,9 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
),
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate,
),
),
children: [
TileLayer(
@@ -343,30 +397,28 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
MarkerLayer(
markers: _buildHopMarkers(hops),
),
if (polylines.isNotEmpty)
PolylineLayer(polylines: polylines),
MarkerLayer(markers: _buildHopMarkers(hops)),
],
),
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,
),
),
),
),
@@ -448,8 +500,8 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
if (hop.hasLocation)
Marker(
point: hop.position!,
width: 40,
height: 40,
width: 35,
height: 35,
child: Container(
decoration: BoxDecoration(
color: Colors.green,
@@ -474,6 +526,39 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
),
),
),
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
Marker(
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
),
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,
),
),
),
),
];
}
@@ -496,7 +581,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
Padding(
padding: const EdgeInsets.all(12),
child: Text(
l10n.channelPath_repeaterHops,
'${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
@@ -509,7 +594,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 +610,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 +652,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 +679,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
+359 -40
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -27,10 +29,9 @@ import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination {
chat,
management,
}
enum RoomLoginDestination { chat, management }
enum ContactOperationType { import, export, zeroHopShare }
class ContactsScreen extends StatefulWidget {
final bool hideBackButton;
@@ -52,16 +53,22 @@ class _ContactsScreenState extends State<ContactsScreen>
List<ContactGroup> _groups = [];
Timer? _searchDebounce;
final Set<ContactOperationType> _pendingOperations = {};
StreamSubscription<Uint8List>? _frameSubscription;
@override
void initState() {
super.initState();
_loadGroups();
_setupFrameListener();
}
@override
void dispose() {
_searchDebounce?.cancel();
_searchController.dispose();
_frameSubscription?.cancel();
super.dispose();
}
@@ -77,6 +84,137 @@ class _ContactsScreenState extends State<ContactsScreen>
await _groupStore.saveGroups(_groups);
}
void _setupFrameListener() {
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;
final frameBuffer = BufferReader(frame);
final code = frameBuffer.readUInt8();
if (code == respCodeExportContact) {
final advertPacket = frameBuffer.readRemainingBytes();
// Validate packet has expected minimum size (98+ bytes per protocol)
if (advertPacket.length < 98) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_invalidAdvertFormat),
),
);
}
_pendingOperations.remove(ContactOperationType.export);
return;
}
final hexString = pubKeyToHex(advertPacket);
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
}
if (code == respCodeOk) {
// Show a snackbar indicating success
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImported)),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
);
}
_pendingOperations.clear();
}
if (code == respCodeErr) {
// Show a snackbar indicating failure
if (!mounted) return;
if (_pendingOperations.contains(ContactOperationType.import)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_contactImportFailed)),
);
}
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
),
);
}
if (_pendingOperations.contains(ContactOperationType.export)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
),
);
}
_pendingOperations.clear();
}
});
}
Future<void> _contactExport(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactFrame = buildExportContactFrame(pubKey);
_pendingOperations.add(ContactOperationType.export);
await connector.sendFrame(exportContactFrame);
}
Future<void> _contactZeroHop(Uint8List pubKey) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
_pendingOperations.add(ContactOperationType.zeroHopShare);
await connector.sendFrame(exportContactZeroHopFrame);
}
Future<void> _contactImport() async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final clipboardData = await Clipboard.getData('text/plain');
if (clipboardData == null || clipboardData.text == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
);
}
return;
}
final text = clipboardData.text!.trim();
if (!text.startsWith('meshcore://')) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
);
}
return;
}
final hexString = text.substring('meshcore://'.length);
try {
final importContactFrame = buildImportContactFrame(hexString);
_pendingOperations.add(ContactOperationType.import);
await connector.sendFrame(importContactFrame);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
);
}
}
}
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
@@ -96,18 +234,94 @@ class _ContactsScreenState extends State<ContactsScreen>
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
onPressed: () => _disconnect(context, connector),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.connect_without_contact),
const SizedBox(width: 8),
Text(context.l10n.contacts_zeroHopAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: false),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.settings_advertisementSent),
),
),
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.cell_tower),
const SizedBox(width: 8),
Text(context.l10n.contacts_floodAdvert),
],
),
onTap: () => {
connector.sendSelfAdvert(flood: true),
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.settings_advertisementSent),
),
),
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.copy),
const SizedBox(width: 8),
Text(context.l10n.contacts_copyAdvertToClipboard),
],
),
onTap: () => _contactExport(Uint8List.fromList([])),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.paste),
const SizedBox(width: 8),
Text(context.l10n.contacts_addContactFromClipboard),
],
),
onTap: () => _contactImport(),
),
],
icon: const Icon(Icons.connect_without_contact),
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.settings),
const SizedBox(width: 8),
Text(context.l10n.settings_title),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
icon: const Icon(Icons.more_vert),
),
],
),
@@ -313,6 +527,14 @@ class _ContactsScreenState extends State<ContactsScreen>
return matchesContactQuery(contact, _searchQuery);
}).toList();
// Filter out own node from the list
if (connector.selfPublicKey != null) {
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
filtered = filtered.where((contact) {
return contact.publicKeyHex != selfPubKeyHex;
}).toList();
}
if (_typeFilter != ContactTypeFilter.all) {
filtered = filtered.where(_matchesTypeFilter).toList();
}
@@ -489,7 +711,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => destination == RoomLoginDestination.management
builder: (context) =>
destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
: ChatScreen(contact: room),
),
@@ -752,7 +975,26 @@ class _ContactsScreenState extends State<ContactsScreen>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isRepeater)
if (isRepeater) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: contact.pathLength > 0
? context.l10n.contacts_repeaterPathTrace
: context.l10n.contacts_repeaterPing,
path: contact.traceRouteBytes ?? Uint8List(0),
),
),
);
},
),
ListTile(
leading: const Icon(Icons.cell_tower, color: Colors.orange),
title: Text(context.l10n.contacts_manageRepeater),
@@ -760,8 +1002,27 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(sheetContext);
_showRepeaterLogin(context, contact);
},
)
else if (isRoom) ...[
),
] else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: contact.pathLength > 0
? context.l10n.contacts_roomPathTrace
: context.l10n.contacts_roomPing,
path: contact.traceRouteBytes ?? Uint8List(0),
),
),
);
},
),
ListTile(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
@@ -771,14 +1032,39 @@ class _ContactsScreenState extends State<ContactsScreen>
},
),
ListTile(
leading: const Icon(Icons.room_preferences, color: Colors.orange),
leading: const Icon(
Icons.room_preferences,
color: Colors.orange,
),
title: Text(context.l10n.room_management),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact, RoomLoginDestination.management);
_showRoomLogin(
context,
contact,
RoomLoginDestination.management,
);
},
),
] else
] else ...[
if (contact.pathLength > 0)
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_pathTraceTo(
contact.name,
),
path: contact.traceRouteBytes ?? Uint8List(0),
),
),
);
},
),
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -787,6 +1073,23 @@ class _ContactsScreenState extends State<ContactsScreen>
_openChat(context, contact);
},
),
],
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.contacts_ShareContact),
onTap: () {
Navigator.pop(sheetContext);
_contactExport(contact.publicKey);
},
),
ListTile(
leading: const Icon(Icons.connect_without_contact),
title: Text(context.l10n.contacts_ShareContactZeroHop),
onTap: () {
Navigator.pop(sheetContext);
_contactZeroHop(contact.publicKey);
},
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(
@@ -852,33 +1155,49 @@ class _ContactTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name),
subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
Text(
_formatLastSeen(context, lastSeen),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
Text(contact.pathLabel),
Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)),
],
),
// Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(
MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (unreadCount > 0) ...[
UnreadBadge(count: unreadCount),
const SizedBox(height: 4),
],
Text(
_formatLastSeen(context, lastSeen),
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.hasLocation)
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
],
),
],
),
),
onTap: onTap,
onLongPress: onLongPress,
);
+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;
}
+31 -23
View File
@@ -56,10 +56,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 +69,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 +181,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 {
@@ -224,10 +224,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
: (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble();
return Scaffold(
appBar: AppBar(
title: Text(l10n.mapCache_title),
centerTitle: true,
),
appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true),
body: Column(
children: [
Expanded(
@@ -290,7 +287,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 +304,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 +314,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 +347,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(
+393 -178
View File
@@ -1,8 +1,10 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
@@ -48,9 +50,14 @@ class _MapScreenState extends State<MapScreen> {
final MapMarkerService _markerService = MapMarkerService();
final Set<String> _hiddenMarkerIds = {};
Set<String> _removedMarkerIds = {};
bool _isBuildingPathTrace = false;
bool _isSelectingPoi = false;
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
@override
void initState() {
@@ -147,6 +154,19 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation)
.toList();
_polylines.clear();
_polylines.addAll(
_points.length > 1
? [
Polyline(
points: _points,
strokeWidth: 4,
color: Colors.blueAccent,
),
]
: <Polyline>[],
);
// Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0);
double initialZoom = 10.0;
@@ -225,13 +245,15 @@ class _MapScreenState extends State<MapScreen> {
}
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
if (!_hasInitializedMap && _removedMarkersLoaded) {
_hasInitializedMap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
if (hasMapContent) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
}
}
final allowBack = !connector.isConnected;
@@ -245,105 +267,165 @@ class _MapScreenState extends State<MapScreen> {
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
onPressed: () => _disconnect(context, connector),
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
tooltip: context.l10n.contacts_pathTrace,
),
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context, connector),
),
PopupMenuItem(
child: Row(
children: [
const Icon(Icons.settings),
const SizedBox(width: 8),
Text(context.l10n.settings_title),
],
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
icon: const Icon(Icons.more_vert),
),
],
),
body: !hasMapContent
? _buildEmptyState()
: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: center,
initialZoom: initialZoom,
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate
),
onTap: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
_isSelectingPoi = false;
});
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
}
},
onLongPress: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
_isSelectingPoi = false;
});
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
return;
}
_showShareMarkerAtPositionSheet(
context: context,
connector: connector,
position: latLng,
);
},
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
MarkerLayer(
markers: [
if (highlightPosition != null)
Marker(
point: highlightPosition,
width: 40,
height: 40,
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
),
),
..._buildMarkers(contactsWithLocation, settings),
...sharedMarkers.map(_buildSharedMarker),
],
),
],
),
_buildLegend(
contactsWithLocation.length,
sharedMarkers.length,
),
],
body: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: center,
initialZoom: initialZoom,
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate,
),
onTap: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
_isSelectingPoi = false;
});
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
}
},
onLongPress: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
_isSelectingPoi = false;
});
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
return;
}
_showShareMarkerAtPositionSheet(
context: context,
connector: connector,
position: latLng,
);
},
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines),
MarkerLayer(
markers: [
if (highlightPosition != null)
Marker(
point: highlightPosition,
width: 40,
height: 40,
child: Icon(
Icons.location_on_outlined,
color: Colors.red[600],
size: 34,
),
),
..._buildMarkers(contactsWithLocation, settings),
...sharedMarkers.map(_buildSharedMarker),
if (connector.selfLatitude != null &&
connector.selfLongitude != null)
Marker(
point: LatLng(
connector.selfLatitude!,
connector.selfLongitude!,
),
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.teal,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
context.l10n.pathTrace_you,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
],
),
],
),
if (!_isBuildingPathTrace)
_buildLegend(
contactsWithLocation.length,
sharedMarkers.length,
),
if (_isBuildingPathTrace) _buildPathTraceOverlay(),
],
),
bottomNavigationBar: SafeArea(
top: false,
child: QuickSwitchBar(
@@ -354,6 +436,7 @@ class _MapScreenState extends State<MapScreen> {
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showFilterDialog(context, settingsService),
tooltip: context.l10n.map_filterNodes,
child: const Icon(Icons.filter_list),
),
),
@@ -362,28 +445,6 @@ class _MapScreenState extends State<MapScreen> {
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.map_noNodesWithLocation,
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
context.l10n.map_nodesNeedGps,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
List<Marker> _buildMarkers(List<Contact> contacts, settings) {
final markers = <Marker>[];
@@ -403,14 +464,18 @@ class _MapScreenState extends State<MapScreen> {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 80,
height: 80,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(context, contact),
onLongPress: () =>
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, contact)
: _showNodeInfo(context, contact),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: _getNodeColor(contact.type),
shape: BoxShape.circle,
@@ -426,7 +491,7 @@ class _MapScreenState extends State<MapScreen> {
child: Icon(
_getNodeIcon(contact.type),
color: Colors.white,
size: 24,
size: 20,
),
),
],
@@ -475,60 +540,102 @@ class _MapScreenState extends State<MapScreen> {
top: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.map_nodesCount(nodeCount),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
setState(() {
_legendExpanded = !_legendExpanded;
});
},
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.map_nodesCount(nodeCount),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
],
),
const SizedBox(width: 8),
AnimatedRotation(
turns: _legendExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(Icons.expand_more, size: 20),
),
],
),
),
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
Colors.blue,
),
_buildLegendItem(
Icons.router,
context.l10n.map_repeater,
Colors.green,
),
_buildLegendItem(
Icons.meeting_room,
context.l10n.map_room,
Colors.purple,
),
_buildLegendItem(
Icons.sensors,
context.l10n.map_sensor,
Colors.orange,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinDm,
Colors.blue,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPrivate,
Colors.purple,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPublic,
Colors.orange,
),
],
),
),
const SizedBox(height: 8),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
Colors.blue,
),
_buildLegendItem(
Icons.router,
context.l10n.map_repeater,
Colors.green,
),
_buildLegendItem(
Icons.meeting_room,
context.l10n.map_room,
Colors.purple,
),
_buildLegendItem(
Icons.sensors,
context.l10n.map_sensor,
Colors.orange,
),
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPrivate,
Colors.purple,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPublic,
Colors.orange,
),
],
),
crossFadeState: _legendExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
),
);
@@ -536,7 +643,7 @@ class _MapScreenState extends State<MapScreen> {
Widget _buildLegendItem(IconData icon, String label, Color color) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(vertical: 1.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -1384,6 +1491,114 @@ class _MapScreenState extends State<MapScreen> {
return context.l10n.time_allTime;
}
}
void _addToPath(BuildContext context, Contact contact) {
setState(() {
_pathTrace.add(
contact.publicKey[0],
); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!));
});
}
void _startPath() {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
}
void _removePath() {
setState(() {
_pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines
});
}
Widget _buildPathTraceOverlay() {
final l10n = context.l10n;
return Positioned(
top: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.contacts_pathTrace,
style: TextStyle(fontWeight: FontWeight.bold),
),
if (_pathTrace.isEmpty) const SizedBox(height: 8),
if (_pathTrace.isEmpty)
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
const SizedBox(height: 6),
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}",
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
),
SelectableText(
_pathTrace
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
),
if (_pathTrace.isEmpty)
ElevatedButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
_pathTrace.clear();
_points.clear();
_polylines.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
),
],
),
],
),
),
),
);
}
}
class _MarkerPayload {
+576
View File
@@ -0,0 +1,576 @@
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/contact.dart';
import 'package:meshcore_open/services/map_tile_cache_service.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) {
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)';
}
class PathTraceData {
final Uint8List pathData;
final Uint8List 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 bool flipPathRound;
final bool reversePathRound;
const PathTraceMapScreen({
super.key,
required this.title,
required this.path,
this.flipPathRound = false,
this.reversePathRound = false,
});
@override
State<PathTraceMapScreen> createState() => _PathTraceMapScreenState();
}
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
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;
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);
final code = frameBuffer.readUInt8();
if (code == respCodeSent) {
frameBuffer.skipBytes(1); //reserved
tagData = frameBuffer.readBytes(4);
final timeoutSeconds = frameBuffer.readUInt32LE();
// Start timeout timer for trace response
_timeoutTimer?.cancel();
_timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () {
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);
}
}
});
}
Future<void> _handleTraceResponse(Uint8List frame) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8();
buffer.skipBytes(5); // Skip Flag byte and tag data
buffer.skipBytes(4); // Skip auth code
Uint8List pathData = buffer.readBytes(pathLength);
Uint8List snrData = buffer.readRemainingBytes();
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);
});
}
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
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!),
],
),
),
);
},
);
}
List<Marker> _buildHopMarkers(List<int> pathData) {
return [
for (final hop in pathData)
if (_traceData!.pathContacts[hop] != null &&
_traceData!.pathContacts[hop]!.hasLocation)
Marker(
point: LatLng(
_traceData!.pathContacts[hop]!.latitude!,
_traceData!.pathContacts[hop]!.longitude!,
),
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
_traceData!.pathContacts[hop]!.publicKey
.sublist(0, 1)
.map(
(b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(),
)
.join(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
if (context.read<MeshCoreConnector>().selfLatitude != null &&
context.read<MeshCoreConnector>().selfLongitude != null)
Marker(
point: LatLng(
context.read<MeshCoreConnector>().selfLatitude!,
context.read<MeshCoreConnector>().selfLongitude!,
),
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,
),
),
),
),
];
}
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,
),
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)),
],
);
}
Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) {
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)}',
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) {
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: SNRIcon(
snr:
pathTraceData.snrData[index].toSigned(
8,
) /
4.0,
),
onTap: () {
// Handle item tap
},
),
],
);
},
),
),
),
],
),
),
),
);
}
}
+61 -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,
@@ -230,7 +240,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 +264,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 +287,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 +311,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 +503,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 +751,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 +788,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 +855,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 +910,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});
}
+1 -1
View File
@@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
repeater.shortPubKeyHex,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
+183 -65
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),
@@ -923,10 +990,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
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) {
@@ -945,10 +1009,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
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;
}
+68 -18
View File
@@ -28,7 +28,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;
@@ -293,7 +294,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 +330,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 +354,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 +377,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 +401,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 +446,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 +482,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 +524,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,
),
),
],
),
@@ -561,7 +608,8 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
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 +646,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);
+126 -34
View File
@@ -1,3 +1,6 @@
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';
@@ -8,9 +11,61 @@ 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(
@@ -25,13 +80,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 +96,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 +120,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 +135,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 +182,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 +219,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 +230,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),
),
],
),
);
}
}
+267 -91
View File
@@ -1,4 +1,5 @@
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';
@@ -20,6 +21,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
bool _showBatteryVoltage = false;
bool _deviceInfoExpanded = false;
String _appVersion = '';
@override
@@ -57,6 +59,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
_buildDebugCard(context),
const SizedBox(height: 16),
_buildExportCard(connector),
const SizedBox(height: 16),
_buildAboutCard(context),
],
);
@@ -71,43 +75,84 @@ class _SettingsScreenState extends State<SettingsScreen> {
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.settings_deviceInfo,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
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)}...',
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),
),
],
),
_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),
),
],
),
);
}
@@ -352,22 +397,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
Color? valueColor,
VoidCallback? onTap,
}) {
final theme = Theme.of(context);
final row = Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (leading != null) ...[leading, const SizedBox(width: 8)],
Text(label, style: TextStyle(color: Colors.grey[600])),
Expanded(
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
Flexible(
child: Text(
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,
),
),
],
@@ -376,11 +432,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;
}
@@ -442,7 +499,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
final currentInterval =
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
showDialog(
@@ -683,6 +741,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 {
@@ -700,6 +862,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() {
@@ -749,6 +912,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (widget.connector.currentTxPower != null) {
_txPowerController.text = widget.connector.currentTxPower.toString();
}
_clientRepeat = widget.connector.clientRepeat ?? false;
}
@override
@@ -780,10 +945,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
return;
}
if (txPower == null || txPower < 0 || txPower > 22) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
final maxTxPower = widget.connector.maxTxPower ?? 22;
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
),
);
return;
}
@@ -795,9 +963,29 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
widget.connector.currentCr,
);
// if the client repeat isnt null then we know its supported
//otherwise we leave it out of the frame to avoid accidentally enabling
final knownRepeat = widget.connector.clientRepeat != null;
if (knownRepeat) {
const validRepeatFreqsKHz = {433000, 869000, 918000};
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
);
return;
}
}
try {
await widget.connector.sendFrame(
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
buildSetRadioParamsFrame(
freqHz,
bwHz,
sf,
cr,
clientRepeat: knownRepeat ? _clientRepeat : null,
),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -836,37 +1024,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(
@@ -932,10 +1108,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,
),
],
],
),
),
@@ -949,15 +1137,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
}
}
class _PresetChip extends StatelessWidget {
final String label;
final VoidCallback onTap;
const _PresetChip({required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return ActionChip(label: Text(label), onPressed: onTap);
}
}
+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(
+11 -7
View File
@@ -82,10 +82,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 +120,16 @@ 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),
);
}
}
+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) {}
+4
View File
@@ -156,6 +156,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
case cmdSendTracePath:
return 'CMD_SEND_TRACE_PATH';
default:
return null;
}
@@ -195,6 +197,8 @@ 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;
}
+29 -26
View File
@@ -42,20 +42,21 @@ class MapTileCacheService {
late final TileProvider tileProvider;
MapTileCacheService({BaseCacheManager? cacheManager})
: cacheManager = cacheManager ??
CacheManager(
Config(
cacheKey,
stalePeriod: const Duration(days: 365),
maxNrOfCacheObjects: 200000,
),
) {
: cacheManager =
cacheManager ??
CacheManager(
Config(
cacheKey,
stalePeriod: const Duration(days: 365),
maxNrOfCacheObjects: 200000,
),
) {
tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager);
}
Map<String, String> get defaultHeaders => {
'User-Agent': 'flutter_map ($userAgentPackageName)',
};
'User-Agent': 'flutter_map ($userAgentPackageName)',
};
Future<void> clearCache() async {
await cacheManager.emptyCache();
@@ -96,17 +97,21 @@ class MapTileCacheService {
final future = cacheManager
.downloadFile(url, key: url, authHeaders: authHeaders)
.then((_) {
completed += 1;
}).catchError((_) {
completed += 1;
failed += 1;
}).whenComplete(() {
onProgress?.call(MapTileCacheProgress(
completed: completed,
total: total,
failed: failed,
));
});
completed += 1;
})
.catchError((_) {
completed += 1;
failed += 1;
})
.whenComplete(() {
onProgress?.call(
MapTileCacheProgress(
completed: completed,
total: total,
failed: failed,
),
);
});
pending.add(future);
if (pending.length >= safeConcurrency) {
@@ -189,11 +194,9 @@ class MapTileCacheService {
int _latToTileY(double lat, int zoom, int maxIndex) {
final n = 1 << zoom;
final rad = lat * math.pi / 180.0;
final value = ((1 -
math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) /
2 *
n)
.floor();
final value =
((1 - math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) / 2 * n)
.floor();
return value.clamp(0, maxIndex);
}
+140 -57
View File
@@ -25,10 +25,7 @@ class _AckHashMapping {
final String messageId;
final DateTime timestamp;
_AckHashMapping({
required this.messageId,
required this.timestamp,
});
_AckHashMapping({required this.messageId, required this.timestamp});
}
class MessageRetryService extends ChangeNotifier {
@@ -39,11 +36,16 @@ class MessageRetryService extends ChangeNotifier {
final Map<String, Message> _pendingMessages = {};
final Map<String, Contact> _pendingContacts = {};
final Map<String, PathSelection> _pendingPathSelections = {};
final Map<String, _AckHashMapping> _ackHashToMessageId = {}; // ackHashHex messageId + timestamp for O(1) lookup
final Map<String, List<Uint8List>> _expectedAckHashes = {}; // Track all expected ACKs for retries (for history)
final List<_AckHistoryEntry> _ackHistory = []; // Rolling buffer of recent ACK hashes
final Map<String, List<String>> _pendingMessageQueuePerContact = {}; // contactPubKeyHex FIFO queue of messageIds (DEPRECATED - will be removed)
final Map<String, String> _expectedHashToMessageId = {}; // expectedAckHashHex messageId (for matching RESP_CODE_SENT by hash)
final Map<String, _AckHashMapping> _ackHashToMessageId =
{}; // ackHashHex messageId + timestamp for O(1) lookup
final Map<String, List<Uint8List>> _expectedAckHashes =
{}; // Track all expected ACKs for retries (for history)
final List<_AckHistoryEntry> _ackHistory =
[]; // Rolling buffer of recent ACK hashes
final Map<String, List<String>> _pendingMessageQueuePerContact =
{}; // contactPubKeyHex FIFO queue of messageIds (DEPRECATED - will be removed)
final Map<String, String> _expectedHashToMessageId =
{}; // expectedAckHashHex messageId (for matching RESP_CODE_SENT by hash)
Function(Contact, String, int, int)? _sendMessageCallback;
Function(String, Message)? _addMessageCallback;
@@ -130,7 +132,8 @@ class MessageRetryService extends ChangeNotifier {
final messagePathBytes =
pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection);
final messagePathLength =
pathLength ?? _resolveMessagePathLength(contact, useFlood, pathSelection);
pathLength ??
_resolveMessagePathLength(contact, useFlood, pathSelection);
final message = Message(
senderKey: contact.publicKey,
text: text,
@@ -167,15 +170,25 @@ class MessageRetryService extends ChangeNotifier {
if (_setContactPathCallback != null && _clearContactPathCallback != null) {
if (message.pathLength != null && message.pathLength! < 0) {
// Flood mode - clear the path
debugPrint('Setting flood mode for retry attempt ${message.retryCount}');
debugPrint(
'Setting flood mode for retry attempt ${message.retryCount}',
);
_clearContactPathCallback!(contact);
} else if (message.pathLength != null && message.pathLength! >= 0) {
// Specific path (including direct neighbor with pathLength=0)
final pathStr = message.pathBytes.isEmpty
? 'direct'
: message.pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',');
debugPrint('Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}');
await _setContactPathCallback!(contact, message.pathBytes, message.pathLength!);
: message.pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(',');
debugPrint(
'Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}',
);
await _setContactPathCallback!(
contact,
message.pathBytes,
message.pathLength!,
);
}
}
@@ -186,22 +199,30 @@ class MessageRetryService extends ChangeNotifier {
// IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash
final selfPubKey = _getSelfPublicKeyCallback?.call();
if (selfPubKey != null) {
final outboundText = _prepareContactOutboundTextCallback?.call(contact, message.text) ?? message.text;
final outboundText =
_prepareContactOutboundTextCallback?.call(contact, message.text) ??
message.text;
final expectedHash = MessageRetryService.computeExpectedAckHash(
timestampSeconds,
attempt,
outboundText,
selfPubKey,
);
final expectedHashHex = expectedHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
final expectedHashHex = expectedHash
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
_expectedHashToMessageId[expectedHashHex] = messageId;
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
_debugLogService?.info(
'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)',
tag: 'AckHash',
);
debugPrint('Computed expected ACK hash $expectedHashHex for message $messageId');
debugPrint(
'Computed expected ACK hash $expectedHashHex for message $messageId',
);
}
// DEPRECATED: Old queue-based matching (kept for fallback)
@@ -209,17 +230,14 @@ class MessageRetryService extends ChangeNotifier {
_pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId);
if (_sendMessageCallback != null) {
_sendMessageCallback!(
contact,
message.text,
attempt,
timestampSeconds,
);
_sendMessageCallback!(contact, message.text, attempt, timestampSeconds);
}
}
void updateMessageFromSent(Uint8List ackHash, int timeoutMs) {
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
final ackHashHex = ackHash
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
// NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches)
String? messageId = _expectedHashToMessageId.remove(ackHashHex);
@@ -230,16 +248,21 @@ class MessageRetryService extends ChangeNotifier {
final message = _pendingMessages[messageId];
if (contact != null && message != null) {
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
_debugLogService?.info(
'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}',
tag: 'AckHash',
);
debugPrint('Hash-based match: ACK hash $ackHashHex → message $messageId');
debugPrint(
'Hash-based match: ACK hash $ackHashHex → message $messageId',
);
// Remove from old queue since we matched
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
false) {
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
}
} else {
@@ -259,7 +282,9 @@ class MessageRetryService extends ChangeNotifier {
'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue',
tag: 'AckHash',
);
debugPrint('Hash-based match failed for $ackHashHex, falling back to queue-based matching');
debugPrint(
'Hash-based match failed for $ackHashHex, falling back to queue-based matching',
);
for (var entry in _pendingMessageQueuePerContact.entries) {
final contactKey = entry.key;
@@ -271,7 +296,9 @@ class MessageRetryService extends ChangeNotifier {
if (_pendingMessages.containsKey(candidateMessageId)) {
messageId = candidateMessageId;
contact = _pendingContacts[candidateMessageId];
debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey');
debugPrint(
'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey',
);
break;
} else {
debugPrint('Dequeued stale message $candidateMessageId - skipping');
@@ -280,7 +307,9 @@ class MessageRetryService extends ChangeNotifier {
if (_pendingMessages.containsKey(nextMessageId)) {
messageId = nextMessageId;
contact = _pendingContacts[nextMessageId];
debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId');
debugPrint(
'Queue-based match (fallback): $ackHashHex → message $messageId',
);
break;
}
}
@@ -306,16 +335,22 @@ class MessageRetryService extends ChangeNotifier {
final selection = _pendingPathSelections[messageId];
if (message == null) {
debugPrint('Message $messageId no longer pending for ACK hash: $ackHashHex');
debugPrint(
'Message $messageId no longer pending for ACK hash: $ackHashHex',
);
_ackHashToMessageId.remove(ackHashHex);
return;
}
// Add this ACK hash to the list of expected ACKs for this message (for history)
_expectedAckHashes[messageId] ??= [];
if (!_expectedAckHashes[messageId]!.any((hash) => listEquals(hash, ackHash))) {
if (!_expectedAckHashes[messageId]!.any(
(hash) => listEquals(hash, ackHash),
)) {
_expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash));
debugPrint('Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})');
debugPrint(
'Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})',
);
}
// Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid
@@ -330,8 +365,13 @@ class MessageRetryService extends ChangeNotifier {
} else {
pathLengthValue = contact.pathLength;
}
actualTimeout = _calculateTimeoutCallback!(pathLengthValue, message.text.length);
debugPrint('Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue');
actualTimeout = _calculateTimeoutCallback!(
pathLengthValue,
message.text.length,
);
debugPrint(
'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue',
);
}
final updatedMessage = message.copyWith(
@@ -364,16 +404,22 @@ class MessageRetryService extends ChangeNotifier {
final selection = _pendingPathSelections[messageId];
if (message == null || contact == null) {
debugPrint('Timeout fired but message $messageId no longer pending (likely already delivered)');
debugPrint(
'Timeout fired but message $messageId no longer pending (likely already delivered)',
);
return;
}
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
_debugLogService?.warn(
'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying',
tag: 'AckHash',
);
debugPrint('Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})');
debugPrint(
'Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})',
);
if (message.retryCount < maxRetries - 1) {
final backoffMs = 1000 * (1 << message.retryCount);
@@ -402,7 +448,9 @@ class MessageRetryService extends ChangeNotifier {
if (_pendingMessages.containsKey(messageId)) {
_attemptSend(messageId);
} else {
debugPrint('Retry cancelled: message $messageId was delivered while waiting');
debugPrint(
'Retry cancelled: message $messageId was delivered while waiting',
);
}
});
} else {
@@ -420,7 +468,8 @@ class MessageRetryService extends ChangeNotifier {
// Clean up the queue entry for this contact
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
false) {
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
}
@@ -430,7 +479,13 @@ class MessageRetryService extends ChangeNotifier {
_clearContactPathCallback!(contact);
}
_recordPathResultFromMessage(contact.publicKeyHex, message, selection, false, null);
_recordPathResultFromMessage(
contact.publicKeyHex,
message,
selection,
false,
null,
);
if (_updateMessageCallback != null) {
_updateMessageCallback!(failedMessage);
@@ -443,18 +498,22 @@ class MessageRetryService extends ChangeNotifier {
void _moveAckHashesToHistory(String messageId) {
final ackHashes = _expectedAckHashes.remove(messageId);
if (ackHashes != null && ackHashes.isNotEmpty) {
_ackHistory.add(_AckHistoryEntry(
messageId: messageId,
ackHashes: ackHashes,
timestamp: DateTime.now(),
));
_ackHistory.add(
_AckHistoryEntry(
messageId: messageId,
ackHashes: ackHashes,
timestamp: DateTime.now(),
),
);
// Trim history to max size (rolling buffer)
while (_ackHistory.length > maxAckHistorySize) {
_ackHistory.removeAt(0);
}
debugPrint('Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})');
debugPrint(
'Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})',
);
}
}
@@ -462,7 +521,9 @@ class MessageRetryService extends ChangeNotifier {
for (final entry in _ackHistory) {
for (final expectedHash in entry.ackHashes) {
if (listEquals(expectedHash, ackHash)) {
debugPrint('Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s');
debugPrint(
'Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s',
);
return true;
}
}
@@ -472,7 +533,9 @@ class MessageRetryService extends ChangeNotifier {
void handleAckReceived(Uint8List ackHash, int tripTimeMs) {
String? matchedMessageId;
final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
final ackHashHex = ackHash
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms');
@@ -502,7 +565,9 @@ class MessageRetryService extends ChangeNotifier {
tag: 'AckHash',
);
// Fallback: Check against ALL expected ACK hashes (from all retry attempts)
debugPrint('ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)');
debugPrint(
'ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)',
);
for (var entry in _expectedAckHashes.entries) {
final messageId = entry.key;
final expectedHashes = entry.value;
@@ -510,7 +575,9 @@ class MessageRetryService extends ChangeNotifier {
for (final expectedHash in expectedHashes) {
if (listEquals(expectedHash, ackHash)) {
matchedMessageId = messageId;
debugPrint('Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})');
debugPrint(
'Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})',
);
break;
}
}
@@ -524,7 +591,9 @@ class MessageRetryService extends ChangeNotifier {
final contact = _pendingContacts[matchedMessageId];
final selection = _pendingPathSelections[matchedMessageId];
final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text;
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
_debugLogService?.info(
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms',
tag: 'AckHash',
@@ -549,8 +618,11 @@ class MessageRetryService extends ChangeNotifier {
// Clean up the queue entry for this contact (remove any remaining references to this message)
if (contact != null) {
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(matchedMessageId);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) {
_pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(
matchedMessageId,
);
if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ??
false) {
_pendingMessageQueuePerContact.remove(contact.publicKeyHex);
}
}
@@ -560,7 +632,13 @@ class MessageRetryService extends ChangeNotifier {
}
if (contact != null) {
_recordPathResultFromMessage(contact.publicKeyHex, message, selection, true, tripTimeMs);
_recordPathResultFromMessage(
contact.publicKeyHex,
message,
selection,
true,
tripTimeMs,
);
}
notifyListeners();
@@ -663,7 +741,12 @@ class MessageRetryService extends ChangeNotifier {
if (_recordPathResultCallback == null) return;
final recordSelection = selection ?? _selectionFromMessage(message);
if (recordSelection == null) return;
_recordPathResultCallback!(contactKey, recordSelection, success, tripTimeMs);
_recordPathResultCallback!(
contactKey,
recordSelection,
success,
tripTimeMs,
);
}
PathSelection? _selectionFromMessage(Message message) {
+286 -30
View File
@@ -1,18 +1,53 @@
import 'dart:ui';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart';
import '../l10n/app_localizations.dart';
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
// Locale for localized notification strings
Locale _locale = const Locale('en');
/// Set the locale for notification strings (call when app locale changes)
void setLocale(Locale locale) {
_locale = locale;
}
AppLocalizations get _l10n => lookupAppLocalizations(_locale);
// Rate limiting to prevent notification storms
// (Added after getting notification-flooded while evaluating RF flood management. The irony.)
static const _minNotificationInterval = Duration(seconds: 3);
static const _batchWindow = Duration(seconds: 5);
DateTime? _lastNotificationTime;
final List<_PendingNotification> _pendingNotifications = [];
bool _isBatchingActive = false;
bool _suppressNotifications = false;
/// Temporarily suppress all notifications (e.g., during sync)
void suppressNotifications(bool suppress) {
_suppressNotifications = suppress;
if (suppress) {
_pendingNotifications.clear();
}
}
Future<void> initialize() async {
if (_isInitialized) return;
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
@@ -32,7 +67,7 @@ class NotificationService {
try {
await _notifications.initialize(
initSettings,
settings: initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
_isInitialized = true;
@@ -47,16 +82,20 @@ class NotificationService {
}
// Request Android 13+ notification permission
final androidPlugin = _notifications.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
final androidPlugin = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (androidPlugin != null) {
final granted = await androidPlugin.requestNotificationsPermission();
return granted ?? false;
}
// iOS permissions are requested during initialization
final iosPlugin = _notifications.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>();
final iosPlugin = _notifications
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
if (iosPlugin != null) {
final granted = await iosPlugin.requestPermissions(
alert: true,
@@ -69,7 +108,7 @@ class NotificationService {
return true;
}
Future<void> showMessageNotification({
Future<void> _showMessageNotificationImpl({
required String contactName,
required String message,
String? contactId,
@@ -110,15 +149,15 @@ class NotificationService {
);
await _notifications.show(
contactId?.hashCode ?? 0,
'New message from $contactName',
message.length > 100 ? '${message.substring(0, 100)}...' : message,
notificationDetails,
id: contactId?.hashCode ?? 0,
title: contactName,
body: message,
notificationDetails: notificationDetails,
payload: 'message:$contactId',
);
}
Future<void> showAdvertNotification({
Future<void> _showAdvertNotificationImpl({
required String contactName,
required String contactType,
String? contactId,
@@ -155,15 +194,15 @@ class NotificationService {
);
await _notifications.show(
contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
'New $contactType discovered',
contactName,
notificationDetails,
id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: _l10n.notification_newTypeDiscovered(contactType),
body: contactName,
notificationDetails: notificationDetails,
payload: 'advert:$contactId',
);
}
Future<void> showChannelMessageNotification({
Future<void> _showChannelMessageNotificationImpl({
required String channelName,
required String message,
int? channelIndex,
@@ -203,24 +242,33 @@ class NotificationService {
macOS: macDetails,
);
final preview = _truncateMessage(message, 30);
final preview = message.trim();
final body = preview.isEmpty
? 'Received new message'
: 'Received new message: $preview';
? _l10n.notification_receivedNewMessage
: preview;
await _notifications.show(
channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
channelName,
body,
notificationDetails,
id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch,
title: channelName,
body: body,
notificationDetails: notificationDetails,
payload: 'channel:$channelIndex',
);
}
String _truncateMessage(String message, int maxLength) {
final trimmed = message.trim();
if (trimmed.length <= maxLength) return trimmed;
return '${trimmed.substring(0, maxLength)}...';
/// Returns a privacy-safe identifier for debug logging.
/// - advert: shows device name (body contains contactName)
/// - message: shows "from: sender" (avoids logging message content)
/// - channelMessage: shows "in: channel" (avoids logging message content)
String _getNotificationIdentifier(_PendingNotification n) {
switch (n.type) {
case _NotificationType.advert:
return n.body;
case _NotificationType.message:
return 'from: ${n.title}';
case _NotificationType.channelMessage:
return 'in: ${n.title}';
}
}
void _onNotificationTapped(NotificationResponse response) {
@@ -237,6 +285,214 @@ class NotificationService {
}
Future<void> cancel(int id) async {
await _notifications.cancel(id);
await _notifications.cancel(id: id);
}
//
// Public notification methods (rate limiting is enforced automatically)
//
Future<void> showMessageNotification({
required String contactName,
required String message,
String? contactId,
int? badgeCount,
}) async {
if (_suppressNotifications) return;
_queueNotification(
_PendingNotification(
type: _NotificationType.message,
title: contactName,
body: message,
id: contactId,
badgeCount: badgeCount,
),
);
}
Future<void> showAdvertNotification({
required String contactName,
required String contactType,
String? contactId,
}) async {
if (_suppressNotifications) return;
_queueNotification(
_PendingNotification(
type: _NotificationType.advert,
title: contactType,
body: contactName,
id: contactId,
),
);
}
Future<void> showChannelMessageNotification({
required String channelName,
required String message,
int? channelIndex,
int? badgeCount,
}) async {
if (_suppressNotifications) return;
_queueNotification(
_PendingNotification(
type: _NotificationType.channelMessage,
title: channelName,
body: message,
id: channelIndex?.toString(),
badgeCount: badgeCount,
),
);
}
void _queueNotification(_PendingNotification notification) {
final now = DateTime.now();
// If we recently showed a notification, start batching
if (_lastNotificationTime != null &&
now.difference(_lastNotificationTime!) < _minNotificationInterval) {
_pendingNotifications.add(notification);
debugPrint(
'[Notification] queued: ${notification.type.name} (${_getNotificationIdentifier(notification)})',
);
// Start batch timer if not already running
if (!_isBatchingActive) {
_isBatchingActive = true;
Future.delayed(_batchWindow, _processBatch);
}
return;
}
// Show immediately if enough time has passed
debugPrint(
'[Notification] sent immediately: ${notification.type.name} (${_getNotificationIdentifier(notification)})',
);
_showNotificationImmediately(notification);
_lastNotificationTime = now;
}
Future<void> _processBatch() async {
_isBatchingActive = false;
if (_pendingNotifications.isEmpty) return;
final batch = List<_PendingNotification>.from(_pendingNotifications);
_pendingNotifications.clear();
if (batch.length == 1) {
// Single notification, show normally
_showNotificationImmediately(batch.first);
} else {
// Multiple notifications, show summary
await _showBatchSummary(batch);
}
_lastNotificationTime = DateTime.now();
}
Future<void> _showNotificationImmediately(
_PendingNotification notification,
) async {
switch (notification.type) {
case _NotificationType.message:
await _showMessageNotificationImpl(
contactName: notification.title,
message: notification.body,
contactId: notification.id,
badgeCount: notification.badgeCount,
);
break;
case _NotificationType.advert:
await _showAdvertNotificationImpl(
contactName: notification.body,
contactType: notification.title,
contactId: notification.id,
);
break;
case _NotificationType.channelMessage:
await _showChannelMessageNotificationImpl(
channelName: notification.title,
message: notification.body,
channelIndex: int.tryParse(notification.id ?? ''),
badgeCount: notification.badgeCount,
);
break;
}
}
Future<void> _showBatchSummary(List<_PendingNotification> batch) async {
if (!_isInitialized) await initialize();
// Group by type
final messages = batch
.where((n) => n.type == _NotificationType.message)
.toList();
final adverts = batch
.where((n) => n.type == _NotificationType.advert)
.toList();
final channelMsgs = batch
.where((n) => n.type == _NotificationType.channelMessage)
.toList();
// Build summary text using localized plurals
final parts = <String>[];
if (messages.isNotEmpty) {
parts.add(_l10n.notification_messagesCount(messages.length));
}
if (channelMsgs.isNotEmpty) {
parts.add(_l10n.notification_channelMessagesCount(channelMsgs.length));
}
if (adverts.isNotEmpty) {
parts.add(_l10n.notification_newNodesCount(adverts.length));
}
if (parts.isEmpty) return;
// Show first few device names in batch summary for debugging (only if adverts exist)
final deviceInfo = adverts.isNotEmpty
? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})'
: '';
debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo');
const androidDetails = AndroidNotificationDetails(
'batch_summary',
'Activity Summary',
channelDescription: 'Batched notification summaries',
importance: Importance.defaultImportance,
priority: Priority.defaultPriority,
icon: '@mipmap/ic_launcher',
);
const notificationDetails = NotificationDetails(android: androidDetails);
await _notifications.show(
id: 'batch_summary'.hashCode,
title: _l10n.notification_activityTitle,
body: parts.join(', '),
notificationDetails: notificationDetails,
payload: 'batch',
);
}
}
// Helper class for pending notifications
enum _NotificationType { message, advert, channelMessage }
class _PendingNotification {
final _NotificationType type;
final String title;
final String body;
final String? id;
final int? badgeCount;
_PendingNotification({
required this.type,
required this.title,
required this.body,
this.id,
this.badgeCount,
});
}
+30 -16
View File
@@ -61,7 +61,10 @@ class PathHistoryService extends ChangeNotifier {
int? tripTimeMs,
}) {
if (selection.useFlood) {
final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats());
final stats = _floodStats.putIfAbsent(
contactPubKeyHex,
() => _FloodStats(),
);
if (success) {
stats.successCount += 1;
if (tripTimeMs != null) stats.lastTripTimeMs = tripTimeMs;
@@ -88,23 +91,28 @@ class PathHistoryService extends ChangeNotifier {
}
PathSelection getNextAutoPathSelection(String contactPubKeyHex) {
final ranked = _getRankedPaths(contactPubKeyHex)
.take(_autoRotationTopCount)
.toList();
final ranked = _getRankedPaths(
contactPubKeyHex,
).take(_autoRotationTopCount).toList();
if (ranked.isEmpty) {
return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
}
_trackAccess(contactPubKeyHex);
final selections = ranked
.map((path) => PathSelection(
pathBytes: path.pathBytes,
hopCount: path.hopCount,
useFlood: false,
))
.toList()
..add(const PathSelection(pathBytes: [], hopCount: -1, useFlood: true));
final selections =
ranked
.map(
(path) => PathSelection(
pathBytes: path.pathBytes,
hopCount: path.hopCount,
useFlood: false,
),
)
.toList()
..add(
const PathSelection(pathBytes: [], hopCount: -1, useFlood: true),
);
final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0;
final selection = selections[currentIndex % selections.length];
@@ -241,7 +249,8 @@ class PathHistoryService extends ChangeNotifier {
}
Future<ContactPathHistory?> _loadHistoryFromStorage(
String contactPubKeyHex) async {
String contactPubKeyHex,
) async {
return await _storage.loadPathHistory(contactPubKeyHex);
}
@@ -308,8 +317,10 @@ class PathHistoryService extends ChangeNotifier {
..removeWhere((p) => p.pathBytes.isEmpty);
ranked.sort((a, b) {
final aRate = (a.successCount + 1) / (a.successCount + a.failureCount + 2);
final bRate = (b.successCount + 1) / (b.successCount + b.failureCount + 2);
final aRate =
(a.successCount + 1) / (a.successCount + a.failureCount + 2);
final bRate =
(b.successCount + 1) / (b.successCount + b.failureCount + 2);
if (aRate != bRate) return bRate.compareTo(aRate);
if (a.successCount != b.successCount) {
return b.successCount.compareTo(a.successCount);
@@ -329,7 +340,10 @@ class PathHistoryService extends ChangeNotifier {
}
void _updateFloodStats(String contactPubKeyHex) {
final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats());
final stats = _floodStats.putIfAbsent(
contactPubKeyHex,
() => _FloodStats(),
);
stats.lastUsed = DateTime.now();
}
+9 -3
View File
@@ -26,7 +26,9 @@ class RepeaterCommandService {
int retries = maxRetries,
}) async {
final repeaterKey = repeater.publicKeyHex;
final hasPending = _pendingCommands.keys.any((id) => id.startsWith(repeaterKey));
final hasPending = _pendingCommands.keys.any(
(id) => id.startsWith(repeaterKey),
);
if (hasPending) {
throw Exception('Another command is still awaiting a response.');
}
@@ -84,7 +86,9 @@ class RepeaterCommandService {
attempt: attempt,
timestampSeconds: timestampSeconds,
);
final responseBytes = frame.length > maxFrameSize ? frame.length : maxFrameSize;
final responseBytes = frame.length > maxFrameSize
? frame.length
: maxFrameSize;
final timeoutMs = _connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: responseBytes,
@@ -97,7 +101,9 @@ class RepeaterCommandService {
() {
final completer = _pendingCommands[commandId];
if (completer != null && !completer.isCompleted) {
completer.completeError('Command timeout after $timeoutSeconds seconds');
completer.completeError(
'Command timeout after $timeoutSeconds seconds',
);
_cleanup(commandId);
}
},
+9 -4
View File
@@ -8,7 +8,9 @@ class StorageService {
static const String _repeaterPasswordsKey = 'repeater_passwords';
Future<void> savePathHistory(
String contactPubKeyHex, ContactPathHistory history) async {
String contactPubKeyHex,
ContactPathHistory history,
) async {
final prefs = PrefsManager.instance;
final key = '$_pathHistoryPrefix$contactPubKeyHex';
final jsonStr = jsonEncode(history.toJson());
@@ -39,8 +41,9 @@ class StorageService {
Future<void> clearAllPathHistories() async {
final prefs = PrefsManager.instance;
final keys = prefs.getKeys();
final pathHistoryKeys =
keys.where((key) => key.startsWith(_pathHistoryPrefix));
final pathHistoryKeys = keys.where(
(key) => key.startsWith(_pathHistoryPrefix),
);
for (final key in pathHistoryKeys) {
await prefs.remove(key);
@@ -74,7 +77,9 @@ class StorageService {
/// Save a repeater password by public key hex
Future<void> saveRepeaterPassword(
String repeaterPubKeyHex, String password) async {
String repeaterPubKeyHex,
String password,
) async {
final prefs = PrefsManager.instance;
final passwords = await loadRepeaterPasswords();
passwords[repeaterPubKeyHex] = password;
+14 -6
View File
@@ -8,7 +8,10 @@ class ChannelMessageStore {
static const String _keyPrefix = 'channel_messages_';
/// Save messages for a specific channel
Future<void> saveChannelMessages(int channelIndex, List<ChannelMessage> messages) async {
Future<void> saveChannelMessages(
int channelIndex,
List<ChannelMessage> messages,
) async {
final prefs = PrefsManager.instance;
final key = '$_keyPrefix$channelIndex';
@@ -96,7 +99,8 @@ class ChannelMessageStore {
pathVariants: (json['pathVariants'] as List<dynamic>?)
?.map((entry) => Uint8List.fromList(base64Decode(entry as String)))
.toList(),
repeats: (json['repeats'] as List<dynamic>?)
repeats:
(json['repeats'] as List<dynamic>?)
?.map((entry) => _repeatFromJson(entry as Map<String, dynamic>))
.toList() ??
const [],
@@ -105,15 +109,19 @@ class ChannelMessageStore {
replyToMessageId: json['replyToMessageId'] as String?,
replyToSenderName: json['replyToSenderName'] as String?,
replyToText: json['replyToText'] as String?,
reactions: (json['reactions'] as Map<String, dynamic>?)?.map(
(key, value) => MapEntry(key, value as int),
) ?? {},
reactions:
(json['reactions'] as Map<String, dynamic>?)?.map(
(key, value) => MapEntry(key, value as int),
) ??
{},
);
}
Map<String, dynamic> _repeatToJson(Repeat repeat) {
return {
'repeaterKey': repeat.repeaterKey != null ? base64Encode(repeat.repeaterKey!) : null,
'repeaterKey': repeat.repeaterKey != null
? base64Encode(repeat.repeaterKey!)
: null,
'repeaterName': repeat.repeaterName,
'tripTimeMs': repeat.tripTimeMs,
'path': repeat.path?.map((bytes) => base64Encode(bytes)).toList() ?? [],
+4 -1
View File
@@ -16,7 +16,10 @@ class ChannelOrderStore {
try {
final decoded = jsonDecode(raw);
if (decoded is List) {
return decoded.map((value) => value is int ? value : int.tryParse('$value')).whereType<int>().toList();
return decoded
.map((value) => value is int ? value : int.tryParse('$value'))
.whereType<int>()
.toList();
}
} catch (_) {
// fall through to legacy parse
+50
View File
@@ -0,0 +1,50 @@
import 'dart:convert';
import 'dart:typed_data';
import '../models/channel.dart';
import 'prefs_manager.dart';
class ChannelStore {
static const String _key = 'channels';
Future<List<Channel>> loadChannels() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_key);
if (jsonStr == null) return [];
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
} catch (_) {
return [];
}
}
Future<void> saveChannels(List<Channel> channels) async {
final prefs = PrefsManager.instance;
final jsonList = channels.map(_toJson).toList();
await prefs.setString(_key, jsonEncode(jsonList));
}
Map<String, dynamic> _toJson(Channel channel) {
return {
'index': channel.index,
'name': channel.name,
'psk': base64Encode(channel.psk),
'unreadCount': channel.unreadCount,
};
}
Channel _fromJson(Map<String, dynamic> json) {
return Channel(
index: json['index'] as int,
name: json['name'] as String? ?? '',
psk: json['psk'] != null
? Uint8List.fromList(base64Decode(json['psk'] as String))
: Uint8List(16),
unreadCount: json['unreadCount'] as int? ?? 0,
);
}
}
+4 -10
View File
@@ -40,7 +40,7 @@ class CommunityStore {
/// Add a new community
Future<void> addCommunity(Community community) async {
final communities = await loadCommunities();
// Check if community with same ID already exists
final existingIndex = communities.indexWhere((c) => c.id == community.id);
if (existingIndex >= 0) {
@@ -49,7 +49,7 @@ class CommunityStore {
} else {
communities.add(community);
}
await saveCommunities(communities);
}
@@ -92,10 +92,7 @@ class CommunityStore {
}
/// Add a hashtag channel to a community
Future<void> addHashtagChannel(
String communityId,
String hashtag,
) async {
Future<void> addHashtagChannel(String communityId, String hashtag) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.addHashtagChannel(hashtag);
@@ -104,10 +101,7 @@ class CommunityStore {
}
/// Remove a hashtag channel from a community
Future<void> removeHashtagChannel(
String communityId,
String hashtag,
) async {
Future<void> removeHashtagChannel(String communityId, String hashtag) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.removeHashtagChannel(hashtag);
+9 -3
View File
@@ -14,7 +14,9 @@ class ContactStore {
try {
final jsonList = jsonDecode(jsonStr) as List<dynamic>;
return jsonList.map((entry) => _fromJson(entry as Map<String, dynamic>)).toList();
return jsonList
.map((entry) => _fromJson(entry as Map<String, dynamic>))
.toList();
} catch (_) {
return [];
}
@@ -57,12 +59,16 @@ class ContactStore {
: Uint8List(0),
pathOverride: json['pathOverride'] as int?,
pathOverrideBytes: json['pathOverrideBytes'] != null
? Uint8List.fromList(base64Decode(json['pathOverrideBytes'] as String))
? Uint8List.fromList(
base64Decode(json['pathOverrideBytes'] as String),
)
: null,
latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(),
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs),
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(lastMessageMs ?? lastSeenMs),
lastMessageAt: DateTime.fromMillisecondsSinceEpoch(
lastMessageMs ?? lastSeenMs,
),
);
}
}

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