Compare commits

..

202 Commits

Author SHA1 Message Date
zjs81 c78c77309e Merge remote-tracking branch 'origin/main' 2026-08-11 00:34:19 -07:00
zjs81 724696cd3c Remove 'jni' from FLUTTER_FFI_PLUGIN_LIST in generated_plugins.cmake files for Linux and Windows
Update version in pubspec.yaml to 9.5.0+16
2026-08-11 00:17:02 -07:00
zjs81 cf6c93c50a rebuild translations 2026-08-11 00:15:45 -07:00
Zach eee72b4c65 Add ability to send images over lora 2026-08-10 23:20:46 -07:00
Zach 736eb064bf Update RadioSettings preset with new spreading factor and coding rate 2026-08-09 19:55:01 -07:00
zjs81 9e6968aa04 Merge pull request #509 from ericszimmermann/ez_maps_high_contrast_PR
add High Contrast filter for maps
2026-08-09 19:35:15 -07:00
zjs81 8904fc7208 Merge pull request #514 from Hacuchino-hash/device-hardware-info
Show connected node hardware and firmware in device info
2026-08-09 19:34:31 -07:00
zjs81 02d3325d0d Merge pull request #517 from Hacuchino-hash/hv-battery-chemistry
Add high voltage LiPo battery chemistry option
2026-08-09 19:31:12 -07:00
Hacuchino-hash 3a08f52fd4 Add high voltage LiPo battery chemistry option 2026-08-07 08:02:01 -05:00
Hacuchino-hash 3a9c14e946 Show connected node hardware and firmware in device info 2026-08-07 08:02:00 -05:00
ericz b15f6450bd dart format 2026-08-04 17:27:51 +02:00
ericz 2879bf6147 fix test/screen/path_trace_test.dart 2026-08-04 17:23:50 +02:00
ericz 36f12ebded add High Contrast filter for maps 2026-07-30 19:26:33 +02:00
zjs81 98f6cf3745 Merge pull request #506 from Warmax356/patch-1
Add SWHID and Archive badges to README
2026-07-28 07:51:25 -07:00
WarMax356 35e4d7fa6c Add SWHID and Archive badges to README
Added SWHID and Archive badges to the README.
2026-07-23 13:04:58 +02:00
zjs81 0fe2502309 Merge pull request #450 from PacoX/dev
Add Support for Multi-Byte Routing Paths
2026-07-09 07:14:53 -07:00
PacoX a8cac76491 resolved conflict and update doc 2026-07-09 09:08:50 +02:00
PacoX 2df17be673 remplace onReorder by onReorderItem 2026-07-09 08:43:55 +02:00
Zach 71fa8c17a4 Add cap for discovered contacts to manage memory usage 2026-07-08 20:48:09 -07:00
Zach 698a69dc03 Improve path formatting in message bubble to use zero-width space for better visual presentation 2026-07-08 20:43:58 -07:00
Zach 8765d0f4cc Add localization for 'Received a GIF' in multiple languages and update chat UI 2026-07-08 20:38:06 -07:00
zjs81 c683dbd8f1 Update funding sources in FUNDING.yml 2026-07-08 20:07:45 -07:00
zjs81 b8e113d269 Merge pull request #467 from sethoscope/set-repeater-key
Add UI for setting repeater identity keys
2026-07-06 09:23:01 -07:00
Seth Golub e963248e37 Stop repeater identity key search when user navigates away
Terminate a search for a repeater key with a specified prefix if the
user navigates away from the settings screen.

This commit also adds dispose() calls for the new repeater key input
controllers, which were previously omitted.
2026-07-04 18:54:27 -07:00
Seth Golub 68d2250afc Fix randomBytes to use specified length
Previously we were creating a 32-byte list no matter what was asked for.
2026-07-04 18:34:11 -07:00
Zach f4cca2fb1d Add ExportOptions.plist for App Store Connect configuration
- Created a new ExportOptions.plist file to define export settings for app distribution.
- Configured the plist with method, team ID, signing style, symbol upload option, and destination.
2026-07-04 00:45:18 -07:00
zjs81 0f3961f3a2 Merge pull request #461 from ericszimmermann/ez_alternative_maps
Alternative Maps
2026-07-04 00:32:58 -07:00
zjs81 08130350d7 Merge pull request #497 from ericszimmermann/ez_zerohop_location_advert_PR
Zero-Hop Location advert
2026-07-04 00:31:52 -07:00
ericszimmermann c1dea86dd1 Merge branch 'zjs81:dev' into ez_zerohop_location_advert_PR 2026-07-01 07:27:44 +02:00
zjs81 e1b8d6ec97 Merge pull request #491 from km-foss/chore/au-mid-preset
Add Australia (Mid) Radio Preset
2026-06-25 18:03:34 -07:00
PacoX 3541b1bbac Fix room login screen 2026-06-24 14:38:35 +02:00
PacoX ef6c1e0898 Fix login screen 2026-06-24 14:24:12 +02:00
km-foss eb89ad1d50 chore: Add Australia (Mid) Radio Preset
This mirrors the config option from the official Meshcore app, and represents the preset now commonly in use in New South Wales.
2026-06-24 09:21:45 +10:00
ericz 5ef976b3b5 Add option for automatic zerohop adverts on posision change. 2026-06-23 18:19:22 +02:00
zjs81 7b4f90d646 Merge pull request #484 from hamid-elaosta/icon-fix
Fix adaptive icon and app label on Android
2026-06-19 22:30:34 -07:00
Seth Golub e1a4900dc0 Add UI for setting repeater identity keys
This adds a simple UI in repeater management settings for remotely
setting a repeater's private key. A key can be pasted from an external
key generator, or a random one can be generated here, optionally with a
specified prefix.

If a prefix is specified, searching is done in 30ms batches in order
to keep the UI responsive and show the progress indicator animation
smoothly. Searches can be interrupted. An estimation of search length is
given as expected number of generated key pairs. (A future improvement
would be to observe keys per second performance and give time estimates
based on that.)

When it generates a new private key on your behalf, it can tell you
what the corresponding public key will be. Unfortunately, if you paste
one in from another source, it can't compute the corresponding public
key. This is because the standard Dart crypto library doesn't provide
that capability, nor does it give access to the elliptic curve functions
it implements so we could do it ourselves. Our only recourse would be to
copy the source for all those things, and I didn't think it was worth
it.

This will not check the validity of a pasted key, but the remote
repeater will. It does limit it to the correct length and hexadecimal
charset.

Prefixes are limited to 4 characters. There's no technical reason for
this, but users are unlikely to want more, and I think it would just be
a bad experience to allow more and spin for a very long time. If someone
really wants a longer one, they can use an external generator like they
do now.

Possible TODO: rewrite background processing using isolates.

Closes #142.
2026-06-19 07:27:44 -07:00
Hamid Elaosta 7fc253cc88 Add adaptive Android icon support and fix app label 2026-06-18 01:18:32 +01:00
PacoX 445a18a83f Fix failed test 2026-06-16 15:01:10 +02:00
PacoX 8273c416b5 Merge pull request #5 from HDDen/update-pacox-multibyte
Updated with dev again
2026-06-16 13:38:05 +02:00
HDDen b44c69f411 Merge branch 'dev' of https://github.com/PacoX/meshcore-open into update-pacox-multibyte 2026-06-16 12:11:34 +03:00
HDDen 6492058a04 Merge branch 'dev' into update-pacox-multibyte 2026-06-16 11:53:59 +03:00
zjs81 5670ab0067 Merge pull request #468 from HDDen/test-regions
Regions: adapted to last dev and add regions fetching from repeaters (continued from PR#379)
2026-06-15 22:53:42 -07:00
zjs81 98a9197cb1 Merge remote-tracking branch 'origin/dev' into test-regions 2026-06-15 22:52:31 -07:00
zjs81 55da952bfd Merge remote-tracking branch 'origin/dev' into test-regions Also added fixes 2026-06-15 22:46:59 -07:00
zjs81 9b4742951d Merge pull request #479 from MrSurly/478-implement-right-click
Issue 478: Implement right click for desktop use
2026-06-15 21:56:13 -07:00
Zach 43f3d439ba Add jni to FFI plugin list for Linux and Windows 2026-06-15 13:12:08 -07:00
PacoX ed294decfb Merge pull request #4 from HDDen/update-pacox-multibyte
Updated to dev
2026-06-15 14:43:27 +02:00
Eric Poulsen 5f54a2cd1b dart format 2026-06-13 22:05:46 -07:00
Eric Poulsen e1536c49b1 tap --> secondaryTap, per review comments 2026-06-13 20:43:30 -07:00
Eric Poulsen bfa62523df Prefer GestureDetector over Listener for right-click in routing sheet 2026-06-13 20:43:13 -07:00
Eric Poulsen 6813a72767 Fix null-returning ternaries with if blocks, per code review comments 2026-06-13 20:32:05 -07:00
Eric Poulsen dbd3a40bdc Rename _handleMapTap to _handleMapContextPress, per review comments. 2026-06-13 20:29:36 -07:00
Eric Poulsen 3009036565 Remove white space per code review 2026-06-13 20:27:58 -07:00
Eric Poulsen 80f4fd5218 Added HitTestBehavior.opaque, per code review comments 2026-06-13 20:26:38 -07:00
Eric Poulsen 321d4b9775 Use kSecondaryMouseButton bitmask check, per code review 2026-06-13 20:23:32 -07:00
Eric Poulsen 1c183d7e67 Add right-click support to routing sheet path history rows 2026-06-13 19:46:00 -07:00
Eric Poulsen a1e6f6967c Added support for left-click copy, and snackbar message about copying the data. 2026-06-13 19:30:27 -07:00
Eric Poulsen f34c2a92c3 Add onSecondaryTap to MeshCard and fix right-click on message text bubbles 2026-06-13 19:22:16 -07:00
Eric Poulsen f1478722b0 Fix for right-click on message bubbles (right click didn't work on the actual text of the bubble) 2026-06-13 19:05:52 -07:00
Eric Poulsen c867225073 onSecondaryTap for line_of_sight_map_screen 2026-06-13 16:37:12 -07:00
Eric Poulsen 06285a02ff onSecondaryTap for map_screen 2026-06-13 16:36:55 -07:00
HDDen e92d215582 upd - fix and format 2026-06-14 01:36:49 +03:00
HDDen d61eaaaff1 Merge branch 'dev' into update-pacox-multibyte 2026-06-14 01:12:49 +03:00
ericz ecde5e1109 fix StadiaMaps Dark and add demo. 2026-06-13 20:01:43 +02:00
ericz f548f95e29 merge zjs81/dev: resolve conflicting implementations 2026-06-13 18:06:31 +02:00
zjs81 9957e1fc1f Merge pull request #474 from zjs81/dev
Merge Dev into main
2026-06-13 02:14:19 -07:00
zjs81 e38d03a32e Fix battery voltage null check in RepeaterStatusScreen 2026-06-13 02:13:34 -07:00
zjs81 ea657a964a Update documentation and dependencies for MeshCore Open
- Replaced sqflite with shared_preferences for local key-value storage in README.md
- Updated gradle.properties to include builtInKotlin and newDsl flags
- Enhanced translation feature documentation in additional-features.md
- Modified BLE protocol documentation to include new command and response codes in ble-protocol.md
- Clarified channel management details in channels.md
- Improved chat and messaging documentation, including message path viewing and translation options in chat-and-messaging.md
- Updated contacts management details in contacts.md
- Revised map and location documentation for inferred locations and user interface changes in map-and-location.md
- Adjusted navigation flow in navigation.md to reflect changes in screen transitions
- Updated notification system details in notifications.md
- Enhanced repeater management documentation in repeater-management.md
- Clarified scanner and connection process in scanner-and-connection.md
- Reorganized settings documentation for better clarity and added new node and location settings in settings.md
2026-06-13 02:12:00 -07:00
zjs81 760d8e1db3 Update localization files and improve path map UI
- Added new localization strings for Swedish, Ukrainian, and Chinese languages in app_sv.arb, app_uk.arb, and app_zh.arb respectively.
- Enhanced the path map UI in channel_message_path_screen.dart for better readability and interaction.
- Improved message retry logic in message_retry_service.dart to prevent double-pumping of queues.
- Bumped version number in pubspec.yaml to 9.5.0+13.
- Cleared untranslated strings in untranslated.json to reflect current localization status.
2026-06-13 01:58:42 -07:00
zjs81 880df84828 Merge pull request #473 from zjs81/dev
Merge Dev into main
2026-06-13 00:42:09 -07:00
zjs81 815534d409 Merge pull request #472 from zjs81/ui
Add shared UI components and refactor MeshCoreConnector
2026-06-13 00:39:46 -07:00
zjs81 becfbedc99 format files 2026-06-13 00:39:13 -07:00
zjs81 7da4e68384 remove md 2026-06-13 00:38:00 -07:00
zjs81 5ea6b17b16 feat: enhance MeshCoreConnector with improved timeout calculation and path resolution; add PathHopResolver for better contact resolution 2026-06-13 00:36:45 -07:00
zjs81 3707acb124 Refactor code structure and remove redundant sections for improved readability and maintainability 2026-06-12 22:55:41 -07:00
zjs81 51d6210920 Add shared UI components for mesh application
- Introduced `mesh_ui.dart` with reusable widgets including SectionHeader, MeshCard, StatusChip, StatTile, AvatarCircle, SignalBars, RouteChip, PulseDot, BottomSheetHeader, ErrorRetryCard, and ListEntrance.
- Implemented `path_map_ui.dart` for path map screens, featuring path distance calculations, playback controls, and a summary list of observed paths.
- Created `themed_map_tile_layer.dart` for shared cached map tiles with automatic dark-mode treatment.
2026-06-12 21:04:02 -07:00
HDDen 27695d372a Updated to last dev 2026-06-12 14:44:09 +03:00
HDDen d06a4c1861 Merge branch 'dev' into update-pacox-multibyte 2026-06-12 13:39:09 +03:00
Zach 6a31d304d3 feat: update license type to nonprofit and add jni to plugin lists; bump dependencies for flutter_local_notifications, package_info_plus, share_plus, flutter_blue_plus_platform_interface, and llamadart 2026-06-11 10:08:31 -07:00
zjs81 26fdf74d69 Refactor UI code for better readability and consistency
- Improved formatting of ListTile icons and text styles in settings_screen.dart, telemetry_screen.dart, usb_screen.dart, gif_picker.dart, path_editor_sheet.dart, repeater_login_dialog.dart, and room_login_dialog.dart for better readability.
- Consolidated TextStyle definitions into single lines where applicable.
- Updated notification_service.dart to enhance readability of notification ID assignment.
- Simplified function signatures in routing_sheet.dart for clarity.
- Cleaned up test assertions in usb_flow_test.dart for conciseness.
- Removed unused translations in untranslated.json to streamline localization files.
2026-06-11 00:28:13 -07:00
zjs81 b1de1b4bf0 chore: untrack .claude/worktrees gitlinks
Remove the accidentally committed Claude Code worktree gitlinks.
.claude/ is already gitignored (prev commit). Worktree files remain
on disk locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:11:56 -07:00
zjs81 3c26ce2d93 chore: stop tracking .claude worktrees and ignore .claude/
The previous commit accidentally added the Claude Code worktree
gitlinks under .claude/worktrees. Untrack them and ignore .claude/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:10:49 -07:00
zjs81 cba1e5950c feat: add contact UI helpers and path editor for routing management
- Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types.
- Created colorForName and firstCharacterOrEmoji functions to enhance contact display.
- Developed PathEditorSheet widget for managing contact paths with a user-friendly interface.
- Introduced RoutingSheet for managing contact routing modes and displaying path history.
- Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation.
2026-06-11 00:07:12 -07:00
zjs81 743ef7f124 Merge pull request #470 from jirogit/fix/ja-repeater-translation
fix(l10n): correct ja repeater terminology
2026-06-10 07:17:06 -07:00
me 13d3a107da fix(l10n): replace flood/repeater machine translations in Japanese
- 洪水 → フラッド (flood routing terminology)
- リピーター → リピータ (consistent katakana without long vowel)
- 中継局 → リピータ
- 繰り返し送信装置 → リピータ
- オフグリッド...の繰り返し → オフグリッドリピータ
- 最大浸水範囲の回数 → フラッドパケットの最大ホップ数
- インバウンドフラッパケット → インバウンドフラッドパケット (typo fix)
- ルーティングループに見えるような、洪水パケットを送信する → ルーティングループを検知する
- カスタムパスには、メッセージを中継できる中間地点が必要です → リピータが必要です
- ワンホップでの広告放送(中継なし)→(リピータなし)
2026-06-08 16:18:18 -07:00
me dfdcafb071 fix(l10n): correct ja repeater terminology
Replace machine-translated repeater terms with consistent Japanese:
- 繰り返し送信装置 → リピーター (map, listFilter)
- 繰り返し設定 → リピーターを自動追加 (contactsSettings)
- ホップの繰り返し → リピーターホップ (channelPath)
- 繰り返し送信する、近隣 → 近隣のリピーター (neighbors)
- 近くの電波中継局 → 近くのリピーター (snrIndicator)
- 送信装置名 → リピーター名 (repeater settings)
- オフグリッド...の繰り返し → オフグリッドリピーター (settings)
- 中継装置およびルームサーバーの設置場所 → リピーター/ルームサーバーの位置情報
- 繰り返し送信に関する情報 → リピーターに関する情報 (repeater_guest)
2026-06-08 15:30:43 -07:00
PacoX c46a5ffa2f Fix direct path override validation 2026-06-05 08:34:57 +02:00
PacoX a15f5e94da fix: path formatting in DM mode 2026-06-04 20:26:46 +02:00
HDDen 9d1251f21e Merge branch 'dev' into test-regions 2026-05-31 16:52:27 +03:00
HDDen 0ca96ce5b1 Regions discovery from nearby repeaters 2026-05-31 16:47:22 +03:00
zjs81 33b3b04294 Merge pull request #459 from HDDen/telemetry-gps-map
Telemetry screen: now included map with received gps mark and autoupdate feature
2026-05-30 19:21:47 -07:00
HDDen b72ade1387 Update channel_chat_screen.dart 2026-05-31 00:22:04 +03:00
HDDen 3fc038682c Merge commit 'refs/pull/379/head' of https://github.com/zjs81/meshcore-open into test-regions 2026-05-31 00:06:16 +03:00
PacoX 9e5b360f28 fix int conversion 2026-05-28 09:31:42 +02:00
PacoX 89360c8bc2 fix channels_screen 2026-05-28 09:18:42 +02:00
ericszimmermann 28df5a97e8 Merge branch 'dev' into ez_alternative_maps 2026-05-28 08:56:54 +02:00
PacoX 2e767730fd Merge branch 'dev' into dev 2026-05-28 08:51:23 +02:00
PacoX b7fe5331d0 Merge pull request #3 from HDDen/update-pacox-multibyte
Updated multibyte
2026-05-28 08:43:18 +02:00
HDDen 61fb39b9f8 Multibyte: some path fixes
- Validate encoded path hop counts before saving or sending custom paths
- Fix multi-byte path reversal by reversing hops instead of raw bytes
- Decode raw log path lengths using packed path_len format
- Localize path hash mode options and optimize map anchor lookup
2026-05-28 03:17:08 +03:00
HDDen 9e18b99e9d Multibyte: fixed path trace repeater icon and potential bugs 2026-05-28 02:52:04 +03:00
HDDen 2e4f26d7cd Multibyte: fix map 2026-05-28 02:20:56 +03:00
PacoX 6e81fdfd46 add clamp for path length 2026-05-27 08:32:16 +02:00
PacoX 5d03f99040 fix tracepath 2026-05-26 20:12:27 +02:00
HDDen 6a7dd981a2 Fix «Dispose the telemetry map controller on widget teardown» 2026-05-26 21:01:18 +03:00
HDDen d68f755677 Merge branch 'dev' into telemetry-gps-map 2026-05-26 20:58:57 +03:00
zjs81 264d2bcc9a Merge pull request #462 from HDDen/sync-progressbar
Onstart syncronization progressbar
2026-05-26 10:15:00 -07:00
PacoX e88281b7b6 fix case-sensitive prefix 2026-05-26 09:04:38 +02:00
PacoX 3a11e35a7a fix path trace overlay 2026-05-26 08:50:54 +02:00
HDDen 8dd385beed Merge branch 'dev' into sync-progressbar 2026-05-26 00:44:08 +03:00
HDDen 2328848400 Onstart sync progressbar: fix potential bug with spinner on interrupt synchronization 2026-05-26 00:37:42 +03:00
ericz 29e551d6cb add Documentation 2026-05-25 22:24:38 +02:00
ericz 531b85b8aa add OSM Auto Map 2026-05-25 11:56:01 +02:00
PacoX 3d956596a5 Merge pull request #2 from ericszimmermann/ez_snr_multibyte2.1
Forgot the bytewise reverse of the outgoing path
2026-05-25 08:30:12 +02:00
zjs81 0287de1862 Merge pull request #438 from ericszimmermann/ez_translate_notification
Translate Notifications
2026-05-24 15:47:47 -07:00
HDDen 4dd472e3c3 Onstart sync progressbar: changed default screen to channels
Because it's synchronization is faster and first in line
2026-05-24 15:33:54 +03:00
ericz bdd0d3dee2 make raster tile source scollable 2026-05-24 11:55:50 +02:00
ericz 11a4f9a373 try OpenStreetMap Dark 2026-05-24 11:37:56 +02:00
ericz acbe4dcc49 remove inversal for outgoing path 2026-05-24 10:04:44 +02:00
ericz 9b99eadcf7 forgot the bytewise reverse of the outgoing path 2026-05-23 23:28:01 +02:00
HDDen ed0e6b6554 Onstart sync progressbar: init 2026-05-23 21:06:52 +03:00
HDDen 6d258154a0 fix Flutter SDK update
PR #458 included
2026-05-23 18:28:16 +03:00
ericz 09a83c4f22 add high dpi stadiamaps 2026-05-23 17:21:23 +02:00
PacoX 5486ae8ccb fix multibyte path in private message and repeter 2026-05-23 13:32:11 +02:00
ericz fc67d8d5aa show cached tiles 2026-05-23 13:10:59 +02:00
ericz 225d07b440 add stadiamaps.com 2026-05-23 13:10:23 +02:00
ericz bac82dc9e8 Fix Flutter SDK update 2026-05-21 00:11:54 +02:00
ericz 8682e6ea67 fix missing _handleQueuedMessageReceived after merge of dev 2026-05-21 00:02:21 +02:00
ericszimmermann 30a1a36ee4 Merge branch 'dev' into ez_translate_notification 2026-05-20 23:26:05 +02:00
ericz 3fe5cdf55d update to current dev a50c0d0b2d 2026-05-20 23:20:16 +02:00
ericz 9ada4ea601 add toggle for autmatically translated messages for notification and chat or manual translation on message action. Due to heavy battery usage. 2026-05-20 21:24:54 +02:00
PacoX 945a118853 expose mode 3 in UI 2026-05-20 08:50:17 +02:00
HDDen 7a823654df Telemetry: room-server request fix #2
The telemetry parser has been expanded and should now support significantly more metrics. It has been ported from the Python implementation of meshcore_py.
2026-05-19 23:17:06 +03:00
HDDen 425229fce8 Telemetry request: map and autorefresh 2026-05-19 20:37:34 +03:00
HDDen c4b3971bdd Squashed commit of the following:
commit 83ffe44025
Author: HDDen <62592944+HDDen@users.noreply.github.com>
Date:   Tue May 19 19:16:48 2026 +0300

    fix Flutter SDK update
2026-05-19 19:17:13 +03:00
PacoX 01d4f002f8 add unit tests 2026-05-19 10:50:18 +02:00
PacoX 9331b02f94 Merge pull request #1 from ericszimmermann/ez_snr_multibyte2
Multibyte Support for SNR-Indicator, channel message_bubble and Setting
2026-05-19 10:14:03 +02:00
PacoX 27f7932db9 fix import 2026-05-19 08:52:44 +02:00
PacoX 4a39a7cd46 fix from copilot review + doc fix 2026-05-19 08:49:52 +02:00
zjs81 a50c0d0b2d Merge pull request #453 from HDDen/mcoa-roomsrv-alfa
Room-server: fixed first message letters trim
2026-05-18 15:49:04 -07:00
ericz 3f1bd54889 fix wrong hop count in channel_message_path_screen observed path chooser. 2026-05-18 22:49:56 +02:00
ericz 91759f531a fix double entry due to mixed 1byte and 2byte paths 2026-05-18 21:30:37 +02:00
ericz 87708e2684 fixes direct repeater mixing 1 byte paths with 2 byte Indicator, and wrong description. 2026-05-18 20:42:40 +02:00
ericz 73e5b80232 add HashMode Setting 2026-05-18 19:17:53 +02:00
ericz 28a5913ba3 multibyte for snr_indicator and map_filtering. 2026-05-18 19:00:07 +02:00
PacoX 091c754584 clean 2026-05-18 09:00:30 +02:00
PacoX cf27c59c84 path aware fix 2026-05-18 08:57:13 +02:00
PacoX 119590434a path aware 2026-05-17 20:14:16 +02:00
PacoX dc82414447 rollback-pr review 2026-05-17 19:37:43 +02:00
PacoX 23f29f2cda rollback-pr review 2026-05-17 17:44:30 +02:00
PacoX d66b16a4e8 PR review modif + packet aware 2026-05-17 15:53:00 +02:00
PacoX d6647f4701 PR review modif + packet aware 2026-05-17 15:50:25 +02:00
HDDen 72448f67d0 Room-server: fixed first message letters trim 2026-05-17 13:20:49 +03:00
ericz bc5f299350 try fix codex sync problem 2026-05-17 10:47:50 +02:00
zjs81 6d97ad6855 Merge pull request #449 from Stempit/add-runssian-presets
Add Russian regional presets
2026-05-15 12:28:10 -07:00
PacoX 87b0fd6fc7 removed distance-based rejection filter 2026-05-14 12:55:46 +02:00
ericz 1fbe1823cb only take translated result if status==completed 2026-05-13 18:06:26 +02:00
PacoX 4a066e08dc fix from PR review 2026-05-13 13:44:42 +02:00
ZIER f941f0dbfa avoid double translation and strip replyInfo from translated text. 2026-05-13 13:20:07 +02:00
PacoX 94ed729985 bug fix 2026-05-13 10:43:57 +02:00
PacoX 8296f44a4e update doc 2026-05-13 09:09:11 +02:00
PacoX 4e70fd2a5e clean 2026-05-13 08:51:44 +02:00
PacoX ca02197414 UI fix 2026-05-13 08:49:24 +02:00
Stempit 352a6c427e Reorder alphabetically 2026-05-13 01:49:51 +03:00
Stempit 5f9259e41f Add Russian regional presets 2026-05-13 01:33:05 +03:00
ZIER 75ae903b99 implement flutter_langdetect 2026-05-12 20:58:46 +02:00
zjs81 8892823337 Merge pull request #444 from sethoscope/fix-channel-chat-icon
Use correct channel icons in channel chat screen
2026-05-12 09:55:30 -07:00
zjs81 e738664f89 Merge pull request #448 from zjs81/main
Main
2026-05-12 09:54:39 -07:00
zjs81 e37616fa15 Merge pull request #145 from pioneer/unread-peoplefirst
Unread badges for tabs
2026-05-12 09:53:33 -07:00
PacoX 6e2abfeb2d multi-byte paths-doc 2026-05-12 16:40:34 +02:00
PacoX b62c1c7338 multi-byte paths-debug 2026-05-12 16:33:33 +02:00
PacoX 17379394bd multi-byte paths 2026-05-12 16:08:19 +02:00
Seth Golub 2763d83fe4 Use correct channel icons in channel chat screen
At the top of the channel chat screen is an icon, indicating the
channel type.

Previously, the public icon was used correctly, but the
hashtag icon was used for all other types.

Now, consistent with the channels screen, we use the lock icon for
private channels, and the composite icons for community public &
community hashtag types.

The fix for private channels was trivial, as we can identify hashtag
channels by their name. Finding out whether a channel belongs to a
community is much more involved. All the hard-working code was copied
from channels_screen.dart. (I tried refactoring to reduce duplication,
but my results were complex and not worth it.)

Closes #432
2026-05-11 21:13:50 -07:00
Serge Tarkovski 77018dc358 Recompute channels unread total after cachedChannels is updated 2026-05-12 00:47:26 +03:00
ericz 1f6b2dacf9 changed translation prompt back to specific input. 2026-05-11 07:15:39 +02:00
zjs81 21c58d4e13 Merge pull request #443 from Maxb0tbeep/dev
Add "NRF52" as a device name prefix
2026-05-10 21:05:59 -07:00
Max Cooley 3af97ff6dd Accidentally wrote quotes instead of backticks...oops 2026-05-10 17:19:57 -07:00
Max Cooley 703d5a1ec4 Add "NRF52" as a device name prefix 2026-05-10 17:12:48 -07:00
ericz d2a6fbe182 translate notifications. 2026-05-10 11:09:25 +02:00
zjs81 e801a497f8 Merge pull request #435 from zjs81/dev
merge dev into main
2026-05-09 19:30:34 -07:00
Serge Tarkovski d3c7d8e43a Red dot unread indicator in bottom tabs, keep numeric unreads only for the lists; fixed unread indicator wasn't on all screens 2026-04-22 01:57:12 +03:00
Serge Tarkovski 0c1e163b88 Reverted Ukrainian translations, will be in a separate PR 2026-04-21 23:50:08 +03:00
Serge Tarkovski d0d6a34fb5 Restore jni to whatever is in main 2026-04-21 23:44:14 +03:00
Serge Tarkovski 16ce1359d7 Remove unused 'Users first' translation key 2026-04-21 23:10:39 +03:00
Serge Tarkovski 9fe4a3710d Add missing users-first translations for hu/ja/ko and regen outputs 2026-04-21 17:01:51 +03:00
Serge Tarkovski 8611adab1f Run dart format and verify analyze 2026-04-21 16:51:10 +03:00
Serge Tarkovski 7d457cb863 Merge main into unread-peoplefirst
Resolved conflicts by accepting refactored state management from main:
- list_filter_widget.dart: Adopt sealed class pattern for filter actions
- contacts_screen.dart: Move state to UiViewStateService instead of local setState
- device_screen.dart: Accept deletion (consolidated into other screens in main)

Main branch includes significant improvements:
- TCP and USB transport support
- Service-based state management with UiViewStateService
- Translation support with message translation buttons
- Signal UI consistency improvements
- Additional language support (hu, ja, ko)
- Comprehensive test coverage
- Discovery screen refactoring
2026-04-21 16:45:43 +03:00
Serge Tarkovski 297516fc80 Update cached unread total when removing contact unread entries
When contacts are removed in removeContact, _handleContact, or _handleContactAdvert,
subtract their unread count from _cachedContactsUnreadTotal immediately so badge
counts reflect the true total without waiting for a full reload.
2026-04-21 16:34:22 +03:00
Stephan Rodemeier 0e074fd806 Add region management
This adds region management: the user can manage a list of available regions
and for each channel pick a region from that list to apply to messages.

Region discovery from nearby repeaters will be done in a separate PR.

This is a part of the work needed for #120.
2026-04-14 13:07:50 +02:00
Serge Tarkovski 1b94442ab6 Fix action constant collision: change _actionTogglePrioritizeUsers from 10 to 11 2026-02-27 21:19:13 +02:00
Serge Tarkovski 3ae14781f0 AI translations for "Users first" 2026-02-27 12:58:32 +02:00
Serge Tarkovski ecc496f2af Merge branch 'main' into unread-peoplefirst 2026-02-27 12:57:59 +02:00
Serge Tarkovski 87b25655d0 Package updates from main 2026-02-27 12:43:21 +02:00
Serge Tarkovski c47a4cb622 fix: filter by _shouldTrackUnreadForContactKey when recalculating cached contacts unread total 2026-02-27 12:28:57 +02:00
Serge Tarkovski a30fc439f3 refactor: use UnreadBadge widget in QuickSwitchBar for consistent badge styling 2026-02-27 12:22:26 +02:00
Serge Tarkovski afcc4db405 fix: clamp cached unread totals to prevent negative badge counts
Clamp both _cachedContactsUnreadTotal and _cachedChannelsUnreadTotal
to >= 0 after decrementing in markContactRead() and markChannelRead().
This prevents the totals from going negative if the cache drifts
out-of-sync, which could cause UI badges to display incorrect values.
2026-02-18 20:37:34 +02:00
Serge Tarkovski 87bcb6a6a3 Proper formatting 2026-02-09 17:40:56 +02:00
Serge Tarkovski 68bb031bb6 "Users first" instead of "People first" everywhere 2026-02-09 17:34:18 +02:00
Serge Tarkovski c4f5c7b171 Cache for unread total 2026-02-09 17:18:21 +02:00
Serge Tarkovski 2bce14224d Update generated plugin registrants after merge 2026-02-09 16:27:53 +02:00
Serge Tarkovski fd305fd55b Update generated plugin registrants after merge 2026-02-09 13:19:31 +02:00
Serge Tarkovski d0dd805244 Merge branch 'main' into unread-peoplefirst 2026-02-09 13:16:05 +02:00
Serge Tarkovski 8668564464 Correct unread badges for tabs; people first contacts sort option 2026-02-09 12:56:38 +02:00
281 changed files with 79710 additions and 19821 deletions
+3
View File
@@ -0,0 +1,3 @@
github: [zjs81]
custom:
- "https://paypal.me/zjs82"
+3
View File
@@ -91,3 +91,6 @@ keystore.properties
# Cloudflare Wrangler # Cloudflare Wrangler
.wrangler .wrangler
# Claude Code local working dir (worktrees, jobs, settings)
.claude/
+10 -9
View File
@@ -41,7 +41,7 @@ lib/
├── models/ # Plain data classes (Contact, Channel, Message, Community, …) ├── models/ # Plain data classes (Contact, Channel, Message, Community, …)
├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …) ├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …)
├── storage/ # SharedPreferences-backed stores, scoped per device key ├── storage/ # SharedPreferences-backed stores, scoped per device key
├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers) ├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers, path hop resolution)
├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs) ├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs)
├── theme/ # MeshPalette (defined, not yet wired in main.dart) ├── theme/ # MeshPalette (defined, not yet wired in main.dart)
├── l10n/ # ARB localization for 18 locales ├── l10n/ # ARB localization for 18 locales
@@ -194,14 +194,14 @@ enum MeshCoreConnectionState {
## Dependencies ## Dependencies
App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2` App version: `9.5.0+13` — Dart SDK constraint: `^3.9.2`
**Connectivity** **Connectivity**
| Package | Version | Purpose | | Package | Version | Purpose |
|---------|---------|---------| |---------|---------|---------|
| flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer | | flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer |
| flutter_blue_plus_platform_interface | ^8.2.1 | Platform-interface layer required by flutter_blue_plus | | flutter_blue_plus_platform_interface | ^9.0.2 | Platform-interface layer required by flutter_blue_plus |
| flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) | | flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) |
**State / Storage** **State / Storage**
@@ -232,7 +232,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
| Package | Version | Purpose | | Package | Version | Purpose |
|---------|---------|---------| |---------|---------|---------|
| material_symbols_icons | ^4.2906.0 | Extended Material Symbols icon set (line-of-sight, etc.) | | material_symbols_icons | ^4.2928.1 | Extended Material Symbols icon set (line-of-sight, etc.) |
| flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) | | flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) |
| cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network | | cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network |
| flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image | | flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image |
@@ -246,7 +246,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
| Package | Version | Purpose | | Package | Version | Purpose |
|---------|---------|---------| |---------|---------|---------|
| flutter_local_notifications | ^20.1.0 | Shows local push notifications for incoming messages | | flutter_local_notifications | ^22.0.0 | Shows local push notifications for incoming messages |
| flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection | | flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection |
**ML / AI** **ML / AI**
@@ -255,7 +255,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|---------|---------|---------| |---------|---------|---------|
| ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts | | ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts |
| ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo | | ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo |
| llamadart | >=0.6.8 <0.7.0 | On-device LLM inference used in `translation_service.dart` for message translation | | llamadart | ^0.8.0 | On-device LLM inference used in `translation_service.dart` for message translation |
| flutter_langdetect | ^0.0.1 | Detects a message's source language in `translation_service.dart` before translating |
**Misc** **Misc**
@@ -263,8 +264,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|---------|---------|---------| |---------|---------|---------|
| http | ^1.2.0 | Fetches tile URLs and any remote API calls | | http | ^1.2.0 | Fetches tile URLs and any remote API calls |
| url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text | | url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text |
| share_plus | ^12.0.1 | Shares files (e.g. exported GPX tracks) via the system share sheet | | share_plus | ^13.1.0 | Shares files (e.g. exported GPX tracks) via the system share sheet |
| package_info_plus | ^9.0.0 | Reads app version/build number displayed in settings | | package_info_plus | ^10.1.0 | Reads app version/build number displayed in settings |
| web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web | | web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web |
| intl | any | Internationalization and locale formatting (required by flutter_localizations) | | intl | any | Internationalization and locale formatting (required by flutter_localizations) |
| build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) | | build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) |
@@ -333,4 +334,4 @@ PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmod
| `lib/services/translation_service.dart` | On-device LLM translation (llamadart) | | `lib/services/translation_service.dart` | On-device LLM translation (llamadart) |
| `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` | | `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` |
| `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect | | `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect |
| `pubspec.yaml` | Dependencies and project metadata (current version `8.0.0+11`) | | `pubspec.yaml` | Dependencies and project metadata (current version `9.5.0+13`) |
+8 -3
View File
@@ -48,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **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 - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
- **Location Sharing**: Share GPS coordinates and custom markers with contacts - **Location Sharing**: Share GPS coordinates and custom markers with contacts
- **Offline Maps**: Download map tiles for offline use in remote areas - **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key)
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
### Device Management ### Device Management
@@ -94,12 +94,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|---------|---------| |---------|---------|
| flutter_blue_plus | Bluetooth Low Energy communication | | flutter_blue_plus | Bluetooth Low Energy communication |
| provider | State management | | provider | State management |
| sqflite | Local database storage | | shared_preferences | Local key-value storage (scoped per device) |
| flutter_map | Interactive map display | | flutter_map | Interactive map display |
| latlong2 | Geographic coordinate handling | | latlong2 | Geographic coordinate handling |
| flutter_local_notifications | Background notification support | | flutter_local_notifications | Background notification support |
| smaz | Message compression |
| pointycastle | Cryptographic operations | | pointycastle | Cryptographic operations |
| llamadart | On-device LLM message translation |
| intl | Internationalization and date formatting | | intl | Internationalization and date formatting |
## Getting Started ## Getting Started
@@ -193,6 +193,7 @@ Devices are discovered by scanning for BLE advertisements with known MeshCore de
- `WisCore-` - `WisCore-`
- `HT-` - `HT-`
- `LowMesh_MC_` - `LowMesh_MC_`
- `NRF52`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`. New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
@@ -223,6 +224,10 @@ Messages are transmitted as binary frames using a custom protocol optimized for
This is an open-source project. Contributions are welcome! This is an open-source project. Contributions are welcome!
## SWHID and Archive badge
[![SWH](https://archive.softwareheritage.org/badge/origin/https://github.com/zjs81/meshcore-open/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/zjs81/meshcore-open)
[![SWH](https://archive.softwareheritage.org/badge/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55/)](https://archive.softwareheritage.org/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55;origin=https://github.com/zjs81/meshcore-open;visit=swh:1:snp:47656c4b55ab40a689ff8d2f045196725f05096b;anchor=swh:1:rev:0fe250230905fdd05dbedc0f546736990beacf53)
### Development Guidelines ### Development Guidelines
- Follow the Flutter style guide - Follow the Flutter style guide
+23 -3
View File
@@ -43,9 +43,22 @@ android {
// arguments += listOf("-DANDROID_STL=c++_shared") // arguments += listOf("-DANDROID_STL=c++_shared")
// } // }
// } // }
// ndk { // arm64-v8a only, deliberately.
// abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") //
// } // * ONNX Runtime (flutter_onnxruntime, used by the AEIC-SE image codec)
// ships a per-ABI .so. arm64-v8a alone costs ~18 MB of APK; a
// universal APK carrying armeabi-v7a and x86_64 as well costs ~56 MB.
// * llamadart only declares android-arm64 and android-x64 backends in
// pubspec.yaml's `hooks.user_defines`, so an armeabi-v7a build already
// has no translation backend at all.
// * The image codec needs ~2.7 GiB peak resident, which no 32-bit
// address space can provide regardless of ABI.
//
// Consequence: this APK will not install on 32-bit-only ARM devices or on
// x86_64 emulators. For emulator work, temporarily add "x86_64" here.
ndk {
abiFilters += listOf("arm64-v8a")
}
} }
signingConfigs { signingConfigs {
@@ -67,6 +80,13 @@ android {
} else { } else {
signingConfigs.getByName("debug") signingConfigs.getByName("debug")
} }
// ONNX Runtime resolves its Java classes from native code by name.
// Without these rules R8 renames them and the process SIGABRTs with
// "java_class == null" the instant the codec runs a model.
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
} }
} }
+19
View File
@@ -0,0 +1,19 @@
# ONNX Runtime looks its Java classes up from native code with FindClass /
# GetMethodID, by literal name. R8 renames them, the lookup returns null, and
# the process dies with:
#
# JNI DETECTED ERROR IN APPLICATION: java_class == null
# at art::JNI<false>::GetMethodID
# from convertToTensorInfo -> Java_ai_onnxruntime_OrtSession_run
#
# It is a hard SIGABRT in native code, so nothing in Dart can catch it: the app
# vanishes the moment the codec touches the model. Keep the whole package
# these are the types the native layer reflects on to build tensors and read
# results back.
-keep class ai.onnxruntime.** { *; }
-keepclassmembers class ai.onnxruntime.** { *; }
-dontwarn ai.onnxruntime.**
# The Flutter plugin's platform-channel handler, reached the same way.
-keep class com.masicai.flutteronnxruntime.** { *; }
-dontwarn com.masicai.flutteronnxruntime.**
+1 -1
View File
@@ -22,7 +22,7 @@
<uses-feature android:name="android.hardware.usb.host" android:required="false"/> <uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application <application
android:label="meshcore_open" android:label="MeshCore Open"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<service <service
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#9F9D95</color>
</resources>
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

+2 -1
View File
@@ -14,7 +14,8 @@ MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences 8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors 9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more 10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture 11. [Routing Paths](routing-paths.md) - Path encoding, validation, device capability detection, and storage
12. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
## App Overview ## App Overview
+2 -1
View File
@@ -183,7 +183,7 @@ An ML-based service that predicts expected delivery timeouts:
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time) - Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
- Features with zero variance are automatically excluded from training - Features with zero variance are automatically excluded from training
- Blends per-contact statistics with ML predictions - Blends per-contact statistics with ML predictions
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data - Falls back to `3000 + 3000 × pathLength` ms when insufficient data. Note: `pathLength` here refers to the stored hop count in the app's model/storage (number of hops), not the on-air encoded byte length.
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost) - Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
--- ---
@@ -198,6 +198,7 @@ Tap the translate button on any received message. On first use, the GGUF model f
### How It Works ### How It Works
- Model files are managed by `TranslationFileStore`; download progress is shown in-place - Model files are managed by `TranslationFileStore`; download progress is shown in-place
- Before translating, the source language is automatically detected using the `flutter_langdetect` package. If the detected language already matches the target language, translation is skipped
- Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android) - Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android)
- Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble - Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble
- Each translation is cached; re-tapping shows the cached result without re-running inference - Each translation is cached; re-tapping shows the cached result without re-running inference
+8 -1
View File
@@ -56,6 +56,7 @@ enum MeshCoreConnectionState {
- `Lilygo` - `Lilygo`
- `HT-` - `HT-`
- `LowMesh_MC_` - `LowMesh_MC_`
- `NRF52`
2. **Connect** with 15-second timeout (6 seconds on Linux) 2. **Connect** with 15-second timeout (6 seconds on Linux)
3. **Request MTU** 185 bytes (non-web only) 3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS 4. **Discover services** and locate NUS
@@ -115,13 +116,17 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
| 32 | CMD_SET_CHANNEL | Set channel name and PSK | | 32 | CMD_SET_CHANNEL | Set channel name and PSK |
| 36 | CMD_SEND_TRACE_PATH | Request path trace | | 36 | CMD_SEND_TRACE_PATH | Request path trace |
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters | | 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry | | 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry |
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables | | 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | | 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request | | 50 | CMD_SEND_BINARY_REQ | Send binary request |
| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) |
| 55 | CMD_SEND_CONTROL_DATA | Send control data (e.g. zero-hop discovery, v8+) |
| 56 | CMD_GET_STATS | Request companion radio stats |
| 57 | CMD_SEND_ANON_REQ | Send anonymous request | | 57 | CMD_SEND_ANON_REQ | Send anonymous request |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | | 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration | | 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
| 61 | CMD_SET_PATH_HASH_MODE | Set path hash width (bytes per hop) |
## Response / Push Codes (Device → App) ## Response / Push Codes (Device → App)
@@ -145,6 +150,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) | | 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition | | 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables | | 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
| 24 | RESP_CODE_STATS | Companion radio stats |
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags | | 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen | | 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact | | 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
@@ -158,6 +164,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered | | 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data | | 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response | | 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
## Data Models ## Data Models
+8 -9
View File
@@ -4,7 +4,7 @@
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh. Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
Up to 8 channels (indices 07) can be active simultaneously on one device. The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login.
## How to Access ## How to Access
@@ -17,7 +17,7 @@ QuickSwitchBar tab 1 (middle) from any main screen.
| Public | Globe | Green | Fixed well-known PSK; any device can join | | Public | Globe | Green | Fixed well-known PSK; any device can join |
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention | | Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key | | Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret | | Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret |
## Channels List Screen ## Channels List Screen
@@ -26,12 +26,12 @@ QuickSwitchBar tab 1 (middle) from any main screen.
- **Search bar** with live text filtering (300ms debounce) - **Search bar** with live text filtering (300ms debounce)
- **Sort/filter button** - **Sort/filter button**
- **Scrollable list of channel cards**, each showing: - **Scrollable list of channel cards**, each showing:
- Type icon with color coding (purple badge overlay for community channels) - Type icon with color coding (magenta badge overlay for community channels)
- Channel name (or "Channel N" if unnamed) - Channel name (or "Channel N" if unnamed)
- Unread badge (if messages are unread) - Unread badge (if messages are unread)
- Drag handle (when manual sort is active) - Drag handle (when manual sort is active)
- **"+" FAB** to add a new channel - **"+" FAB** to add a new channel
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings - **Overflow menu**: Disconnect, Manage Communities, Settings
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown. If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
@@ -59,7 +59,7 @@ Tap the "+" FAB to open a dialog with six options:
| Action | Description | | Action | Description |
|---|---| |---|---|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) | | Edit | Change name, PSK (with a dice icon to generate a random PSK), SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit), or Cyr2Lat encoding toggle (transliterates Cyrillic to Latin for compatibility) |
| Mute / Unmute | Toggle push notification suppression for this channel | | Mute / Unmute | Toggle push notification suppression for this channel |
| Delete | Remove the channel from the device (confirmation required) | | Delete | Remove the channel from the device (confirmation required) |
@@ -100,8 +100,7 @@ Tap a channel card to open the channel chat screen.
### Message Path Viewing ### Message Path Viewing
- **Mobile**: Tap a message bubble to view its routing path - **All platforms**: Long-press (or right-click on desktop) a message bubble → "Path"
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md)) - Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
### Context Actions (Long-Press / Right-Click) ### Context Actions (Long-Press / Right-Click)
@@ -109,7 +108,7 @@ Tap a channel card to open the channel chat screen.
| Action | Availability | Description | | Action | Availability | Description |
|---|---|---| |---|---|---|
| Reply | All messages | Triggers reply mode | | Reply | All messages | Triggers reply mode |
| Path | Desktop only | Opens message path view | | Path | All messages | Opens message path view |
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) | | Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
| Copy | All messages | Copies text to clipboard | | Copy | All messages | Copies text to clipboard |
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread | | Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
@@ -142,7 +141,7 @@ From the channels screen overflow menu → "Manage Communities". Opens a draggab
- **Tap a community** to directly show its QR code for sharing - **Tap a community** to directly show its QR code for sharing
- **Popup menu** per community: - **Popup menu** per community:
- **Show QR** — displays the QR code for sharing with new members - **Show QR** — displays the QR code for sharing with new members
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed) - **Leave Community** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
## How Channels Differ from Direct Messages ## How Channels Differ from Direct Messages
+6 -7
View File
@@ -18,17 +18,15 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
- **Title**: Contact name - **Title**: Contact name
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details. - **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
- **Action buttons**: - **Action button**:
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing - **Overflow menu** (⋮ icon): Contains Routing, Info, Telemetry, Settings, and Clear Chat. Routing opens the routing sheet where you can switch between Auto, Direct, and Flood routing and manage recent paths (hop count, round-trip time, age, success count, color-coded by repeater). Info shows a dialog with contact type, path, GPS coordinates, and public key.
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
### Message List ### Message List
- Scrollable list with newest messages at the bottom - Scrollable list with newest messages at the bottom
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background - **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name) - **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
- Bubble width capped at 65% of screen width - Bubble width capped at 72% of screen width
- Hyperlinks rendered as tappable green underlined text - Hyperlinks rendered as tappable green underlined text
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset - **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset
- **Jump to bottom**: Floating button appears when scrolled away from the bottom - **Jump to bottom**: Floating button appears when scrolled away from the bottom
@@ -87,7 +85,7 @@ When a direct message is sent:
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed 1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts 2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate) 3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise from the device's own `est_timeout` in `RESP_CODE_SENT` (clamped to the physics range); otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate). The result is capped at 45 seconds.
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...) 4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
5. **Max retries**: Configurable (default 5, range 210) 5. **Max retries**: Configurable (default 5, range 210)
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered" 6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
@@ -114,8 +112,9 @@ Add emoji reactions to incoming messages (not your own):
| Action | Availability | Description | | Action | Availability | Description |
|---|---|---| |---|---|---|
| Add reaction | Incoming messages only | Opens emoji picker | | Add reaction | Incoming messages only | Opens emoji picker |
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path | | View path | All platforms: long-press/right-click menu | Shows message routing path |
| Copy | All messages | Copies text to clipboard | | Copy | All messages | Copies text to clipboard |
| Translate | Incoming messages only (when translation is enabled and not yet translated) | Translates the message on-demand using the on-device model |
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread | | Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
| Delete | All messages | Removes locally (not from mesh) | | Delete | All messages | Removes locally (not from mesh) |
| Retry | Failed outgoing messages | Re-sends the message | | Retry | Failed outgoing messages | Re-sends the message |
+17 -18
View File
@@ -6,18 +6,17 @@ The Contacts screen is the primary hub for managing mesh nodes your radio has a
## How to Access ## How to Access
- Automatically shown after connecting to a device - QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting)
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
- Back navigation from Chat or Settings screens - Back navigation from Chat or Settings screens
## Contact Types ## Contact Types
| Type | Avatar Color | Icon | Description | | Type | Avatar Color | Icon | Description |
|---|---|---|---| |---|---|---|---|
| Chat | Blue | Chat bubble | Another user's mesh radio | | Chat | Blue | Initials / emoji | Another user's mesh radio |
| Repeater | Orange | Cell tower | A mesh repeater/relay node | | Repeater | Amber | Cell tower | A mesh repeater/relay node |
| Room | Purple | Group | A room server for group chat | | Room | Magenta | Meeting room | A room server for group chat |
| Sensor | Green | Sensors | A sensor device | | Sensor | Teal | Sensors | A sensor device |
## Contact List ## Contact List
@@ -73,42 +72,42 @@ Groups are stored per radio identity (scoped by public key).
| Action | Availability | Description | | Action | Availability | Description |
|---|---|---| |---|---|---|
| Ping | Repeaters (always) | Opens PathTraceMapScreen targeting the repeater | | Ping | Repeaters only | Opens PathTraceMapScreen targeting the repeater |
| Path Trace | Rooms (always); Chat/Sensor if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available | | Path Trace | Rooms (always); Chat/Sensor only if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen | | Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
| Room Login | Rooms only | Login dialog → ChatScreen | | Room Login | Rooms only | Login dialog → ChatScreen |
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) | | Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
| Open Chat | Chat/Sensor | Same as single tap |
| Add/Remove Favorite | All types | Toggles the favorite flag | | Add/Remove Favorite | All types | Toggles the favorite flag |
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard | | Share Contact | All types | Requests advert from device → copies `meshcore://<hex>` URI to clipboard |
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop | | Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages | | Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
## App Bar Menus ## App Bar Menus
The Contacts screen has **two separate popup menus** in the app bar: The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar:
**Antenna icon menu** (contact sharing): - Discovered Contacts — opens the DiscoveryScreen
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
- *(divider)*
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes - Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
- Flood Advert — broadcasts across the full mesh network - Flood Advert — broadcasts across the full mesh network
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally - Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it - *(divider)*
**Three-dot overflow menu**:
- Disconnect — disconnects from the device - Disconnect — disconnects from the device
- Discovered Contacts — opens the DiscoveryScreen
- Settings — opens the Settings screen - Settings — opens the Settings screen
A **floating action button** (person-add icon) provides a shortcut sheet to "Add Contact from Clipboard" or "Discovered Contacts".
## Adding Contacts ## Adding Contacts
### Automatic (Passive) ### Automatic (Passive)
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings). When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
### Import from Clipboard ### Import from Clipboard
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device. Overflow menu (or the FAB shortcut) → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
### Import from Discovered Contacts ### Import from Discovered Contacts
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts. Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
## Contact Sharing Format ## Contact Sharing Format
+25 -18
View File
@@ -35,9 +35,9 @@ Location pins shared in chat messages are displayed as flags:
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent). Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
### Predicted / Guessed Locations (Semi-Transparent) ### Predicted / Guessed Locations
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers. Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers.
#### Why guessed locations exist #### Why guessed locations exist
@@ -55,19 +55,19 @@ In a mesh network, every message hops through one or more repeaters on its way t
5. **Compute the estimated position**: 5. **Compute the estimated position**:
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other. - **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80120m) applied for visual separation. - **Two or more anchors**: The position is a weighted average of all anchor coordinates (each subsequent anchor weighted at half the previous one, biasing toward the first), with a smaller offset radius (120m for 2 anchors, 80m for 3+) applied for visual separation.
6. **Assign confidence level**: 6. **Assign confidence level**:
- **High confidence** (2+ anchors): Displayed at 55% opacity. - **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border).
- **Low confidence** (1 anchor): Displayed at 30% opacity. - **Low confidence** (1 anchor): The marker border is rendered in a muted grey.
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild. 7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
#### How to read guessed locations on the map #### How to read guessed locations on the map
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix. - **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix.
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions. - **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only. - **Grey border**: Lower confidence — based on a single repeater anchor only.
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated. - Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle). - Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
@@ -110,9 +110,16 @@ A map with a polyline showing the route from your node through repeater hops to
- **Green circles**: Hops with known GPS coordinates - **Green circles**: Hops with known GPS coordinates
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts) - **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
- **Red endpoint**: Target contact with known GPS - **Red endpoint**: Target contact with known GPS
- **Purple semi-transparent endpoint**: Target with guessed position - **Magenta endpoint**: Target with guessed position
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance. A bottom panel shows each hop pair with SNR quality icons and total path distance. When multiple observed paths are available, a **Single / Combined** toggle appears at the top of the map. In Combined view, all paths are overlaid; shared segments are highlighted with a white halo and a path count badge appears on shared nodes.
The bottom panel also provides **packet animation controls**:
- **Animation toggle** (on/off)
- **Step back / Play / Step forward / Replay** buttons
- **Follow packet lock** — keeps the map camera centered on the moving packet dot
- **Speed selector** (0.5×, 1×, 2×, 4×)
- A live **"Hop x of y · from → to"** label that tracks the active segment
### How It Works ### How It Works
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte. Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
@@ -125,17 +132,17 @@ Sends a trace request frame over the mesh. The repeater network traces the path
From the main map, tap the terrain/antenna icon. From the main map, tap the terrain/antenna icon.
### What the User Sees ### What the User Sees
A full-screen map with a collapsible control panel containing: A full-screen map with a draggable bottom sheet containing:
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow) - **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow); obstruction points are marked as clickable dots on the chart
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance - **Status summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button - **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button
### Key Interactions ### Key Interactions
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable) - **Long-press the map** to add custom endpoints (pushpin markers, renameable/deleteable)
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set - **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
- **Antenna heights** are adjustable for both endpoints - **Antenna heights** are adjustable for both endpoints
- **Map line** between endpoints is colored green (clear) or red (blocked) - **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked)
- Terrain elevation is fetched from the Open-Meteo API (2181 sample points, cached 24 hours) - Terrain elevation is fetched from the Open-Meteo API (21, 41, or 81 sample points depending on link distance, cached 24 hours)
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz - K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
--- ---
@@ -149,7 +156,7 @@ Settings → App Settings → Map Display → Offline Map Cache
- Map with a blue polygon overlay showing previously selected cache bounds - Map with a blue polygon overlay showing previously selected cache bounds
- Bounding box coordinates card - Bounding box coordinates card
- **Cache Area** controls: "Use Current View" and Clear buttons - **Cache Area** controls: "Use Current View" and Clear buttons
- **Zoom Range** slider (318) with estimated tile count - **Zoom Range** range slider (318, dual-handle for min and max) with estimated tile count
- **Download progress** bar (when downloading) - **Download progress** bar (when downloading)
- **Download Tiles** and **Clear Cache** buttons - **Download Tiles** and **Clear Cache** buttons
+10 -10
View File
@@ -5,7 +5,7 @@
The app follows this general flow: The app follows this general flow:
``` ```
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Channels Screen
``` ```
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**. After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
@@ -24,7 +24,7 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal
## Disconnection ## Disconnection
- The disconnect button (available in the Settings screen and other main screens) shows a confirmation dialog before disconnecting - The disconnect button (available in the overflow menu of each main screen) shows a confirmation dialog before disconnecting
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback) - If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens - This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
@@ -38,11 +38,11 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal
``` ```
ScannerScreen (root, always on stack) ScannerScreen (root, always on stack)
├─ [BLE connect] → push → ContactsScreen ├─ [BLE connect] → push → ChannelsScreen
├─ [TCP FAB] → push → TcpScreen ├─ [TCP icon button] → push → TcpScreen
│ └─ [TCP connected] → pushReplacement → ContactsScreen │ └─ [TCP connected] → pushReplacement → ChannelsScreen
└─ [USB FAB] → push → UsbScreen └─ [USB icon button] → push → UsbScreen
└─ [USB connected] → pushReplacement → ContactsScreen └─ [USB connected] → pushReplacement → ChannelsScreen
ContactsScreen (selected=0) ContactsScreen (selected=0)
├─ [quick-switch 1] → pushReplacement → ChannelsScreen ├─ [quick-switch 1] → pushReplacement → ChannelsScreen
@@ -60,9 +60,9 @@ ChannelsScreen (selected=1)
MapScreen (selected=2) MapScreen (selected=2)
├─ [quick-switch 0] → pushReplacement → ContactsScreen ├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 1] → pushReplacement → ChannelsScreen ├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [radar button] → push → PathTraceMapScreen ├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built)
├─ [terrain button] → push → LineOfSightMapScreen ├─ [terrain menu item] → push → LineOfSightMapScreen
└─ [long-press] → share marker / set location └─ [long-press] → share marker sheet
Settings (push from any main screen) Settings (push from any main screen)
└─ [App Settings] → push → AppSettingsScreen └─ [App Settings] → push → AppSettingsScreen
+2 -2
View File
@@ -22,7 +22,7 @@ MeshCore Open provides both **system notifications** (push-style OS alerts) and
### 3. Advertisement Notifications ### 3. Advertisement Notifications
- **Triggered when**: A new node is discovered on the mesh for the first time - **Triggered when**: A new node is discovered on the mesh for the first time
- **Title**: "New [type] discovered" (e.g., "New chat node discovered") - **Title**: "New [type] discovered" (e.g., "New Chat discovered")
- **Body**: Contact's name - **Body**: Contact's name
- **Priority**: Default - **Priority**: Default
- **Android channel**: `adverts` - **Android channel**: `adverts`
@@ -43,7 +43,7 @@ Red numeric badges appear throughout the UI:
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages - **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
- **Channels list**: Each channel row shows an unread badge - **Channels list**: Each channel row shows an unread badge
- **Chat screen subtitle**: Shows unread count inline - **Chat screen subtitle**: Shows unread count inline
- Badges cap at "99+" for display - Badges cap at "9999+" for display
### How Unread Counts Work ### How Unread Counts Work
+6 -4
View File
@@ -34,8 +34,8 @@ The central management screen showing:
|---|---|---| |---|---|---|
| Status | Repeater Status Screen | All users | | Status | Repeater Status Screen | All users |
| Telemetry | Telemetry Screen | All users | | Telemetry | Telemetry Screen | All users |
| CLI | Repeater CLI Screen | Admin only |
| Neighbors | Neighbors Screen | All users | | Neighbors | Neighbors Screen | All users |
| CLI | Repeater CLI Screen | Admin only |
| Settings | Repeater Settings Screen | Admin only | | Settings | Repeater Settings Screen | Admin only |
The battery chemistry selector and CLI/Settings cards are hidden from guest users. The battery chemistry selector and CLI/Settings cards are hidden from guest users.
@@ -89,7 +89,7 @@ A terminal-style interface for sending commands directly to the repeater.
- Type a command and press send (or Enter on desktop) - Type a command and press send (or Enter on desktop)
- Up/down arrows navigate through command history - Up/down arrows navigate through command history
- Quick-command buttons populate and send common commands - Quick-command buttons populate and send common commands
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty) - Overflow menu (three-dot icon): "Debug next command" option shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately - Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
- Clear icon: Wipes the command/response history - Clear icon: Wipes the command/response history
- Failed/timed-out commands are automatically retried once - Failed/timed-out commands are automatically retried once
@@ -112,7 +112,9 @@ The in-app help reference (help icon) documents all known commands. Categories:
**Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv` **Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv`
**Sensors**: `sensor get {key}` **Sensors**: `sensor get {key}`, `sensor set {key} {value}`, `sensor list [start]`
**GPS Management**: `gps`, `gps {on|off}`, `gps sync`, `gps setloc`, `gps advert`, `gps advert {none|share|prefs}`
**Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied` **Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied`
@@ -207,7 +209,7 @@ Nine configuration cards, each with its own per-field refresh button(s):
**Danger Zone** (red-styled card) **Danger Zone** (red-styled card)
- Reboot repeater (sends `reboot` with confirmation dialog) - Reboot repeater (sends `reboot` with confirmation dialog)
- Erase filesystem (serial-only; shows informational snackbar only — no command is sent over the air) - Erase filesystem (serial-only; shows a confirmation dialog, then an informational snackbar — no command is sent over the air)
### Key Interactions ### Key Interactions
- **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater - **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater
+79
View File
@@ -0,0 +1,79 @@
# Routing Paths
This page covers how MeshCore Open represents, selects, validates, and stores routing paths in the UI and data layer.
## Path Routing
MeshCore supports variable-length multi-byte routing paths so the app can scale from small meshes to very large node sets.
### Hash Width and Multi-Byte Paths
The device capability determines the hash width (number of bytes per hop):
| Width | Max Unique Nodes | Typical Use |
|-------|-----------------|-------------|
| 1 byte | 256 | Single-byte node IDs |
| 2 bytes | 65,536 | Medium meshes |
| 3 bytes | 16.7M | Large networks |
| 4 bytes | 4.3G | Very large meshes / future-proofing |
### Device Capability Detection
On device connection, the app reads the firmware capability to set `pathHashByteWidth`:
```dart
// Read from device info response (offset 81)
final modeRaw = firmwareBytes.length >= 82
? (firmwareBytes[81] & 0xFF)
: 0;
final mode = modeRaw.clamp(0, 3);
_pathHashByteWidth = mode + 1; // 1, 2, 3, or 4 bytes per hop
```
The connector reads a single-mode byte and clamps to `0..3`, so the supported hop width is `1..4` bytes. UI code also clamps widths when rendering (typically to `1..4`) so 4-byte hops are handled end-to-end in the current codebase.
### Path Data Structure
Paths in messages and storage consist of:
- **`pathLength` (model/storage)**: Hop count (number of hops). Negative values (e.g. `-1`) are used as a flood sentinel.
- **On-air `path_len` byte**: A packed byte that encodes hop count + hash width and is decoded into `pathLength` + `pathBytes` when parsing frames.
- **`pathBytes`**: Raw bytes of the path (concatenated hop prefixes), grouped by `pathHashByteWidth`.
- **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth`.
- **Example**: With `pathHashByteWidth=2`, a 3-hop path has 6 bytes (`pathBytes.length = 6`) and `pathLength = 3`:
- `pathBytes = [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]`
- Hops: `[0xA1A2]`, `[0xB1B2]`, `[0xC1C2]`
### Hop Count Calculation
Convert path byte length to hop count:
```dart
int hopCount = (byteCount + hashByteWidth - 1) ~/ hashByteWidth;
```
Use this consistently when displaying hop counts in UI. Do not treat `pathLength` as a hop count when the path uses multi-byte hop hashes.
### Path Usage in Different Message Types
- **Direct messages**: Extract path from decrypted payload to trace sender route.
- **Channel messages**: Decrypt hop-by-hop routing chain from payload; the header carries the encoded byte length for the path blob, not the derived hop count.
- **Contact storage**: Path length in byte 32, raw path bytes in bytes 33-96, grouped by detected `pathHashByteWidth`.
### UI Hop Count Display
⚠️ **Important**: In screens like "Channel Message Path", prefer the actual decoded `pathBytes` hop count over `pathLength` metadata:
```dart
// Preferred: use actual observed path length
final effectiveHopCount = (pathBytes.length + width - 1) ~/ width;
// Avoid: using encoded byte length as if it were a hop count
// pathLength is bytes; converting it twice causes inflated counts
```
Example scenario:
- Radio header reports `pathLength: 32` bytes
- Decoded path bytes: `[0xAB, 0xCD]` (2 bytes = 1 hop with width=2)
- **Display**: "1 hop" (from `pathBytes`), not "32 hops" (which would double-count the encoded length)
+15 -13
View File
@@ -28,9 +28,11 @@ The BLE Scanner is the app's home screen, displayed immediately on launch.
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets. **Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
**Bottom FAB Row**: Up to three floating action buttons: **App Bar Actions**: Icon buttons in the top-right corner of the app bar:
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only) - **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms) - **TCP/IP** icon button - Opens TCP connection screen (all non-web platforms)
**Bottom FAB**: A single floating action button:
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off - **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
### Device Tile ### Device Tile
@@ -51,7 +53,7 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
### How Scanning Works ### How Scanning Works
- Filters for devices with names starting with one of the known prefixes: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_` - Filters for devices advertising the Nordic UART Service UUID (so community forks with non-standard names are still found). Known name prefixes used by stock firmware builds for reference: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`, `NRF52`
- Uses low-latency scan mode on Android - Uses low-latency scan mode on Android
- Scans for 10 seconds then auto-stops - Scans for 10 seconds then auto-stops
- On iOS/macOS, waits for BLE adapter initialization before starting - On iOS/macOS, waits for BLE adapter initialization before starting
@@ -65,7 +67,7 @@ Tap a device tile or its Connect button:
3. Requests MTU 185 bytes for optimal throughput 3. Requests MTU 185 bytes for optimal throughput
4. Discovers BLE services and locates the Nordic UART Service 4. Discovers BLE services and locates the Nordic UART Service
5. Subscribes to TX notifications for receiving data 5. Subscribes to TX notifications for receiving data
6. On success, automatically navigates to the Contacts screen 6. On success, automatically navigates to the Channels screen
7. On failure, shows a red error snackbar 7. On failure, shows a red error snackbar
--- ---
@@ -74,7 +76,7 @@ Tap a device tile or its Connect button:
### How to Access ### How to Access
From the Scanner screen, tap the **USB** FAB button. From the Scanner screen, tap the **USB** icon button in the app bar.
### What the User Sees ### What the User Sees
@@ -82,15 +84,15 @@ From the Scanner screen, tap the **USB** FAB button.
- A list of detected USB serial ports, each showing: - A list of detected USB serial ports, each showing:
- Friendly display name - Friendly display name
- Raw port name (subtitle, only shown when it differs from the display name) - Raw port name (subtitle, only shown when it differs from the display name)
- "Connect" button - Chevron trailing icon (the entire tile is tappable to connect)
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP) - Transport switcher buttons (outlined, not FABs) to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
### Key Interactions ### Key Interactions
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected) - On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
- On mobile: tap the "Scan" FAB to manually refresh - On mobile: tap the "Scan" FAB to manually refresh
- Tap a port or its Connect button to connect - Tap a port tile to connect
- On successful connection, navigates to Contacts screen - On successful connection, navigates to Channels screen
- On connection failure, the port list automatically refreshes - On connection failure, the port list automatically refreshes
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more) - Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
@@ -100,7 +102,7 @@ From the Scanner screen, tap the **USB** FAB button.
### How to Access ### How to Access
From the Scanner screen, tap the **TCP/IP** FAB button. From the Scanner screen, tap the **TCP/IP** icon button in the app bar.
### What the User Sees ### What the User Sees
@@ -108,7 +110,7 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
- **Host address** text field - **Host address** text field
- **Port number** text field - **Port number** text field
- **Connect** button - **Connect** button
- FABs at the bottom to switch to USB or BLE - Transport switcher buttons (outlined, not FABs) to switch to USB or BLE
### Key Interactions ### Key Interactions
@@ -119,6 +121,6 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
- Validation errors are shown as red snackbars - Validation errors are shown as red snackbars
- The Connect button shows a spinner and "Connecting..." label while in progress - The Connect button shows a spinner and "Connecting..." label while in progress
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000") - The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
- On success, navigates to Contacts screen and saves the host/port to settings - On success, navigates to Channels screen and saves the host/port to settings
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000") - On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
- Error messages for timeout, unsupported platform, and connection failures - Error messages for timeout, unsupported platform, and connection failures
+96 -74
View File
@@ -12,12 +12,13 @@ Settings are only accessible while a device is connected.
The settings screen is a scrollable list of cards: The settings screen is a scrollable list of cards:
1. [Device Info](#device-info) 1. [Device Info](#device-info)
2. [App Settings](#app-settings) (link to sub-screen) 2. [Node Settings](#node-settings)
3. [Node Settings](#node-settings) 3. [Location](#location)
4. [Actions](#actions) 4. [App Settings](#app-settings) (link to sub-screen)
5. [Debug](#debug) 5. [Actions](#actions)
6. [Export](#export) 6. [Export](#export)
7. [About](#about) 7. [Debug](#debug)
8. [About](#about)
--- ---
@@ -40,56 +41,6 @@ Battery shows an alert icon and orange text when at 15% or below. The toggle onl
--- ---
## App Settings
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
### Appearance
- **Theme**: System / Light / Dark
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Notifications
- **Master enable/disable**: Requests OS permission when enabling
- **Message notifications**: New direct message alerts
- **Channel message notifications**: New channel message alerts
- **Advertisement notifications**: New node discovery alerts
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (110, default 5, integer steps)
- Initial Route Weight (0.55.0, default 3.0)
- Success Increment (0.12.0, default 0.5, 0.1 steps)
- Failure Decrement (0.12.0, default 0.2, 0.1 steps)
- Max Message Retries (210, default 5)
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Translation
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
- **Enable Translation**: Translates incoming messages into the selected target language
- **Translate Composer**: Translates outgoing messages from the target language back before sending
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
- **Preset Model**: Download a curated preset model with one tap
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial
- **Offline Map Cache**: Navigate to tile download screen
### Debug
- **App Debug Logging**: Enable the in-app debug log
---
## Node Settings ## Node Settings
These settings are sent directly to the connected device firmware. These settings are sent directly to the connected device firmware.
@@ -101,7 +52,7 @@ These settings are sent directly to the connected device firmware.
### Radio Settings ### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains: Opens a dialog pre-populated with the device's current radio settings. Contains:
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA, WA, QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918 - **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
- **Frequency** (MHz): Free text, validated 3002500 MHz - **Frequency** (MHz): Free text, validated 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz) - **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12 - **Spreading Factor**: SF5SF12
@@ -109,6 +60,13 @@ Opens a dialog pre-populated with the device's current radio settings. Contains:
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm) - **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies - **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
### Companion Radio Stats
Opens the RF statistics screen (RSSI, SNR, packet counts) for the paired radio. Only enabled when connected to a device that supports companion radio stats.
---
## Location
### Location ### Location
Opens a dialog pre-populated with the device's current coordinates (if known): Opens a dialog pre-populated with the device's current coordinates (if known):
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value - Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
@@ -125,8 +83,72 @@ Five toggles controlling which node types are auto-added when heard:
- Auto-add Sensors - Auto-add Sensors
- Overwrite Oldest (when contact list is full) - Overwrite Oldest (when contact list is full)
### Privacy Mode ### Privacy
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements. Opens a dialog with controls for how the node shares telemetry and location data:
- **Advert Location**: Toggle whether the node broadcasts its location in advertisements
- **Multi-Ack**: Toggle multi-ack delivery confirmations
- **Telemetry Base Mode**: Deny All / Allow by Contact / Allow All
- **Telemetry Location Mode**: Deny All / Allow by Contact / Allow All
- **Telemetry Environment Mode**: Deny All / Allow by Contact / Allow All
Settings take effect when saved. A snackbar confirms the update.
---
## App Settings
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
### Appearance
- **Theme**: System / Light / Dark
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
### Notifications
- **Master enable/disable**: Requests OS permission when enabling
- **Message notifications**: New direct message alerts
- **Channel message notifications**: New channel message alerts
- **Advertisement notifications**: New node discovery alerts
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (110, default 5, integer steps)
- Initial Route Weight (0.55.0, default 3.0)
- Success Increment (0.12.0, default 0.5, 0.1 steps)
- Failure Decrement (0.12.0, default 0.2, 0.1 steps)
- Max Message Retries (210, default 5)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial
- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data:
- OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.)
- Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.)
There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles.
- **Offline Map Cache**: Navigate to tile download screen
### Translation
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
- **Enable Translation**: Translates incoming messages into the selected target language
- **Translate Composer**: Translates outgoing messages from the target language back before sending
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
- **Preset Model**: Download a curated preset model with one tap
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
### Cyrillic-to-Latin (Cyr2Lat)
Controls character substitution profiles used to render Cyrillic text in Latin characters. A dropdown selects the active profile; Add, Edit, and Delete buttons manage the profile list (the last remaining profile cannot be deleted). Each profile stores a JSON character map.
### Debug
- **App Debug Logging**: Enable the in-app debug log
--- ---
@@ -136,10 +158,24 @@ One-tap device operations:
| Action | Description | | Action | Description |
|---|---| |---|---|
| Send Advertisement | Floods the mesh with your node's advertisement |
| Sync Time | Sends current Unix timestamp to the device | | Sync Time | Sends current Unix timestamp to the device |
| Refresh Contacts | Re-requests the full contact list | | Refresh Contacts | Re-requests the full contact list |
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) | | Reboot Device | Confirmation dialog → reboots the device (shown in warning color) |
| Delete All Paths | Confirmation dialog → clears all stored routing paths (shown in alert color) |
---
## Export
Three GPX export options (not available on web):
| Option | Exports |
|---|---|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
| Export Contacts | Chat contacts with GPS coordinates |
| Export All | All contacts with GPS coordinates |
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
--- ---
@@ -160,20 +196,6 @@ Structured log entries (Info / Warning / Error), with tag, message, and timestam
--- ---
## Export
Three GPX export options (not available on web):
| Option | Exports |
|---|---|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
| Export Contacts | Chat contacts with GPS coordinates |
| Export All | All contacts with GPS coordinates |
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
---
## About ## About
Shows the standard Flutter about dialog with app name, version, and legal notice. Shows the standard Flutter about dialog with app name, version, and legal notice.
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store-connect</string>
<key>teamID</key>
<string>X74VJ4UAST</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadSymbols</key>
<true/>
<key>destination</key>
<string>export</string>
</dict>
</plist>
-52
View File
@@ -1,73 +1,21 @@
PODS: PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_foreground_task (0.0.1): - flutter_foreground_task (0.0.1):
- Flutter - Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.4.5):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - 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_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/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- share_plus (from `.symlinks/plugins/share_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`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_foreground_task: flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios" :path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
share_plus:
:path: ".symlinks/plugins/share_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"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
+46 -34
View File
@@ -11,6 +11,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -52,6 +53,7 @@
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>"; }; 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>"; }; 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>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -67,6 +69,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */, 9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -85,6 +88,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -180,13 +184,15 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */, F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
); );
name = Runner; name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@@ -220,6 +226,9 @@
Base, Base,
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@@ -283,39 +292,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 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;
};
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
);
name = "Fix Native Asset Minimum OS";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
};
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = { DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -338,6 +314,22 @@
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"; 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; showEnvVarsInLog = 0;
}; };
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
);
name = "Fix Native Asset Minimum OS";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -446,7 +438,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -625,7 +619,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -647,7 +643,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -696,6 +694,20 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }
@@ -5,6 +5,24 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
+2
View File
@@ -37,6 +37,8 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string> <string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string> <string>This app uses the camera to scan QR codes for joining communities.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app lets you pick a photo to compress and send over the mesh.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,7 @@ class MeshCoreUsbManager {
String? get activePortKey => _activePortKey; String? get activePortKey => _activePortKey;
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey; String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
bool get isConnected => _service.isConnected; bool get isConnected => _service.isConnected;
Object? get lastError => _service.lastError;
Stream<Uint8List> get frameStream => _service.frameStream; Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration --- // --- Configuration ---
+101 -4
View File
@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking // Buffer Reader - sequential binary data reader with pointer tracking
@@ -206,6 +207,8 @@ const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40; const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41; const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50; const int cmdSendBinaryReq = 50;
const int cmdSetFloodScope = 54;
const int cmdSendControlData = 55;
const int cmdGetStats = 56; const int cmdGetStats = 56;
const int cmdSendAnonReq = 57; const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58; const int cmdSetAutoAddConfig = 58;
@@ -224,6 +227,18 @@ const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05; const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbors = 0x06; const int reqTypeGetNeighbors = 0x06;
Uint8List buildTelemetryBinaryPayload() {
// Room servers/repeaters read byte 1 as an inverse telemetry permission mask.
// Zero means "request every telemetry field allowed for this contact".
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
}
const int anonReqTypeRegions = 0x01;
// Control data sub-types used by MeshCore discovery packets.
const int controlSubtypeDiscoverReq = 0x08;
const int controlSubtypeDiscoverResp = 0x09;
// Repeater response codes // Repeater response codes
const int respServerLoginOk = 0; const int respServerLoginOk = 0;
@@ -266,6 +281,7 @@ const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A; const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B; const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C; const int pushCodeBinaryResponse = 0x8C;
const int pushCodeControlData = 0x8E;
// Contact/advertisement types // Contact/advertisement types
const int advTypeChat = 1; const int advTypeChat = 1;
@@ -451,8 +467,13 @@ String pubKeyToHex(Uint8List pubKey) {
// Helper to convert hex string to public key // Helper to convert hex string to public key
Uint8List hexToPubKey(String hex) { Uint8List hexToPubKey(String hex) {
if (hex.length != pubKeySize * 2) {
throw FormatException(
'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}',
);
}
final result = Uint8List(pubKeySize); final result = Uint8List(pubKeySize);
for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) { for (int i = 0; i < pubKeySize; i++) {
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16); result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
} }
return result; return result;
@@ -566,9 +587,9 @@ Uint8List buildGetStatsFrame(int statsType) {
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]); return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
} }
/// Path hash width on air: [61][0][mode], mode 0..2 (mode+1) bytes per hop hash. /// Path hash width on air: [61][0][mode], mode 0..3 (mode+1) bytes per hop hash.
Uint8List buildSetPathHashModeFrame(int mode) { Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2); final m = mode.clamp(0, 3).toInt();
return Uint8List.fromList([cmdSetPathHashMode, 0, m]); return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
} }
@@ -855,6 +876,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
return writer.toBytes(); return writer.toBytes();
} }
Uint8List buildSendControlDataFrame(Uint8List payload) {
final writer = BufferWriter();
writer.writeByte(cmdSendControlData);
writer.writeBytes(payload);
return writer.toBytes();
}
Uint8List buildDiscoveryRequestPayload(
int tag, {
bool prefixOnly = false,
int typeMask = 1 << advTypeRepeater,
}) {
final writer = BufferWriter();
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
// subtype 0x8, with the low bit selecting short/full public keys in replies.
writer.writeByte(
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
);
writer.writeByte(typeMask);
writer.writeUInt32LE(tag);
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
return writer.toBytes();
}
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
if (path.isEmpty) return Uint8List(0);
final width = pathHashWidth.clamp(1, 4).toInt();
if (path.length % width != 0) {
return Uint8List.fromList(path.reversed.toList());
}
final reversed = Uint8List(path.length);
final hops = path.length ~/ width;
for (var i = 0; i < hops; i++) {
final from = (hops - 1 - i) * width;
reversed.setRange(i * width, (i + 1) * width, path, from);
}
return reversed;
}
// Build CMD_SEND_ANON_REQ frame.
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
Uint8List buildSendAnonReqFrame(
Uint8List repeaterPubKey, {
required int requestType,
Uint8List? replyPath,
int replyHopCount = 0,
int pathHashWidth = pathHashSize,
}) {
final width = pathHashWidth.clamp(1, 4).toInt();
final path = replyPath ?? Uint8List(0);
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
final writer = BufferWriter();
writer.writeByte(cmdSendAnonReq);
writer.writeBytes(repeaterPubKey);
writer.writeByte(requestType);
writer.writeByte(encodedPathLen);
writer.writeBytes(_reversePathByHop(path, width));
return writer.toBytes();
}
//Build a trace request frame //Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload] //[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) { Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
@@ -945,7 +1027,22 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
writer.writeBytes(Uint8List(3)); // reserved bytes writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey); writer.writeBytes(pubKey);
} else { } else {
writer.writeBytes(Uint8List(4)); // reserved bytes writer.writeBytes(Uint8List(3)); // reserved bytes
} }
return writer.toBytes(); return writer.toBytes();
} }
//Build CMD_SET_FLOOD_SCOPE
// Format: [cmd][scope]
Uint8List buildSetFloodScopeFrame(String region) {
if (region == '') {
// reset scope
return Uint8List.fromList([cmdSetFloodScope, 0]);
}
final name = region.startsWith('#') ? region : '#$region';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
final scope = Uint8List.fromList(hash.sublist(0, 16));
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
}
+5
View File
@@ -3,6 +3,10 @@ class MeshCoreUuids {
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
/// Discovery no longer filters on these (it filters on the [service] UUID so
/// that community forks with custom names are still found); kept for
/// reference and possible future display heuristics.
static const List<String> deviceNamePrefixes = [ static const List<String> deviceNamePrefixes = [
"MeshCore-", "MeshCore-",
"Whisper-", "Whisper-",
@@ -11,5 +15,6 @@ class MeshCoreUuids {
"Lilygo", "Lilygo",
"HT-", "HT-",
"LowMesh_MC_", "LowMesh_MC_",
"NRF52",
]; ];
} }
+209 -9
View File
@@ -96,6 +96,34 @@ class CayenneLpp {
} }
switch (type) { switch (type) {
case lppDigitalInput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppDigitalOutput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppAnalogInput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppAnalogOutput:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppGenericSensor: case lppGenericSensor:
telemetry.add({ telemetry.add({
'channel': channel, 'channel': channel,
@@ -131,6 +159,17 @@ class CayenneLpp {
'value': buffer.readUInt8() / 2, 'value': buffer.readUInt8() / 2,
}); });
break; break;
case lppAccelerometer:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'x': buffer.readInt16BE() / 1000,
'y': buffer.readInt16BE() / 1000,
'z': buffer.readInt16BE() / 1000,
},
});
break;
case lppBarometricPressure: case lppBarometricPressure:
telemetry.add({ telemetry.add({
'channel': channel, 'channel': channel,
@@ -138,6 +177,13 @@ class CayenneLpp {
'value': buffer.readUInt16BE() / 10, 'value': buffer.readUInt16BE() / 10,
}); });
break; break;
case lppAltitude:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE(),
});
break;
case lppVoltage: case lppVoltage:
telemetry.add({ telemetry.add({
'channel': channel, 'channel': channel,
@@ -152,6 +198,13 @@ class CayenneLpp {
'value': buffer.readInt16BE() / 1000, 'value': buffer.readInt16BE() / 1000,
}); });
break; break;
case lppFrequency:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppPercentage: case lppPercentage:
telemetry.add({ telemetry.add({
'channel': channel, 'channel': channel,
@@ -173,6 +226,56 @@ class CayenneLpp {
'value': buffer.readUInt16BE(), 'value': buffer.readUInt16BE(),
}); });
break; break;
case lppDistance:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE() / 1000,
});
break;
case lppEnergy:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE() / 1000,
});
break;
case lppDirection:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppUnixTime:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppGyrometer:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'x': buffer.readInt16BE() / 100,
'y': buffer.readInt16BE() / 100,
'z': buffer.readInt16BE() / 100,
},
});
break;
case lppColour:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'red': buffer.readUInt8(),
'green': buffer.readUInt8(),
'blue': buffer.readUInt8(),
},
});
break;
case lppGps: case lppGps:
telemetry.add({ telemetry.add({
'channel': channel, 'channel': channel,
@@ -184,6 +287,24 @@ class CayenneLpp {
}, },
}); });
break; break;
case lppSwitch:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppPolyline:
final size = buffer.readUInt8();
telemetry.add({
'channel': channel,
'type': type,
'value': {
'size': size,
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
},
});
break;
default: default:
return telemetry; return telemetry;
} }
@@ -216,6 +337,19 @@ class CayenneLpp {
); );
switch (type) { switch (type) {
case lppDigitalInput:
channelData['values']['digitalInput'] = buffer.readUInt8();
break;
case lppDigitalOutput:
channelData['values']['digitalOutput'] = buffer.readUInt8();
break;
case lppAnalogInput:
channelData['values']['analogInput'] = buffer.readInt16BE() / 100.0;
break;
case lppAnalogOutput:
channelData['values']['analogOutput'] =
buffer.readInt16BE() / 100.0;
break;
case lppGenericSensor: case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE(); channelData['values']['generic'] = buffer.readUInt32BE();
break; break;
@@ -231,15 +365,29 @@ class CayenneLpp {
case lppRelativeHumidity: case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0; channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break; break;
case lppAccelerometer:
channelData['values']['accelerometer'] = {
'x': buffer.readInt16BE() / 1000.0,
'y': buffer.readInt16BE() / 1000.0,
'z': buffer.readInt16BE() / 1000.0,
};
break;
case lppBarometricPressure: case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0; channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break; break;
case lppAltitude:
// MeshCore encodes standalone barometric altitude as LPP type 121.
channelData['values']['altitude'] = buffer.readInt16BE();
break;
case lppVoltage: case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0; channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break; break;
case lppCurrent: case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0; channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break; break;
case lppFrequency:
channelData['values']['frequency'] = buffer.readUInt32BE();
break;
case lppPercentage: case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8(); channelData['values']['percentage'] = buffer.readUInt8();
break; break;
@@ -249,6 +397,32 @@ class CayenneLpp {
case lppPower: case lppPower:
channelData['values']['power'] = buffer.readUInt16BE(); channelData['values']['power'] = buffer.readUInt16BE();
break; break;
case lppDistance:
channelData['values']['distance'] = buffer.readUInt32BE() / 1000.0;
break;
case lppEnergy:
channelData['values']['energy'] = buffer.readUInt32BE() / 1000.0;
break;
case lppDirection:
channelData['values']['direction'] = buffer.readUInt16BE();
break;
case lppUnixTime:
channelData['values']['time'] = buffer.readUInt32BE();
break;
case lppGyrometer:
channelData['values']['gyrometer'] = {
'x': buffer.readInt16BE() / 100.0,
'y': buffer.readInt16BE() / 100.0,
'z': buffer.readInt16BE() / 100.0,
};
break;
case lppColour:
channelData['values']['colour'] = {
'red': buffer.readUInt8(),
'green': buffer.readUInt8(),
'blue': buffer.readUInt8(),
};
break;
case lppGps: case lppGps:
channelData['values']['gps'] = { channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0, 'latitude': buffer.readInt24BE() / 10000.0,
@@ -256,22 +430,48 @@ class CayenneLpp {
'altitude': buffer.readInt24BE() / 100.0, 'altitude': buffer.readInt24BE() / 100.0,
}; };
break; break;
// Add more types as needed... case lppSwitch:
channelData['values']['switch'] = buffer.readUInt8() != 0;
break;
case lppPolyline:
final size = buffer.readUInt8();
channelData['values']['polyline'] = {
'size': size,
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
};
break;
default: default:
//Stopped parsing to avoid misalignment // Stop parsing to avoid losing alignment on an unknown LPP type.
return channels.values.toList(); return _sortedChannelValues(channels);
} }
} }
final List<Map<String, dynamic>> channelsOut = channels.values.toList(); return _sortedChannelValues(channels);
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
} catch (e) { } catch (e) {
// Handle parsing errors, possibly due to malformed data // Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e'); appLogger.error('Error parsing Cayenne LPP data: $e');
return < // Preserve any fields parsed before the malformed value.
Map<String, dynamic> return _sortedChannelValues(channels);
>[]; // Return an empty list on error to avoid crashing the app
} }
} }
static Uint8List _readPolylinePayload(BufferReader buffer, int size) {
final declaredPayloadSize = size > 0 ? size - 1 : 0;
final availablePayloadSize = declaredPayloadSize <= buffer.remaining
? declaredPayloadSize
: buffer.remaining;
return buffer.readBytes(availablePayloadSize);
}
static List<Map<String, dynamic>> _sortedChannelValues(
Map<int, Map<String, dynamic>> channels,
) {
final channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
} }
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_protocol.dart';
import '../utils/emoji_utils.dart';
IconData contactTypeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.chat;
case advTypeRepeater:
return Icons.cell_tower;
case advTypeRoom:
return Icons.group;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
Color contactTypeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.orange;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.green;
default:
return Colors.grey;
}
}
Color colorForName(String name) {
const colors = [
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.indigo,
Colors.cyan,
Colors.amber,
Colors.deepOrange,
];
return colors[name.hashCode.abs() % colors.length];
}
String firstCharacterOrEmoji(String name) {
if (name.isEmpty) return '?';
final emoji = firstEmoji(name);
if (emoji != null) return emoji;
final runes = name.runes.toList();
if (runes.isEmpty) return '?';
return String.fromCharCode(runes[0]).toUpperCase();
}
+78
View File
@@ -0,0 +1,78 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../l10n/l10n.dart';
import '../widgets/image_send_codec_binding.dart';
import '../widgets/image_send_preview_sheet.dart';
/// Shared "attach an image" flow, used by both the direct-message and channel
/// chat screens so the two surfaces cannot drift apart.
///
/// Picks a photo, then shows [ImageSendPreviewSheet] so the user sees the
/// packet count and airtime *before* committing to a send that may occupy the
/// channel for seconds. Returns null if the user backed out at either step.
///
/// The caller owns the actual transmission: this only produces the payload.
Future<ImageSendPreviewResult?> pickAndPreviewImage({
required BuildContext context,
required ImageSendCodec codec,
ImagePicker? picker,
}) async {
final XFile? picked;
try {
picked = await (picker ?? ImagePicker()).pickImage(
source: ImageSource.gallery,
// The codec centre-crops to 512x512 anyway, so there is nothing to gain
// from decoding a 12 MP original -- but stay well above 512 so the crop
// still has detail to work with.
maxWidth: 2048,
maxHeight: 2048,
);
} on Exception catch (e) {
debugPrint('image pick failed: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
);
}
return null;
}
if (picked == null) return null; // user cancelled the picker
final Uint8List bytes;
int originalBytes;
try {
bytes = await picked.readAsBytes();
originalBytes = bytes.length;
if (!kIsWeb) {
// length() is the on-disk size, which is what we want to show against the
// transmitted size; fall back to the in-memory length if it fails.
try {
originalBytes = await File(picked.path).length();
} on FileSystemException {
// keep the in-memory length
}
}
} on Exception catch (e) {
debugPrint('image read failed: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
);
}
return null;
}
if (!context.mounted) return null;
return showImageSendPreviewSheet(
context: context,
imageBytes: bytes,
originalFileBytes: originalBytes,
codec: codec,
);
}
+11 -1
View File
@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -20,6 +21,7 @@ class LinkHandler {
required String text, required String text,
required TextStyle style, required TextStyle style,
TextStyle? linkStyle, TextStyle? linkStyle,
VoidCallback? onSecondaryTap,
}) { }) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style); final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false); const options = LinkifyOptions(humanize: false, defaultToHttps: false);
@@ -27,7 +29,7 @@ class LinkHandler {
void onOpen(LinkableElement link) => handleLinkTap(context, link.url); void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) { if (PlatformInfo.isDesktop) {
return SelectableLinkify( final linkify = SelectableLinkify(
text: text, text: text,
style: style, style: style,
linkStyle: effectiveLinkStyle, linkStyle: effectiveLinkStyle,
@@ -35,6 +37,14 @@ class LinkHandler {
linkifiers: linkifiers, linkifiers: linkifiers,
onOpen: onOpen, onOpen: onOpen,
); );
if (onSecondaryTap == null) return linkify;
return Listener(
onPointerDown: (event) {
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
},
behavior: HitTestBehavior.translucent,
child: linkify,
);
} }
return Linkify( return Linkify(
text: text, text: text,
+76 -16
View File
@@ -1,5 +1,7 @@
import '../models/contact.dart'; import 'package:flutter/foundation.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
class PathHelper { class PathHelper {
static String formatPathHex(List<int> pathBytes) { static String formatPathHex(List<int> pathBytes) {
@@ -8,24 +10,82 @@ class PathHelper {
.join(','); .join(',');
} }
static String hopHex(int byte) {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
}
static String formatHopHex(List<int> hopBytes) {
return hopBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join();
}
static String? hopName(int byte, List<Contact> allContacts) {
final matches = allContacts
.where(
(c) =>
c.publicKey.isNotEmpty &&
c.publicKey.first == byte &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return null;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
}
static List<Uint8List> splitPathBytes(
List<int> pathBytes,
int hashByteWidth,
) {
if (pathBytes.isEmpty) return const [];
final width = hashByteWidth.clamp(1, 4).toInt();
final hops = <Uint8List>[];
for (int i = 0; i < pathBytes.length; i += width) {
final endIdx = (i + width).clamp(0, pathBytes.length).toInt();
final hopBytes = pathBytes.sublist(i, endIdx);
if (hopBytes.isNotEmpty) {
hops.add(Uint8List.fromList(hopBytes));
}
}
return hops;
}
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
///
/// Groups path bytes according to [hashByteWidth]:
/// - 1: Single byte per hop (256 unique nodes)
/// - 2: Two bytes per hop (65K unique nodes)
/// - 3: Three bytes per hop (16M unique nodes)
/// - 4: Four bytes per hop (4.3G unique nodes)
static String resolvePathNames( static String resolvePathNames(
List<int> pathBytes, List<int> pathBytes,
List<Contact> allContacts, List<Contact> allContacts,
int hashByteWidth,
) { ) {
return pathBytes if (pathBytes.isEmpty) return '';
.map((b) {
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase(); final parts = <String>[];
final matches = allContacts
.where( for (final hopBytes in splitPathBytes(pathBytes, hashByteWidth)) {
(c) => final hex = formatHopHex(hopBytes);
c.publicKey.first == b &&
(c.type == advTypeRepeater || c.type == advTypeRoom), final matches = allContacts.where((c) {
) if (c.publicKey.length < hopBytes.length) return false;
.toList(); if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
if (matches.isEmpty) return hex; return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
if (matches.length == 1) return matches.first.name; }).toList();
return matches.map((c) => c.name).join(' | ');
}) if (matches.isEmpty) {
.join(' \u2192 '); parts.add(hex);
} else if (matches.length == 1) {
parts.add(matches.first.name);
} else {
parts.add(matches.map((c) => c.name).join(' | '));
}
}
return parts.join(' \u2192 ');
} }
} }
+76
View File
@@ -0,0 +1,76 @@
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_protocol.dart';
import 'path_helper.dart';
import '../models/contact.dart';
class PathHopResolver {
const PathHopResolver._();
static List<Contact?> resolve({
required List<int> pathBytes,
required List<Contact> contacts,
LatLng? endpoint,
bool resolveFromEnd = false,
int pathHashByteWidth = 1,
}) {
final width = pathHashByteWidth.clamp(1, 4).toInt();
final candidatesByPrefix = <String, List<Contact>>{};
for (final contact in contacts) {
if (contact.publicKey.length < width) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue;
}
final prefix = PathHelper.formatHopHex(
contact.publicKey.sublist(0, width),
);
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
}
for (final candidates in candidatesByPrefix.values) {
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
}
final hops = PathHelper.splitPathBytes(pathBytes, width);
final resolved = List<Contact?>.filled(hops.length, null);
final indexes = resolveFromEnd
? List<int>.generate(hops.length, (i) => hops.length - 1 - i)
: List<int>.generate(hops.length, (i) => i);
final distance = Distance();
var previousPosition = endpoint;
for (final index in indexes) {
final candidates =
candidatesByPrefix[PathHelper.formatHopHex(hops[index])];
if (candidates == null || candidates.isEmpty) continue;
var bestIndex = 0;
if (previousPosition != null && candidates.length > 1) {
double? nearestDistance;
for (var i = 0; i < candidates.length; i++) {
final position = _positionOf(candidates[i]);
if (position == null) continue;
final candidateDistance = distance(previousPosition, position);
if (nearestDistance == null || candidateDistance < nearestDistance) {
nearestDistance = candidateDistance;
bestIndex = i;
}
}
}
final contact = candidates.removeAt(bestIndex);
resolved[index] = contact;
previousPosition = _positionOf(contact) ?? previousPosition;
}
return resolved;
}
static LatLng? _positionOf(Contact contact) {
if (!contact.hasLocation ||
contact.latitude == null ||
contact.longitude == null) {
return null;
}
return LatLng(contact.latitude!, contact.longitude!);
}
}
+13 -1
View File
@@ -25,7 +25,19 @@ void showDismissibleSnackBar(
DismissDirection? dismissDirection, DismissDirection? dismissDirection,
Clip? clipBehavior, Clip? clipBehavior,
}) { }) {
final messenger = ScaffoldMessenger.of(context); // Callers often reach here after an async gap; the context may already be
// unmounted, or deactivated (popped but not yet disposed) ancestor
// lookups on a deactivated element throw. Showing nothing is the right
// outcome in both cases.
if (!context.mounted) return;
var isActive = true;
assert(() {
isActive = (context as Element).debugIsActive;
return true;
}());
if (!isActive) return;
final messenger = ScaffoldMessenger.maybeOf(context);
if (messenger == null) return;
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( SnackBar(
key: key, key: key,
+719 -204
View File
File diff suppressed because it is too large Load Diff
+918 -297
View File
File diff suppressed because it is too large Load Diff
+590 -75
View File
@@ -28,6 +28,12 @@
"common_remove": "Remove", "common_remove": "Remove",
"common_enable": "Enable", "common_enable": "Enable",
"common_disable": "Disable", "common_disable": "Disable",
"common_undo": "Undo",
"messageStatus_sent": "Sent",
"messageStatus_delivered": "Delivered",
"messageStatus_pending": "Sending",
"messageStatus_failed": "Failed to send",
"messageStatus_repeated": "Heard repeated",
"common_reboot": "Reboot", "common_reboot": "Reboot",
"common_loading": "Loading...", "common_loading": "Loading...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -47,6 +53,8 @@
} }
} }
}, },
"common_autoRefresh": "Autorefresh",
"common_interval": "Interval",
"scanner_title": "MeshCore Open", "scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB", "connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceBluetoothLabel": "Bluetooth",
@@ -135,6 +143,7 @@
"scanner_chromeRequired": "Chrome Browser Required", "scanner_chromeRequired": "Chrome Browser Required",
"scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.", "scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.",
"scanner_enableBluetooth": "Enable Bluetooth", "scanner_enableBluetooth": "Enable Bluetooth",
"scanner_bluetoothWebUnsupported": "Bluetooth isn't available in the browser. Connect over USB instead.",
"device_quickSwitch": "Quick switch", "device_quickSwitch": "Quick switch",
"device_meshcore": "MeshCore", "device_meshcore": "MeshCore",
"settings_title": "Settings", "settings_title": "Settings",
@@ -149,6 +158,25 @@
"settings_radioSettings": "Radio Settings", "settings_radioSettings": "Radio Settings",
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor", "settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
"settings_radioSettingsUpdated": "Radio settings updated", "settings_radioSettingsUpdated": "Radio settings updated",
"settings_regionSettings": "Regions",
"settings_regionSettingsSubtitle": "Manage stored regions",
"settings_regionManagement_screenTitle": "Region Management",
"settings_regionNameHint": "Enter region name",
"settings_regionAddRegion": "Add region",
"settings_regionFetchRegions": "Fetch regions from repeaters",
"settings_regionFetchRegionsFail": "No regions were found",
"settings_regionFetchRegionsAlreadyExists": "This region has already been added",
"settings_regionName": "Region Name",
"settings_regionDeleted": "Region deleted",
"settings_deleteRegion": "Delete Region",
"settings_deleteRegionConfirm": "Remove \"{region}\" from region list?",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"settings_location": "Location", "settings_location": "Location",
"settings_locationSubtitle": "GPS coordinates", "settings_locationSubtitle": "GPS coordinates",
"settings_locationUpdated": "Location and GPS settings updated", "settings_locationUpdated": "Location and GPS settings updated",
@@ -178,6 +206,8 @@
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode", "settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location", "settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.", "settings_advertLocationSubtitle": "Include location in advert.",
"settings_autoZeroHopAdvertOnGpsUpdate": "Auto Zero-Hop Advert On GPS Update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "When GPS location changes, send a zero-hop advert (requires Advert Location).",
"settings_multiAck": "Multi-ACKs", "settings_multiAck": "Multi-ACKs",
"settings_telemetryModeUpdated": "Telemetry mode updated", "settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions", "settings_actions": "Actions",
@@ -218,6 +248,8 @@
"settings_infoPublicKey": "Public Key", "settings_infoPublicKey": "Public Key",
"settings_infoContactsCount": "Contacts Count", "settings_infoContactsCount": "Contacts Count",
"settings_infoChannelCount": "Channel Count", "settings_infoChannelCount": "Channel Count",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"settings_presets": "Presets", "settings_presets": "Presets",
"settings_frequency": "Frequency (MHz)", "settings_frequency": "Frequency (MHz)",
"settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyHelper": "300.0 - 2500.0",
@@ -262,6 +294,10 @@
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский", "appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська", "appSettings_languageUk": "Українська",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_enableMessageTracing": "Enable Message Tracing", "appSettings_enableMessageTracing": "Enable Message Tracing",
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
"appSettings_notifications": "Notifications", "appSettings_notifications": "Notifications",
@@ -295,17 +331,6 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery", "appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
"appSettings_maxMessageRetries": "Max Message Retries", "appSettings_maxMessageRetries": "Max Message Retries",
"appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed", "appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery", "appSettings_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry", "appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -320,6 +345,7 @@
"appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)",
"appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)",
"appSettings_batteryLipo": "LiPo (3.0-4.2V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"appSettings_mapDisplay": "Map Display", "appSettings_mapDisplay": "Map Display",
"appSettings_showRepeaters": "Show Repeaters", "appSettings_showRepeaters": "Show Repeaters",
"appSettings_showRepeatersSubtitle": "Display repeater nodes on the map", "appSettings_showRepeatersSubtitle": "Display repeater nodes on the map",
@@ -344,6 +370,19 @@
"appSettings_last6Hours": "Last 6 hours", "appSettings_last6Hours": "Last 6 hours",
"appSettings_last24Hours": "Last 24 hours", "appSettings_last24Hours": "Last 24 hours",
"appSettings_lastWeek": "Last week", "appSettings_lastWeek": "Last week",
"appSettings_rasterTileSource": "Raster Tile Source",
"appSettings_stadiaEndpoint": "Stadia Endpoint",
"appSettings_stadiaApiKey": "Stadia API Key",
"appSettings_stadiaApiKeyRequired": "Required for Stadia Maps usage",
"appSettings_stadiaApiKeyConfigured": "Configured: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Enter your Stadia Maps API key. This app uses it for raster tile requests.",
"appSettings_offlineMapCache": "Offline Map Cache", "appSettings_offlineMapCache": "Offline Map Cache",
"appSettings_unitsTitle": "Units", "appSettings_unitsTitle": "Units",
"appSettings_unitsMetric": "Metric (m / km)", "appSettings_unitsMetric": "Metric (m / km)",
@@ -451,6 +490,9 @@
} }
}, },
"contacts_newGroup": "New Group", "contacts_newGroup": "New Group",
"contacts_moreOptions": "More options",
"contacts_searchOpen": "Search contacts",
"contacts_searchClose": "Close search",
"contacts_groupName": "Group name", "contacts_groupName": "Group name",
"contacts_groupNameRequired": "Group name is required", "contacts_groupNameRequired": "Group name is required",
"contacts_groupNameReserved": "This group name is reserved", "contacts_groupNameReserved": "This group name is reserved",
@@ -634,6 +676,18 @@
"channels_scanQrCodeComingSoon": "Coming soon", "channels_scanQrCodeComingSoon": "Coming soon",
"channels_enterHashtag": "Enter hashtag", "channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team", "channels_hashtagHint": "e.g. #team",
"channels_regionSetTo": "Region: {region}",
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"channels_regionNotSet": "Region: none",
"channels_regionSelect_Title": "Select a region",
"channels_clearRegion": "Clear region",
"chat_noMessages": "No messages yet", "chat_noMessages": "No messages yet",
"chat_sendMessage": "Send message", "chat_sendMessage": "Send message",
"chat_sendMessageTo": "Send a message to {contactName}", "chat_sendMessageTo": "Send a message to {contactName}",
@@ -687,6 +741,12 @@
} }
}, },
"chat_sendGif": "Send GIF", "chat_sendGif": "Send GIF",
"chat_sendImage": "Send image",
"chat_imagePickFailed": "Couldn't open that image",
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"chat_receivedGif": "Received a GIF",
"chat_reply": "Reply", "chat_reply": "Reply",
"chat_addReaction": "Add Reaction", "chat_addReaction": "Add Reaction",
"chat_me": "Me", "chat_me": "Me",
@@ -775,15 +835,6 @@
} }
}, },
"debugFrame_hexDump": "Hex Dump:", "debugFrame_hexDump": "Hex Dump:",
"chat_pathManagement": "Path Management",
"chat_ShowAllPaths": "Show all paths",
"chat_routingMode": "Routing mode",
"chat_autoUseSavedPath": "Auto (use saved path)",
"chat_forceFloodMode": "Force Flood Mode",
"chat_recentAckPaths": "Recent ACK Paths (tap to use):",
"chat_pathHistoryFull": "Path history is full. Remove entries to add new ones.",
"chat_hopSingular": "hop",
"chat_hopPlural": "hops",
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
"@chat_hopsCount": { "@chat_hopsCount": {
"placeholders": { "placeholders": {
@@ -792,31 +843,80 @@
} }
} }
}, },
"chat_successes": "successes",
"chat_score": "Score",
"chat_removePath": "Remove path", "chat_removePath": "Remove path",
"chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.", "chat_noPathHistoryYet": "No path history yet.\nSend a message to discover paths.",
"chat_pathActions": "Path Actions:",
"chat_setCustomPath": "Set Custom Path",
"chat_setCustomPathSubtitle": "Manually specify routing path",
"chat_clearPath": "Clear Path",
"chat_clearPathSubtitle": "Force rediscovery on next send",
"chat_pathCleared": "Path cleared. Next message will rediscover route.", "chat_pathCleared": "Path cleared. Next message will rediscover route.",
"chat_floodModeSubtitle": "Use routing toggle in app bar",
"chat_floodModeEnabled": "Flood mode enabled. Toggle back via routing icon in app bar.",
"chat_fullPath": "Full Path", "chat_fullPath": "Full Path",
"chat_pathDetailsNotAvailable": "Path details not available yet. Try sending a message to refresh.", "routing_title": "Routing",
"chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "routing_modeAuto": "Auto",
"@chat_pathSetHops": { "routing_modeFlood": "Flood",
"routing_modeManual": "Manual",
"routing_modeAutoHint": "Picks the best known path automatically, flooding when none is known.",
"routing_modeFloodHint": "Broadcasts through every repeater. Most reliable, but uses more airtime.",
"routing_modeManualHint": "Always sends along the exact path you set.",
"routing_currentRoute": "Current route",
"routing_directNoHops": "Direct — no repeater hops",
"routing_noPathYet": "No path yet. The next message floods until a route is discovered.",
"routing_floodBroadcast": "Broadcast through every repeater",
"routing_editPath": "Edit path",
"routing_forgetPath": "Forget path",
"routing_knownPaths": "Known paths",
"routing_knownPathsHint": "Tap a path to switch to it.",
"routing_inUse": "In use",
"routing_qualityStrong": "Strong first hop",
"routing_qualityGood": "Good first hop",
"routing_qualityFair": "Fair first hop",
"routing_qualityWorked": "Has delivered",
"routing_qualityFlood": "Heard via flood",
"routing_qualityUntested": "Untested",
"routing_lastWorked": "worked {when}",
"@routing_lastWorked": {
"placeholders": { "placeholders": {
"hopCount": { "when": {
"type": "int"
},
"status": {
"type": "String" "type": "String"
} }
} }
}, },
"routing_neverWorked": "never confirmed",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"routing_floodDelivery": "Flood delivery",
"pathEditor_title": "Build Path",
"pathEditor_hopCounter": "{count} of 64 hops",
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathEditor_noHops": "No hops yet. Tap repeaters below to add them in order, or save with no hops to send direct.",
"pathEditor_addHops": "Add hops in order",
"pathEditor_searchRepeaters": "Search repeaters",
"pathEditor_advancedHex": "Advanced: raw hex path",
"pathEditor_hexLabel": "Hex prefixes",
"pathEditor_hexHelper": "Two hex characters per hop, separated by commas",
"pathEditor_invalidTokens": "Invalid: {tokens}",
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"pathEditor_tooManyHops": "Maximum 64 hops",
"pathEditor_usePath": "Use this path",
"pathEditor_removeHop": "Remove hop",
"pathEditor_unknownHop": "Unknown repeater",
"chat_pathSavedLocally": "Saved locally. Connect to sync.", "chat_pathSavedLocally": "Saved locally. Connect to sync.",
"chat_pathDeviceConfirmed": "Device confirmed.", "chat_pathDeviceConfirmed": "Device confirmed.",
"chat_pathDeviceNotConfirmed": "Device not confirmed yet.", "chat_pathDeviceNotConfirmed": "Device not confirmed yet.",
@@ -860,6 +960,17 @@
}, },
"chat_invalidLink": "Invalid link format", "chat_invalidLink": "Invalid link format",
"map_title": "Node Map", "map_title": "Node Map",
"map_searchHint": "Search node name or ID",
"map_activity": "Activity",
"map_online": "Online",
"map_recent": "Recent",
"map_stale": "Stale",
"map_visible": "Visible",
"map_hidden": "Hidden",
"map_centerOnNode": "Center on node",
"map_details": "Details",
"map_noGps": "No GPS",
"map_noResults": "No matching nodes",
"map_lineOfSight": "Line of Sight", "map_lineOfSight": "Line of Sight",
"map_losScreenTitle": "Line of Sight", "map_losScreenTitle": "Line of Sight",
"map_noNodesWithLocation": "No nodes with location data", "map_noNodesWithLocation": "No nodes with location data",
@@ -1008,6 +1119,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1098,41 +1259,8 @@
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.", "login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
"common_reload": "Reload", "common_reload": "Reload",
"common_clear": "Clear", "common_clear": "Clear",
"path_currentPath": "Current path: {path}",
"@path_currentPath": {
"placeholders": {
"path": {
"type": "String"
}
}
},
"path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path",
"@path_usingHopsPath": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"path_enterCustomPath": "Enter Custom Path",
"path_currentPathLabel": "Current path", "path_currentPathLabel": "Current path",
"path_hexPrefixInstructions": "Enter 2-character hex prefixes for each hop, separated by commas.",
"path_hexPrefixExample": "Example: A1,F2,3C (each node uses first byte of its public key)",
"path_labelHexPrefixes": "Path (hex prefixes)",
"path_helperMaxHops": "Max 64 hops. Each prefix is 2 hex characters (1 byte)",
"path_selectFromContacts": "Or select from contacts:",
"path_noRepeatersFound": "No repeaters or room servers found.", "path_noRepeatersFound": "No repeaters or room servers found.",
"path_customPathsRequire": "Custom paths require intermediate hops that can relay messages.",
"path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}",
"@path_invalidHexPrefixes": {
"placeholders": {
"prefixes": {
"type": "String"
}
}
},
"path_tooLong": "Path too long. Maximum 64 hops allowed.",
"path_setPath": "Set Path",
"repeater_management": "Repeater Management", "repeater_management": "Repeater Management",
"room_management": "Room Server Management", "room_management": "Room Server Management",
"repeater_guest": "Repeater Information", "repeater_guest": "Repeater Information",
@@ -1159,9 +1287,6 @@
}, },
"repeater_statusTitle": "Repeater Status", "repeater_statusTitle": "Repeater Status",
"repeater_routingMode": "Routing mode", "repeater_routingMode": "Routing mode",
"repeater_autoUseSavedPath": "Auto (use saved path)",
"repeater_forceFloodMode": "Force Flood Mode",
"repeater_pathManagement": "Path management",
"repeater_refresh": "Refresh", "repeater_refresh": "Refresh",
"repeater_statusRequestTimeout": "Status request timed out.", "repeater_statusRequestTimeout": "Status request timed out.",
"repeater_errorLoadingStatus": "Error loading status: {error}", "repeater_errorLoadingStatus": "Error loading status: {error}",
@@ -1361,7 +1486,24 @@
"repeater_advancedSettings": "Advanced", "repeater_advancedSettings": "Advanced",
"repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators", "repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators",
"repeater_pathHashMode": "Path hash mode", "repeater_pathHashMode": "Path hash mode",
"repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.", "repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.",
"repeater_keySettings": "Change Identity Keys",
"repeater_keySettingsSubtitle": "Change the public/private keypair",
"repeater_prvKey": "Private key",
"repeater_prvKeyHelper": "A new private key for the repeater, a 128-character hex string.",
"repeater_generatePrvKey": "Generate a random keypair",
"repeater_stopGeneratingPrvKey": "Interrupt search for keypair",
"repeater_pubKey": "Public key",
"repeater_pubKeyHelper": "This is the public key that goes with the generated private key. You can't set this directly.",
"repeater_pubKeyPrefix": "Desired prefix",
"repeater_pubKeyPrefixHelper": "Find a public key that starts with these hex digits. Expected tries needed: {tries}.",
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"repeater_txDelay": "Flood TX delay", "repeater_txDelay": "Flood TX delay",
"repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.", "repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.",
"repeater_directTxDelay": "Direct TX delay", "repeater_directTxDelay": "Direct TX delay",
@@ -1542,7 +1684,7 @@
"repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).", "repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).",
"repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.", "repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.",
"repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.", "repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.",
"repeater_cliHelpSetPrvKey": "(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.", "repeater_cliHelpSetPrvKey": "Replaces the device identity private key. Reboot required to apply. Generates a new public key.",
"repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.", "repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.",
"repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.", "repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.",
"repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.", "repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.",
@@ -1661,6 +1803,120 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Digital Input",
"telemetry_digitalOutputLabel": "Digital Output",
"telemetry_analogInputLabel": "Analog Input",
"telemetry_analogOutputLabel": "Analog Output",
"telemetry_genericLabel": "Generic Sensor",
"telemetry_luminosityLabel": "Luminosity",
"telemetry_presenceLabel": "Presence",
"telemetry_humidityLabel": "Humidity",
"telemetry_accelerometerLabel": "Accelerometer",
"telemetry_pressureLabel": "Pressure",
"telemetry_altitudeLabel": "Altitude",
"telemetry_frequencyLabel": "Frequency",
"telemetry_percentageLabel": "Percentage",
"telemetry_concentrationLabel": "Concentration",
"telemetry_powerLabel": "Power",
"telemetry_distanceLabel": "Distance",
"telemetry_energyLabel": "Energy",
"telemetry_directionLabel": "Direction",
"telemetry_timeLabel": "Time",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Colour",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Switch",
"telemetry_polylineLabel": "Polyline",
"telemetry_altitudeValue": "{meters} m",
"@telemetry_altitudeValue": {
"placeholders": {
"meters": {
"type": "String"
}
}
},
"telemetry_frequencyValue": "{hertz} Hz",
"@telemetry_frequencyValue": {
"placeholders": {
"hertz": {
"type": "String"
}
}
},
"telemetry_pressureValue": "{hpa} hPa",
"@telemetry_pressureValue": {
"placeholders": {
"hpa": {
"type": "String"
}
}
},
"telemetry_luminosityValue": "{lux} lx",
"@telemetry_luminosityValue": {
"placeholders": {
"lux": {
"type": "String"
}
}
},
"telemetry_powerValue": "{watts} W",
"@telemetry_powerValue": {
"placeholders": {
"watts": {
"type": "String"
}
}
},
"telemetry_distanceValue": "{meters} m",
"@telemetry_distanceValue": {
"placeholders": {
"meters": {
"type": "String"
}
}
},
"telemetry_energyValue": "{kilowattHours} kWh",
"@telemetry_energyValue": {
"placeholders": {
"kilowattHours": {
"type": "String"
}
}
},
"telemetry_directionValue": "{degrees}°",
"@telemetry_directionValue": {
"placeholders": {
"degrees": {
"type": "String"
}
}
},
"telemetry_concentrationValue": "{ppm} ppm",
"@telemetry_concentrationValue": {
"placeholders": {
"ppm": {
"type": "String"
}
}
},
"telemetry_percentageValue": "{percent}%",
"@telemetry_percentageValue": {
"placeholders": {
"percent": {
"type": "String"
}
}
},
"telemetry_analogValue": "{value}",
"@telemetry_analogValue": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"telemetry_autoFetchQuantity": "Requests quantity",
"telemetry_error": "Unable to retrieve data",
"neighbors_receivedData": "Received Neighbors Data", "neighbors_receivedData": "Received Neighbors Data",
"neighbors_requestTimedOut": "Neighbors request timed out.", "neighbors_requestTimedOut": "Neighbors request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}", "neighbors_errorLoading": "Error loading neighbors: {error}",
@@ -2284,10 +2540,24 @@
"radioStats_settingsTile": "Radio stats", "radioStats_settingsTile": "Radio stats",
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime", "radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
"translation_title": "Translation", "translation_title": "Translation",
"imageMessages_enableTitle": "Enable image messages",
"imageMessages_enableSubtitle": "Send images over the mesh. Requires a one-time image model download.",
"imageMessages_modelSectionTitle": "Image model",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Cancel",
"imageMessages_removeModel": "Remove model",
"imageMessages_modelReady": "Ready",
"imageMessages_modelNotPublished": "Not published yet — this build cannot download it.",
"imageMessages_downloadFailed": "The image model could not be downloaded.",
"imageMessages_autoProcessTitle": "Process images automatically",
"imageMessages_autoProcessSubtitle": "Reconstruct every image as soon as it arrives. Uses about 2 GB of memory for a second each time; leave off to reconstruct with a tap instead.",
"translation_enableTitle": "Enable translation", "translation_enableTitle": "Enable translation",
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.", "translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
"translation_composerTitle": "Translate before sending", "translation_composerTitle": "Translate before sending",
"translation_composerSubtitle": "Controls the default state of the composer translation icon.", "translation_composerSubtitle": "Controls the default state of the composer translation icon.",
"translation_autoIncomingTitle": "Auto-translate incoming messages",
"translation_autoIncomingSubtitle": "Translates Messages for notification and for chat or channel automatically.",
"translation_translateMessage": "Translate message",
"translation_targetLanguage": "Target language", "translation_targetLanguage": "Target language",
"translation_useAppLanguage": "Use app language", "translation_useAppLanguage": "Use app language",
"translation_downloadedModelLabel": "Downloaded model", "translation_downloadedModelLabel": "Downloaded model",
@@ -2367,5 +2637,250 @@
"contact_typeRoom": "Room", "contact_typeRoom": "Room",
"contact_typeSensor": "Sensor", "contact_typeSensor": "Sensor",
"contact_typeUnknown": "Unknown", "contact_typeUnknown": "Unknown",
"contact_connectCompanion": "Connect to a companion to access repeater and room server features." "map_zoomIn": "Zoom in",
"map_zoomOut": "Zoom out",
"map_centerMap": "Center map",
"chrome_bluetoothRequiresChromium": "Web Bluetooth requires a Chromium browser",
"channels_communityShortId": "ID: {id}...",
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"pathTrace_legendGpsConfirmed": "GPS confirmed",
"pathTrace_legendInferred": "Inferred position",
"pathMap_viewSingle": "Single",
"pathMap_viewCombined": "Combined",
"pathMap_play": "Play",
"pathMap_pause": "Pause",
"pathMap_replay": "Replay",
"pathMap_stepBack": "Previous hop",
"pathMap_stepForward": "Next hop",
"pathMap_animationOn": "Show packet animation",
"pathMap_animationOff": "Hide packet animation",
"pathMap_hopOf": "Hop {current} of {total}",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"pathMap_observedPaths": "Observed paths: {count}",
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathMap_primary": "Primary",
"pathMap_alternate": "Alt {index}",
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"pathMap_hopCount": "{count, plural, =1{1 hop} other{{count} hops}}",
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"pathMap_legendShared": "Shared segment",
"pathMap_legendEstimated": "Estimated segment",
"pathMap_sharedNodeCount": "Used by {count} paths",
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathMap_partialAnimation": "{count, plural, =1{1 hop has no location — the shown path is partial} other{{count} hops have no location — the shown path is partial}}",
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"pathMap_showAllPaths": "Show all",
"pathMap_hidePath": "Hide path",
"pathMap_showPath": "Show path",
"pathMap_collapsePanel": "Collapse panel",
"pathMap_expandPanel": "Expand panel",
"pathMap_noLocation": "No location",
"pathMap_followPacket": "Lock view to packet",
"pathMap_unfollowPacket": "Unlock view from packet",
"imageSend_title": "Send image",
"imageSend_cropNote": "Resized to 512 × 512 · aspect ratio not preserved",
"imageSend_originalSize": "Original",
"imageSend_onAirSize": "On air",
"imageSend_quality": "Quality",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "High",
"imageSend_packetsLabel": "Packets",
"imageSend_airtimeLabel": "Time on air",
"imageSend_sizeLabel": "Payload",
"imageSend_packetsCount": "{count} {count, plural, =1{packet} other{packets}}",
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"imageSend_range": "{min}{max}",
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radio settings unknown",
"imageSend_radioUnknownBody": "Connect to a device so the time on air can be calculated.",
"imageSend_longSendTitle": "Long transmission",
"imageSend_longSendBody": "This will hold the channel for about {duration}.",
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"imageSend_floodNote": "Flood routing: every repeater in range retransmits each packet, so the channel stays busy longer than this.",
"imageSend_parityTitle": "Add recovery packet",
"imageSend_paritySubtitle": "One extra packet. Group messages are not acknowledged, so this lets the receiver rebuild the image if a single packet is lost.",
"imageSend_send": "Send",
"imageSend_cancel": "Cancel",
"imageSend_encodeFailed": "This image could not be encoded.",
"imageSend_codecDownloading": "The image model is still downloading.",
"imageSend_codecUnavailable": "Image sending is not available on this device.",
"imageSend_codecDisabled": "Image messages are turned off in settings.",
"imageSend_deviceUnsupported": "This radio cannot send image packets. Connect a device running companion firmware 13 or newer.",
"imageSend_directMessagesUnsupported": "Images travel as group data, so they can only be sent to a channel — not in a direct message.",
"imageSend_tooLarge": "That image encoded to more packets than the mesh format allows.",
"imageSend_sentConfirmation": "Image sent as {count} {count, plural, =1{packet} other{packets}}.",
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"imageSend_sendFailed": "The image could not be sent: {error}",
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"imageSend_sendingProgress": "Sending image — packet {sent} of {total}",
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"receivedImage_senderPrefix": "Node {prefix}",
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"receivedImage_incoming": "{received} of {total} packets",
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"receivedImage_queued": "Waiting to decode",
"receivedImage_tapToDecode": "Tap to decode",
"receivedImage_decoding": "Reconstructing… about 1 s",
"receivedImage_incomplete": "Image incomplete — {received} of {total} packets arrived",
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"receivedImage_corrupt": "Image could not be reconstructed",
"receivedImage_decoderMissing": "Image received — image decoding is off",
"receivedImage_evicted": "Image no longer stored",
"receivedImage_retry": "Try again",
"receivedImage_decodeAgain": "Decode again",
"receivedImage_openSettings": "Set up",
"receivedImage_tapToProcess": "Tap to process",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{packet} other{packets}}",
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"imageSend_secondsValue": "{seconds} s",
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s",
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
}
} }
+864 -243
View File
File diff suppressed because it is too large Load Diff
+816 -174
View File
File diff suppressed because it is too large Load Diff
+1489 -974
View File
File diff suppressed because it is too large Load Diff
+898 -249
View File
File diff suppressed because it is too large Load Diff
+1006 -395
View File
File diff suppressed because it is too large Load Diff
+751 -140
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Verwijderen", "common_remove": "Verwijderen",
"common_enable": "Activeren", "common_enable": "Activeren",
"common_disable": "Uitschakelen", "common_disable": "Uitschakelen",
"common_autoRefresh": "Automatisch vernieuwen",
"common_interval": "Tijdsinterval",
"common_reboot": "Herstarten", "common_reboot": "Herstarten",
"common_loading": "Laden...", "common_loading": "Laden...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
"settings_latitude": "Breedtegraad", "settings_latitude": "Breedtegraad",
"settings_longitude": "Lengtegraad", "settings_longitude": "Lengtegraad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische zero-hop-advertentie bij GPS-update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).",
"settings_privacyMode": "Privacy-modus", "settings_privacyMode": "Privacy-modus",
"settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties", "settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.", "settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "Afgelopen 6 uur", "appSettings_last6Hours": "Afgelopen 6 uur",
"appSettings_last24Hours": "Afgelopen 24 uur", "appSettings_last24Hours": "Afgelopen 24 uur",
"appSettings_lastWeek": "Afgelopen week", "appSettings_lastWeek": "Afgelopen week",
"appSettings_rasterTileSource": "Rastertegelbron",
"appSettings_stadiaEndpoint": "Stadia-eindpunt",
"appSettings_stadiaApiKey": "Stadia API-sleutel",
"appSettings_stadiaApiKeyRequired": "Vereist voor gebruik van Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Geconfigureerd: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.",
"appSettings_offlineMapCache": "Offline Kaartcache", "appSettings_offlineMapCache": "Offline Kaartcache",
"appSettings_noAreaSelected": "Geen gebied geselecteerd", "appSettings_noAreaSelected": "Geen gebied geselecteerd",
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
@@ -767,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1105,7 +1172,7 @@
"repeater_advancedSettings": "Geavanceerd", "repeater_advancedSettings": "Geavanceerd",
"repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers", "repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers",
"repeater_pathHashMode": "Hash-modus voor paden", "repeater_pathHashMode": "Hash-modus voor paden",
"repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden alleen na het activeren van het netwerk.", "repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.",
"repeater_txDelay": "Vertraging bij Flood TX", "repeater_txDelay": "Vertraging bij Flood TX",
"repeater_txDelayHelper": "Herzendinterval voor verkeer tijdens overstromingen, als een veelvoud van de tijd die het pakket nodig heeft (0-2, standaard 0.5). Een hoger getal betekent minder botsingen, maar ook een langere leveringstijd.", "repeater_txDelayHelper": "Herzendinterval voor verkeer tijdens overstromingen, als een veelvoud van de tijd die het pakket nodig heeft (0-2, standaard 0.5). Een hoger getal betekent minder botsingen, maar ook een langere leveringstijd.",
"repeater_directTxDelay": "Directe vertraging", "repeater_directTxDelay": "Directe vertraging",
@@ -1280,6 +1347,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Digitale ingang",
"telemetry_digitalOutputLabel": "Digitale uitgang",
"telemetry_analogInputLabel": "Analoge ingang",
"telemetry_analogOutputLabel": "Analoge uitgang",
"telemetry_genericLabel": "Algemene sensor",
"telemetry_luminosityLabel": "Lichtsterkte",
"telemetry_presenceLabel": "Aanwezigheid",
"telemetry_humidityLabel": "Luchtvochtigheid",
"telemetry_accelerometerLabel": "Versnellingsmeter",
"telemetry_pressureLabel": "Druk",
"telemetry_altitudeLabel": "Hoogte",
"telemetry_frequencyLabel": "Frequentie",
"telemetry_percentageLabel": "Percentage",
"telemetry_concentrationLabel": "Concentratie",
"telemetry_powerLabel": "Vermogen",
"telemetry_distanceLabel": "Afstand",
"telemetry_energyLabel": "Energie",
"telemetry_directionLabel": "Richting",
"telemetry_timeLabel": "Tijd",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Kleur",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Schakelaar",
"telemetry_polylineLabel": "Polylijn",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Aantal aanvragen",
"telemetry_error": "Kan gegevens niet ophalen",
"telemetry_noData": "Geen telemetriedata beschikbaar.", "telemetry_noData": "Geen telemetriedata beschikbaar.",
"telemetry_channelTitle": "Kanaal {channel}", "telemetry_channelTitle": "Kanaal {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2101,6 +2205,9 @@
"translation_composerTitle": "Vertaal voor verzending", "translation_composerTitle": "Vertaal voor verzending",
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.", "translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
"translation_useAppLanguage": "Gebruik de taal van de app", "translation_useAppLanguage": "Gebruik de taal van de app",
"translation_autoIncomingTitle": "Berichten automatisch vertalen",
"translation_autoIncomingSubtitle": "Vertaalt berichten automatisch voor meldingen en voor chats of kanalen.",
"translation_translateMessage": "Bericht vertalen",
"translation_targetLanguage": "Doeltaal", "translation_targetLanguage": "Doeltaal",
"translation_downloadedModelLabel": "Gedownloade model", "translation_downloadedModelLabel": "Gedownloade model",
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model", "translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
@@ -2312,5 +2419,413 @@
"chat_markAsUnread": "Markeer als ongelezen", "chat_markAsUnread": "Markeer als ongelezen",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data", "settings_companionDebugLogSubtitle": "BLE/TCP/USB commando's, antwoorden en ruwe data",
"repeater_chanUtil": "Gebruik van het kanaal", "repeater_chanUtil": "Gebruik van het kanaal",
"contact_connectCompanion": "Maak verbinding met een companion om repeater- en kamerserverfuncties te gebruiken." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Verzonden",
"common_undo": "Achterhalen/Annuleren",
"messageStatus_delivered": "Leverd",
"messageStatus_pending": "Verzenden",
"messageStatus_failed": "Niet verzonden",
"messageStatus_repeated": "Hearsay, herhaald",
"contacts_moreOptions": "Meer opties",
"contacts_searchOpen": "Zoek contactpersonen",
"contacts_searchClose": "Zoeken",
"routing_title": "Routeplanning",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Overstroming",
"routing_modeManual": "Handleiding",
"routing_modeAutoHint": "Selecteert automatisch het bekendste pad, en gebruikt een flood-algoritme als er geen bekend pad is.",
"routing_modeFloodHint": "Uitzendingen via elke zender. De meest betrouwbare methode, maar vereist meer uitzendtijd.",
"routing_modeManualHint": "Stuurt altijd de exacte route die u heeft aangegeven.",
"routing_currentRoute": "Huidige route",
"routing_directNoHops": "Direct zonder tussenliggende schakels",
"routing_noPathYet": "Er is nog geen route gevonden. De berichten blijven binnenkomen totdat een route is ontdekt.",
"routing_floodBroadcast": "Uitgestoten via elke zender.",
"routing_editPath": "Pad bewerken",
"routing_forgetPath": "Vergeet het pad",
"routing_knownPaths": "Bekende routes",
"routing_knownPathsHint": "Maak een route om er naartoe te gaan.",
"routing_inUse": "In gebruik",
"routing_qualityStrong": "Sterke eerste sprong",
"routing_qualityGood": "Een goede eerste stap",
"routing_qualityFair": "Een goede eerste hop",
"routing_qualityWorked": "Is geleverd",
"routing_qualityFlood": "Hears via een overstroming",
"routing_qualityUntested": "Niet getest",
"routing_neverWorked": "nooit bevestigd",
"routing_deliveryCounts": "{successes} zijn behaald, {failures} zijn mislukt",
"routing_floodDelivery": "Levering bij overstroming",
"pathEditor_title": "Pad creëren",
"pathEditor_hopCounter": "{count} van 64 hopgranen",
"pathEditor_noHops": "Er zijn nog geen hop toegevoegd. Klik op de onderstaande knoppen om ze in de juiste volgorde toe te voegen, of sla de bestelling op zonder hop om deze direct te versturen.",
"pathEditor_addHops": "Voeg hop toe in de juiste volgorde.",
"pathEditor_searchRepeaters": "Zoek naar herhaaldelijke zenders",
"pathEditor_advancedHex": "Geavanceerd: ruwe hex-pad",
"pathEditor_hexLabel": "Hex-voorkanten",
"pathEditor_hexHelper": "Twee hex-tekens per stap, gescheiden door komma's",
"pathEditor_invalidTokens": "Ongeldig: {tokens}",
"pathEditor_tooManyHops": "Maximaal 64 hopken",
"pathEditor_usePath": "Gebruik deze route.",
"pathEditor_removeHop": "Verwijder de hop",
"pathEditor_unknownHop": "Onbekend type zender",
"map_zoomIn": "Inzoomen",
"routing_lastWorked": "worked {when}",
"map_zoomOut": "Inzoomen",
"map_centerMap": "Centraal overzicht",
"chrome_bluetoothRequiresChromium": "Web Bluetooth vereist een Chromium-browser.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS-locatie bevestigd",
"pathTrace_legendInferred": "Afgeleide positie",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_activity": "Activiteit",
"map_searchHint": "Zoek op naam of ID van de knoop",
"scanner_bluetoothWebUnsupported": "Bluetooth is niet beschikbaar in de browser. Verbind dan via USB.",
"map_online": "Online",
"map_recent": "Recent",
"map_stale": "Verouderd",
"map_visible": "Zichtbaar",
"map_hidden": "Verborgen",
"map_centerOnNode": "Centreer op node",
"map_details": "Details",
"map_noGps": "Geen GPS",
"map_noResults": "Geen overeenkomende nodes",
"pathMap_viewSingle": "Enkel",
"pathMap_viewCombined": "Gezamenlijk",
"pathMap_play": "Afspelen",
"pathMap_pause": "Pauze",
"pathMap_replay": "Herhalen",
"pathMap_stepBack": "Vorige hop",
"pathMap_stepForward": "Volgende hop",
"pathMap_animationOn": "Pakketanimatie tonen",
"pathMap_animationOff": "Pakketanimatie verbergen",
"pathMap_hopOf": "Hop {current} van {total}",
"pathMap_observedPaths": "Waargenomen paden: {count}",
"pathMap_primary": "Primair",
"pathMap_alternate": "Alternatief {index}",
"pathMap_hopCount": "{count, plural, =1{1 hop} other{{count} hops}}",
"pathMap_legendShared": "Gedeeld segment",
"pathMap_legendEstimated": "Geschat segment",
"pathMap_sharedNodeCount": "Gebruikt door {count} paden",
"pathMap_partialAnimation": "{count, plural, =1{1 hop heeft geen locatie — het weergegeven pad is onvolledig} other{{count} hops hebben geen locatie — het weergegeven pad is onvolledig}}",
"pathMap_showAllPaths": "Toon alles",
"pathMap_hidePath": "Verberg pad",
"pathMap_showPath": "Toon pad",
"pathMap_collapsePanel": "Paneel inklappen",
"pathMap_expandPanel": "Paneel uitklappen",
"pathMap_noLocation": "Geen locatie",
"pathMap_followPacket": "Weergave vergrendelen op pakket",
"pathMap_unfollowPacket": "Weergave ontgrendelen van pakket",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regio's",
"settings_regionSettingsSubtitle": "Beheer van opgeslagen regio's",
"settings_regionManagement_screenTitle": "Regionale Bestuur",
"settings_regionNameHint": "Regio naam invoeren",
"settings_regionAddRegion": "Regio toevoegen",
"settings_regionFetchRegions": "Haal regio's uit herhalers",
"settings_regionFetchRegionsFail": "Er zijn geen regio's gevonden",
"settings_regionFetchRegionsAlreadyExists": "Deze regio is al toegevoegd.",
"settings_regionName": "Regio Naam",
"settings_regionDeleted": "Regio verwijderd",
"settings_deleteRegion": "Regio verwijden",
"settings_deleteRegionConfirm": "Verwijder \"{region}\" uit de regio-lijst?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Regio: {region}",
"channels_regionNotSet": "Regio: geen",
"channels_regionSelect_Title": "Kies een regio",
"channels_clearRegion": "Duidelijke regio",
"chat_sendImage": "Beeld verzenden",
"chat_imagePickFailed": "Kunde die afbeelding niet openen",
"chat_receivedGif": "Ontvangen van een GIF",
"repeater_keySettings": "Verander de Identiteitskies",
"repeater_keySettingsSubtitle": "Wijzig de openbare/private sleutelpaar",
"repeater_prvKey": "Geheime sleutel",
"repeater_prvKeyHelper": "Een nieuwe private sleutel voor de repeater, een 128-karakter lange hexadecimale string.",
"repeater_generatePrvKey": "Stel een willekeurige sleutelpaar op",
"repeater_stopGeneratingPrvKey": "Stop zoek naar sleutelpaar",
"repeater_pubKey": "Openbaar sleutel",
"repeater_pubKeyHelper": "Dit is de openbare sleutel die past bij de gegenereerde privésleutel. U kunt dit niet direct instellen.",
"repeater_pubKeyPrefix": "Verlangde voorvoegsel",
"repeater_pubKeyPrefixHelper": "Zoek een openbare sleutel die begint met deze hexadecimale cijfers. Verwacht aantal pogingen: {tries}.",
"imageMessages_enableTitle": "Bijbehorende afbeeldingen activeren",
"imageMessages_enableSubtitle": "Beeldjes verzenden via het netwerk. Vereist eenmalig afdaling van een beeldmodel.",
"imageMessages_modelSectionTitle": "Beeldmodel",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Annuleren",
"imageMessages_removeModel": "Model verwijderen",
"imageMessages_modelReady": "Klaar",
"imageMessages_modelNotPublished": "Nog niet gepubliceerd — deze versie kan dit niet downloaden.",
"imageMessages_downloadFailed": "Het beeldmodel kon niet worden gedownload.",
"imageMessages_autoProcessTitle": "Beeldverwerking automatisch",
"imageMessages_autoProcessSubtitle": "Reconstrueer elk beeld direct nadat het binnenkomt. Het gebruikt ongeveer 2 GB geheugen voor elke keer; schakel het reconstrueren uit met een tik als je het niet nodig hebt.",
"imageSend_title": "Beeld verzenden",
"imageSend_cropNote": "Aangepast aan 512 × 512 · het aspectverhouding is niet behouden",
"imageSend_originalSize": "Oorspronkelijk",
"imageSend_onAirSize": "Op de lucht",
"imageSend_quality": "Kwaliteit",
"imageSend_qualityStandard": "Standaard",
"imageSend_qualityHigh": "Hoog",
"imageSend_packetsLabel": "Pakketten",
"imageSend_airtimeLabel": "Tijd op de lucht",
"imageSend_sizeLabel": "Belasting",
"imageSend_packetsCount": "{count} {count, plural, =1{pakket} other{pakketten}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radio-instellingen onbekend",
"imageSend_radioUnknownBody": "Verbinding maken met een apparaat zodat de uitzendtijd kan worden berekend.",
"imageSend_longSendTitle": "Lange transmissie",
"imageSend_longSendBody": "Dit houdt de kanaal ongeveer voor {duration} stand.",
"imageSend_floodNote": "Overstromingsrouting: elke repeater binnen bereik herzendsends elk pakket opnieuw, waardoor het kanaal langer bezig blijft dan dit.",
"imageSend_parityTitle": "Herstelpakket",
"imageSend_paritySubtitle": "Een extra pakje. Groepsberichten worden niet bevestigd, waardoor de ontvanger het beeld kan herstellen als een enkel pakket verloren gaat.",
"imageSend_send": "Stuur",
"imageSend_cancel": "Annuleren",
"imageSend_encodeFailed": "Dit beeld kon niet worden gecodeerd.",
"imageSend_codecDownloading": "Het beeldmodel wordt nog steeds gedownload.",
"imageSend_codecUnavailable": "Het verzenden van een afbeelding is niet mogelijk op dit apparaat.",
"imageSend_codecDisabled": "Boodschappen met afbeeldingen zijn uitgeschakeld in de instellingen.",
"imageSend_deviceUnsupported": "Dit radio kan geen beeldpakketten verzenden. Verbind een apparaat met de bijbehorende firmware versie 13 of nieuwer.",
"imageSend_directMessagesUnsupported": "Afbeeldingen worden verzonden als groepsgegevens, dus ze kunnen alleen naar een kanaal worden gestuurd — niet via een directe boodschap.",
"imageSend_tooLarge": "Die afbeelding werd gecodeerd in meer pakketten dan het mesh-formaat toelaat.",
"imageSend_sentConfirmation": "Afbeelding verzonden als {count} {count, plural, =1{pakket} other{pakketten}}.",
"imageSend_sendFailed": "Het beeld kon niet worden verzonden: {error}",
"imageSend_sendingProgress": "Beeld verzenden — pakket {sent} van {total}",
"receivedImage_senderPrefix": "Knipper {prefix}",
"receivedImage_incoming": "{received} van de {total} pakketten",
"receivedImage_queued": "Wachten op het ontcijferen",
"receivedImage_tapToDecode": "Tik om te decoderen",
"receivedImage_decoding": "Herstellen… ongeveer 1 seconde",
"receivedImage_incomplete": "Afbeelding onvolledig — {received} van {total} pakketten ontvangen",
"receivedImage_corrupt": "Afbeelding kon niet worden gereconstrueerd",
"receivedImage_decoderMissing": "Beeld ontvangen — beeldontcipheringsapparatuur is uitgeschakeld",
"receivedImage_evicted": "Afbeelding niet langer opgeslagen",
"receivedImage_retry": "Probeer het opnieuw",
"receivedImage_decodeAgain": "Decode opnieuw",
"receivedImage_openSettings": "Inricht",
"receivedImage_tapToProcess": "Tik om te verwerken",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pakket} other{pakketten}}",
"imageSend_secondsValue": "{seconds} seconden",
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s"
} }
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Usuń", "common_remove": "Usuń",
"common_enable": "Włącz", "common_enable": "Włącz",
"common_disable": "Wyłącz", "common_disable": "Wyłącz",
"common_autoRefresh": "Automatyczne odświeżanie",
"common_interval": "Interwał",
"common_reboot": "Uruchom ponownie", "common_reboot": "Uruchom ponownie",
"common_loading": "Ładowanie...", "common_loading": "Ładowanie...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.", "settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
"settings_latitude": "Szerokość", "settings_latitude": "Szerokość",
"settings_longitude": "Długość", "settings_longitude": "Długość",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatyczne ogłoszenie zero-hop przy aktualizacji GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).",
"settings_privacyMode": "Tryb prywatności", "settings_privacyMode": "Tryb prywatności",
"settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach", "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
"settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.", "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "Ostatnie 6 godzin", "appSettings_last6Hours": "Ostatnie 6 godzin",
"appSettings_last24Hours": "Ostatnie 24 godziny", "appSettings_last24Hours": "Ostatnie 24 godziny",
"appSettings_lastWeek": "Ostatni tydzień", "appSettings_lastWeek": "Ostatni tydzień",
"appSettings_rasterTileSource": "Źródło kafelków rastrowych",
"appSettings_stadiaEndpoint": "Punkt końcowy Stadia",
"appSettings_stadiaApiKey": "Klucz API Stadia",
"appSettings_stadiaApiKeyRequired": "Wymagane do korzystania ze Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Skonfigurowano: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.",
"appSettings_offlineMapCache": "Pamięć podręczna map offline", "appSettings_offlineMapCache": "Pamięć podręczna map offline",
"appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.", "appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.",
"appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})",
@@ -777,6 +794,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1115,7 +1182,7 @@
"repeater_advancedSettings": "Zaawansowany", "repeater_advancedSettings": "Zaawansowany",
"repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów", "repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów",
"repeater_pathHashMode": "Tryb haszujący ścieżkę", "repeater_pathHashMode": "Tryb haszujący ścieżkę",
"repeater_pathHashModeHelper": "Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.", "repeater_pathHashModeHelper": "Bajty ywane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.",
"repeater_txDelay": "Opóźnienie w Flood, TX", "repeater_txDelay": "Opóźnienie w Flood, TX",
"repeater_txDelayHelper": "Ustawienie odstępu dla ruchu związanego z powodzią, jako mnożnik czasu przesyłania pakietu (0-2, domyślnie 0,5). Wyższe wartości oznaczają mniejszą liczbę kolizji, ale wolniejszą prędkość przesyłania.", "repeater_txDelayHelper": "Ustawienie odstępu dla ruchu związanego z powodzią, jako mnożnik czasu przesyłania pakietu (0-2, domyślnie 0,5). Wyższe wartości oznaczają mniejszą liczbę kolizji, ale wolniejszą prędkość przesyłania.",
"repeater_directTxDelay": "Bezpośrednie opóźnienie sygnału TX", "repeater_directTxDelay": "Bezpośrednie opóźnienie sygnału TX",
@@ -1290,6 +1357,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Wejście cyfrowe",
"telemetry_digitalOutputLabel": "Wyjście cyfrowe",
"telemetry_analogInputLabel": "Wejście analogowe",
"telemetry_analogOutputLabel": "Wyjście analogowe",
"telemetry_genericLabel": "Czujnik ogólny",
"telemetry_luminosityLabel": "Jasność",
"telemetry_presenceLabel": "Obecność",
"telemetry_humidityLabel": "Wilgotność",
"telemetry_accelerometerLabel": "Akcelerometr",
"telemetry_pressureLabel": "Ciśnienie",
"telemetry_altitudeLabel": "Wysokość",
"telemetry_frequencyLabel": "Częstotliwość",
"telemetry_percentageLabel": "Procent",
"telemetry_concentrationLabel": "Stężenie",
"telemetry_powerLabel": "Moc",
"telemetry_distanceLabel": "Odległość",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Kierunek",
"telemetry_timeLabel": "Czas",
"telemetry_gyrometerLabel": "Żyrometr",
"telemetry_colourLabel": "Kolor",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Przełącznik",
"telemetry_polylineLabel": "Polilinia",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Liczba żądań",
"telemetry_error": "Nie udało się pobrać danych",
"telemetry_noData": "Brak dostępnych danych telemetrycznych.", "telemetry_noData": "Brak dostępnych danych telemetrycznych.",
"telemetry_channelTitle": "Kanał {channel}", "telemetry_channelTitle": "Kanał {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2137,6 +2241,9 @@
"translation_enableTitle": "Włącz tłumaczenie", "translation_enableTitle": "Włącz tłumaczenie",
"translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.", "translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.",
"translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.", "translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.",
"translation_autoIncomingTitle": "Automatycznie tłumacz wiadomości",
"translation_autoIncomingSubtitle": "Automatycznie tłumaczy wiadomości do powiadomień oraz do czatów lub kanałów.",
"translation_translateMessage": "Przetłumacz wiadomość",
"translation_targetLanguage": "Język docelowy", "translation_targetLanguage": "Język docelowy",
"translation_useAppLanguage": "Użyj języka aplikacji", "translation_useAppLanguage": "Użyj języka aplikacji",
"translation_downloadedModelLabel": "Pobudowany model", "translation_downloadedModelLabel": "Pobudowany model",
@@ -2350,5 +2457,413 @@
"chat_markAsUnread": "Oznacz jako nieprzeczytane", "chat_markAsUnread": "Oznacz jako nieprzeczytane",
"settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)", "settings_companionDebugLog": "Log debugowania (dla pomocy w rozwiązywaniu problemów)",
"repeater_chanUtil": "Wykorzystanie kanału", "repeater_chanUtil": "Wykorzystanie kanału",
"contact_connectCompanion": "Połącz się z towarzyszem, aby uzyskać dostęp do funkcji powtarzacza i serwera pokoi." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Wysłane",
"messageStatus_delivered": "Dostarczone",
"messageStatus_pending": "Wysyłanie",
"common_undo": "Wycofaj",
"messageStatus_failed": "Nie udało się wysłać",
"messageStatus_repeated": "Usłyszałem to wielokrotnie",
"contacts_moreOptions": "Więcej opcji",
"contacts_searchOpen": "Wyszukaj kontakty",
"contacts_searchClose": "Zaawansowane wyszukiwanie",
"routing_title": "Planowanie tras",
"routing_modeAuto": "Samochód",
"routing_modeFlood": "Powódź",
"routing_modeManual": "Instrukcja obsługi",
"routing_modeAutoHint": "Automatycznie wybiera najpopularniejszą ścieżkę, a w przypadku braku znanej, przechodzi do trybu \"przepływu\".",
"routing_modeFloodHint": "Transmisje za pośrednictwem każdego repeatera. Najbardziej niezawodna metoda, ale zużywa więcej czasu transmisji.",
"routing_modeManualHint": "Zawsze prowadzi dokładnie po trasie, którą określiłeś.",
"routing_currentRoute": "Obecna trasa",
"routing_directNoHops": "Bezpośrednio bez pośrednictwa repeaterów",
"routing_noPathYet": "Na razie nie ma żadnej ścieżki. Komunikacja trwa do momentu, gdy zostanie odkryta trasa.",
"routing_floodBroadcast": "Transmisja za pośrednictwem każdego urządzenia powielającego",
"routing_editPath": "Edytuj ścieżkę",
"routing_forgetPath": "Zapomnij o ścieżce",
"routing_knownPaths": "Znane trasy",
"routing_knownPathsHint": "Wybierz ścieżkę, aby przełączyć się na nią.",
"routing_inUse": "W użyciu",
"routing_qualityStrong": "Silny pierwszy skok",
"routing_qualityGood": "Świetny początek",
"routing_qualityFair": "Świetny pierwszy krzak",
"routing_qualityWorked": "Zostało dostarczone",
"routing_qualityFlood": "Usłyszano dzięki doniesieniom",
"routing_qualityUntested": "Nieużywany",
"routing_lastWorked": "pracował {when}",
"routing_neverWorked": "nigdy nie zostało potwierdzone",
"routing_floodDelivery": "Dostawa w przypadku powodzi",
"pathEditor_title": "Stworzenie ścieżki",
"pathEditor_hopCounter": "{count} z 64 rodzajów chmielu",
"pathEditor_noHops": "Na razie nie dodano żadnych chmielu. Aby dodać je w odpowiedniej kolejności, kliknij w odpowiednie przyciski poniżej, lub zapisz przepis bez chmielu, aby wysłać go bezpośrednio.",
"pathEditor_addHops": "Dodawaj chmiel zgodnie z kolejnością.",
"pathEditor_searchRepeaters": "Funkcje powtarzania",
"pathEditor_advancedHex": "Zaawansowane: ścieżka w formacie szesnastkowym",
"pathEditor_hexLabel": "Prefiksy heksadecymalne",
"pathEditor_hexHelper": "Dwa znaki szesnastkowe na każdym kroku, oddzielone przecinkami",
"pathEditor_invalidTokens": "Nieprawidłowe: {tokens}",
"pathEditor_tooManyHops": "Maksymalnie 64 hopów",
"pathEditor_usePath": "Użyj tej ścieżki.",
"pathEditor_removeHop": "Usuń dziką psiankę",
"pathEditor_unknownHop": "Nieznany repeater",
"map_zoomIn": "Przybliż",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Przybliż z powrotem",
"map_centerMap": "Mapa centrum",
"chrome_bluetoothRequiresChromium": "Web Bluetooth wymaga przeglądarki Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potwierdzone",
"pathTrace_legendInferred": "Wywnioskowana pozycja",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_online": "Online",
"map_searchHint": "Wyszukaj nazwę lub identyfikator węzła",
"scanner_bluetoothWebUnsupported": "Bluetooth nie jest dostępny w przeglądarce. Połącz się przez USB.",
"map_activity": "Aktywność",
"map_recent": "Ostatnie",
"map_stale": "Nieaktualne",
"map_visible": "Widoczny",
"map_hidden": "Ukryty",
"map_centerOnNode": "Wyśrodkuj na węźle",
"map_details": "Szczegóły",
"map_noGps": "Brak GPS",
"map_noResults": "Brak pasujących węzłów",
"pathMap_viewSingle": "Pojedyncza",
"pathMap_viewCombined": "Połączone",
"pathMap_play": "Odtwórz",
"pathMap_pause": "Wstrzymaj",
"pathMap_replay": "Odtwórz ponownie",
"pathMap_stepBack": "Poprzedni skok",
"pathMap_stepForward": "Następny skok",
"pathMap_animationOn": "Pokaż animację pakietu",
"pathMap_animationOff": "Ukryj animację pakietu",
"pathMap_hopOf": "Skok {current} z {total}",
"pathMap_observedPaths": "Obserwowane trasy: {count}",
"pathMap_primary": "Główna",
"pathMap_alternate": "Alt. {index}",
"pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoki} many{{count} skoków} other{{count} skoku}}",
"pathMap_legendShared": "Wspólny segment",
"pathMap_legendEstimated": "Szacunkowy segment",
"pathMap_sharedNodeCount": "Wykorzystywane przez {count} ścieżek",
"pathMap_partialAnimation": "{count, plural, =1{1 skok nie ma lokalizacji — pokazana ścieżka jest niekompletna} few{{count} skoki nie mają lokalizacji — pokazana ścieżka jest niekompletna} many{{count} skoków nie ma lokalizacji — pokazana ścieżka jest niekompletna} other{{count} skoku nie ma lokalizacji — pokazana ścieżka jest niekompletna}}",
"pathMap_showAllPaths": "Pokaż wszystkie",
"pathMap_hidePath": "Ukryj ścieżkę",
"pathMap_showPath": "Wyświetl trasę",
"pathMap_collapsePanel": "Zwiń panel",
"pathMap_expandPanel": "Rozwiń panel",
"pathMap_noLocation": "Brak lokalizacji",
"pathMap_followPacket": "Śledź pakiet",
"pathMap_unfollowPacket": "Przestań śledzić pakiet",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiony",
"settings_regionSettingsSubtitle": "Zarządzanie zapisanymi regionami",
"settings_regionManagement_screenTitle": "Zarządzanie Regionami",
"settings_regionNameHint": "Wprowadź nazwę regionu",
"settings_regionAddRegion": "Dodaj region",
"settings_regionFetchRegions": "Pobierz regiony z repeaterów",
"settings_regionFetchRegionsFail": "Nie znaleziono żadnych regionów",
"settings_regionFetchRegionsAlreadyExists": "Ten region został już dodany",
"settings_regionName": "Nazwa Regionu",
"settings_regionDeleted": "Region usunięto",
"settings_deleteRegion": "Usuń Region",
"settings_deleteRegionConfirm": "Czy usunąć \"{region}\" z listy regionów?",
"settings_infoHardware": "Sprzęt",
"settings_infoFirmware": "Oprogramowanie wbudowane",
"repeater_pathHashModeOption0": "0 - 1 bajt",
"repeater_pathHashModeOption1": "1 - 2 bity",
"repeater_pathHashModeOption2": "2 - 3 bajty",
"repeater_pathHashModeOption3": "3 - 4 bajty",
"appSettings_batteryLipoHv": "Bateria LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: brak",
"channels_regionSelect_Title": "Wybierz region",
"channels_clearRegion": "Przejrzyste obszar",
"chat_sendImage": "Wyślij obraz",
"chat_imagePickFailed": "Nie mogłem otworzyć tego obrazu",
"chat_receivedGif": "Otrzymano GIF",
"repeater_keySettings": "Zmiana kluczy tożsamości",
"repeater_keySettingsSubtitle": "Zmień parę kluczy publiczną/prywatną",
"repeater_prvKey": "Klucz prywatny",
"repeater_prvKeyHelper": "Nowy klucz prywatny dla wzmacniacza, 128-znakowa string hexadecyimalny.",
"repeater_generatePrvKey": "Wygeneruj losowy klucz parowy",
"repeater_stopGeneratingPrvKey": "Przerwij wyszukiwanie kluczowego paru",
"repeater_pubKey": "Klucz publiczny",
"repeater_pubKeyHelper": "To jest klucz publiczny, który odpowiada wygenerowanemu kluczowi prywatnemu. Nie można go ustawić bezpośrednio.",
"repeater_pubKeyPrefix": "Pożądane przedrostek",
"repeater_pubKeyPrefixHelper": "Znajdź klucz publiczny, który zaczyna się od tych cyfr hex. Oczekiwana liczba prób: {tries}.",
"imageMessages_enableTitle": "Włącz wiadomości ze zdjęciami",
"imageMessages_enableSubtitle": "Przesyłaj zdjęcia przez siatkę. Wymaga jednorazowego pobrania modelu obrazu.",
"imageMessages_modelSectionTitle": "Model obrazowy",
"imageMessages_downloadModel": "Pobierz",
"imageMessages_cancelDownload": "Anuluj",
"imageMessages_removeModel": "Usuń model",
"imageMessages_modelReady": "Gotowy",
"imageMessages_modelNotPublished": "Nie opublikowane — ten build nie może tego pobrać.",
"imageMessages_downloadFailed": "Model obrazowy nie mógł zostać pobrany.",
"imageMessages_autoProcessTitle": "Obrazki są przetwarzane automatycznie.",
"imageMessages_autoProcessSubtitle": "Odbuduj każde zdjęcie, jak tylko przyjdzie. Zużywa około 2 GB pamięci za każdą drugą sekundę; wyłącz odbudowywanie jednym dotknięciem.",
"imageSend_title": "Wyślij obraz",
"imageSend_cropNote": "Zmniejszone do 512 × 512 · proporcje nie zostały zachowane",
"imageSend_originalSize": "Oryginalny",
"imageSend_onAirSize": "W eterze",
"imageSend_quality": "Jakość",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Wysoko",
"imageSend_packetsLabel": "Pakiet",
"imageSend_airtimeLabel": "Czas w eterze",
"imageSend_sizeLabel": "Payload",
"imageSend_packetsCount": "{count} {count, plural, =1{pakiet} other{pakietów}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Ustawienia radia nieznane",
"imageSend_radioUnknownBody": "Połącz urządzenie, aby można było obliczyć czas trwania transmisji.",
"imageSend_longSendTitle": "Długa transmisja",
"imageSend_longSendBody": "Ten kanał będzie dostępny przez około {duration}.",
"imageSend_floodNote": "Przepływ pakietów: każdy retransmitujący w zasięgu przekazuje każdy pakiet, przez co kanał pozostaje zajęty dłużej niż ten czas.",
"imageSend_parityTitle": "Pakiet odzysku",
"imageSend_paritySubtitle": "Jedna dodatkowa paczka. Wiadomości grupowe nie są potwierdzane, dzięki czemu odbiorca może odbudować obraz, jeśli zostanie utracona pojedyncza paczka.",
"imageSend_send": "Wyślij",
"imageSend_cancel": "Anuluj",
"imageSend_encodeFailed": "Ten obraz nie mógł zostać zakodowany.",
"imageSend_codecDownloading": "Model obrazowy jest nadal pobierany.",
"imageSend_codecUnavailable": "Wysyłanie obrazu nie jest dostępne na tym urządzeniu.",
"imageSend_codecDisabled": "Wiadomości ze zdjęciami są wyłączone w ustawieniach.",
"imageSend_deviceUnsupported": "Ten radiodysk nie może wysyłać pakietów obrazu. Podłącz urządzenie z oprogramowaniem firmware 13 lub nowszym.",
"imageSend_directMessagesUnsupported": "Obrazy są przesyłane jako dane grupowe, więc można je wysłać tylko do kanału — nie w wiadomości prywatnej.",
"imageSend_tooLarge": "Obraz zakodowany do więcej pakietów niż pozwala format siatki.",
"imageSend_sentConfirmation": "Obraz wysłany jako {count} {count, plural, =1{pakiet} other{pakietów}}.",
"imageSend_sendFailed": "Obraz nie mogł zostać wysłany: {error}",
"imageSend_sendingProgress": "Wysyłanie obrazu — pakiet {sent} z {total}",
"receivedImage_senderPrefix": "Węzeł {prefix}",
"receivedImage_incoming": "Otrzymano: {received} z {total} pakietów",
"receivedImage_queued": "Oczekiwanie na rozszyfrowanie",
"receivedImage_tapToDecode": "Kliknij, aby odszyfrować",
"receivedImage_decoding": "Odtwarzanie… około 1 sekundy",
"receivedImage_incomplete": "Obraz niepełny — {received} z {total} pakietów dotarło",
"receivedImage_corrupt": "Obraz nie może zostać zrekonstruowany",
"receivedImage_decoderMissing": "Otrzymano obraz — dekodowanie obrazu jest nieprawidłowe.",
"receivedImage_evicted": "Obraz nie jest już przechowywany",
"receivedImage_retry": "Spróbuj ponownie",
"receivedImage_decodeAgain": "Zdekoduj ponownie",
"receivedImage_openSettings": "Ustaw",
"receivedImage_tapToProcess": "Kliknij, aby przetworzyć",
"receivedImage_awaiting": "{bytes} bajtów · {packets} {packets, plural, =1{pakiet} other{pakiety}}",
"imageSend_secondsValue": "{seconds} sekund",
"imageSend_minutesSecondsValue": "{minutes} minut {seconds} sekundy"
} }
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Remover", "common_remove": "Remover",
"common_enable": "Ativar", "common_enable": "Ativar",
"common_disable": "Desativar", "common_disable": "Desativar",
"common_autoRefresh": "Atualização automática",
"common_interval": "Intervalo",
"common_reboot": "Reiniciar", "common_reboot": "Reiniciar",
"common_loading": "Carregando...", "common_loading": "Carregando...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Latitude ou longitude inválidos.", "settings_locationInvalid": "Latitude ou longitude inválidos.",
"settings_latitude": "Latitude", "settings_latitude": "Latitude",
"settings_longitude": "Longitude", "settings_longitude": "Longitude",
"settings_autoZeroHopAdvertOnGpsUpdate": "Anúncio zero-hop automático na atualização do GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).",
"settings_privacyMode": "Modo de Privacidade", "settings_privacyMode": "Modo de Privacidade",
"settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios",
"settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "Últimos 6 horas", "appSettings_last6Hours": "Últimos 6 horas",
"appSettings_last24Hours": "Últimas 24 horas", "appSettings_last24Hours": "Últimas 24 horas",
"appSettings_lastWeek": "Da última semana", "appSettings_lastWeek": "Da última semana",
"appSettings_rasterTileSource": "Fonte de blocos raster",
"appSettings_stadiaEndpoint": "Endpoint da Stadia",
"appSettings_stadiaApiKey": "Chave da API Stadia",
"appSettings_stadiaApiKeyRequired": "Obrigatório para usar o Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.",
"appSettings_offlineMapCache": "Cache de Mapa Offline", "appSettings_offlineMapCache": "Cache de Mapa Offline",
"appSettings_noAreaSelected": "Nenhuma área selecionada", "appSettings_noAreaSelected": "Nenhuma área selecionada",
"appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})",
@@ -767,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1105,7 +1172,7 @@
"repeater_advancedSettings": "Avançado", "repeater_advancedSettings": "Avançado",
"repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes", "repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes",
"repeater_pathHashMode": "Modo de hash de caminho", "repeater_pathHashMode": "Modo de hash de caminho",
"repeater_pathHashModeHelper": "Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).", "repeater_pathHashModeHelper": "Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.",
"repeater_txDelay": "Atraso na entrega em Flood, TX", "repeater_txDelay": "Atraso na entrega em Flood, TX",
"repeater_txDelayHelper": "Ajuste de espaçamento para tráfego de inundações, como um multiplicador do tempo de transmissão (0-2, padrão 0,5). Quanto maior, menos colisões, mas uma entrega mais lenta.", "repeater_txDelayHelper": "Ajuste de espaçamento para tráfego de inundações, como um multiplicador do tempo de transmissão (0-2, padrão 0,5). Quanto maior, menos colisões, mas uma entrega mais lenta.",
"repeater_directTxDelay": "Atraso direto no sinal TX", "repeater_directTxDelay": "Atraso direto no sinal TX",
@@ -1280,6 +1347,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Entrada digital",
"telemetry_digitalOutputLabel": "Saída digital",
"telemetry_analogInputLabel": "Entrada analógica",
"telemetry_analogOutputLabel": "Saída analógica",
"telemetry_genericLabel": "Sensor genérico",
"telemetry_luminosityLabel": "Luminosidade",
"telemetry_presenceLabel": "Presença",
"telemetry_humidityLabel": "Humidade",
"telemetry_accelerometerLabel": "Acelerómetro",
"telemetry_pressureLabel": "Pressão",
"telemetry_altitudeLabel": "Altitude",
"telemetry_frequencyLabel": "Frequência",
"telemetry_percentageLabel": "Percentagem",
"telemetry_concentrationLabel": "Concentração",
"telemetry_powerLabel": "Potência",
"telemetry_distanceLabel": "Distância",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Direção",
"telemetry_timeLabel": "Hora",
"telemetry_gyrometerLabel": "Girómetro",
"telemetry_colourLabel": "Cor",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Interruptor",
"telemetry_polylineLabel": "Polilinha",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Número de solicitações",
"telemetry_error": "Não foi possível obter os dados",
"telemetry_noData": "Não estão disponíveis dados de telemetria.", "telemetry_noData": "Não estão disponíveis dados de telemetria.",
"telemetry_channelTitle": "Canal {channel}", "telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2100,6 +2204,9 @@
"translation_enableTitle": "Ativar a tradução", "translation_enableTitle": "Ativar a tradução",
"translation_title": "Tradução", "translation_title": "Tradução",
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.", "translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
"translation_autoIncomingTitle": "Traduzir mensagens automaticamente",
"translation_autoIncomingSubtitle": "Traduz automaticamente mensagens para notificações e para chats ou canais.",
"translation_translateMessage": "Traduzir mensagem",
"translation_targetLanguage": "Língua-alvo", "translation_targetLanguage": "Língua-alvo",
"translation_useAppLanguage": "Utilize o idioma da aplicação", "translation_useAppLanguage": "Utilize o idioma da aplicação",
"translation_downloadedModelLabel": "Modelo baixado", "translation_downloadedModelLabel": "Modelo baixado",
@@ -2312,5 +2419,413 @@
"chat_markAsUnread": "Marcar como não lido", "chat_markAsUnread": "Marcar como não lido",
"chat_newMessages": "Novas mensagens", "chat_newMessages": "Novas mensagens",
"repeater_chanUtil": "Utilização do canal", "repeater_chanUtil": "Utilização do canal",
"contact_connectCompanion": "Conecte-se a um dispositivo companion para acessar as funcionalidades de repetidor e servidor de salas." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Desfazer",
"messageStatus_sent": "Enviado",
"messageStatus_pending": "Enviar",
"messageStatus_delivered": "Entregue",
"messageStatus_failed": "Falhou ao enviar",
"messageStatus_repeated": "Ouvi repetidamente",
"contacts_moreOptions": "Mais opções",
"contacts_searchOpen": "Pesquisar contatos",
"contacts_searchClose": "Pesquisa avançada",
"routing_title": "Rotas",
"routing_modeAuto": "Carro",
"routing_modeFlood": "Inundação",
"routing_modeManual": "Manual",
"routing_modeAutoHint": "Seleciona automaticamente o caminho mais conhecido, e, se nenhum caminho conhecido for encontrado, utiliza a estratégia de \"inundação\".",
"routing_modeFloodHint": "Transmissão através de todos os repetidores. É a opção mais confiável, mas utiliza mais tempo de transmissão.",
"routing_modeManualHint": "Sempre segue exatamente o caminho que você define.",
"routing_currentRoute": "Rota atual",
"routing_directNoHops": "Direto sem saltos de repetidor",
"routing_noPathYet": "Ainda não há um caminho definido. A mensagem continua a ser enviada até que uma rota seja encontrada.",
"routing_floodBroadcast": "Transmissão através de todos os repetidores",
"routing_editPath": "Editar caminho",
"routing_forgetPath": "Esqueça o caminho",
"routing_knownPaths": "Rotas conhecidas",
"routing_knownPathsHint": "Toque em um caminho para alternar para ele.",
"routing_inUse": "Em uso",
"routing_qualityStrong": "Primeiro salto notável",
"routing_qualityGood": "Primeiro salto bem-sucedido",
"routing_qualityFair": "Primeira etapa bem-sucedida",
"routing_qualityWorked": "Foi entregue",
"routing_qualityFlood": "Informação obtida através de relatos generalizados.",
"routing_qualityUntested": "Não testado",
"routing_neverWorked": "nunca confirmado",
"routing_floodDelivery": "Entrega em áreas afetadas por inundações",
"pathEditor_title": "Criar Caminho",
"pathEditor_hopCounter": "{count} de 64 gramas de lúpulo",
"pathEditor_noHops": "Ainda não há lúpulos adicionados. Clique nos repetidores abaixo para adicioná-los na ordem desejada, ou salve sem adicionar lúpulos para enviar diretamente.",
"pathEditor_addHops": "Adicione os lúpulos na seguinte ordem.",
"pathEditor_searchRepeaters": "Encontrar repetidores",
"pathEditor_advancedHex": "Avançado: caminho hexadecimal bruto",
"pathEditor_hexLabel": "Prefixos hexadecimais",
"pathEditor_hexHelper": "Dois caracteres hexadecimais por salto, separados por vírgulas.",
"pathEditor_invalidTokens": "Inválido: {tokens}",
"routing_lastWorked": "worked {when}",
"pathEditor_tooManyHops": "Máximo de 64 saltos",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_usePath": "Utilize este caminho.",
"pathEditor_removeHop": "Remova o lúpulo",
"pathEditor_unknownHop": "Repetidor desconhecido",
"map_zoomIn": "Ampliar",
"map_zoomOut": "Ampliar",
"map_centerMap": "Mapa do centro",
"chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS confirmado",
"pathTrace_legendInferred": "Posição inferida",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_online": "Online",
"map_activity": "Atividade",
"scanner_bluetoothWebUnsupported": "A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.",
"map_searchHint": "Pesquisar por nome ou ID do nó",
"map_recent": "Recente",
"map_stale": "Vencido",
"map_visible": "Visível",
"map_hidden": "Escondido",
"map_centerOnNode": "Centralizar no nó",
"map_details": "Detalhes",
"map_noGps": "Sem GPS",
"map_noResults": "Nenhum nó encontrado",
"pathMap_viewSingle": "Único",
"pathMap_viewCombined": "Combinado",
"pathMap_play": "Reproduzir",
"pathMap_pause": "Pausa",
"pathMap_stepBack": "Salto anterior",
"pathMap_replay": "Repetir",
"pathMap_stepForward": "Próximo salto",
"pathMap_animationOn": "Exibir animação do pacote",
"pathMap_animationOff": "Ocultar a animação do pacote",
"pathMap_hopOf": "Salto {current} de {total}",
"pathMap_observedPaths": "Caminhos observados: {count}",
"pathMap_primary": "Primário",
"pathMap_alternate": "Alt {index}",
"pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
"pathMap_legendShared": "Segmento compartilhado",
"pathMap_legendEstimated": "Segmento estimado",
"pathMap_sharedNodeCount": "Utilizado em {count} caminhos",
"pathMap_partialAnimation": "{count, plural, =1{1 salto não tem localização — o caminho mostrado é parcial} other{{count} saltos não têm localização — o caminho mostrado é parcial}}",
"pathMap_showAllPaths": "Mostrar tudo",
"pathMap_hidePath": "Esconder caminho",
"pathMap_showPath": "Mostrar o caminho",
"pathMap_collapsePanel": "Recolher painel",
"pathMap_expandPanel": "Expandir painel",
"pathMap_noLocation": "Sem localização",
"pathMap_followPacket": "Fixar vista no pacote",
"pathMap_unfollowPacket": "Liberar vista do pacote",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiões",
"settings_regionSettingsSubtitle": "Gerenciar regiões armazenadas",
"settings_regionManagement_screenTitle": "Gestão de Região",
"settings_regionNameHint": "Nome da região",
"settings_regionAddRegion": "Adicionar região",
"settings_regionFetchRegions": "Regiões de obtenção a partir de repetidores",
"settings_regionFetchRegionsFail": "Nenhuma região foi encontrada",
"settings_regionFetchRegionsAlreadyExists": "Esta região já foi adicionada",
"settings_regionName": "Nome da Região",
"settings_regionDeleted": "Região excluída",
"settings_deleteRegion": "Excluir Região",
"settings_deleteRegionConfirm": "Remover \"{region}\" da lista de regiões?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35V)",
"channels_regionSetTo": "Região: {region}",
"channels_regionNotSet": "Região: nenhuma",
"channels_regionSelect_Title": "Escolha uma região",
"channels_clearRegion": "Região desimpedida",
"chat_sendImage": "Enviar imagem",
"chat_imagePickFailed": "Não consegui abrir essa imagem",
"chat_receivedGif": "Recebi um GIF",
"repeater_keySettings": "Alterar Chaves de Identidade",
"repeater_keySettingsSubtitle": "Altere a chave pública/chave privada",
"repeater_prvKey": "Chave privada",
"repeater_prvKeyHelper": "Uma nova chave privada para o repetidor, uma string hexadecimal de 128 caracteres.",
"repeater_generatePrvKey": "Gere um par de chaves aleatório",
"repeater_stopGeneratingPrvKey": "Interrompa a busca por par de chaves",
"repeater_pubKey": "Chave pública",
"repeater_pubKeyHelper": "Esta é a chave pública que acompanha a chave privada gerada. Você não pode definir isso diretamente.",
"repeater_pubKeyPrefix": "Prefixo desejado",
"repeater_pubKeyPrefixHelper": "Encontre uma chave pública que comece com esses dígitos hexadecimais. Número esperado de tentativas: {tries}.",
"imageMessages_enableTitle": "Mensagens de imagem",
"imageMessages_enableSubtitle": "Envie as imagens pela malha. Requer o download de um modelo de imagem único.",
"imageMessages_modelSectionTitle": "Modelo de imagem",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Cancelar",
"imageMessages_removeModel": "Remover modelo",
"imageMessages_modelReady": "Pronto",
"imageMessages_modelNotPublished": "Ainda não publicado — esta versão não pode baixá-lo.",
"imageMessages_downloadFailed": "O modelo de imagem não pôde ser baixado.",
"imageMessages_autoProcessTitle": "Processar imagens automaticamente",
"imageMessages_autoProcessSubtitle": "Reconstrua cada imagem assim que ela chegar. Utiliza cerca de 2 GB de memória por segundo; dispense a reconstrução com um toque.",
"imageSend_title": "Enviar imagem",
"imageSend_cropNote": "Redimensionado para 512 × 512 · a proporção de aspecto não foi preservada",
"imageSend_originalSize": "Texto Original",
"imageSend_onAirSize": "Ao ar",
"imageSend_quality": "Qualidade",
"imageSend_qualityStandard": "Padrão",
"imageSend_qualityHigh": "Alto",
"imageSend_packetsLabel": "Pacotes",
"imageSend_airtimeLabel": "Tempo em transmissão",
"imageSend_sizeLabel": "Carga útil",
"imageSend_packetsCount": "{count} {count, plural, =1{pacote} other{pacotes}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Configurações de rádio desconhecidas",
"imageSend_radioUnknownBody": "Conecte-se a um dispositivo para que o horário de transmissão possa ser calculado.",
"imageSend_longSendTitle": "Transmissão longa",
"imageSend_longSendBody": "Isso manterá o canal por cerca de {duration}.",
"imageSend_floodNote": "Roteamento de inundação: cada repetidor na faixa retransmite cada pacote, fazendo com que o canal permaneça ocupado por mais tempo do que isso.",
"imageSend_parityTitle": "Pacote de recuperação",
"imageSend_paritySubtitle": "Um pacote extra. As mensagens em grupo não são reconhecidas, assim permitindo que o destinatário reconstrua a imagem caso um pacote seja perdido.",
"imageSend_send": "Enviar",
"imageSend_cancel": "Cancelar",
"imageSend_encodeFailed": "Esta imagem não pode ser codificada.",
"imageSend_codecDownloading": "O modelo de imagem ainda está baixando.",
"imageSend_codecUnavailable": "O envio de imagem não está disponível neste dispositivo.",
"imageSend_codecDisabled": "As mensagens de imagem estão desativadas nas configurações.",
"imageSend_deviceUnsupported": "Este rádio não pode enviar pacotes de imagem. Conecte um dispositivo com firmware companheiro versão 13 ou superior.",
"imageSend_directMessagesUnsupported": "As imagens viajam como dados em grupo, portanto, só podem ser enviadas para um canal — não em mensagem direta.",
"imageSend_tooLarge": "Essa imagem foi codificada em mais pacotes do que o formato de malha permite.",
"imageSend_sentConfirmation": "Imagem enviada como {count} {count, plural, =1{pacote} other{pacotes}}.",
"imageSend_sendFailed": "A imagem não pôde ser enviada: {error}",
"imageSend_sendingProgress": "Envio da imagem — pacote {sent} de {total}",
"receivedImage_senderPrefix": "Nó {prefix}",
"receivedImage_incoming": "{received} de {total} pacotes",
"receivedImage_queued": "Aguardando para decodificar",
"receivedImage_tapToDecode": "Toque para decodificar",
"receivedImage_decoding": "Reconstruindo… cerca de 1 segundo",
"receivedImage_incomplete": "Imagem incompleta — {received} de {total} pacotes chegaram",
"receivedImage_corrupt": "A imagem não pôde ser reconstruída",
"receivedImage_decoderMissing": "Imagem recebida — decodificação de imagem incorreta",
"receivedImage_evicted": "Imagem não armazenada",
"receivedImage_retry": "Tente novamente",
"receivedImage_decodeAgain": "Decodifique novamente",
"receivedImage_openSettings": "Configurar",
"receivedImage_tapToProcess": "Toque para processar",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pacote} other{pacotes}}",
"imageSend_secondsValue": "{seconds} segundos",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+517 -2
View File
@@ -39,6 +39,8 @@
"common_notAvailable": "—", "common_notAvailable": "—",
"common_voltageValue": "{volts} В", "common_voltageValue": "{volts} В",
"common_percentValue": "{percent}%", "common_percentValue": "{percent}%",
"common_autoRefresh": "Автообновление",
"common_interval": "Интервал",
"scanner_title": "MeshCore Open", "scanner_title": "MeshCore Open",
"scanner_scanning": "Поиск устройств...", "scanner_scanning": "Поиск устройств...",
"scanner_connecting": "Подключение...", "scanner_connecting": "Подключение...",
@@ -75,6 +77,8 @@
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
"settings_latitude": "Широта", "settings_latitude": "Широта",
"settings_longitude": "Долгота", "settings_longitude": "Долгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).",
"settings_privacyMode": "Режим конфиденциальности", "settings_privacyMode": "Режим конфиденциальности",
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
@@ -188,6 +192,19 @@
"appSettings_last6Hours": "Последние 6 часов", "appSettings_last6Hours": "Последние 6 часов",
"appSettings_last24Hours": "Последние 24 часа", "appSettings_last24Hours": "Последние 24 часа",
"appSettings_lastWeek": "Последнюю неделю", "appSettings_lastWeek": "Последнюю неделю",
"appSettings_rasterTileSource": "Источник растровых тайлов",
"appSettings_stadiaEndpoint": "Конечная точка Stadia",
"appSettings_stadiaApiKey": "Ключ API Stadia",
"appSettings_stadiaApiKeyRequired": "Требуется для использования Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Настроено: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.",
"appSettings_offlineMapCache": "Кэш офлайн-карты", "appSettings_offlineMapCache": "Кэш офлайн-карты",
"appSettings_noAreaSelected": "Область не выбрана", "appSettings_noAreaSelected": "Область не выбрана",
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})", "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})",
@@ -436,6 +453,56 @@
"mapCache_downloadTilesButton": "Загрузить плитки", "mapCache_downloadTilesButton": "Загрузить плитки",
"mapCache_clearCacheButton": "Очистить кэш", "mapCache_clearCacheButton": "Очистить кэш",
"mapCache_failedDownloads": "Неудачных загрузок: {count}", "mapCache_failedDownloads": "Неудачных загрузок: {count}",
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
"time_justNow": "Только что", "time_justNow": "Только что",
"time_minutesAgo": "{minutes} мин назад", "time_minutesAgo": "{minutes} мин назад",
@@ -686,6 +753,43 @@
"telemetry_voltageValue": "{volts}В", "telemetry_voltageValue": "{volts}В",
"telemetry_currentValue": "{amps}А", "telemetry_currentValue": "{amps}А",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"telemetry_digitalInputLabel": "Цифровой вход",
"telemetry_digitalOutputLabel": "Цифровой выход",
"telemetry_analogInputLabel": "Аналоговый вход",
"telemetry_analogOutputLabel": "Аналоговый выход",
"telemetry_genericLabel": "Общий датчик",
"telemetry_luminosityLabel": "Освещённость",
"telemetry_presenceLabel": "Присутствие",
"telemetry_humidityLabel": "Влажность",
"telemetry_accelerometerLabel": "Акселерометр",
"telemetry_pressureLabel": "Давление",
"telemetry_altitudeLabel": "Высота",
"telemetry_frequencyLabel": "Частота",
"telemetry_percentageLabel": "Процент",
"telemetry_concentrationLabel": "Концентрация",
"telemetry_powerLabel": "Мощность",
"telemetry_distanceLabel": "Расстояние",
"telemetry_energyLabel": "Энергия",
"telemetry_directionLabel": "Направление",
"telemetry_timeLabel": "Время",
"telemetry_gyrometerLabel": "Гирометр",
"telemetry_colourLabel": "Цвет",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Переключатель",
"telemetry_polylineLabel": "Полилиния",
"telemetry_altitudeValue": "{meters} м",
"telemetry_frequencyValue": "{hertz} Гц",
"telemetry_pressureValue": "{hpa} гПа",
"telemetry_luminosityValue": "{lux} лк",
"telemetry_powerValue": "{watts} Вт",
"telemetry_distanceValue": "{meters} м",
"telemetry_energyValue": "{kilowattHours} кВт⋅ч",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Количество запросов",
"telemetry_error": "Не удалось получить данные",
"neighbors_receivedData": "Полученные данные о соседях", "neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}", "neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
@@ -1268,6 +1372,9 @@
"translation_title": "Перевод", "translation_title": "Перевод",
"translation_enableTitle": "Включить перевод", "translation_enableTitle": "Включить перевод",
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.", "translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
"translation_autoIncomingTitle": "Автоматически переводить сообщения",
"translation_autoIncomingSubtitle": "Автоматически переводит сообщения для уведомлений, а также для чатов и каналов.",
"translation_translateMessage": "Перевести сообщение",
"translation_targetLanguage": "Целевой язык", "translation_targetLanguage": "Целевой язык",
"translation_useAppLanguage": "Используйте язык приложения", "translation_useAppLanguage": "Используйте язык приложения",
"translation_downloadedModelLabel": "Загруженная модель", "translation_downloadedModelLabel": "Загруженная модель",
@@ -1436,7 +1543,7 @@
"repeater_advancedSettings": "Продвинутый", "repeater_advancedSettings": "Продвинутый",
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов", "repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
"repeater_pathHashMode": "Режим хеширования пути", "repeater_pathHashMode": "Режим хеширования пути",
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.", "repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.",
"repeater_txDelay": "Задержка в работе системы Flood TX", "repeater_txDelay": "Задержка в работе системы Flood TX",
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.", "repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
"repeater_directTxDelay": "Прямая задержка сигнала TX", "repeater_directTxDelay": "Прямая задержка сигнала TX",
@@ -1615,5 +1722,413 @@
"settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.", "settings_companionDebugLogSubtitle": "Команды, ответы и необработанные данные, используемые для протоколов BLE, TCP и USB.",
"repeater_chanUtil": "Использование канала", "repeater_chanUtil": "Использование канала",
"settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)", "settings_companionDebugLog": "Журнал отладки (для сопутствующего приложения)",
"contact_connectCompanion": "Подключитесь к компаньону, чтобы получить доступ к функциям ретранслятора и сервера комнат." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_pending": "Отправка",
"common_undo": "Отменить",
"messageStatus_delivered": "Доставлено",
"messageStatus_sent": "Отправлено",
"messageStatus_failed": "Не удалось отправить",
"messageStatus_repeated": "Услышал несколько раз",
"contacts_moreOptions": "Больше вариантов",
"contacts_searchOpen": "Найти контакты",
"contacts_searchClose": "Закрыть поиск",
"routing_title": "Маршрутизация",
"routing_modeAuto": "Авто",
"routing_modeFlood": "Наводнение",
"routing_modeManual": "Инструкция",
"routing_modeAutoHint": "Автоматически выбирает наиболее известный путь, и если такой путь неизвестен, использует алгоритм поиска пути.",
"routing_modeFloodHint": "Передача сигнала через все ретрансляторы. Самый надежный способ, но требует больше времени на передачу.",
"routing_modeManualHint": "Всегда следует точно по указанному вами маршруту.",
"routing_currentRoute": "Текущий маршрут",
"routing_directNoHops": "Прямое соединение – без использования ретрансляторов",
"routing_noPathYet": "Пока нет пути. Следующее сообщение будет отправлено до тех пор, пока не будет обнаружен маршрут.",
"routing_floodBroadcast": "Транслируется через все ретрансляторы",
"routing_editPath": "Изменить путь",
"routing_forgetPath": "Забудьте о маршруте",
"routing_knownPaths": "Известные маршруты",
"routing_knownPathsHint": "Создайте маршрут для переключения на этот пункт.",
"routing_inUse": "В эксплуатации",
"routing_qualityStrong": "Сильный первый скачок",
"routing_qualityGood": "Хорошее начало",
"routing_qualityFair": "Первый хороший урожай",
"routing_qualityWorked": "Осуществлено",
"routing_qualityFlood": "Узнал из новостей, распространяющихся в интернете.",
"routing_qualityUntested": "Непроверенный",
"routing_neverWorked": "никогда не было подтверждено",
"routing_floodDelivery": "Доставка при затоплении",
"pathEditor_title": "Создать маршрут",
"pathEditor_hopCounter": "{count} из 64 хмеля",
"pathEditor_noHops": "На данный момент хмель еще не добавлен. Чтобы добавить его, нажмите на соответствующие кнопки ниже в нужном порядке, или сохраните рецепт без хмеля, чтобы отправить его напрямую.",
"pathEditor_addHops": "Добавляйте хмель в соответствии с указанным порядком.",
"pathEditor_searchRepeaters": "Поиск повторителей",
"pathEditor_advancedHex": "Продвинутый уровень: прямой путь в шестнадцатеричном формате",
"pathEditor_hexLabel": "Префиксы шестнадцатеричной системы",
"pathEditor_hexHelper": "Два шестнадцатеричных символа на каждом шаге, разделенные запятыми.",
"pathEditor_invalidTokens": "Неверно: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Максимальное количество ингредиентов – 64",
"pathEditor_usePath": "Используйте этот путь",
"pathEditor_removeHop": "Удалить хмель",
"pathEditor_unknownHop": "Неизвестный ретранслятор",
"map_zoomIn": "Увеличить масштаб",
"map_zoomOut": "Увеличить масштаб",
"map_centerMap": "Карта центра",
"chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.",
"channels_communityShortId": "Идентификатор: {id}...",
"pathTrace_legendGpsConfirmed": "GPS подтверждено",
"pathTrace_legendInferred": "Выведенная позиция",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_searchHint": "Поиск по имени или ID узла",
"map_online": "Онлайн",
"scanner_bluetoothWebUnsupported": "Bluetooth недоступен в браузере. Подключитесь через USB.",
"map_activity": "Активность",
"map_recent": "Недавно",
"map_stale": "Устаревший",
"map_visible": "Видимый",
"map_hidden": "Скрытый",
"map_centerOnNode": "Центрировать на узле",
"map_details": "Детали",
"map_noGps": "Без GPS",
"map_noResults": "Не найдено соответствующих узлов",
"pathMap_viewSingle": "Одиночный",
"pathMap_viewCombined": "Объединённые",
"pathMap_play": "Воспроизвести",
"pathMap_pause": "Пауза",
"pathMap_replay": "Повтор",
"pathMap_stepBack": "Предыдущий хоп",
"pathMap_stepForward": "Следующий хоп",
"pathMap_animationOn": "Показать анимацию пакета",
"pathMap_animationOff": "Скрыть анимацию пакета",
"pathMap_hopOf": "Хоп {current} из {total}",
"pathMap_observedPaths": "Наблюдаемые маршруты: {count}",
"pathMap_primary": "Основной",
"pathMap_alternate": "Альт {index}",
"pathMap_hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопов}}",
"pathMap_legendShared": "Общий сегмент",
"pathMap_legendEstimated": "Расчётный сегмент",
"pathMap_sharedNodeCount": "Используется в {count} маршрутах",
"pathMap_partialAnimation": "{count, plural, one{{count} хоп не имеет координат — показанный путь неполный} few{{count} хопа не имеют координат — показанный путь неполный} many{{count} хопов не имеют координат — показанный путь неполный} other{{count} хопов не имеют координат — показанный путь неполный}}",
"pathMap_showAllPaths": "Показать всё",
"pathMap_hidePath": "Скрыть путь",
"pathMap_collapsePanel": "Скрыть панель",
"pathMap_showPath": "Показать маршрут",
"pathMap_expandPanel": "Расширить панель",
"pathMap_noLocation": "Нет координат",
"pathMap_followPacket": "Следить за пакетом",
"pathMap_unfollowPacket": "Не следить за пакетом",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Регионы",
"settings_regionSettingsSubtitle": "Управление сохранёнными регионами",
"settings_regionManagement_screenTitle": "Управление регионами",
"settings_regionNameHint": "Введите название региона",
"settings_regionAddRegion": "Добавить регион",
"settings_regionFetchRegions": "Извлекать регионы с ретрансляторов",
"settings_regionFetchRegionsFail": "Не были найдены никакие регионы",
"settings_regionFetchRegionsAlreadyExists": "Этот регион уже был добавлен",
"settings_regionName": "Название региона",
"settings_regionDeleted": "Регион удалён",
"settings_deleteRegion": "Удалить регион",
"settings_deleteRegionConfirm": "Удалить \"{region}\" из списка регионов?",
"settings_infoHardware": "Оборудование",
"settings_infoFirmware": "Производственное программное обеспечение",
"repeater_pathHashModeOption0": "0 - 1 байт",
"repeater_pathHashModeOption1": "1 - 2 байта",
"repeater_pathHashModeOption2": "2 - 3 байта",
"repeater_pathHashModeOption3": "3 - 4 байта",
"appSettings_batteryLipoHv": "LiPo HV (3,04,35 В)",
"channels_regionSetTo": "РЕГИОН: {region}",
"channels_regionNotSet": "Регион: отсутствует",
"channels_regionSelect_Title": "Выберите регион",
"channels_clearRegion": "Чистый регион",
"chat_sendImage": "Отправьте изображение",
"chat_imagePickFailed": "Не удалось открыть это изображение",
"chat_receivedGif": "Получен GIF",
"repeater_keySettings": "Изменить ключи идентичности",
"repeater_keySettingsSubtitle": "Измените пару публичных/частных ключей",
"repeater_prvKey": "Личный ключ",
"repeater_prvKeyHelper": "Для повторителя генерируется новый приватный ключ — шестнадцатеричная строка из 128 символов.",
"repeater_generatePrvKey": "Сгенерируйте случайный ключ-пару",
"repeater_stopGeneratingPrvKey": "Прервать поиск ключной пары",
"repeater_pubKey": "Публичный ключ",
"repeater_pubKeyHelper": "Это публичный ключ, который соответствует сгенерированному приватному ключу. Вы не можете установить его напрямую.",
"repeater_pubKeyPrefix": "Желаемый префикс",
"repeater_pubKeyPrefixHelper": "Найдите публичный ключ, начинающийся этими шестнадцатеричными цифрами. Ожидаемое количество попыток: {tries}.",
"imageMessages_enableTitle": "Включить изображения в сообщения",
"imageMessages_enableSubtitle": "Отправьте изображения через сеть. Требуется однократная загрузка шаблона изображения.",
"imageMessages_modelSectionTitle": "Модель изображения",
"imageMessages_downloadModel": "Скачать",
"imageMessages_cancelDownload": "Отмена",
"imageMessages_removeModel": "Удалить модель",
"imageMessages_modelReady": "Готов",
"imageMessages_modelNotPublished": "Пока не опубликовано — эта сборка не может скачать это.",
"imageMessages_downloadFailed": "Модель изображения не удалось скачать.",
"imageMessages_autoProcessTitle": "Автоматически обрабатывать изображения",
"imageMessages_autoProcessSubtitle": "Примерно каждое изображение реконструируйте сразу после его поступления. Для этого на каждый раз используется около 2 ГБ памяти; отключите реконструкцию касанием.",
"imageSend_title": "Отправьте изображение",
"imageSend_cropNote": "Меняется размер на 512 × 512 · соотношение сторон не сохраняется",
"imageSend_originalSize": "Оригинал",
"imageSend_onAirSize": "В эфире",
"imageSend_quality": "Качество",
"imageSend_qualityStandard": "Стандарт",
"imageSend_qualityHigh": "Высота",
"imageSend_packetsLabel": "Пакеты",
"imageSend_airtimeLabel": "Время на эфире",
"imageSend_sizeLabel": "Пакет",
"imageSend_packetsCount": "{count} {count, plural, one{пакет} few{пакета} many{пакетов} other{пакета}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Настройки радио неизвестны",
"imageSend_radioUnknownBody": "Подключитесь к устройству, чтобы можно было рассчитать время эфирного времени.",
"imageSend_longSendTitle": "Долгая передача",
"imageSend_longSendBody": "Это будет поддерживать канал примерно {duration}.",
"imageSend_floodNote": "Руководство по маршрутизации потоков: каждый ретрансатор в зоне перераспределяет каждый пакет, поэтому канал остаётся занятным дольше, чем это.",
"imageSend_parityTitle": "Пакет восстановления",
"imageSend_paritySubtitle": "Один дополнительный пакет. Групповые сообщения не подтверждаются, поэтому это позволяет получателю восстановить изображение, если один пакет потерян.",
"imageSend_send": "Отправить",
"imageSend_cancel": "Отмена",
"imageSend_encodeFailed": "Это изображение не может быть закодировано.",
"imageSend_codecDownloading": "Модель изображения всё ещё загружается.",
"imageSend_codecUnavailable": "Отправка изображения на этом устройстве недоступна.",
"imageSend_codecDisabled": "В настройках отключены сообщения с изображениями.",
"imageSend_deviceUnsupported": "Это радио не может отправлять пакеты изображений. Подключите устройство, работающее на прошивке-компаньоне версии 13 или выше.",
"imageSend_directMessagesUnsupported": "Изображения передаются как групповые данные, поэтому их можно отправлять только в канал, а не в личное сообщение.",
"imageSend_tooLarge": "Изображение было закодировано в больше пакетов, чем позволяет формат сетки.",
"imageSend_sentConfirmation": "Изображение отправлено как {count} {count, plural, =1{пакет} other{пакетов}}.",
"imageSend_sendFailed": "Изображение не может быть отправлено: {error}",
"imageSend_sendingProgress": "Отправка изображения — пакет {sent} из {total}",
"receivedImage_senderPrefix": "Узел {prefix}",
"receivedImage_incoming": "{received} из {total} пакетов",
"receivedImage_queued": "Ожидание расшифровки",
"receivedImage_tapToDecode": "Нажмите для декодирования",
"receivedImage_decoding": "Реконструкция… примерно за 1 секунду",
"receivedImage_incomplete": "Отсутствует изображение — {received} из {total} пакетов пришло",
"receivedImage_corrupt": "Изображение не может быть восстановлено",
"receivedImage_decoderMissing": "Получен изображение — декодирование изображения неправильное",
"receivedImage_evicted": "Изображение больше не сохраняется",
"receivedImage_retry": "Попробуйте снова",
"receivedImage_decodeAgain": "Декодируйте снова",
"receivedImage_openSettings": "Настройте",
"receivedImage_tapToProcess": "Нажмите для обработки",
"receivedImage_awaiting": "{bytes} байт · {packets} {packets, plural, =1{пакет} other{пакетов}}",
"imageSend_secondsValue": "{seconds} секунд",
"imageSend_minutesSecondsValue": "{minutes} м {seconds} с"
} }
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Odstrániť", "common_remove": "Odstrániť",
"common_enable": "Povolit", "common_enable": "Povolit",
"common_disable": "Zakázať", "common_disable": "Zakázať",
"common_autoRefresh": "Automatické obnovenie",
"common_interval": "Časový interval",
"common_reboot": "Restartovať", "common_reboot": "Restartovať",
"common_loading": "Načítavanie...", "common_loading": "Načítavanie...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Neplatná šírka alebo dĺžka.", "settings_locationInvalid": "Neplatná šírka alebo dĺžka.",
"settings_latitude": "Súradnica", "settings_latitude": "Súradnica",
"settings_longitude": "Dĺžka", "settings_longitude": "Dĺžka",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatický zero-hop inzerát pri aktualizácii GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).",
"settings_privacyMode": "Režim ochrany súkromia", "settings_privacyMode": "Režim ochrany súkromia",
"settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách", "settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách",
"settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.", "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "Posledné 6 hodín", "appSettings_last6Hours": "Posledné 6 hodín",
"appSettings_last24Hours": "Posledných 24 hodín", "appSettings_last24Hours": "Posledných 24 hodín",
"appSettings_lastWeek": "Minul týždeň", "appSettings_lastWeek": "Minul týždeň",
"appSettings_rasterTileSource": "Zdroj rastrových dlaždíc",
"appSettings_stadiaEndpoint": "Koncový bod Stadia",
"appSettings_stadiaApiKey": "Kľúč API Stadia",
"appSettings_stadiaApiKeyRequired": "Vyžaduje sa na používanie Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Nakonfigurované: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.",
"appSettings_offlineMapCache": "Offline Mapa Pamäť", "appSettings_offlineMapCache": "Offline Mapa Pamäť",
"appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť",
"appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})",
@@ -767,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1105,7 +1172,7 @@
"repeater_advancedSettings": "Pokročilé", "repeater_advancedSettings": "Pokročilé",
"repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov", "repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov",
"repeater_pathHashMode": "Režim hashovania cesty", "repeater_pathHashMode": "Režim hashovania cesty",
"repeater_pathHashModeHelper": "Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy fungujú len, keď je sieť aktivovaná.", "repeater_pathHashModeHelper": "Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.",
"repeater_txDelay": "Zpoždanie v Flood, TX", "repeater_txDelay": "Zpoždanie v Flood, TX",
"repeater_txDelayHelper": "Nastavenie pre opakované vysielanie pre dopravu počas povodní, ako násobok času, ktorý paket využije (0-2, výchoce hodnota 0,5). Vyššia hodnota znamená menej kolízii, ale pomalšie doručovanie.", "repeater_txDelayHelper": "Nastavenie pre opakované vysielanie pre dopravu počas povodní, ako násobok času, ktorý paket využije (0-2, výchoce hodnota 0,5). Vyššia hodnota znamená menej kolízii, ale pomalšie doručovanie.",
"repeater_directTxDelay": "Priame oneskorenie TX", "repeater_directTxDelay": "Priame oneskorenie TX",
@@ -1280,6 +1347,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Digitálny vstup",
"telemetry_digitalOutputLabel": "Digitálny výstup",
"telemetry_analogInputLabel": "Analógový vstup",
"telemetry_analogOutputLabel": "Analógový výstup",
"telemetry_genericLabel": "Všeobecný senzor",
"telemetry_luminosityLabel": "Osvetlenie",
"telemetry_presenceLabel": "Prítomnosť",
"telemetry_humidityLabel": "Vlhkosť",
"telemetry_accelerometerLabel": "Akcelerometer",
"telemetry_pressureLabel": "Tlak",
"telemetry_altitudeLabel": "Nadmorská výška",
"telemetry_frequencyLabel": "Frekvencia",
"telemetry_percentageLabel": "Percento",
"telemetry_concentrationLabel": "Koncentrácia",
"telemetry_powerLabel": "Výkon",
"telemetry_distanceLabel": "Vzdialenosť",
"telemetry_energyLabel": "Energia",
"telemetry_directionLabel": "Smer",
"telemetry_timeLabel": "Čas",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Farba",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Prepínač",
"telemetry_polylineLabel": "Lomená čiara",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Počet požiadaviek",
"telemetry_error": "Nepodarilo sa získať údaje",
"telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.",
"telemetry_channelTitle": "Kanál {channel}", "telemetry_channelTitle": "Kanál {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2100,6 +2204,9 @@
"translation_composerTitle": "Preložte pred odeslaním", "translation_composerTitle": "Preložte pred odeslaním",
"translation_title": "Preklad", "translation_title": "Preklad",
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.", "translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
"translation_autoIncomingTitle": "Automaticky prekladať správy",
"translation_autoIncomingSubtitle": "Automaticky prekladá správy pre upozornenia aj pre čet alebo kanál.",
"translation_translateMessage": "Preložiť správu",
"translation_targetLanguage": "Cieľový jazyk", "translation_targetLanguage": "Cieľový jazyk",
"translation_useAppLanguage": "Použite jazyk aplikácie", "translation_useAppLanguage": "Použite jazyk aplikácie",
"translation_downloadedModelLabel": "Stiahnutý model", "translation_downloadedModelLabel": "Stiahnutý model",
@@ -2312,5 +2419,413 @@
"settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)", "settings_companionDebugLog": "Logovanie pre ladenie (sprievodný log)",
"chat_newMessages": "Nové správy", "chat_newMessages": "Nové správy",
"repeater_chanUtil": "Využitie kanálu", "repeater_chanUtil": "Využitie kanálu",
"contact_connectCompanion": "Pripojte sa k sprievodcovi a získajte prístup k funkciám opakovača a serveru miestností." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Odoslané",
"messageStatus_delivered": "Doručené",
"messageStatus_pending": "Odoslanie",
"common_undo": "Zrušiť",
"messageStatus_failed": "Neúspešné odeslanie",
"messageStatus_repeated": "Slyšal som to opakovane",
"contacts_moreOptions": "Ďalšie možnosti",
"contacts_searchOpen": "Vyhľadajte kontakty",
"contacts_searchClose": "Zavrieť vyhľadávanie",
"routing_title": "Navigácia",
"routing_modeAuto": "Auto",
"routing_modeFlood": "Povodňová vlna",
"routing_modeManual": "Ručná príručka",
"routing_modeAutoHint": "Automaticky vyberá najznámejší trasa, a ak žiadna nie je známa, použije náhodnú trasu.",
"routing_modeFloodHint": "Prenos prostredníctvom všetkých opakovačov. Najspoľahlivejší spôsob, ale vyžaduje viac času vysielania.",
"routing_modeManualHint": "Vždy dodáva presne podľa zadaného trasy.",
"routing_currentRoute": "Aktuálna trasa",
"routing_directNoHops": "Priamo bez prechodných trás",
"routing_noPathYet": "Zatiaľ neexistuje žiadna cesta. Nasledujúce správy budú pokračovať, kým sa nenájde trasa.",
"routing_floodBroadcast": "Prenos prostredníctvom každého opakovača",
"routing_editPath": "Upraviť trasu",
"routing_forgetPath": "Zabudnite na trasu",
"routing_knownPaths": "Známe cesty",
"routing_knownPathsHint": "Kliknite na cestu, aby ste sa k nej presunuli.",
"routing_inUse": "V prevádzke",
"routing_qualityStrong": "Silný prvý krok",
"routing_qualityGood": "Úspešný prvý krok",
"routing_qualityFair": "Prvá, spravodlivá fáza",
"routing_qualityWorked": "Dosiahnutý úspech",
"routing_qualityFlood": "Zistil som to z informácií, ktoré som získal v dôsledku povodňovej situácie.",
"routing_qualityUntested": "Neotestované",
"routing_neverWorked": "nikedy nebolo potvrdené",
"routing_floodDelivery": "Doručenie v prípade povodní",
"pathEditor_title": "Vytvorenie cesty",
"pathEditor_hopCounter": "{count} z 64 chmelových zŕš",
"pathEditor_noHops": "Zatiaľ žiadne chmel. Kliknite na opakované, aby ste ich pridali postupne, alebo uložte bez chmelu, aby ste ho mohli poslať priamo.",
"pathEditor_addHops": "Pridávajte chmel podľa zadaného poriadku.",
"pathEditor_searchRepeaters": "Hľadať opakované",
"pathEditor_advancedHex": "Pokročilé: pôvodná hexová cesta",
"pathEditor_hexLabel": "Prefiksy pre hexadecimálne čísla",
"pathEditor_hexHelper": "Dve hexové čísla na každý krok, oddelené čiarkami",
"routing_lastWorked": "worked {when}",
"pathEditor_invalidTokens": "Neplatné: {tokens}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Maximálne 64 krokov",
"pathEditor_usePath": "Použite túto cestu",
"pathEditor_removeHop": "Odstráňte chmel",
"pathEditor_unknownHop": "Neznáme zariadenie na opakované vysielanie",
"map_zoomIn": "Zväčšiť",
"map_zoomOut": "Zmenť zamer zblízka",
"map_centerMap": "Mapa centra",
"chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potvrdilo",
"pathTrace_legendInferred": "Odvodená poloha",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_online": "Online",
"scanner_bluetoothWebUnsupported": "Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.",
"map_searchHint": "Vyhľadajte podľa názvu alebo ID uzla",
"map_activity": "Aktivita",
"map_recent": "Nedávne",
"map_stale": "Neaktuálne",
"map_hidden": "Skrytý",
"map_visible": "Viditeľný",
"map_centerOnNode": "Nacentrovať na uzol",
"map_details": "Podrobnosti",
"map_noGps": "Bez GPS",
"map_noResults": "Nenašli sa žiadne zodpovedajúce uzly.",
"pathMap_viewSingle": "Jednotlivý",
"pathMap_viewCombined": "Spojené",
"pathMap_play": "Prehrať",
"pathMap_pause": "Pozastaviť",
"pathMap_replay": "Prehrať znova",
"pathMap_stepBack": "Predchádzajúci skok",
"pathMap_stepForward": "Nasledujúci skok",
"pathMap_animationOn": "Zobraziť animáciu paketu",
"pathMap_animationOff": "Skryť animáciu paketu",
"pathMap_hopOf": "Skok {current} z {total}",
"pathMap_observedPaths": "Pozorované cesty: {count}",
"pathMap_primary": "Primárna",
"pathMap_alternate": "Alternatívny {index}",
"pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoky} other{{count} skokov}}",
"pathMap_legendShared": "Spoločný segment",
"pathMap_legendEstimated": "Odhadovaný segment",
"pathMap_sharedNodeCount": "Používané {count} cestami",
"pathMap_partialAnimation": "{count, plural, =1{1 skok nemá polohu — zobrazená trasa je neúplná} few{{count} skoky nemajú polohu — zobrazená trasa je neúplná} other{{count} skokov nemá polohu — zobrazená trasa je neúplná}}",
"pathMap_showAllPaths": "Zobraziť všetky",
"pathMap_hidePath": "Skryť cestu",
"pathMap_showPath": "Zobraziť trasu",
"pathMap_collapsePanel": "Zatvoriť panel",
"pathMap_expandPanel": "Rozbaliť panel",
"pathMap_noLocation": "Bez polohy",
"pathMap_followPacket": "Uzamknúť pohľad na paket",
"pathMap_unfollowPacket": "Odomknúť pohľad od paketu",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiony",
"settings_regionSettingsSubtitle": "Zohľať uložené regiony",
"settings_regionManagement_screenTitle": "Regiónne vedenie",
"settings_regionNameHint": "Vstuptejte název regionu",
"settings_regionAddRegion": "Dodajte region",
"settings_regionFetchRegions": "Odberanie regionov z repeaterov",
"settings_regionFetchRegionsFail": "Nebylo našli žiadné regiony",
"settings_regionFetchRegionsAlreadyExists": "Tento oblast je už dodán",
"settings_regionName": "Regionné název",
"settings_regionDeleted": "Regionu zníteno",
"settings_deleteRegion": "Vyniknúcia regionu",
"settings_deleteRegionConfirm": "Odstraniť {region} z listy regionov?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byt",
"repeater_pathHashModeOption1": "1 - 2 bity",
"repeater_pathHashModeOption2": "2 - 3 bity",
"repeater_pathHashModeOption3": "3 - 4 bity",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: žiadné",
"channels_regionSelect_Title": "Vyberte region",
"channels_clearRegion": "Jasný údajný oblasť",
"chat_sendImage": "Vyslat obraz",
"chat_imagePickFailed": "Nesmiela jsem otvoriť tento obrázok",
"chat_receivedGif": "Odeslaný GIF",
"repeater_keySettings": "Zmena identititačných klíčov",
"repeater_keySettingsSubtitle": "Zmena prehľadného/osobného klíčového zbraň",
"repeater_prvKey": "Prívatná klíč",
"repeater_prvKeyHelper": "Nová prívna klíčová číslo pre repeater, 128-znáčlový šestnáctorýč.",
"repeater_generatePrvKey": "Vytvorenie príma kľúčového paru",
"repeater_stopGeneratingPrvKey": "Prerušenie hľadania klíčového paru",
"repeater_pubKey": "Public key",
"repeater_pubKeyHelper": "Táto je veľká klíč, ktorý sa vztahuje s generovaným manéčným klíčom. Nemožné je to ustanovit priateľne.",
"repeater_pubKeyPrefix": "Požadovaný prefiks",
"repeater_pubKeyPrefixHelper": "Zahľadnite sa veľkú publicenu klíču, ktorá začíná tými šestícih štiastkami. Očekivané potrebné pokusy: {tries}.",
"imageMessages_enableTitle": "Zabezpečte zobrazi",
"imageMessages_enableSubtitle": "Odeslanie obrazov pre mšťanie. Potrebujete jednorazové stáhnutie modelu obrazov.",
"imageMessages_modelSectionTitle": "Model v kínajme",
"imageMessages_downloadModel": "Stáhnie",
"imageMessages_cancelDownload": "Zastavenie",
"imageMessages_removeModel": "Vyhniť model",
"imageMessages_modelReady": "Čiačný",
"imageMessages_modelNotPublished": "Nesprávne vypisáné — tento prechovka nie môže jej stáhnuť.",
"imageMessages_downloadFailed": "Model s obrázkom nemôžno sťažovať.",
"imageMessages_autoProcessTitle": "Automaticky prepojiť slučaje.",
"imageMessages_autoProcessSubtitle": "Rekonstruujte každý obraz tak, ako pridá. Používajete okolo 2 GB pamäti pre drugú hodinu; odložte rekonstrukciu tým kliknutím.",
"imageSend_title": "Vyslat obraz",
"imageSend_cropNote": "Zmeneno na 512 × 512 · vzorýst nie byl zachovan",
"imageSend_originalSize": "Výborný text:\n\n\"Language is the road map of a culture. It tells us where to go as a society and helps us understand the world around us. Learning a new language opens up new perspectives and opportunities, allowing us to connect with people from different backgrounds and build bridges between cultures.\"\n\nSlovenská preklada:\n\n\"Jazyk je cesta pre zručnosti a odráža kultúru. Dá nám uvedomi, dokádať, kúdy sa pohladneme a pomáha našej společnosti poradiť sa v rôznych okolnosti a porozumieť o svetu kolem nás. Nauka nového jazyka otviera nové perspektívy a možnosti, čo pohybuje našu schopnosť sa spoluprávať s ľudmi z rôznych okolnosti a budovať mosty medzi kultúry.\"",
"imageSend_onAirSize": "V prúce",
"imageSend_quality": "Kvalita",
"imageSend_qualityStandard": "Stándard",
"imageSend_qualityHigh": "Vysoká",
"imageSend_packetsLabel": "Pakety",
"imageSend_airtimeLabel": "Čas na anteni",
"imageSend_sizeLabel": "Základ",
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paketov}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Nastavenia radio nezná",
"imageSend_radioUnknownBody": "Zkonečte s určým prijemcem, aby sa mohlo výpočet času, ktorý je v prúbežu.",
"imageSend_longSendTitle": "Dlouhá transmisia",
"imageSend_longSendBody": "Tento dôvod udrží kanál priamo na {duration}.",
"imageSend_floodNote": "Povodný routing: každý repeater v rozsahu zrňa každý paket, takže kanál zostá zajetší dlouше, než tu.",
"imageSend_parityTitle": "Poučný paket pre očudenie",
"imageSend_paritySubtitle": "Jedný dodatný paket. Skúsené súhlasy nie sa uznávaú, takže to umožňuje prechovávaču opraviť obraz, pokiaľ sa jedný paket niebyl ztrácený.",
"imageSend_send": "Odeslať",
"imageSend_cancel": "Zastavenie",
"imageSend_encodeFailed": "Tento obráz nie byla byla schována.",
"imageSend_codecDownloading": "Model prehľadovania prebeža stále stále stále prechádza.",
"imageSend_codecUnavailable": "Zobrazenie nie je dostupný na tomto zariadení.",
"imageSend_codecDisabled": "Zobrazy s prešteľkami sú v nastavení vypnené.",
"imageSend_deviceUnsupported": "Tento radiok nie môže odeslať obrazové pakety. Podeďte správne určité zařízení s oprogramovaním spolupráce 13 alebo novším.",
"imageSend_directMessagesUnsupported": "Obrazy sa prechádzajú jako skupinové údaje, preto sa ich môžu odeslať len na kanál — nie v príbeh sa.",
"imageSend_tooLarge": "Tento obraz byl skodyrovany do viac paketov, ako umožňuje si sítka format.",
"imageSend_sentConfirmation": "Obrázok odoslaný ako {count} {count, plural, =1{paket} other{paketov}}.",
"imageSend_sendFailed": "Obraz nemôžlo odeslať: {error}",
"imageSend_sendingProgress": "Vysyťanie obrazu — pakét {sent} z {total}",
"receivedImage_senderPrefix": "Vęzlo {prefix}",
"receivedImage_incoming": "Otrzymané {received} z {total} pakotí",
"receivedImage_queued": "Čekanie na dekódovanie",
"receivedImage_tapToDecode": "Kliknite, aby dekódovať",
"receivedImage_decoding": "Rekonstrukcia… o čoľve 1 s",
"receivedImage_incomplete": "Obraz nieješťajný — {received} z {total} paciek príšlo",
"receivedImage_corrupt": "Obraz nemôžu byť prepojený",
"receivedImage_decoderMissing": "Otrzymaná slúba — dekódovanie slúby je vypadejce",
"receivedImage_evicted": "Obraz nemá uložený",
"receivedImage_retry": "Zkôr to opítajte",
"receivedImage_decodeAgain": "Zdeľte sa znovu",
"receivedImage_openSettings": "Založte",
"receivedImage_tapToProcess": "Ukliknite, aby sa príjomili",
"receivedImage_awaiting": "{bytes} bajtov · {packets} {packets, plural, =1{paket} other{paketov}}",
"imageSend_secondsValue": "{seconds} s",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Izbrisati", "common_remove": "Izbrisati",
"common_enable": "Omogoči", "common_enable": "Omogoči",
"common_disable": "Izklopiti", "common_disable": "Izklopiti",
"common_autoRefresh": "Samodejno osveževanje",
"common_interval": "Časovni interval",
"common_reboot": "Ponoviti", "common_reboot": "Ponoviti",
"common_loading": "Naložanje...", "common_loading": "Naložanje...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
"settings_latitude": "Širina", "settings_latitude": "Širina",
"settings_longitude": "Dolžina", "settings_longitude": "Dolžina",
"settings_autoZeroHopAdvertOnGpsUpdate": "Samodejni zero-hop oglas ob posodobitvi GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).",
"settings_privacyMode": "Zasebnost", "settings_privacyMode": "Zasebnost",
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last6Hours": "Zadnjih 6 ur",
"appSettings_last24Hours": "Zadnjih 24 ur", "appSettings_last24Hours": "Zadnjih 24 ur",
"appSettings_lastWeek": "Prejšnji teden", "appSettings_lastWeek": "Prejšnji teden",
"appSettings_rasterTileSource": "Vir rastrskih ploščic",
"appSettings_stadiaEndpoint": "Končna točka Stadia",
"appSettings_stadiaApiKey": "Ključ API Stadia",
"appSettings_stadiaApiKeyRequired": "Obvezno za uporabo Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Nastavljeno: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.",
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
"appSettings_noAreaSelected": "Območje ni izbrano", "appSettings_noAreaSelected": "Območje ni izbrano",
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
@@ -767,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1105,7 +1172,7 @@
"repeater_advancedSettings": "Napredno", "repeater_advancedSettings": "Napredno",
"repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike", "repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike",
"repeater_pathHashMode": "Način ustvarjanja hash-a poti", "repeater_pathHashMode": "Način ustvarjanja hash-a poti",
"repeater_pathHashModeHelper": "Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.", "repeater_pathHashModeHelper": "Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.",
"repeater_txDelay": "Zatemnitevanje zaradi poplav v Texasu", "repeater_txDelay": "Zatemnitevanje zaradi poplav v Texasu",
"repeater_txDelayHelper": "Uporaba intervalov za ponovno pošiljanje v primeru prometa zaradi poplav, kot pomnožnik časovne trajanje paketa (0-2, privzeto 0,5). Veje vrednost = manjše kolizije, vendar počasnejše dostavo.", "repeater_txDelayHelper": "Uporaba intervalov za ponovno pošiljanje v primeru prometa zaradi poplav, kot pomnožnik časovne trajanje paketa (0-2, privzeto 0,5). Veje vrednost = manjše kolizije, vendar počasnejše dostavo.",
"repeater_directTxDelay": "Neposredni časovno odlašanje", "repeater_directTxDelay": "Neposredni časovno odlašanje",
@@ -1280,6 +1347,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Digitalni vhod",
"telemetry_digitalOutputLabel": "Digitalni izhod",
"telemetry_analogInputLabel": "Analogni vhod",
"telemetry_analogOutputLabel": "Analogni izhod",
"telemetry_genericLabel": "Splošni senzor",
"telemetry_luminosityLabel": "Osvetljenost",
"telemetry_presenceLabel": "Prisotnost",
"telemetry_humidityLabel": "Vlažnost",
"telemetry_accelerometerLabel": "Merilnik pospeška",
"telemetry_pressureLabel": "Tlak",
"telemetry_altitudeLabel": "Nadmorska višina",
"telemetry_frequencyLabel": "Frekvenca",
"telemetry_percentageLabel": "Odstotek",
"telemetry_concentrationLabel": "Koncentracija",
"telemetry_powerLabel": "Moč",
"telemetry_distanceLabel": "Razdalja",
"telemetry_energyLabel": "Energija",
"telemetry_directionLabel": "Smer",
"telemetry_timeLabel": "Čas",
"telemetry_gyrometerLabel": "Žiroskop",
"telemetry_colourLabel": "Barva",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Stikalo",
"telemetry_polylineLabel": "Polilinija",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Število zahtev",
"telemetry_error": "Podatkov ni bilo mogoče pridobiti",
"telemetry_noData": "Niso na voljo podatki o telemetriji.", "telemetry_noData": "Niso na voljo podatki o telemetriji.",
"telemetry_channelTitle": "Kanal {channel}", "telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2099,6 +2203,9 @@
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.", "translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
"translation_enableTitle": "Omogočite prevod", "translation_enableTitle": "Omogočite prevod",
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.", "translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
"translation_autoIncomingTitle": "Samodejno prevajaj sporočila",
"translation_autoIncomingSubtitle": "Samodejno prevaja sporočila za obvestila ter za klepete ali kanale.",
"translation_translateMessage": "Prevedi sporočilo",
"translation_targetLanguage": "Ciljna jezika", "translation_targetLanguage": "Ciljna jezika",
"translation_useAppLanguage": "Uporabite jezik aplikacije", "translation_useAppLanguage": "Uporabite jezik aplikacije",
"translation_downloadedModelLabel": "Naložen model", "translation_downloadedModelLabel": "Naložen model",
@@ -2312,5 +2419,413 @@
"chat_newMessages": "Nove novice", "chat_newMessages": "Nove novice",
"settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.", "settings_companionDebugLogSubtitle": "Navodila, odgovori in surova podatka za BLE/TCP/USB.",
"repeater_chanUtil": "Uporaba kanala", "repeater_chanUtil": "Uporaba kanala",
"contact_connectCompanion": "Povežite se s spremljevalnikom za dostop do funkcij ponavljalnika in strežnika sob." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"common_undo": "Preobrn",
"messageStatus_delivered": "Dostavljeno",
"messageStatus_sent": "Pošljeno",
"messageStatus_pending": "Pošiljanje",
"messageStatus_failed": "Uspešno ni bilo mogo, da se sporočilo pošlje",
"messageStatus_repeated": "Slišal sem večkrat",
"contacts_moreOptions": "Več možnosti",
"contacts_searchOpen": "Iskanje kontaktov",
"contacts_searchClose": "Izklopi iskanje",
"routing_title": "Navigacija",
"routing_modeAuto": "Avto",
"routing_modeFlood": "Poplavo",
"routing_modeManual": "Navodilo",
"routing_modeAutoHint": "Samodejno izbere najbolj poznano pot, in sicer, ko ni na voljo nobena.",
"routing_modeFloodHint": "Prenosi preko vseh repetitorjev. Najzanesljivejši način, vendar zahteva več časa.",
"routing_modeManualHint": "Vedno sledi natančni poti, ki jo ste določili.",
"routing_currentRoute": "Trenutna pot",
"routing_directNoHops": "Neposredno brez prehodov",
"routing_noPathYet": "Žep trenutno ni mogoče najti. Naslednje sporočilo bo posredovano, dokler ne bo ugotovljeno, kje je pot.",
"routing_floodBroadcast": "Prenos preko vseh repetitiv",
"routing_editPath": "Uredi pot",
"routing_forgetPath": "Pozabi na pot",
"routing_knownPaths": "Poznati poti",
"routing_knownPathsHint": "Kliknite na pot, da jo izberete.",
"routing_inUse": "V uporabi",
"routing_qualityStrong": "Močan prvi korak",
"routing_qualityGood": "Prva uspešna faza",
"routing_qualityFair": "Prva, uspešna faza",
"routing_qualityWorked": "Izpolnil",
"routing_qualityFlood": "Slišano preko poplave",
"routing_qualityUntested": "Ne preizkušen",
"routing_lastWorked": "delal/a {when}",
"routing_neverWorked": "nikoli ni bilo potrjeno",
"routing_floodDelivery": "Dostava zaradi poplave",
"pathEditor_title": "Izgradnja poti",
"pathEditor_hopCounter": "{count} od 64 različnih sort hropa",
"pathEditor_noHops": "Še niso dodani hmelji. Za dodajanje hmelja v vrstnem redu kliknite na povezavo spodaj, ali pa shranite brez dodanega hmelja, da ga lahko posredujete neposredno.",
"pathEditor_addHops": "Dodajte suho travo v skladu s postopkom.",
"pathEditor_searchRepeaters": "Iskanje ponovitev",
"pathEditor_advancedHex": "Napredno: surovi šestnajstni pot",
"pathEditor_hexLabel": "Predfiks za heksadecimalno šifro",
"pathEditor_hexHelper": "Dva šestbitna znaka na vsak skok, ločena z vejico",
"pathEditor_invalidTokens": "Neveljaven: {tokens}",
"pathEditor_tooManyHops": "Največ 64 hopov",
"pathEditor_usePath": "Uporabite to poto",
"pathEditor_removeHop": "Odstranite hmelj",
"pathEditor_unknownHop": "Neznani ponovitelj",
"map_zoomIn": "Povečaj",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Povečajte pogled",
"map_centerMap": "Krajšarska karta",
"chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS potrdilo",
"pathTrace_legendInferred": "Izpeljana lokacija",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"scanner_bluetoothWebUnsupported": "Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.",
"map_searchHint": "Iščite ime ali ID vozlišča",
"map_online": "V omrežju",
"map_activity": "Dejavnost",
"map_recent": "Nedavni",
"map_stale": "Zastarelo",
"map_visible": "Vidno",
"map_hidden": "Skrit",
"map_centerOnNode": "Centriraj na vozlišče",
"map_details": "Podrobnosti",
"map_noGps": "Brez GPS",
"map_noResults": "Ni ujemajočih se vozlišč",
"pathMap_viewSingle": "Posamično",
"pathMap_viewCombined": "Skupno",
"pathMap_play": "Predvajaj",
"pathMap_pause": "Premor",
"pathMap_replay": "Ponovitev",
"pathMap_stepBack": "Prejšnji skok",
"pathMap_stepForward": "Naslednji skok",
"pathMap_animationOn": "Prikaži animacijo paketa",
"pathMap_animationOff": "Skrij animacijo paketa",
"pathMap_hopOf": "Skok {current} od {total}",
"pathMap_observedPaths": "Opazovane poti: {count}",
"pathMap_primary": "Primarna",
"pathMap_alternate": "Alternativa {index}",
"pathMap_legendShared": "Deljen segment",
"pathMap_legendEstimated": "Ocenjen segment",
"pathMap_sharedNodeCount": "Uporablja {count} poti",
"pathMap_partialAnimation": "{count, plural, =1{1 skok nima lokacije — prikazana pot je delna} =2{2 skoka nimata lokacije — prikazana pot je delna} few{{count} skoki nimajo lokacije — prikazana pot je delna} other{{count} skokov nima lokacije — prikazana pot je delna}}",
"pathMap_hopCount": "{count, plural, =1{1 skok} =2{2 skoka} few{{count} skoki} other{{count} skokov}}",
"pathMap_showAllPaths": "Pokaži vse",
"pathMap_hidePath": "Skrij pot",
"pathMap_showPath": "Pokaži pot",
"pathMap_collapsePanel": "Strni ploščo",
"pathMap_expandPanel": "Razširi ploščo",
"pathMap_noLocation": "Brez lokacije",
"pathMap_followPacket": "Zakleni pogled na paket",
"pathMap_unfollowPacket": "Odkleni pogled od paketa",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regioni",
"settings_regionSettingsSubtitle": "Upravljanje opisanih regionov",
"settings_regionManagement_screenTitle": "Regijonsko upravljanje",
"settings_regionNameHint": "Naslov občine",
"settings_regionAddRegion": "Dodaj region",
"settings_regionFetchRegions": "Pobražujte izvajne področja iz ponavljalnikov",
"settings_regionFetchRegionsFail": "Nižnih občin ni izkrali",
"settings_regionFetchRegionsAlreadyExists": "Taj oblasi je več pomanjkan.",
"settings_regionName": "Naziv Regiona",
"settings_regionDeleted": "Oblikačena regija",
"settings_deleteRegion": "Prihoda Region",
"settings_deleteRegionConfirm": "Ostani {region} iz listy regionov?",
"settings_infoHardware": "Hardver",
"settings_infoFirmware": "Pravilnik",
"repeater_pathHashModeOption0": "0 - 1 bajt",
"repeater_pathHashModeOption1": "1 - 2 bite",
"repeater_pathHashModeOption2": "2 - 3 bite",
"repeater_pathHashModeOption3": "3 - 4 bite",
"appSettings_batteryLipoHv": "LiPo HV (3,04,35 V)",
"channels_regionSetTo": "Osebno: {region}",
"channels_regionNotSet": "Region: brak",
"channels_regionSelect_Title": "Vablajte region",
"channels_clearRegion": "Jasna regija",
"chat_sendImage": "Sločaji slik",
"chat_imagePickFailed": "Nispeval sem tega ta slik",
"chat_receivedGif": "Otrvljen GIF",
"repeater_keySettings": "Zmeničke ključe identitete",
"repeater_keySettingsSubtitle": "Zamenjajte publican ali privatni ključni par",
"repeater_prvKey": "Privarna ključa",
"repeater_prvKeyHelper": "Nova privatna ključa za prekaznik, 128- znakovni šestnastik.",
"repeater_generatePrvKey": "Sporočna ključna para",
"repeater_stopGeneratingPrvKey": "Prekajanje odkrivanja ključne paro",
"repeater_pubKey": "Public key (publična ključa)",
"repeater_pubKeyHelper": "Taj je publicna ključ, ki pomeni pripovedno privatno ključ. Ne možeš to vrediti direktno.",
"repeater_pubKeyPrefix": "Poželana prefiks",
"repeater_pubKeyPrefixHelper": "Najdi publicen ključ, ki začne s tih hexnih čitov. Očekivani pokusi: {tries}.",
"imageMessages_enableTitle": "Pozivni izkričile slike",
"imageMessages_enableSubtitle": "Prešli vzorce pre mrežo. Potrebno je jednokratno stworiti model vzorca.",
"imageMessages_modelSectionTitle": "Obrazni model",
"imageMessages_downloadModel": "Storba",
"imageMessages_cancelDownload": "Odločite",
"imageMessages_removeModel": "Ubrani model",
"imageMessages_modelReady": "Gotov",
"imageMessages_modelNotPublished": "Jo še niso izdana — ta izdelka te ne može prebaviti.",
"imageMessages_downloadFailed": "Obrazni model ne može biti preuzet.",
"imageMessages_autoProcessTitle": "Avtomatizirane slike",
"imageMessages_autoProcessSubtitle": "Katero je oblikovanje svega oblikov, ko se pošteva. Za drugo okolo časa se koristi ok. 2 GB pamične prostore; odločite se za oblikovanje s tukom.",
"imageSend_title": "Sločaji slik",
"imageSend_cropNote": "Zadovoljno izmenjeno na 512 × 512 · vzorovni odnos ne ohranjen",
"imageSend_originalSize": "Original text:\n\nThe early bird catches the worm.\n\nTranslation:\n\nPojdi prvi, zlati v kroku.",
"imageSend_onAirSize": "Na vodi",
"imageSend_quality": "Kakovost",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Vysoko",
"imageSend_packetsLabel": "Pakete",
"imageSend_airtimeLabel": "Čas v eteru",
"imageSend_sizeLabel": "Napravo",
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paketov}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Naslušniki radio neznane",
"imageSend_radioUnknownBody": "Poučajte se na stroj, da se lahko izračunuje čas v prikazu.",
"imageSend_longSendTitle": "Dluga preposlana",
"imageSend_longSendBody": "Ovo bo ohranilo kanal prihodno za obliko {duration}.",
"imageSend_floodNote": "Povlatni potok: sve repeaterje v območju pretrasirajo svak paket, pa kanál ostane zanudnejši od tega.",
"imageSend_parityTitle": "Pobavni paket zdržbe",
"imageSend_paritySubtitle": "Jedna dodatna paczka. Grupni izjasi ne izvajajo potvarnosti, pa ta izdelka omogoča preuzimanju ponovno izgleda, če je zgubjen pojedni paket.",
"imageSend_send": "Posyłaj",
"imageSend_cancel": "Odločite",
"imageSend_encodeFailed": "Ovo slikovo ne more biti zakodirano.",
"imageSend_codecDownloading": "Obrazni model še pospeva preborjanje.",
"imageSend_codecUnavailable": "Poslanje slike nema na tem zelo naročnem naročnem.",
"imageSend_codecDisabled": "Obrazi s pomanjkanjem so v nastavnih.",
"imageSend_deviceUnsupported": "Taj radio ne može posredovati slike pakuete. Prepoštijo izbirni napravnik s kompanijskimi programi na čevem 13 ali novšejšim.",
"imageSend_directMessagesUnsupported": "Obrazy se pojavijo kot skupnih podatkov, pa lahko ih poslujejo samo na kanal — ne v prilogih.",
"imageSend_tooLarge": "Taj slik je zakodiran v viših pakotnih od športne formate, ki omogoča mrežni format.",
"imageSend_sentConfirmation": "Slika poslana kot {count} {count, plural, =1{paket} other{paketov}}.",
"imageSend_sendFailed": "Obraz ne može biti poslany: {error}",
"imageSend_sendingProgress": "Prekazivanje izkaza — paket {sent} od {total}",
"receivedImage_senderPrefix": "Vrtitev {prefix}",
"receivedImage_incoming": "Otrteno od {received} na {total} poslovanih potokov",
"receivedImage_queued": "Čekanje, da se dekodira",
"receivedImage_tapToDecode": "Kliknite, da razkrivete",
"receivedImage_decoding": "Ostvarjanje… o prihodnosti za okolo 1 s",
"receivedImage_incomplete": "Obraz nije kompletan — {received} od {total} poslaničev je prishodil",
"receivedImage_corrupt": "Obraz ne more lahko rekonstruirati",
"receivedImage_decoderMissing": "Otrgena slika — dekodirana slika je izključena",
"receivedImage_evicted": "Obraz ni več niso zakajiren",
"receivedImage_retry": "Pokušajte znova",
"receivedImage_decodeAgain": "Dekodiraj ponovno",
"receivedImage_openSettings": "Nastavite",
"receivedImage_tapToProcess": "Kliknij za obravnavanje",
"receivedImage_awaiting": "{bytes} bajtov · {packets} {packets, plural, =1{paket} other{paketov}}",
"imageSend_secondsValue": "{seconds} s",
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s"
} }
+517 -2
View File
@@ -33,6 +33,8 @@
"common_remove": "Ta bort", "common_remove": "Ta bort",
"common_enable": "Aktivera", "common_enable": "Aktivera",
"common_disable": "Inaktivera", "common_disable": "Inaktivera",
"common_autoRefresh": "Automatisk uppdatering",
"common_interval": "Intervall",
"common_reboot": "Start om", "common_reboot": "Start om",
"common_loading": "Laddar...", "common_loading": "Laddar...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -98,6 +100,8 @@
"settings_locationInvalid": "Ogiltig latitud eller longitud.", "settings_locationInvalid": "Ogiltig latitud eller longitud.",
"settings_latitude": "Latitud", "settings_latitude": "Latitud",
"settings_longitude": "Längdgrad", "settings_longitude": "Längdgrad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatisk zero-hop-annons vid GPS-uppdatering",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).",
"settings_privacyMode": "Privatläge", "settings_privacyMode": "Privatläge",
"settings_privacyModeSubtitle": "Dölj namn/plats i annonser", "settings_privacyModeSubtitle": "Dölj namn/plats i annonser",
"settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.",
@@ -238,6 +242,19 @@
"appSettings_last6Hours": "De senaste 6 timmarna", "appSettings_last6Hours": "De senaste 6 timmarna",
"appSettings_last24Hours": "De senaste 24 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna",
"appSettings_lastWeek": "Förra veckan", "appSettings_lastWeek": "Förra veckan",
"appSettings_rasterTileSource": "Källa för rasterplattor",
"appSettings_stadiaEndpoint": "Stadia-slutpunkt",
"appSettings_stadiaApiKey": "Stadia API-nyckel",
"appSettings_stadiaApiKeyRequired": "Krävs för att använda Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Konfigurerad: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.",
"appSettings_offlineMapCache": "Offline Kartcache", "appSettings_offlineMapCache": "Offline Kartcache",
"appSettings_noAreaSelected": "Ingen area markerad", "appSettings_noAreaSelected": "Ingen area markerad",
"appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})",
@@ -767,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1105,7 +1172,7 @@
"repeater_advancedSettings": "Avancerad", "repeater_advancedSettings": "Avancerad",
"repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare", "repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare",
"repeater_pathHashMode": "Hash-läge för sökväg", "repeater_pathHashMode": "Hash-läge för sökväg",
"repeater_pathHashModeHelper": "Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar endast en gång när nätverket är aktiverat (från och med version 1.14).", "repeater_pathHashModeHelper": "Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.",
"repeater_txDelay": "Försening i Flood TX", "repeater_txDelay": "Försening i Flood TX",
"repeater_txDelayHelper": "Återöverföringsintervall för trafik under perioder med hög belastning, som en multiplikator av paketets överföringstid (0-2, standard 0,5). Högre värde = färre kollisioner, men långsammare leverans.", "repeater_txDelayHelper": "Återöverföringsintervall för trafik under perioder med hög belastning, som en multiplikator av paketets överföringstid (0-2, standard 0,5). Högre värde = färre kollisioner, men långsammare leverans.",
"repeater_directTxDelay": "Direkt TX-fördröjning", "repeater_directTxDelay": "Direkt TX-fördröjning",
@@ -1280,6 +1347,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Digital ingång",
"telemetry_digitalOutputLabel": "Digital utgång",
"telemetry_analogInputLabel": "Analog ingång",
"telemetry_analogOutputLabel": "Analog utgång",
"telemetry_genericLabel": "Allmän sensor",
"telemetry_luminosityLabel": "Ljusstyrka",
"telemetry_presenceLabel": "Närvaro",
"telemetry_humidityLabel": "Luftfuktighet",
"telemetry_accelerometerLabel": "Accelerometer",
"telemetry_pressureLabel": "Tryck",
"telemetry_altitudeLabel": "Höjd",
"telemetry_frequencyLabel": "Frekvens",
"telemetry_percentageLabel": "Procent",
"telemetry_concentrationLabel": "Koncentration",
"telemetry_powerLabel": "Effekt",
"telemetry_distanceLabel": "Avstånd",
"telemetry_energyLabel": "Energi",
"telemetry_directionLabel": "Riktning",
"telemetry_timeLabel": "Tid",
"telemetry_gyrometerLabel": "Gyrometer",
"telemetry_colourLabel": "Färg",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Brytare",
"telemetry_polylineLabel": "Polylinje",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Antal förfrågningar",
"telemetry_error": "Det gick inte att hämta data",
"telemetry_noData": "Inga telemetridata tillgängliga.", "telemetry_noData": "Inga telemetridata tillgängliga.",
"telemetry_channelTitle": "Kanal {channel}", "telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2100,6 +2204,9 @@
"translation_title": "Översättning", "translation_title": "Översättning",
"translation_composerTitle": "Översätt innan du skickar", "translation_composerTitle": "Översätt innan du skickar",
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.", "translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
"translation_autoIncomingTitle": "Översätt meddelanden automatiskt",
"translation_autoIncomingSubtitle": "Översätter meddelanden automatiskt för aviseringar och för chattar eller kanaler.",
"translation_translateMessage": "Översätt meddelande",
"translation_targetLanguage": "Målmedvetet språk", "translation_targetLanguage": "Målmedvetet språk",
"translation_useAppLanguage": "Använd appens språk", "translation_useAppLanguage": "Använd appens språk",
"translation_downloadedModelLabel": "Nedladdad modell", "translation_downloadedModelLabel": "Nedladdad modell",
@@ -2312,5 +2419,413 @@
"chat_newMessages": "Nya meddelanden", "chat_newMessages": "Nya meddelanden",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata", "settings_companionDebugLogSubtitle": "BLE/TCP/USB-kommandon, svar och rådata",
"repeater_chanUtil": "Användning av kanal", "repeater_chanUtil": "Användning av kanal",
"contact_connectCompanion": "Anslut till en sällskapstjänst för att komma åt upprepning och rumsserverfunktioner." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "Sen",
"messageStatus_delivered": "Levererad",
"common_undo": "Ångra",
"messageStatus_pending": "Skicka",
"messageStatus_failed": "Misslyckades med att skicka",
"messageStatus_repeated": "Hördes upprepade gånger",
"contacts_moreOptions": "Fler alternativ",
"contacts_searchOpen": "Sök efter kontakter",
"contacts_searchClose": "Avancerad sökning",
"routing_title": "Ruttplanering",
"routing_modeAuto": "Bil",
"routing_modeFlood": "Översvämning",
"routing_modeManual": "Instruktioner",
"routing_modeAutoHint": "Väljer automatiskt den bästa kända vägen, och använder en \"flooding\"-strategi om ingen väg är känd.",
"routing_modeFloodHint": "Sändningar via alla repetrar. Det mest pålitliga alternativet, men kräver mer sändtid.",
"routing_modeManualHint": "Skickar alltid den exakta väg du har angivit.",
"routing_currentRoute": "Nuvarande rutt",
"routing_directNoHops": "Direkt utan mellanliggande routrar",
"routing_noPathYet": "Ingen väg hittad ännu. Nästa meddelande skickas tills en rutt har upptäckts.",
"routing_floodBroadcast": "Sändas via alla repetrar",
"routing_editPath": "Redigera sökväg",
"routing_forgetPath": "Glöm vägen",
"routing_knownPaths": "Kända vägar",
"routing_knownPathsHint": "Välj en väg för att byta till den.",
"routing_inUse": "I användning",
"routing_qualityStrong": "En stark start",
"routing_qualityGood": "Bra första steg",
"routing_qualityFair": "Bra första hopp",
"routing_qualityWorked": "Har levererat",
"routing_qualityFlood": "Fått information via nyhetsflöde",
"routing_qualityUntested": "Ej testat",
"routing_lastWorked": "arbetade {when}",
"routing_neverWorked": "aldrig bekräftat",
"routing_floodDelivery": "Leverans vid översvämningsområde",
"pathEditor_title": "Skapa väg",
"pathEditor_hopCounter": "{count} av 64 humlor",
"pathEditor_noHops": "Inga humle än. Använd knapparna nedan för att lägga till dem i rätt ordning, eller spara utan humle för att skicka direkt.",
"pathEditor_addHops": "Tillsätt humlen i rätt ordning.",
"pathEditor_searchRepeaters": "Sök efter återupptagna samtal",
"pathEditor_advancedHex": "Avancerat: rå hex-sökväg",
"pathEditor_hexLabel": "Hex-prefikser",
"pathEditor_hexHelper": "Två hex-tecken per steg, separerade med kommatecken.",
"pathEditor_invalidTokens": "Ogiltigt: {tokens}",
"pathEditor_tooManyHops": "Maximalt 64 humlörter",
"pathEditor_usePath": "Använd denna väg",
"pathEditor_removeHop": "Ta bort humlen",
"pathEditor_unknownHop": "Okänd förstärkare",
"map_zoomIn": "Zooma in",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"map_zoomOut": "Zooma ut",
"map_centerMap": "Kartöversikt",
"chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS-verifierat",
"pathTrace_legendInferred": "Antagen position",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_online": "Online",
"scanner_bluetoothWebUnsupported": "Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.",
"map_activity": "Aktivitet",
"map_searchHint": "Sök efter nodens namn eller ID",
"map_recent": "Nyligen",
"map_stale": "Inaktuell",
"map_visible": "Synlig",
"map_hidden": "Dold",
"map_centerOnNode": "Centrera på nod",
"map_details": "Detaljer",
"map_noGps": "Ingen GPS",
"map_noResults": "Inga matchande noder",
"pathMap_viewSingle": "Enkel",
"pathMap_viewCombined": "Kombinerat",
"pathMap_play": "Spela",
"pathMap_pause": "Pausa",
"pathMap_replay": "Återspela",
"pathMap_stepBack": "Föregående hopp",
"pathMap_stepForward": "Nästa hopp",
"pathMap_animationOn": "Visa paketanimering",
"pathMap_animationOff": "Dölj paketanimering",
"pathMap_hopOf": "Hopp {current} av {total}",
"pathMap_observedPaths": "Observerade vägar: {count}",
"pathMap_primary": "Primär",
"pathMap_alternate": "Alternativ {index}",
"pathMap_hopCount": "{count, plural, =1{1 hopp} other{{count} hopp}}",
"pathMap_legendShared": "Delat segment",
"pathMap_legendEstimated": "Uppskattat segment",
"pathMap_sharedNodeCount": "Används av {count} vägar",
"pathMap_partialAnimation": "{count, plural, =1{1 hopp saknar position — den visade vägen är ofullständig} other{{count} hopp saknar position — den visade vägen är ofullständig}}",
"pathMap_showAllPaths": "Visa allt",
"pathMap_hidePath": "Dölj väg",
"pathMap_showPath": "Visa väg",
"pathMap_collapsePanel": "Fäll ihop panel",
"pathMap_expandPanel": "Expandera panel",
"pathMap_noLocation": "Ingen position",
"pathMap_unfollowPacket": "Lås upp vy från paket",
"pathMap_followPacket": "Lås vy till paket",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regioner",
"settings_regionSettingsSubtitle": "Hämtade regioner",
"settings_regionManagement_screenTitle": "Regionförvaltning",
"settings_regionNameHint": "Enter regionnamn",
"settings_regionAddRegion": "Lag till region",
"settings_regionFetchRegions": "Hämtar regioner från repeater",
"settings_regionFetchRegionsFail": "Ingen region hittades",
"settings_regionFetchRegionsAlreadyExists": "Denna region har redan lagt till",
"settings_regionName": "Regionnavn",
"settings_regionDeleted": "Regionens kallt offentlig",
"settings_deleteRegion": "Förhålla regio",
"settings_deleteRegionConfirm": "Fjärna \"{region}\" ur regionlistan?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Föredata",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 byte",
"repeater_pathHashModeOption2": "2 - 3 byte",
"repeater_pathHashModeOption3": "3 - 4 byte",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: ingen",
"channels_regionSelect_Title": "Välj en region",
"channels_clearRegion": "klar region",
"chat_sendImage": "Send vagnbild",
"chat_imagePickFailed": "Kunde inte öppna det bildet",
"chat_receivedGif": "Mottagit en GIF",
"repeater_keySettings": "Ändra identitetsnyckel",
"repeater_keySettingsSubtitle": "Vänd nya offentliga/privata nyckelpar",
"repeater_prvKey": "Privat nyckel",
"repeater_prvKeyHelper": "En ny privat nyckel för repeatern, en hexsträng med 128 tecken.",
"repeater_generatePrvKey": "Generera en kära nyckelpaar",
"repeater_stopGeneratingPrvKey": "Avbröka sökande efter nyckelpa",
"repeater_pubKey": "Öffentlig nyckel",
"repeater_pubKeyHelper": "Detta är den offentliga nyckeln som tillhör genererad privaten nyckel. Du kan inte setta den direkt.",
"repeater_pubKeyPrefix": "Önskad prefix",
"repeater_pubKeyPrefixHelper": "Hitta en offentlig nyckel som börjar med dessa hexadecimala siffror. Förväntad antal försök: {tries}.",
"imageMessages_enableTitle": "Bildermeddelanden",
"imageMessages_enableSubtitle": "Send bilder över mönstret. Det kräver en enkelt bildmodellhämtning.",
"imageMessages_modelSectionTitle": "Bildmodell",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Avbud",
"imageMessages_removeModel": "Förhåll dig från modellen",
"imageMessages_modelReady": "Färdig",
"imageMessages_modelNotPublished": "Ingen publikerad än — denna byggnads kan inte laddas ner av det.",
"imageMessages_downloadFailed": "Bildmodellen kunde inte laddas ner.",
"imageMessages_autoProcessTitle": "Automatiska bilder automatiserar",
"imageMessages_autoProcessSubtitle": "Rekonstruera varje bild så snart den ankommer. Använder cirka 2 GB minne varje gång; låt det omför att rekonstruera genom ett tap.",
"imageSend_title": "Send vagnbild",
"imageSend_cropNote": "Reserverat till 512 × 512 · förhållandet mellan storlek inte bevarat",
"imageSend_originalSize": "Ovanstående text (engelska)\n\nTranslation (svenskt):",
"imageSend_onAirSize": "Underför hjärnan",
"imageSend_quality": "Kvalitet",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Hög",
"imageSend_packetsLabel": "Paket",
"imageSend_airtimeLabel": "Tid i övrighet",
"imageSend_sizeLabel": "Ladning",
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paket}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radioindikatorer inte kända",
"imageSend_radioUnknownBody": "Förkoppna till ett enhet så kan tiden på air beräknas.",
"imageSend_longSendTitle": "Långförsending",
"imageSend_longSendBody": "Detta kommer att bevara kanalen roughly {duration}.",
"imageSend_floodNote": "Flodledning: varje repeater i ramen retransmitterar varje paket, så kan kanalen vara på arbete långare än den här tiden.",
"imageSend_parityTitle": "Omedlningspaket",
"imageSend_paritySubtitle": "En ytterligare paket. Gruppmeddelanden inte akceras, så den här tar hänsyn till att mottar kan återuppbygga bilden om ett enkelt paket förlorats.",
"imageSend_send": "Sänd",
"imageSend_cancel": "Avbud",
"imageSend_encodeFailed": "Denna bild kan inte kodas.",
"imageSend_codecDownloading": "Bildmodellen downloads fortfarande.",
"imageSend_codecUnavailable": "Bildsendning är inte tillgänglig på detta enhet.",
"imageSend_codecDisabled": "Bildmeddelanden är offna i inställningarna.",
"imageSend_deviceUnsupported": "Denna radi kan inte senda bildpaket. Använd ett äventyrsgerät med companionsystemfirmware 13 eller nyare.",
"imageSend_directMessagesUnsupported": "Bilder senders som gruppod, så de kan endast skickas till ett kanal inte i en direkt meddelning.",
"imageSend_tooLarge": "Bilden kodades till mer paket än meshformaten tillåter.",
"imageSend_sentConfirmation": "Bilden skickades som {count} {count, plural, =1{paket} other{paket}}.",
"imageSend_sendFailed": "Bilden kunde inte skickas: {error}",
"imageSend_sendingProgress": "Bild sendning — paket {sent} av {total}",
"receivedImage_senderPrefix": "Nod {prefix}",
"receivedImage_incoming": "VANTA {received} av {total} paket",
"receivedImage_queued": "Att väta att decodera",
"receivedImage_tapToDecode": "Tap för att decodera",
"receivedImage_decoding": "Rekonstruktion… om 1 sekund",
"receivedImage_incomplete": "Bilden är okomplett — {received} av {total} paket har ankomit",
"receivedImage_corrupt": "Bilden kunde inte rekonstrueras",
"receivedImage_decoderMissing": "Bild mottagen — bilddekoderingen är avbrott",
"receivedImage_evicted": "Bild inte lagrad längre",
"receivedImage_retry": "Omförs igen",
"receivedImage_decodeAgain": "Kodera igen",
"receivedImage_openSettings": "Skapa en uppställning",
"receivedImage_tapToProcess": "Tap för att behandla",
"receivedImage_awaiting": "{bytes} byte · {packets} {packets, plural, =1{paket} other{paket}}",
"imageSend_secondsValue": "{seconds} sekunder",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+517 -2
View File
@@ -34,6 +34,8 @@
"common_remove": "Прибрати", "common_remove": "Прибрати",
"common_enable": "Увімкнути", "common_enable": "Увімкнути",
"common_disable": "Вимкнути", "common_disable": "Вимкнути",
"common_autoRefresh": "Автооновлення",
"common_interval": "Інтервал",
"common_reboot": "Перезавантажити", "common_reboot": "Перезавантажити",
"common_loading": "Завантаження...", "common_loading": "Завантаження...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -99,6 +101,8 @@
"settings_locationInvalid": "Некоректна широта або довгота.", "settings_locationInvalid": "Некоректна широта або довгота.",
"settings_latitude": "Широта", "settings_latitude": "Широта",
"settings_longitude": "Довгота", "settings_longitude": "Довгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).",
"settings_privacyMode": "Режим приватності", "settings_privacyMode": "Режим приватності",
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.", "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
@@ -240,6 +244,19 @@
"appSettings_last6Hours": "Останні 6 годин", "appSettings_last6Hours": "Останні 6 годин",
"appSettings_last24Hours": "Останні 24 години", "appSettings_last24Hours": "Останні 24 години",
"appSettings_lastWeek": "Минулий тиждень", "appSettings_lastWeek": "Минулий тиждень",
"appSettings_rasterTileSource": "Джерело растрових тайлів",
"appSettings_stadiaEndpoint": "Кінцева точка Stadia",
"appSettings_stadiaApiKey": "Ключ API Stadia",
"appSettings_stadiaApiKeyRequired": "Потрібно для використання Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Налаштовано: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.",
"appSettings_offlineMapCache": "Офлайн-кеш карти", "appSettings_offlineMapCache": "Офлайн-кеш карти",
"appSettings_noAreaSelected": "Область не вибрано", "appSettings_noAreaSelected": "Область не вибрано",
"appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})",
@@ -777,6 +794,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1115,7 +1182,7 @@
"repeater_advancedSettings": "Просунутий", "repeater_advancedSettings": "Просунутий",
"repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів", "repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів",
"repeater_pathHashMode": "Режим хешування шляху", "repeater_pathHashMode": "Режим хешування шляху",
"repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.", "repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.",
"repeater_txDelay": "Затримка у Flood, штат Техас", "repeater_txDelay": "Затримка у Flood, штат Техас",
"repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.", "repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.",
"repeater_directTxDelay": "Пряме затримка TX", "repeater_directTxDelay": "Пряме затримка TX",
@@ -1291,6 +1358,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "Цифровий вхід",
"telemetry_digitalOutputLabel": "Цифровий вихід",
"telemetry_analogInputLabel": "Аналоговий вхід",
"telemetry_analogOutputLabel": "Аналоговий вихід",
"telemetry_genericLabel": "Загальний датчик",
"telemetry_luminosityLabel": "Освітленість",
"telemetry_presenceLabel": "Присутність",
"telemetry_humidityLabel": "Вологість",
"telemetry_accelerometerLabel": "Акселерометр",
"telemetry_pressureLabel": "Тиск",
"telemetry_altitudeLabel": "Висота",
"telemetry_frequencyLabel": "Частота",
"telemetry_percentageLabel": "Відсоток",
"telemetry_concentrationLabel": "Концентрація",
"telemetry_powerLabel": "Потужність",
"telemetry_distanceLabel": "Відстань",
"telemetry_energyLabel": "Енергія",
"telemetry_directionLabel": "Напрямок",
"telemetry_timeLabel": "Час",
"telemetry_gyrometerLabel": "Гірометр",
"telemetry_colourLabel": "Колір",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "Перемикач",
"telemetry_polylineLabel": "Полілінія",
"telemetry_altitudeValue": "{meters} м",
"telemetry_frequencyValue": "{hertz} Гц",
"telemetry_pressureValue": "{hpa} гПа",
"telemetry_luminosityValue": "{lux} лк",
"telemetry_powerValue": "{watts} Вт",
"telemetry_distanceValue": "{meters} м",
"telemetry_energyValue": "{kilowattHours} кВт⋅год",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "Кількість запитів",
"telemetry_error": "Не вдалося отримати дані",
"telemetry_noData": "Дані телеметрії недоступні.", "telemetry_noData": "Дані телеметрії недоступні.",
"telemetry_channelTitle": "Канал {channel}", "telemetry_channelTitle": "Канал {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2109,6 +2213,9 @@
"translation_enableTitle": "Увімкнути переклад", "translation_enableTitle": "Увімкнути переклад",
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.", "translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.", "translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
"translation_autoIncomingTitle": "Автоматично перекладати повідомлення",
"translation_autoIncomingSubtitle": "Автоматично перекладає повідомлення для сповіщень, а також для чатів і каналів.",
"translation_translateMessage": "Перекласти повідомлення",
"translation_targetLanguage": "Цільова мова", "translation_targetLanguage": "Цільова мова",
"translation_useAppLanguage": "Використовувати мову застосунку", "translation_useAppLanguage": "Використовувати мову застосунку",
"translation_downloadedModelLabel": "Завантажений шаблон", "translation_downloadedModelLabel": "Завантажений шаблон",
@@ -2292,5 +2399,413 @@
"chat_newMessages": "Нові повідомлення", "chat_newMessages": "Нові повідомлення",
"chat_markAsUnread": "Позначити як непрочитане", "chat_markAsUnread": "Позначити як непрочитане",
"repeater_chanUtil": "Використання каналу", "repeater_chanUtil": "Використання каналу",
"contact_connectCompanion": "Підключіться до супутнього пристрою, щоб отримати доступ до функцій ретранслятора та сервера кімнат." "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_delivered": "Доставлено",
"messageStatus_sent": "Надіслано",
"common_undo": "Скасувати",
"messageStatus_pending": "Надсилання",
"messageStatus_failed": "Не вдалося надіслати",
"messageStatus_repeated": "Почув неодноразово",
"contacts_moreOptions": "Більше можливостей",
"contacts_searchOpen": "Пошук контактів",
"contacts_searchClose": "Закрити пошук",
"routing_title": "Маршрутизація",
"routing_modeAuto": "Автомобіль",
"routing_modeFlood": "Повені",
"routing_modeManual": "Інструкція",
"routing_modeAutoHint": "Автоматично обирає найкращий відомий шлях, та у разі відсутності відомого шляху, використовує алгоритм \"занурення\".",
"routing_modeFloodHint": "Передавання через усі ретранслятори. Найбільш надійний спосіб, але потребує більше часу.",
"routing_modeManualHint": "Завжди доставляє точно за вказаним вами маршрутом.",
"routing_currentRoute": "Поточний маршрут",
"routing_directNoHops": "Пряме з'єднання – без проміжних ретрансляторів",
"routing_noPathYet": "Поки що немає жодного шляху. Повідомлення продовжуються надходити, поки не буде знайдено маршрут.",
"routing_floodBroadcast": "Поширення через усі ретранслятори",
"routing_editPath": "Редагувати шлях",
"routing_forgetPath": "Забудь про шлях",
"routing_knownPaths": "Відомі маршрути",
"routing_knownPathsHint": "Виберіть опцію, щоб переключитися на неї.",
"routing_inUse": "У робочому стані",
"routing_qualityStrong": "Сильний перший стрибок",
"routing_qualityGood": "Чудова перша спроба",
"routing_qualityFair": "Перший, але вдалий, крок",
"routing_qualityWorked": "Доставлено",
"routing_qualityFlood": "Дізнався через новини",
"routing_qualityUntested": "Не протестовано",
"routing_neverWorked": "ніколи не підтверджено",
"routing_floodDelivery": "Доставка під час повені",
"pathEditor_title": "Створити маршрут",
"pathEditor_hopCounter": "{count} з 64 штук хмелю",
"pathEditor_noHops": "Ще не додано хміль. Натисніть на відповідні кнопки, щоб додати його в потрібному порядку, або збережіть рецепт без хмілю, щоб відправити його безпосередньо.",
"pathEditor_addHops": "Додавайте хміль у наступній послідовності.",
"pathEditor_searchRepeaters": "Пошук повторювачів",
"pathEditor_advancedHex": "Просунутий рівень: пряма шлях у форматі шестнадцяткової системи.",
"pathEditor_hexLabel": "Префікси для шестнадцяткової системи числення",
"pathEditor_hexHelper": "Два шестизначні символи на кожний крок, розділені комами",
"pathEditor_invalidTokens": "Неправильно: {tokens}",
"routing_lastWorked": "worked {when}",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_tooManyHops": "Максимум 64 хмелеві колоди",
"pathEditor_usePath": "Використовуйте цей шлях",
"pathEditor_removeHop": "Видалити хміль",
"pathEditor_unknownHop": "Невідомий ретранслятор",
"map_zoomIn": "Увійти в режим збільшення",
"map_zoomOut": "Видалити зум",
"map_centerMap": "Карта центру",
"chrome_bluetoothRequiresChromium": "Web Bluetooth вимагає браузера на основі Chromium",
"channels_communityShortId": "ID: {id}...",
"pathTrace_legendGpsConfirmed": "GPS підтверджено",
"pathTrace_legendInferred": "Висновок щодо положення",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"scanner_bluetoothWebUnsupported": "Bluetooth недоступний у браузері. Підключіться через USB.",
"map_searchHint": "Назва або ID вузла",
"map_activity": "Активність",
"map_online": "Онлайн",
"map_recent": "Нещодавні",
"map_stale": "Застаріло",
"map_visible": "Видимий",
"map_hidden": "Прихований",
"map_centerOnNode": "Центрувати на вузлі",
"map_details": "Деталі",
"map_noGps": "Без GPS",
"map_noResults": "Не знайдено відповідних вузлів",
"pathMap_viewSingle": "Один",
"pathMap_viewCombined": "Об'єднаний",
"pathMap_play": "Відтворити",
"pathMap_pause": "Призупинити",
"pathMap_replay": "Повтор",
"pathMap_stepForward": "Наступний перехід",
"pathMap_stepBack": "Попередній перехід",
"pathMap_animationOn": "Відобразити анімацію пакета",
"pathMap_animationOff": "Приховати анімацію пакета",
"pathMap_hopOf": "Перехід {current} з {total}",
"pathMap_observedPaths": "Зафіксовані маршрути: {count}",
"pathMap_primary": "Основний",
"pathMap_alternate": "Альт. {index}",
"pathMap_hopCount": "{count, plural, =1{1 перехід} few{{count} переходи} many{{count} переходів} other{{count} переходів}}",
"pathMap_legendShared": "Об'єднаний сегмент",
"pathMap_legendEstimated": "Орієнтовний сегмент",
"pathMap_sharedNodeCount": "Використовується {count} шляхами",
"pathMap_partialAnimation": "{count, plural, =1{1 перехід не має геопозиції — показаний шлях є частковим} few{{count} переходи не мають геопозиції — показаний шлях є частковим} many{{count} переходів не мають геопозиції — показаний шлях є частковим} other{{count} переходів не мають геопозиції — показаний шлях є частковим}}",
"pathMap_showAllPaths": "Показати все",
"pathMap_hidePath": "Приховати шлях",
"pathMap_showPath": "Показати шлях",
"pathMap_collapsePanel": "Згорнути панель",
"pathMap_expandPanel": "Розгорнути панель",
"pathMap_noLocation": "Без геопозиції",
"pathMap_followPacket": "Прив'язати вигляд до пакету",
"pathMap_unfollowPacket": "Відв'язати вигляд від пакету",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Ріси",
"settings_regionSettingsSubtitle": "Управляти збережених регіонів",
"settings_regionManagement_screenTitle": "Рégіональний управління",
"settings_regionNameHint": "Назва регіону",
"settings_regionAddRegion": "Добачить регіон",
"settings_regionFetchRegions": "Збираємо регіони з репітерів",
"settings_regionFetchRegionsFail": "Ні відкриті регіони",
"settings_regionFetchRegionsAlreadyExists": "Цей регіон вже було доданий",
"settings_regionName": "Ріонна назва",
"settings_regionDeleted": "Ріон витертений",
"settings_deleteRegion": "Видалення регіону",
"settings_deleteRegionConfirm": "Вирішіть \"{region}\" з списку регіонів?",
"settings_infoHardware": "Оборудовання",
"settings_infoFirmware": "Програмне вміст (фirmware)",
"repeater_pathHashModeOption0": "0 - 1 байт",
"repeater_pathHashModeOption1": "1 - 2 байти",
"repeater_pathHashModeOption2": "2 - 3 байти",
"repeater_pathHashModeOption3": "3 - 4 байта",
"appSettings_batteryLipoHv": "ЛиPo HV (3,0-4,35 В)",
"channels_regionSetTo": "Ріон: {region}",
"channels_regionNotSet": "Ріон: нема",
"channels_regionSelect_Title": "Вybірте регіон",
"channels_clearRegion": "Чистя зона",
"chat_sendImage": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
"chat_imagePickFailed": "Неможливо відкрити це изображення",
"chat_receivedGif": "Підймовано GIF",
"repeater_keySettings": "Зміна ключів ідентифікації",
"repeater_keySettingsSubtitle": "Замініть публічний/приватний ключ",
"repeater_prvKey": "Приватний ключ",
"repeater_prvKeyHelper": "Нова приватна ключ для репітера, 128-символова штучна штука в шестнадцатеричному форматі.",
"repeater_generatePrvKey": "Синхронізujте ключовий пару",
"repeater_stopGeneratingPrvKey": "Прервити шук ключового пару",
"repeater_pubKey": "Публічна ключ",
"repeater_pubKeyHelper": "Це публична ключ, який пов'язано з створеною приватною ключом. Ви не можете встановлювати його напрямую.",
"repeater_pubKeyPrefix": "Високорейтингове прикінання",
"repeater_pubKeyPrefixHelper": "Найдіть відповідний открытий ключ, який починається ціми шестнадцатыми цифрами. Ожидається кількість потрібних попыток: {tries}.",
"imageMessages_enableTitle": "Підтримка зображень для посилання",
"imageMessages_enableSubtitle": "Пошукуєте изображення через сеть. Вимagaєте однократне скачання моделю изображення.",
"imageMessages_modelSectionTitle": "Модель для зображень",
"imageMessages_downloadModel": "Скачання",
"imageMessages_cancelDownload": "Завершити",
"imageMessages_removeModel": "Видалим модель",
"imageMessages_modelReady": "Готово",
"imageMessages_modelNotPublished": "Щоб не було опубліковано — це версія не дозволяє зробити скачання.",
"imageMessages_downloadFailed": "Модель для зображень не можна сказати.",
"imageMessages_autoProcessTitle": "Автоматично оброблювати obraзи",
"imageMessages_autoProcessSubtitle": "Кожна картинка повинні бути відтворена на мій момент прибуття. Використовує приблизно 2 ГБ пам'яті на кожен раз; відключіть від відтворення за допомогою кlikу.",
"imageSend_title": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
"imageSend_cropNote": "Манітовано до 512 × 512 · соотношення варість не зберігається",
"imageSend_originalSize": "Оригінал",
"imageSend_onAirSize": "На ері",
"imageSend_quality": "Качество",
"imageSend_qualityStandard": "Стандарт",
"imageSend_qualityHigh": "Висок",
"imageSend_packetsLabel": "Пакети",
"imageSend_airtimeLabel": "Час на ері",
"imageSend_sizeLabel": "Підлог",
"imageSend_packetsCount": "{count} {count, plural, one{пакет} few{пакети} many{пакетів} other{пакета}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Настройки радио неизвестні",
"imageSend_radioUnknownBody": "Залучіться до пристрою, щоб можна було розрахунювати час в етері.",
"imageSend_longSendTitle": "Длуга передача",
"imageSend_longSendBody": "Це забезпечить канал на approximativno {duration} хвилин.",
"imageSend_floodNote": "Рухування потоків: кожен ретрансатор у діапазоні відправлює кожен пакет, тому канал залишається занятшим довше, ніж це.",
"imageSend_parityTitle": "Пакет відновлення",
"imageSend_paritySubtitle": "Один дополний пакет. Групові посилання не відповідають, тому це дозволяє відвідувачу відтворити зображення, якщо один пакет втрачений.",
"imageSend_send": "Пошутить",
"imageSend_cancel": "Завершити",
"imageSend_encodeFailed": "Це изображення не може бути кодировано.",
"imageSend_codecDownloading": "Модель для відтворення изображень ще збирається.",
"imageSend_codecUnavailable": "Постій для відправлення изображень на цьому пристрої не доступний.",
"imageSend_codecDisabled": "Пictesovani posyly wyключені в настройках.",
"imageSend_deviceUnsupported": "Цей радіопередач не може отправляти пакети зображень. Зконнете пристрої, що працює з обладнанням керемонітом компаньєр 13 або новшим.",
"imageSend_directMessagesUnsupported": "Фотографії передаються як дані групою, тому їх можна відправляти лише на канал — не в прямому посилання.",
"imageSend_tooLarge": "Це изображення кодує більше пакітів, ніж дозволяє формат сети.",
"imageSend_sentConfirmation": "Зображення надіслано як {count} {count, plural, =1{пакет} other{пакетів}}.",
"imageSend_sendFailed": "Изображення не можна відправити: {error}",
"imageSend_sendingProgress": "Пошускаємо изображення — пакет {sent} з {total}",
"receivedImage_senderPrefix": "Нод {prefix}",
"receivedImage_incoming": "Підходяче: {received} з {total} пакітів",
"receivedImage_queued": "Очікування розслівання коду",
"receivedImage_tapToDecode": "Кliknite для декодування",
"receivedImage_decoding": "Повноювання… за прибуття 1 с.",
"receivedImage_incomplete": "Образок не повний — {received} з {total} пакітів прибавлено",
"receivedImage_corrupt": "Obraz не можна відтворити",
"receivedImage_decoderMissing": "Під час приємки — розшифровка зображення не вдалося.",
"receivedImage_evicted": "Изображення не зберігається",
"receivedImage_retry": "Попробуй ще раз",
"receivedImage_decodeAgain": "Декодуй ще раз",
"receivedImage_openSettings": "Заведіть",
"receivedImage_tapToProcess": "Кликніть для обробки",
"receivedImage_awaiting": "{bytes} байтів · {packets} {packets, plural, =1{пакет} other{пакетів}}",
"imageSend_secondsValue": "{seconds} секунди",
"imageSend_minutesSecondsValue": "{minutes} хв {seconds} с"
} }
+517 -2
View File
@@ -34,6 +34,8 @@
"common_remove": "移除", "common_remove": "移除",
"common_enable": "启用", "common_enable": "启用",
"common_disable": "禁用", "common_disable": "禁用",
"common_autoRefresh": "自动刷新",
"common_interval": "间隔",
"common_reboot": "重启", "common_reboot": "重启",
"common_loading": "正在加载...", "common_loading": "正在加载...",
"common_notAvailable": "—", "common_notAvailable": "—",
@@ -103,6 +105,8 @@
"settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。",
"settings_latitude": "纬度", "settings_latitude": "纬度",
"settings_longitude": "经度", "settings_longitude": "经度",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。",
"settings_privacyMode": "隐私模式", "settings_privacyMode": "隐私模式",
"settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置",
"settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。",
@@ -252,6 +256,19 @@
"appSettings_last6Hours": "过去6小时", "appSettings_last6Hours": "过去6小时",
"appSettings_last24Hours": "过去24小时", "appSettings_last24Hours": "过去24小时",
"appSettings_lastWeek": "上周", "appSettings_lastWeek": "上周",
"appSettings_rasterTileSource": "栅格瓦片源",
"appSettings_stadiaEndpoint": "Stadia 端点",
"appSettings_stadiaApiKey": "Stadia API 密钥",
"appSettings_stadiaApiKeyRequired": "使用 Stadia Maps 时必需",
"appSettings_stadiaApiKeyConfigured": "已配置:{maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。",
"appSettings_offlineMapCache": "离线地图缓存", "appSettings_offlineMapCache": "离线地图缓存",
"appSettings_noAreaSelected": "未选择任何区域", "appSettings_noAreaSelected": "未选择任何区域",
"appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom}", "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom}",
@@ -794,6 +811,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1135,7 +1202,7 @@
"repeater_advancedSettings": "高级", "repeater_advancedSettings": "高级",
"repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮", "repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮",
"repeater_pathHashMode": "路径哈希模式", "repeater_pathHashMode": "路径哈希模式",
"repeater_pathHashModeHelper": "用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。", "repeater_pathHashModeHelper": "用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 ),1=2 字节(65K 个 ID,最多 32 ),2=3 字节(16M 个 ID,最多 21 )。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。",
"repeater_txDelay": "洪水(德克萨斯州)延误", "repeater_txDelay": "洪水(德克萨斯州)延误",
"repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。", "repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。",
"repeater_directTxDelay": "直接的 TX 延迟", "repeater_directTxDelay": "直接的 TX 延迟",
@@ -1310,6 +1377,43 @@
} }
} }
}, },
"telemetry_digitalInputLabel": "数字输入",
"telemetry_digitalOutputLabel": "数字输出",
"telemetry_analogInputLabel": "模拟输入",
"telemetry_analogOutputLabel": "模拟输出",
"telemetry_genericLabel": "通用传感器",
"telemetry_luminosityLabel": "照度",
"telemetry_presenceLabel": "存在检测",
"telemetry_humidityLabel": "湿度",
"telemetry_accelerometerLabel": "加速度计",
"telemetry_pressureLabel": "气压",
"telemetry_altitudeLabel": "高度",
"telemetry_frequencyLabel": "频率",
"telemetry_percentageLabel": "百分比",
"telemetry_concentrationLabel": "浓度",
"telemetry_powerLabel": "功率",
"telemetry_distanceLabel": "距离",
"telemetry_energyLabel": "能量",
"telemetry_directionLabel": "方向",
"telemetry_timeLabel": "时间",
"telemetry_gyrometerLabel": "陀螺仪",
"telemetry_colourLabel": "颜色",
"telemetry_gpsLabel": "GPS",
"telemetry_switchLabel": "开关",
"telemetry_polylineLabel": "折线",
"telemetry_altitudeValue": "{meters} m",
"telemetry_frequencyValue": "{hertz} Hz",
"telemetry_pressureValue": "{hpa} hPa",
"telemetry_luminosityValue": "{lux} lx",
"telemetry_powerValue": "{watts} W",
"telemetry_distanceValue": "{meters} m",
"telemetry_energyValue": "{kilowattHours} kWh",
"telemetry_directionValue": "{degrees}°",
"telemetry_concentrationValue": "{ppm} ppm",
"telemetry_percentageValue": "{percent}%",
"telemetry_analogValue": "{value}",
"telemetry_autoFetchQuantity": "请求次数",
"telemetry_error": "无法获取数据",
"telemetry_noData": "暂无遥测数据", "telemetry_noData": "暂无遥测数据",
"telemetry_channelTitle": "频道 {channel}", "telemetry_channelTitle": "频道 {channel}",
"@telemetry_channelTitle": { "@telemetry_channelTitle": {
@@ -2105,6 +2209,9 @@
"translation_composerTitle": "在发送之前进行翻译", "translation_composerTitle": "在发送之前进行翻译",
"translation_enableTitle": "启用翻译功能", "translation_enableTitle": "启用翻译功能",
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。", "translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
"translation_autoIncomingTitle": "自动翻译消息",
"translation_autoIncomingSubtitle": "自动为通知以及聊天或频道翻译消息。",
"translation_translateMessage": "翻译消息",
"translation_targetLanguage": "目标语言", "translation_targetLanguage": "目标语言",
"translation_useAppLanguage": "使用应用程序语言", "translation_useAppLanguage": "使用应用程序语言",
"translation_downloadedModelLabel": "下载的模型", "translation_downloadedModelLabel": "下载的模型",
@@ -2317,5 +2424,413 @@
"chat_newMessages": "新的消息", "chat_newMessages": "新的消息",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据", "settings_companionDebugLogSubtitle": "BLE/TCP/USB 协议、响应和原始数据",
"repeater_chanUtil": "频道利用率", "repeater_chanUtil": "频道利用率",
"contact_connectCompanion": "连接伴机以访问中继器和房间服务器功能。" "@routing_lastWorked": {
"placeholders": {
"when": {
"type": "String"
}
}
},
"@routing_deliveryCounts": {
"placeholders": {
"successes": {
"type": "int"
},
"failures": {
"type": "int"
}
}
},
"@pathEditor_hopCounter": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathEditor_invalidTokens": {
"placeholders": {
"tokens": {
"type": "String"
}
}
},
"@channels_communityShortId": {
"placeholders": {
"id": {
"type": "String"
}
}
},
"messageStatus_sent": "发送",
"common_undo": "撤销",
"messageStatus_delivered": "已送达",
"messageStatus_pending": "发送",
"messageStatus_failed": "发送失败",
"messageStatus_repeated": "多次听到",
"contacts_moreOptions": "更多选择",
"contacts_searchOpen": "搜索联系人",
"contacts_searchClose": "高级搜索",
"routing_title": "路由",
"routing_modeAuto": "汽车",
"routing_modeFlood": "洪水",
"routing_modeManual": "手册",
"routing_modeAutoHint": "自动选择已知最佳路径,当没有已知路径时,则进行“洪水”搜索。",
"routing_modeFloodHint": "通过所有中继站进行广播。 这种方式最可靠,但占用更多的时间。",
"routing_modeManualHint": "总是按照您设置的路径进行导航。",
"routing_currentRoute": "当前路线",
"routing_directNoHops": "直接连接— 无中继跳",
"routing_noPathYet": "目前还没有找到路径。直到找到路径,才会收到后续消息。",
"routing_floodBroadcast": "通过所有中继器进行广播",
"routing_editPath": "编辑路径",
"routing_forgetPath": "忘记原路",
"routing_knownPaths": "已知的路径",
"routing_knownPathsHint": "点击该路径以切换到它。",
"routing_inUse": "使用中",
"routing_qualityStrong": "强劲的初始阶段",
"routing_qualityGood": "不错的开端",
"routing_qualityFair": "第一次尝试,结果良好",
"routing_qualityWorked": "已完成",
"routing_qualityFlood": "通过新闻报道",
"routing_qualityUntested": "未经测试",
"routing_lastWorked": "工作于 {when}",
"routing_neverWorked": "从未得到证实",
"routing_floodDelivery": "洪水配送",
"pathEditor_title": "构建路径",
"pathEditor_noHops": "目前还没有添加任何啤酒花。点击下面的“添加”按钮,按顺序添加,或者直接保存,不添加任何啤酒花。",
"pathEditor_addHops": "按照顺序添加啤酒花",
"pathEditor_searchRepeaters": "重复搜索",
"pathEditor_advancedHex": "高级:原始十六进制路径",
"pathEditor_hexLabel": "十六进制前缀",
"pathEditor_hexHelper": "每次跳跃,使用两个十六进制字符,用逗号分隔。",
"pathEditor_invalidTokens": "无效:{tokens}",
"pathEditor_tooManyHops": "最多 64 个跳跃",
"pathEditor_usePath": "请使用此路径",
"pathEditor_removeHop": "去除啤酒花",
"routing_deliveryCounts": "{successes} delivered, {failures} failed",
"pathEditor_hopCounter": "{count} of 64 hops",
"pathEditor_unknownHop": "未知的重复器",
"map_zoomIn": "放大",
"map_zoomOut": "放大",
"map_centerMap": "中心地图",
"chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器",
"channels_communityShortId": "ID{id}...",
"pathTrace_legendGpsConfirmed": "通过GPS确认",
"pathTrace_legendInferred": "推测的位置",
"@pathMap_hopOf": {
"placeholders": {
"current": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_observedPaths": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_alternate": {
"placeholders": {
"index": {
"type": "int"
}
}
},
"@pathMap_hopCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_gpsCount": {
"placeholders": {
"confirmed": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@pathMap_sharedNodeCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@pathMap_partialAnimation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"map_online": "在线",
"scanner_bluetoothWebUnsupported": "浏览器不支持蓝牙,请改用 USB 连接。",
"map_activity": "活动",
"map_searchHint": "搜索节点名称或ID",
"map_recent": "最近",
"map_visible": "可见",
"map_stale": "过时",
"map_hidden": "已隐藏",
"map_centerOnNode": "以节点为中心",
"map_details": "详细信息",
"map_noGps": "无 GPS",
"map_noResults": "未找到匹配的节点",
"pathMap_viewSingle": "单条",
"pathMap_viewCombined": "综合",
"pathMap_play": "播放",
"pathMap_pause": "暂停",
"pathMap_replay": "重播",
"pathMap_stepBack": "上一跳",
"pathMap_stepForward": "下一跳",
"pathMap_animationOn": "显示数据包动画",
"pathMap_animationOff": "隐藏数据包动画",
"pathMap_hopOf": "第 {current} 跳,共 {total} 跳",
"pathMap_observedPaths": "观测到的路径:{count}",
"pathMap_primary": "主路径",
"pathMap_alternate": "备用 {index}",
"pathMap_hopCount": "{count, plural, =1{1 跳} other{{count} 跳}}",
"pathMap_legendShared": "共享路段",
"pathMap_legendEstimated": "估算路段",
"pathMap_sharedNodeCount": "已被 {count} 条路径使用",
"pathMap_showAllPaths": "显示全部",
"pathMap_hidePath": "隐藏路径",
"pathMap_showPath": "显示路径",
"pathMap_collapsePanel": "收起面板",
"pathMap_expandPanel": "展开面板",
"pathMap_noLocation": "无位置",
"pathMap_followPacket": "锁定视图跟随数据包",
"pathMap_unfollowPacket": "解锁视图跟随",
"pathMap_gpsCount": "{confirmed}/{total} GPS",
"pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "地区",
"settings_regionSettingsSubtitle": "管理已存储的区域",
"settings_regionManagement_screenTitle": "区域管理",
"settings_regionNameHint": "区域名称",
"settings_regionAddRegion": "增加地区",
"settings_regionFetchRegions": "从中继器获取区域",
"settings_regionFetchRegionsFail": "未发现任何地区",
"settings_regionFetchRegionsAlreadyExists": "该地区已被添加",
"settings_regionName": "地区名称",
"settings_regionDeleted": "区域已删除",
"settings_deleteRegion": "删除区域",
"settings_deleteRegionConfirm": "从 {region} 列表中删除?",
"settings_infoHardware": "硬件",
"settings_infoFirmware": "固件",
"repeater_pathHashModeOption0": "0 - 1 字节",
"repeater_pathHashModeOption1": "1 - 2 字节",
"repeater_pathHashModeOption2": "2 - 3 字节",
"repeater_pathHashModeOption3": "3 - 4 字节",
"appSettings_batteryLipoHv": "锂多压电源(3.0-4.35伏)",
"channels_regionSetTo": "地区:{region}",
"channels_regionNotSet": "地区:无",
"channels_regionSelect_Title": "选择一个地区",
"channels_clearRegion": "无碍区域",
"chat_sendImage": "发送图片",
"chat_imagePickFailed": "无法打开该图片",
"chat_receivedGif": "收到一个GIF",
"repeater_keySettings": "更改身份密钥",
"repeater_keySettingsSubtitle": "更换公钥/私钥对",
"repeater_prvKey": "私钥",
"repeater_prvKeyHelper": "中继器的新私钥,为一个128位的十六进制字符串。",
"repeater_generatePrvKey": "生成一个随机的密钥对",
"repeater_stopGeneratingPrvKey": "中断密钥对搜索",
"repeater_pubKey": "公钥",
"repeater_pubKeyHelper": "这是与生成的私钥配对的公钥。您不能直接设置此内容。",
"repeater_pubKeyPrefix": "期望的前缀",
"repeater_pubKeyPrefixHelper": "找到一组以这些十六进制数字开头的公钥。预计尝试次数:{tries}",
"imageMessages_enableTitle": "启用图片消息",
"imageMessages_enableSubtitle": "通过网格发送图像。需要一次性下载图像模型。",
"imageMessages_modelSectionTitle": "图像模型",
"imageMessages_downloadModel": "下载",
"imageMessages_cancelDownload": "取消",
"imageMessages_removeModel": "移除模型",
"imageMessages_modelReady": "准备就绪",
"imageMessages_modelNotPublished": "尚未发布——此版本无法下载该内容。",
"imageMessages_downloadFailed": "图像模型无法下载。",
"imageMessages_autoProcessTitle": "自动处理图像",
"imageMessages_autoProcessSubtitle": "每次图像到达后立即重建,每次需要约2GB的内存;若想关闭重建功能,可通过点击取消开启。",
"imageSend_title": "发送图片",
"imageSend_cropNote": "调整大小至512 × 512 · 宽高比未保留",
"imageSend_originalSize": "原文",
"imageSend_onAirSize": "直播中",
"imageSend_quality": "质量",
"imageSend_qualityStandard": "标准",
"imageSend_qualityHigh": "高",
"imageSend_packetsLabel": "数据包",
"imageSend_airtimeLabel": "播出时间",
"imageSend_sizeLabel": "有效载荷",
"imageSend_packetsCount": "{count} {count, plural, =1{个数据包} other{个数据包}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "无线电设置未知",
"imageSend_radioUnknownBody": "连接设备以计算播出时间。",
"imageSend_longSendTitle": "长距离传输",
"imageSend_longSendBody": "这将维持频道大约 {duration} 时间。",
"imageSend_floodNote": "洪水路由:每个中继站都会转发每个数据包,因此信道比这段时间更长地处于繁忙状态。",
"imageSend_parityTitle": "恢复包",
"imageSend_paritySubtitle": "多一个数据包。群发消息不被确认,因此如果某个数据包丢失,接收方可以重新构建图像。",
"imageSend_send": "发送",
"imageSend_cancel": "取消",
"imageSend_encodeFailed": "此图像无法编码。",
"imageSend_codecDownloading": "图像模型仍在下载中。",
"imageSend_codecUnavailable": "此设备无法发送图片。",
"imageSend_codecDisabled": "图片消息在设置中已关闭。",
"imageSend_deviceUnsupported": "此无线电无法发送图像数据包。请连接运行伴侣固件13或更高版本的设备。",
"imageSend_directMessagesUnsupported": "图片作为群组数据传输,因此只能发送到频道,而不能直接发送给个人。",
"imageSend_tooLarge": "该图像编码的数据包数量超过了网格格式所允许的范围。",
"imageSend_sentConfirmation": "图像已作为 {count} {count, plural, =1{个数据包} other{个数据包}} 发送。",
"imageSend_sendFailed": "无法发送图片:{error}",
"imageSend_sendingProgress": "发送图像 — 已发送 {sent} 包含 {total}",
"receivedImage_senderPrefix": "节点 {prefix}",
"receivedImage_incoming": "已收到的 {received} 个包含在 {total} 个包裹中",
"receivedImage_queued": "等待解码",
"receivedImage_tapToDecode": "点击解锁",
"receivedImage_decoding": "重建……大约1秒",
"receivedImage_incomplete": "图片不完整 — {received} 已收到 {total} 个数据包",
"receivedImage_corrupt": "图像无法重建",
"receivedImage_decoderMissing": "收到图片 — 图像解码失败",
"receivedImage_evicted": "图片已不再存储",
"receivedImage_retry": "再试一次",
"receivedImage_decodeAgain": "重新解码",
"receivedImage_openSettings": "建立",
"receivedImage_tapToProcess": "点击处理",
"receivedImage_awaiting": "{bytes} 字节 · {packets} {packets, plural, =1{个数据包} other{个数据包}}",
"imageSend_secondsValue": "{seconds} 秒",
"imageSend_minutesSecondsValue": "{minutes} 分 {seconds} 秒"
} }
+20 -3
View File
@@ -1,4 +1,5 @@
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/path_helper.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import 'app_localizations.dart'; import 'app_localizations.dart';
@@ -23,14 +24,30 @@ extension ContactLocalization on Contact {
} }
} }
String pathLabel(AppLocalizations l10n) { String pathLabel(
AppLocalizations l10n, {
int pathHashByteWidth = pathHashSize,
}) {
if (pathOverride != null) { if (pathOverride != null) {
if (pathOverride! < 0) return l10n.chat_floodForced; if (pathOverride! < 0) return l10n.chat_floodForced;
if (pathOverride == 0) return l10n.chat_directForced; if (pathOverride == 0) return l10n.chat_directForced;
return l10n.chat_hopsForced(pathOverride!); return l10n.chat_hopsForced(
_displayHopCount(pathOverrideBytes, pathOverride!, pathHashByteWidth),
);
} }
if (pathLength < 0) return l10n.channelPath_floodPath; if (pathLength < 0) return l10n.channelPath_floodPath;
if (pathLength == 0) return l10n.chat_direct; if (pathLength == 0) return l10n.chat_direct;
return l10n.chat_hopsCount(pathLength); return l10n.chat_hopsCount(
_displayHopCount(path, pathLength, pathHashByteWidth),
);
}
int _displayHopCount(
List<int>? pathBytes,
int storedHopCount,
int hashWidth,
) {
if (pathBytes == null || pathBytes.isEmpty) return storedHopCount;
return PathHelper.splitPathBytes(pathBytes, hashWidth).length;
} }
} }
+278 -35
View File
@@ -1,14 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'screens/chrome_required_screen.dart'; import 'screens/chrome_required_screen.dart';
import 'screens/contacts_screen.dart';
import 'utils/platform_info.dart'; import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart'; import 'connector/meshcore_connector.dart';
import 'models/image_codec_support.dart';
import 'screens/scanner_screen.dart';
import 'services/image_chunk_transport.dart';
import 'services/image_codec_service.dart';
import 'services/image_codec_settings_store.dart';
import 'services/received_image_blob_store_factory.dart';
import 'services/received_image_store.dart';
import 'services/storage_service.dart'; import 'services/storage_service.dart';
import 'services/message_retry_service.dart'; import 'services/message_retry_service.dart';
import 'services/path_history_service.dart'; import 'services/path_history_service.dart';
@@ -23,11 +32,22 @@ import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart'; import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart'; import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart'; import 'storage/prefs_manager.dart';
import 'theme/mesh_theme.dart';
import 'utils/app_logger.dart'; import 'utils/app_logger.dart';
import 'widgets/image_send_codec_binding.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// On desktop, debugPrint is not suppressed in release builds and every
// call is a synchronous stdout write. The connector logs heavily on hot
// paths (frame handling, queue/channel sync), which shows up as syscall
// overhead on low-end Linux machines (issue #202). The in-app debug log
// screens are unaffected they store entries themselves.
if (kReleaseMode) {
debugPrint = (String? message, {int? wrapWidth}) {};
}
// Initialize SharedPreferences cache // Initialize SharedPreferences cache
await PrefsManager.initialize(); await PrefsManager.initialize();
@@ -40,15 +60,60 @@ void main() async {
final bleDebugLogService = BleDebugLogService(); final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService(); final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService(); final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService(); final mapTileCacheService = MapTileCacheService(
appSettingsService: appSettingsService,
);
final chatTextScaleService = ChatTextScaleService(); final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService); final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService(); final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage); final timeoutPredictionService = TimeoutPredictionService(storage);
// Load settings // Load settings before anything reads them. The image stack below takes its
// model registry and its "process automatically" default straight off
// `appSettingsService.settings`, so this cannot stay where it used to be
// (after the constructions) without those two starting out wrong.
await appSettingsService.loadSettings(); await appSettingsService.loadSettings();
// ---- image messages (AEIC over GRP_DATA) --------------------------------
// The codec owns the ONNX decoder; the store owns received-image state and
// the decode queue; the reassembler/transport pair is the receive path the
// connector feeds raw frames into.
final imageCodecService = ImageCodecService(
appSettingsService,
settingsStore: AppSettingsImageCodecStore(appSettingsService),
);
final receivedImageStore = ReceivedImageStore(
// Without a file-backed store this silently falls back to memory and every
// received image sidecar included is gone at the next launch.
blobs: createReceivedImageBlobStore(),
decoder: ImageCodecServiceDecoder(imageCodecService),
processAutomatically: appSettingsService.settings.imageProcessAutomatically,
);
// A decode peaks around 2.16 GiB, so the setting is the user's consent to
// spend it. Mirrored on every settings change; the store applies it to
// future arrivals only, so turning it on does not decode a backlog.
appSettingsService.addListener(() {
receivedImageStore.processAutomatically =
appSettingsService.settings.imageProcessAutomatically;
});
// Availability/isBusy changes are the only thing that un-parks a decode
// queue that stopped because the codec was unusable or busy.
imageCodecService.addListener(receivedImageStore.notifyDecoderChanged);
final imageReassembler = ImageStreamReassembler(store: receivedImageStore);
final imageTransport = ImageChunkTransport(
reassembler: imageReassembler,
// The UI sends whole chunk sets through connector.sendImageChunks so it
// gets real inter-chunk pacing and per-chunk progress; this closure only
// keeps ImageChunkTransport.sendImage() usable on its own.
send: (blob, channelIndex) => connector.sendImageChunks(
<Uint8List>[blob],
channelIndex: channelIndex,
interChunkDelay: Duration.zero,
),
// Overwritten by onImageSenderPrefix as soon as SELF_INFO lands.
senderPrefix: 0,
);
// Initialize app logger // Initialize app logger
appLogger.initialize( appLogger.initialize(
appDebugLogService, appDebugLogService,
@@ -66,6 +131,8 @@ void main() async {
await chatTextScaleService.initialize(); await chatTextScaleService.initialize();
await translationService.refreshDownloadedModels(); await translationService.refreshDownloadedModels();
await imageCodecService.refreshDownloadedModels();
await receivedImageStore.load();
await uiViewStateService.initialize(); await uiViewStateService.initialize();
await timeoutPredictionService.initialize(); await timeoutPredictionService.initialize();
@@ -79,10 +146,21 @@ void main() async {
appDebugLogService: appDebugLogService, appDebugLogService: appDebugLogService,
backgroundService: backgroundService, backgroundService: backgroundService,
timeoutPredictionService: timeoutPredictionService, timeoutPredictionService: timeoutPredictionService,
imageCodecService: imageCodecService,
imageTransport: imageTransport,
// ImageStreamReassembler.addChunk already forwards every outcome to the
// store together with the channel index (which ImageChunkOutcome itself
// does not carry), so onImageChunk would be a second, channel-blind path.
onImageSenderPrefix: (prefix) => imageReassembler.selfPrefix = prefix,
); );
await connector.restoreLastCompanionScope(); await connector.loadContactCache();
await connector.loadAllCachedDataForCurrentCompanion(); await connector.loadChannelSettings();
await connector.loadCachedChannels();
// Load persisted channel messages
await connector.loadAllChannelMessages();
await connector.loadUnreadState();
runApp( runApp(
MeshCoreApp( MeshCoreApp(
@@ -98,10 +176,107 @@ void main() async {
translationService: translationService, translationService: translationService,
uiViewStateService: uiViewStateService, uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService, timeoutPredictionService: timeoutPredictionService,
imageCodecService: imageCodecService,
receivedImageStore: receivedImageStore,
imageReassembler: imageReassembler,
), ),
); );
} }
/// [ReceivedImageDecoder] over [ImageCodecService].
///
/// Pure delegation. It exists so `received_image_store.dart` never imports
/// `image_codec_service.dart` (and so the store stays testable without an
/// 872 MB ONNX graph).
class ImageCodecServiceDecoder implements ReceivedImageDecoder {
final ImageCodecService service;
const ImageCodecServiceDecoder(this.service);
@override
ImageCodecAvailability get availability => service.availability;
@override
bool get isBusy => service.isBusy;
@override
Future<ImageCodecResult?> decodeBitstream({
required Uint8List bitstream,
required AeicRatePoint ratePoint,
required int resolution,
}) => service.decodeBitstream(
bitstream: bitstream,
ratePoint: ratePoint,
resolution: resolution,
);
@override
void cancelCodecJob() => service.cancelCodecJob();
}
/// [ImageCodecSettingsStore] backed by [AppSettingsService].
///
/// Replaces `PrefsImageCodecSettingsStore` now that the five `imageCodec*`
/// fields live on [AppSettings]; the JSON keys were already the same, so a
/// user upgrading keeps their model registry only if it was written through
/// this adapter the old standalone `image_codec_settings` blob is not
/// migrated, because a placeholder-URL build cannot have produced one.
class AppSettingsImageCodecStore implements ImageCodecSettingsStore {
final AppSettingsService _service;
const AppSettingsImageCodecStore(this._service);
@override
ImageCodecPreferences get preferences => _service.settings.imageCodec;
@override
Future<void> load() async {
// AppSettingsService.loadSettings() already ran in main().
}
@override
Future<void> save(ImageCodecPreferences preferences) =>
_service.setImageCodecPreferences(preferences);
}
/// An [ImageReassembler] that (a) learns the local sender prefix after
/// SELF_INFO and (b) forwards every outcome to [ReceivedImageStore] together
/// with the channel index.
///
/// Both exist because of upstream shapes this file cannot change:
/// * `ImageReassembler.selfPrefix` is `final`, but the local public key does
/// not exist until `RESP_CODE_SELF_INFO`, so a reassembler built here would
/// start with a null prefix and never be able to drop our own flood echo.
/// Overriding the getter is the smallest fix that does not touch the
/// transport; see the report's "needs from others".
/// * `ImageChunkOutcome` carries no channel index, so the connector's
/// `onImageChunk` callback cannot call `handleOutcome`, which requires one.
/// Intercepting `addChunk` the only place the index is in scope can.
class ImageStreamReassembler extends ImageReassembler {
final ReceivedImageStore store;
int? _selfPrefix;
ImageStreamReassembler({required this.store})
: super(onFailed: ((failure) => unawaited(store.handleFailure(failure))));
@override
int? get selfPrefix => _selfPrefix;
set selfPrefix(int? value) => _selfPrefix = value;
@override
ImageChunkOutcome addChunk(
Uint8List blob, {
int channelIndex = 0,
DateTime? now,
}) {
final outcome = super.addChunk(blob, channelIndex: channelIndex, now: now);
unawaited(store.handleOutcome(outcome, channelIndex: channelIndex));
return outcome;
}
}
void _registerThirdPartyLicenses() { void _registerThirdPartyLicenses() {
LicenseRegistry.addLicense(() async* { LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks( yield const LicenseEntryWithLineBreaks(
@@ -123,7 +298,7 @@ https://creativecommons.org/licenses/by/4.0/
}); });
} }
class MeshCoreApp extends StatelessWidget { class MeshCoreApp extends StatefulWidget {
final MeshCoreConnector connector; final MeshCoreConnector connector;
final MessageRetryService retryService; final MessageRetryService retryService;
final PathHistoryService pathHistoryService; final PathHistoryService pathHistoryService;
@@ -136,6 +311,9 @@ class MeshCoreApp extends StatelessWidget {
final TranslationService translationService; final TranslationService translationService;
final UiViewStateService uiViewStateService; final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService; final TimeoutPredictionService timeoutPredictionService;
final ImageCodecService imageCodecService;
final ReceivedImageStore receivedImageStore;
final ImageStreamReassembler imageReassembler;
const MeshCoreApp({ const MeshCoreApp({
super.key, super.key,
@@ -151,24 +329,83 @@ class MeshCoreApp extends StatelessWidget {
required this.translationService, required this.translationService,
required this.uiViewStateService, required this.uiViewStateService,
required this.timeoutPredictionService, required this.timeoutPredictionService,
required this.imageCodecService,
required this.receivedImageStore,
required this.imageReassembler,
}); });
@override
State<MeshCoreApp> createState() => _MeshCoreAppState();
}
/// How often abandoned image streams are swept.
///
/// `ImageReassembler.evictExpired` otherwise only runs from `addChunk`, so a
/// sender that stops mid-image would leave its bubble stuck on "2 of 3
/// packets" until some unrelated image arrived.
const Duration _kImageSweepInterval = Duration(seconds: 5);
class _MeshCoreAppState extends State<MeshCoreApp> with WidgetsBindingObserver {
Timer? _imageSweepTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_imageSweepTimer = Timer.periodic(
_kImageSweepInterval,
(_) => widget.imageReassembler.evictExpired(),
);
}
@override
void dispose() {
_imageSweepTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
widget.receivedImageStore.setForeground(state == AppLifecycleState.resumed);
// ~2.7 GiB resident in a backgrounded app is a low-memory-killer kill, so
// unlike the translation stack the graph is dropped on background rather
// than held until the radio disconnects.
if (state == AppLifecycleState.paused ||
state == AppLifecycleState.hidden) {
unawaited(widget.imageCodecService.handleMemoryPressure());
unawaited(widget.receivedImageStore.handleMemoryPressure());
}
}
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
unawaited(widget.imageCodecService.handleMemoryPressure());
unawaited(widget.receivedImageStore.handleMemoryPressure());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final connector = widget.connector;
final storage = widget.storage;
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: connector), ChangeNotifierProvider.value(value: connector),
ChangeNotifierProvider.value(value: retryService), ChangeNotifierProvider.value(value: widget.retryService),
ChangeNotifierProvider.value(value: pathHistoryService), ChangeNotifierProvider.value(value: widget.pathHistoryService),
ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: widget.appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: widget.bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService), ChangeNotifierProvider.value(value: widget.appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService), ChangeNotifierProvider.value(value: widget.chatTextScaleService),
ChangeNotifierProvider.value(value: translationService), ChangeNotifierProvider.value(value: widget.translationService),
ChangeNotifierProvider.value(value: uiViewStateService), ChangeNotifierProvider.value(value: widget.uiViewStateService),
Provider.value(value: storage), Provider.value(value: storage),
Provider.value(value: mapTileCacheService), ChangeNotifierProvider.value(value: widget.mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService), ChangeNotifierProvider.value(value: widget.timeoutPredictionService),
ChangeNotifierProvider.value(value: widget.imageCodecService),
ChangeNotifierProvider.value(value: widget.receivedImageStore),
], ],
child: Consumer<AppSettingsService>( child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) { builder: (context, settingsService, child) {
@@ -185,23 +422,8 @@ class MeshCoreApp extends StatelessWidget {
locale: _localeFromSetting( locale: _localeFromSetting(
settingsService.settings.languageOverride, settingsService.settings.languageOverride,
), ),
theme: ThemeData( theme: MeshTheme.light(),
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), darkTheme: MeshTheme.dark(),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
useMaterial3: true,
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
),
themeMode: _themeModeFromSetting( themeMode: _themeModeFromSetting(
settingsService.settings.themeMode, settingsService.settings.themeMode,
), ),
@@ -209,11 +431,14 @@ class MeshCoreApp extends StatelessWidget {
// Update notification service with resolved locale // Update notification service with resolved locale
final locale = Localizations.localeOf(context); final locale = Localizations.localeOf(context);
NotificationService().setLocale(locale); NotificationService().setLocale(locale);
return child ?? const SizedBox.shrink(); return AnnotatedRegion<SystemUiOverlayStyle>(
value: _systemUiOverlayStyle(context),
child: child ?? const SizedBox.shrink(),
);
}, },
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
? const ChromeRequiredScreen() ? const ChromeRequiredScreen()
: const ContactsScreen(), : const ScannerScreen(),
); );
}, },
), ),
@@ -231,6 +456,24 @@ class MeshCoreApp extends StatelessWidget {
} }
} }
SystemUiOverlayStyle _systemUiOverlayStyle(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
final iconBrightness = isDark ? Brightness.light : Brightness.dark;
// Keep Android system bars aligned with the resolved Flutter theme.
return SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: iconBrightness,
statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
systemNavigationBarColor: colorScheme.surface,
systemNavigationBarIconBrightness: iconBrightness,
systemNavigationBarDividerColor: colorScheme.surface,
systemNavigationBarContrastEnforced: false,
);
}
Locale? _localeFromSetting(String? languageCode) { Locale? _localeFromSetting(String? languageCode) {
if (languageCode == null) return null; if (languageCode == null) return null;
return Locale(languageCode); return Locale(languageCode);
+148 -4
View File
@@ -1,3 +1,4 @@
import 'image_codec_support.dart';
import 'translation_support.dart'; import 'translation_support.dart';
enum UnitSystem { metric, imperial } enum UnitSystem { metric, imperial }
@@ -76,6 +77,7 @@ class Cyr2LatProfile {
class AppSettings { class AppSettings {
static const Object _unset = Object(); static const Object _unset = Object();
static const String stadiaDemo = '51bd0381-4685-4666-bae8-48940f6d77c0';
final bool clearPathOnMaxRetry; final bool clearPathOnMaxRetry;
final bool mapShowRepeaters; final bool mapShowRepeaters;
@@ -91,10 +93,15 @@ class AppSettings {
final Map<String, double>? mapCacheBounds; final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom; final int mapCacheMinZoom;
final int mapCacheMaxZoom; final int mapCacheMaxZoom;
final String mapRasterSourceId;
final String mapTileEndpointId;
final String? mapTileApiKey;
final bool notificationsEnabled; final bool notificationsEnabled;
final bool notifyOnNewMessage; final bool notifyOnNewMessage;
final bool notifyOnNewChannelMessage; final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert; final bool notifyOnNewAdvert;
final bool autoSendZeroHopAdvertOnGpsUpdate;
final int gpsIntervalSeconds;
final bool autoRouteRotationEnabled; final bool autoRouteRotationEnabled;
final double maxRouteWeight; final double maxRouteWeight;
final double initialRouteWeight; final double initialRouteWeight;
@@ -112,7 +119,31 @@ class AppSettings {
final String tcpServerAddress; final String tcpServerAddress;
final int tcpServerPort; final int tcpServerPort;
final bool jumpToOldestUnread; final bool jumpToOldestUnread;
final bool imageMessagesEnabled;
/// Whether a received image is decoded as soon as it is reassembled.
///
/// Off by default and deliberately so: a decode peaks around 2.16 GiB
/// resident and takes about a second, so an unattended chat must not be able
/// to trigger one per arriving image. When false, `ReceivedImageStore` parks
/// the arrival as a "Tap to process" placeholder instead of queueing it.
final bool imageProcessAutomatically;
// ---- neural image codec (AEIC-SE) ---------------------------------------
// Structural twins of the translation block above; the JSON keys match
// ImageCodecPreferences.toJson so ImageCodecService reads them unchanged.
final bool imageCodecEnabled;
final String? imageCodecSelectedModelId;
final String? imageCodecModelSourceUrl;
/// [AeicRatePoint.wireValue] of the composer's default rate point.
/// 4 == ft32, the only rate point this build ships.
final int imageCodecRatePoint;
final List<ImageCodecModelRecord> imageCodecDownloadedModels;
final bool translationEnabled; final bool translationEnabled;
final bool autoTranslateIncomingMessages;
final String? translationTargetLanguageCode; final String? translationTargetLanguageCode;
final bool composerTranslationEnabled; final bool composerTranslationEnabled;
final String? translationModelSourceUrl; final String? translationModelSourceUrl;
@@ -121,6 +152,26 @@ class AppSettings {
final List<Cyr2LatProfile> cyr2latProfiles; final List<Cyr2LatProfile> cyr2latProfiles;
final String selectedCyr2latProfileId; final String selectedCyr2latProfileId;
/// The five `imageCodec*` fields as the value object `ImageCodecService`
/// consumes. Assembled rather than stored so the settings blob stays flat.
ImageCodecPreferences get imageCodec => ImageCodecPreferences(
enabled: imageCodecEnabled,
selectedModelId: imageCodecSelectedModelId,
modelSourceUrl: imageCodecModelSourceUrl,
ratePoint: imageCodecRatePoint,
downloadedModels: imageCodecDownloadedModels,
);
String get effectiveMapTileApiKey {
final apiKey = mapTileApiKey?.trim();
if (apiKey == null || apiKey.isEmpty) {
return stadiaDemo;
}
return apiKey;
}
bool get usesstadiaDemo => effectiveMapTileApiKey == stadiaDemo;
Map<String, String> get cyr2latCharMap { Map<String, String> get cyr2latCharMap {
final profile = cyr2latProfiles.firstWhere( final profile = cyr2latProfiles.firstWhere(
(p) => p.id == selectedCyr2latProfileId, (p) => p.id == selectedCyr2latProfileId,
@@ -140,15 +191,20 @@ class AppSettings {
this.mapKeyPrefix = '', this.mapKeyPrefix = '',
this.mapShowMarkers = true, this.mapShowMarkers = true,
this.mapShowGuessedLocations = true, this.mapShowGuessedLocations = true,
this.enableMessageTracing = false, this.enableMessageTracing = true,
this.mapCacheBounds, this.mapCacheBounds,
this.mapCacheMinZoom = 10, this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15, this.mapCacheMaxZoom = 15,
this.mapRasterSourceId = 'osm_auto',
this.mapTileEndpointId = 'standard_2x',
this.mapTileApiKey,
this.notificationsEnabled = true, this.notificationsEnabled = true,
this.notifyOnNewMessage = true, this.notifyOnNewMessage = true,
this.notifyOnNewChannelMessage = true, this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true, this.notifyOnNewAdvert = true,
this.autoRouteRotationEnabled = false, this.autoSendZeroHopAdvertOnGpsUpdate = false,
this.gpsIntervalSeconds = 900,
this.autoRouteRotationEnabled = true,
this.maxRouteWeight = 5.0, this.maxRouteWeight = 5.0,
this.initialRouteWeight = 3.0, this.initialRouteWeight = 3.0,
this.routeWeightSuccessIncrement = 0.5, this.routeWeightSuccessIncrement = 0.5,
@@ -165,7 +221,15 @@ class AppSettings {
this.tcpServerAddress = '', this.tcpServerAddress = '',
this.tcpServerPort = 0, this.tcpServerPort = 0,
this.jumpToOldestUnread = false, this.jumpToOldestUnread = false,
this.imageMessagesEnabled = false,
this.imageProcessAutomatically = false,
this.imageCodecEnabled = false,
this.imageCodecSelectedModelId,
this.imageCodecModelSourceUrl,
this.imageCodecRatePoint = 4,
List<ImageCodecModelRecord>? imageCodecDownloadedModels,
this.translationEnabled = false, this.translationEnabled = false,
this.autoTranslateIncomingMessages = true,
this.translationTargetLanguageCode, this.translationTargetLanguageCode,
this.composerTranslationEnabled = false, this.composerTranslationEnabled = false,
this.translationModelSourceUrl, this.translationModelSourceUrl,
@@ -176,6 +240,7 @@ class AppSettings {
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {}, mutedChannels = mutedChannels ?? {},
imageCodecDownloadedModels = imageCodecDownloadedModels ?? const [],
translationDownloadedModels = translationDownloadedModels ?? const [], translationDownloadedModels = translationDownloadedModels ?? const [],
cyr2latProfiles = cyr2latProfiles =
cyr2latProfiles ?? cyr2latProfiles ??
@@ -204,10 +269,16 @@ class AppSettings {
'map_cache_bounds': mapCacheBounds, 'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom, 'map_cache_max_zoom': mapCacheMaxZoom,
'map_raster_source_id': mapRasterSourceId,
'map_tile_endpoint_id': mapTileEndpointId,
'map_tile_api_key': mapTileApiKey,
'notifications_enabled': notificationsEnabled, 'notifications_enabled': notificationsEnabled,
'notify_on_new_message': notifyOnNewMessage, 'notify_on_new_message': notifyOnNewMessage,
'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert, 'notify_on_new_advert': notifyOnNewAdvert,
'auto_send_zero_hop_advert_on_gps_update':
autoSendZeroHopAdvertOnGpsUpdate,
'gps_interval_seconds': gpsIntervalSeconds,
'auto_route_rotation_enabled': autoRouteRotationEnabled, 'auto_route_rotation_enabled': autoRouteRotationEnabled,
'max_route_weight': maxRouteWeight, 'max_route_weight': maxRouteWeight,
'initial_route_weight': initialRouteWeight, 'initial_route_weight': initialRouteWeight,
@@ -225,7 +296,17 @@ class AppSettings {
'tcp_server_address': tcpServerAddress, 'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort, 'tcp_server_port': tcpServerPort,
'jump_to_oldest_unread': jumpToOldestUnread, 'jump_to_oldest_unread': jumpToOldestUnread,
'image_messages_enabled': imageMessagesEnabled,
'image_process_automatically': imageProcessAutomatically,
'image_codec_enabled': imageCodecEnabled,
'image_codec_selected_model_id': imageCodecSelectedModelId,
'image_codec_model_source_url': imageCodecModelSourceUrl,
'image_codec_rate_point': imageCodecRatePoint,
'image_codec_downloaded_models': imageCodecDownloadedModels
.map((model) => model.toJson())
.toList(),
'translation_enabled': translationEnabled, 'translation_enabled': translationEnabled,
'auto_translate_incoming_messages': autoTranslateIncomingMessages,
'translation_target_language_code': translationTargetLanguageCode, 'translation_target_language_code': translationTargetLanguageCode,
'composer_translation_enabled': composerTranslationEnabled, 'composer_translation_enabled': composerTranslationEnabled,
'translation_model_source_url': translationModelSourceUrl, 'translation_model_source_url': translationModelSourceUrl,
@@ -261,19 +342,26 @@ class AppSettings {
mapShowMarkers: json['map_show_markers'] as bool? ?? true, mapShowMarkers: json['map_show_markers'] as bool? ?? true,
mapShowGuessedLocations: mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true, json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, enableMessageTracing: json['enable_message_tracing'] as bool? ?? true,
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( 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, mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
mapRasterSourceId: json['map_raster_source_id'] as String? ?? 'osm_auto',
mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard',
mapTileApiKey: json['map_tile_api_key'] as String?,
notificationsEnabled: json['notifications_enabled'] as bool? ?? true, notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
notifyOnNewChannelMessage: notifyOnNewChannelMessage:
json['notify_on_new_channel_message'] as bool? ?? true, json['notify_on_new_channel_message'] as bool? ?? true,
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoSendZeroHopAdvertOnGpsUpdate:
json['auto_send_zero_hop_advert_on_gps_update'] as bool? ?? false,
gpsIntervalSeconds:
(json['gps_interval_seconds'] as num?)?.toInt() ?? 900,
autoRouteRotationEnabled: autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? false, json['auto_route_rotation_enabled'] as bool? ?? true,
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
initialRouteWeight: initialRouteWeight:
(json['initial_route_weight'] as num?)?.toDouble() ?? 3.0, (json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
@@ -306,7 +394,26 @@ class AppSettings {
tcpServerAddress: json['tcp_server_address'] as String? ?? '', tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0, tcpServerPort: json['tcp_server_port'] as int? ?? 0,
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false, jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
imageMessagesEnabled: json['image_messages_enabled'] as bool? ?? false,
imageProcessAutomatically:
json['image_process_automatically'] as bool? ?? false,
imageCodecEnabled: json['image_codec_enabled'] as bool? ?? false,
imageCodecSelectedModelId:
json['image_codec_selected_model_id'] as String?,
imageCodecModelSourceUrl: json['image_codec_model_source_url'] as String?,
imageCodecRatePoint: json['image_codec_rate_point'] as int? ?? 4,
imageCodecDownloadedModels:
(json['image_codec_downloaded_models'] as List<dynamic>?)
?.map(
(entry) => ImageCodecModelRecord.fromJson(
Map<String, dynamic>.from(entry as Map),
),
)
.toList() ??
const [],
translationEnabled: json['translation_enabled'] as bool? ?? false, translationEnabled: json['translation_enabled'] as bool? ?? false,
autoTranslateIncomingMessages:
json['auto_translate_incoming_messages'] as bool? ?? true,
translationTargetLanguageCode: translationTargetLanguageCode:
json['translation_target_language_code'] as String?, json['translation_target_language_code'] as String?,
composerTranslationEnabled: composerTranslationEnabled:
@@ -374,10 +481,15 @@ class AppSettings {
Object? mapCacheBounds = _unset, Object? mapCacheBounds = _unset,
int? mapCacheMinZoom, int? mapCacheMinZoom,
int? mapCacheMaxZoom, int? mapCacheMaxZoom,
String? mapRasterSourceId,
String? mapTileEndpointId,
Object? mapTileApiKey = _unset,
bool? notificationsEnabled, bool? notificationsEnabled,
bool? notifyOnNewMessage, bool? notifyOnNewMessage,
bool? notifyOnNewChannelMessage, bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert, bool? notifyOnNewAdvert,
bool? autoSendZeroHopAdvertOnGpsUpdate,
int? gpsIntervalSeconds,
bool? autoRouteRotationEnabled, bool? autoRouteRotationEnabled,
double? maxRouteWeight, double? maxRouteWeight,
double? initialRouteWeight, double? initialRouteWeight,
@@ -395,7 +507,15 @@ class AppSettings {
String? tcpServerAddress, String? tcpServerAddress,
int? tcpServerPort, int? tcpServerPort,
bool? jumpToOldestUnread, bool? jumpToOldestUnread,
bool? imageMessagesEnabled,
bool? imageProcessAutomatically,
bool? imageCodecEnabled,
Object? imageCodecSelectedModelId = _unset,
Object? imageCodecModelSourceUrl = _unset,
int? imageCodecRatePoint,
List<ImageCodecModelRecord>? imageCodecDownloadedModels,
bool? translationEnabled, bool? translationEnabled,
bool? autoTranslateIncomingMessages,
Object? translationTargetLanguageCode = _unset, Object? translationTargetLanguageCode = _unset,
bool? composerTranslationEnabled, bool? composerTranslationEnabled,
Object? translationModelSourceUrl = _unset, Object? translationModelSourceUrl = _unset,
@@ -422,11 +542,20 @@ class AppSettings {
: mapCacheBounds as Map<String, double>?, : mapCacheBounds as Map<String, double>?,
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
mapTileApiKey: mapTileApiKey == _unset
? this.mapTileApiKey
: mapTileApiKey as String?,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
notifyOnNewChannelMessage: notifyOnNewChannelMessage:
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoSendZeroHopAdvertOnGpsUpdate:
autoSendZeroHopAdvertOnGpsUpdate ??
this.autoSendZeroHopAdvertOnGpsUpdate,
gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds,
autoRouteRotationEnabled: autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight, maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
@@ -452,7 +581,22 @@ class AppSettings {
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress, tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort, tcpServerPort: tcpServerPort ?? this.tcpServerPort,
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread, jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
imageMessagesEnabled: imageMessagesEnabled ?? this.imageMessagesEnabled,
imageProcessAutomatically:
imageProcessAutomatically ?? this.imageProcessAutomatically,
imageCodecEnabled: imageCodecEnabled ?? this.imageCodecEnabled,
imageCodecSelectedModelId: imageCodecSelectedModelId == _unset
? this.imageCodecSelectedModelId
: imageCodecSelectedModelId as String?,
imageCodecModelSourceUrl: imageCodecModelSourceUrl == _unset
? this.imageCodecModelSourceUrl
: imageCodecModelSourceUrl as String?,
imageCodecRatePoint: imageCodecRatePoint ?? this.imageCodecRatePoint,
imageCodecDownloadedModels:
imageCodecDownloadedModels ?? this.imageCodecDownloadedModels,
translationEnabled: translationEnabled ?? this.translationEnabled, translationEnabled: translationEnabled ?? this.translationEnabled,
autoTranslateIncomingMessages:
autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages,
translationTargetLanguageCode: translationTargetLanguageCode == _unset translationTargetLanguageCode: translationTargetLanguageCode == _unset
? this.translationTargetLanguageCode ? this.translationTargetLanguageCode
: translationTargetLanguageCode as String?, : translationTargetLanguageCode as String?,
+38
View File
@@ -4,6 +4,9 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import 'community.dart';
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
class Channel { class Channel {
final int index; final int index;
@@ -24,6 +27,10 @@ class Channel {
bool get isPublicChannel => pskHex == publicChannelPsk; bool get isPublicChannel => pskHex == publicChannelPsk;
bool get isHashtagChannel => name.startsWith('#');
bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel;
static Channel? fromFrame(Uint8List frame) { static Channel? fromFrame(Uint8List frame) {
// CHANNEL_INFO format: // CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18) // [0] = RESP_CODE_CHANNEL_INFO (18)
@@ -111,5 +118,36 @@ class Channel {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
} }
static bool isCommunityChannel(ChannelType channelType) {
switch (channelType) {
case ChannelType.communityPublic:
case ChannelType.communityHashtag:
return true;
case ChannelType.public:
case ChannelType.private:
case ChannelType.hashtag:
return false;
}
}
static ChannelType getChannelType(
Channel channel,
CommunityPskIndex communityIndex,
) {
Community? community = communityIndex.getCommunityForChannel(channel);
if (community != null) {
if (Community.isCommunityPublicChannel(channel, community)) {
return ChannelType.communityPublic;
}
return ChannelType.communityHashtag;
}
if (channel.isPublicChannel) {
return ChannelType.public;
} else if (channel.name.startsWith('#')) {
return ChannelType.hashtag;
}
return ChannelType.private;
}
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72'; static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
} }
+17 -5
View File
@@ -41,6 +41,7 @@ class ChannelMessage {
final List<Repeat> repeats; final List<Repeat> repeats;
final int repeatCount; final int repeatCount;
final int? pathLength; final int? pathLength;
final int? pathHashWidth;
final Uint8List pathBytes; final Uint8List pathBytes;
final List<Uint8List> pathVariants; final List<Uint8List> pathVariants;
final int? channelIndex; final int? channelIndex;
@@ -66,6 +67,7 @@ class ChannelMessage {
this.repeats = const [], this.repeats = const [],
this.repeatCount = 0, this.repeatCount = 0,
this.pathLength, this.pathLength,
this.pathHashWidth,
Uint8List? pathBytes, Uint8List? pathBytes,
List<Uint8List>? pathVariants, List<Uint8List>? pathVariants,
this.channelIndex, this.channelIndex,
@@ -93,6 +95,7 @@ class ChannelMessage {
List<Repeat>? repeats, List<Repeat>? repeats,
int? repeatCount, int? repeatCount,
int? pathLength, int? pathLength,
int? pathHashWidth,
Uint8List? pathBytes, Uint8List? pathBytes,
List<Uint8List>? pathVariants, List<Uint8List>? pathVariants,
String? packetHash, String? packetHash,
@@ -129,6 +132,7 @@ class ChannelMessage {
repeats: repeats ?? this.repeats, repeats: repeats ?? this.repeats,
repeatCount: repeatCount ?? this.repeatCount, repeatCount: repeatCount ?? this.repeatCount,
pathLength: pathLength ?? this.pathLength, pathLength: pathLength ?? this.pathLength,
pathHashWidth: pathHashWidth ?? this.pathHashWidth,
pathBytes: pathBytes ?? this.pathBytes, pathBytes: pathBytes ?? this.pathBytes,
pathVariants: pathVariants ?? this.pathVariants, pathVariants: pathVariants ?? this.pathVariants,
channelIndex: channelIndex, channelIndex: channelIndex,
@@ -155,6 +159,7 @@ class ChannelMessage {
int pathLen; int pathLen;
int txtType; int txtType;
int? packetPathHashWidth;
Uint8List pathBytes = Uint8List(0); Uint8List pathBytes = Uint8List(0);
int channelIdx; int channelIdx;
if (code == respCodeChannelMsgRecvV3) { if (code == respCodeChannelMsgRecvV3) {
@@ -163,12 +168,18 @@ class ChannelMessage {
final hasPath = (flags & 0x01) != 0; final hasPath = (flags & 0x01) != 0;
reader.skipBytes(1); // Skip reserved byte reader.skipBytes(1); // Skip reserved byte
channelIdx = reader.readByte(); channelIdx = reader.readByte();
pathLen = reader.readInt8(); final pathByte = reader.readUInt8();
txtType = reader.readByte(); // pathByte packs: top 2 bits = hash width mode, low 6 bits = hop count
if (hasPath && pathLen > 0) { packetPathHashWidth = ((pathByte & 0xC0) >> 6) + 1;
reader.rewind(); // Rewind to read path length again for pathBytes final hopCount = pathByte & 0x3F;
pathBytes = reader.readBytes(pathLen); pathLen = hopCount;
// If a path is present, read hopCount * width bytes
if (hasPath && hopCount > 0) {
final totalPathBytes = hopCount * packetPathHashWidth;
pathBytes = reader.readBytes(totalPathBytes);
} }
// After consuming optional path bytes, read the text type byte.
txtType = reader.readByte();
} else { } else {
channelIdx = reader.readByte(); channelIdx = reader.readByte();
pathLen = reader.readInt8(); pathLen = reader.readInt8();
@@ -209,6 +220,7 @@ class ChannelMessage {
isOutgoing: false, isOutgoing: false,
status: ChannelMessageStatus.sent, status: ChannelMessageStatus.sent,
pathLength: pathLen, pathLength: pathLen,
pathHashWidth: packetPathHashWidth,
pathBytes: pathBytes, pathBytes: pathBytes,
channelIndex: channelIdx, channelIndex: channelIdx,
); );
+36 -12
View File
@@ -1,8 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/utils/keys.dart';
import 'channel.dart';
/// Represents a community with a shared secret for deriving channel PSKs. /// Represents a community with a shared secret for deriving channel PSKs.
/// ///
@@ -35,11 +37,7 @@ class Community {
/// Generate a new community with a random 32-byte secret /// 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 = randomBytes(32);
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community( return Community(
id: id, id: id,
name: name, name: name,
@@ -162,6 +160,12 @@ class Community {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim(); return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
} }
/// Returns true if this is the community's public channel
static bool isCommunityPublicChannel(Channel channel, Community community) {
final publicPsk = community.deriveCommunityPublicPsk();
return channel.pskHex == Channel.formatPskHex(publicPsk);
}
/// Add a hashtag channel to this community's list /// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) { Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag); final normalized = _normalizeCommunityHashtag(hashtag);
@@ -202,12 +206,7 @@ class Community {
/// Create a copy of this community with a regenerated random secret /// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() { Community withRegeneratedSecret() {
final random = Random.secure(); return withNewSecret(randomBytes(32));
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
} }
/// Extract secret from QR data (for updating existing community) /// Extract secret from QR data (for updating existing community)
@@ -237,3 +236,28 @@ class Community {
@override @override
int get hashCode => id.hashCode; int get hashCode => id.hashCode;
} }
class CommunityPskIndex {
// Cache of PSK hex -> Community for quick lookup
final Map<String, Community> _pskToCommunity = {};
void initialize(List<Community> communities) {
_pskToCommunity.clear();
for (final community in communities) {
// Map the community public channel PSK
final publicPsk = community.deriveCommunityPublicPsk();
_pskToCommunity[Channel.formatPskHex(publicPsk)] = community;
// Map all known hashtag channel PSKs
for (final hashtag in community.hashtagChannels) {
final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag);
_pskToCommunity[Channel.formatPskHex(hashtagPsk)] = community;
}
}
}
/// Returns the community this channel belongs to, or null if not a community channel
Community? getCommunityForChannel(Channel channel) {
return _pskToCommunity[channel.pskHex];
}
}
+14 -4
View File
@@ -119,7 +119,7 @@ class Contact {
String pathFormattedIdList(int hashByteWidth) { String pathFormattedIdList(int hashByteWidth) {
final pathBytes = pathBytesForDisplay; final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return ''; if (pathBytes.isEmpty) return '';
final w = hashByteWidth.clamp(1, 8); final w = hashByteWidth.clamp(1, 4);
final parts = <String>[]; final parts = <String>[];
for (int i = 0; i < pathBytes.length; i += w) { for (int i = 0; i < pathBytes.length; i += w) {
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length; final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
@@ -166,8 +166,18 @@ class Contact {
final type = reader.readByte(); final type = reader.readByte();
final flags = reader.readByte(); final flags = reader.readByte();
final pathLen = reader.readByte(); final pathLen = reader.readByte();
final safePathLen = pathLen > 0 int hopCount = 0;
? (pathLen > maxPathSize ? maxPathSize : pathLen) int byteLen = 0;
if (pathLen == 0xFF) {
hopCount = -1;
} else {
final mode = (pathLen & 0xC0) >> 6;
hopCount = pathLen & 0x3F;
final width = mode + 1;
byteLen = hopCount * width;
}
final safePathLen = byteLen > 0
? (byteLen > maxPathSize ? maxPathSize : byteLen)
: 0; : 0;
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize); final name = reader.readCStringGreedy(maxNameSize);
@@ -213,7 +223,7 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name, name: name.isEmpty ? 'Unknown' : name,
type: type, type: type,
flags: flags, flags: flags,
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen, pathLength: hopCount,
path: pathBytes, path: pathBytes,
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
+70
View File
@@ -0,0 +1,70 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:latlong2/latlong.dart';
import 'path_history.dart';
/// One observed route rendered on the path map the live traced path
/// (primary) or an alternate from the contact's path history — resolved to
/// map coordinates with per-hop confidence flags.
class DisplayPath {
final String id;
final String label;
final Color color;
final bool isPrimary;
/// Outbound hop prefixes, including hops that could not be placed on the map.
final List<Uint8List> hopBytes;
/// Resolved map points: self, each locatable hop, then the target when its
/// position is known. Hops with no position are skipped here but still
/// counted in [unresolvedHops].
final List<LatLng> points;
/// Display name for each entry of [points].
final List<String> pointLabels;
/// Whether each entry of [points] is a GPS-grade position (vs inferred).
final List<bool> pointConfirmed;
/// Per segment (length points-1): true when either endpoint is inferred or
/// unlocatable hops were skipped in between rendered dashed.
final List<bool> segmentEstimated;
/// Per segment: the transmission ordinal of the segment's destination,
/// used to highlight the matching hop-list row during animation.
final List<int> rowForSegment;
/// Total transmissions on the full route (including unlocatable hops).
final int totalTransmissions;
/// True when the route ends with a chat-target endpoint row.
final bool hasTargetEndpoint;
final int gpsConfirmedHops;
final int unresolvedHops;
final double distanceMeters;
/// History metadata; null for the live traced (primary) path.
final PathRecord? record;
const DisplayPath({
required this.id,
required this.label,
required this.color,
required this.isPrimary,
required this.hopBytes,
required this.points,
required this.pointLabels,
required this.pointConfirmed,
required this.segmentEstimated,
required this.rowForSegment,
required this.totalTransmissions,
required this.hasTargetEndpoint,
required this.gpsConfirmedHops,
required this.unresolvedHops,
required this.distanceMeters,
this.record,
});
}
+835
View File
@@ -0,0 +1,835 @@
import 'dart:typed_data';
import '../widgets/image_send_codec_binding.dart';
/// Model registry + value types for the neural image codec (AEIC-SE).
///
/// Mirrors `lib/models/translation_support.dart` field-for-field so the
/// settings screen, the file store and the download machinery can be copied
/// from the translation stack with no structural changes.
enum ImageCodecStatus { none, pending, completed, failed, skipped }
extension ImageCodecStatusValue on ImageCodecStatus {
String get value {
switch (this) {
case ImageCodecStatus.pending:
return 'pending';
case ImageCodecStatus.completed:
return 'completed';
case ImageCodecStatus.failed:
return 'failed';
case ImageCodecStatus.skipped:
return 'skipped';
case ImageCodecStatus.none:
return 'none';
}
}
}
ImageCodecStatus parseImageCodecStatus(dynamic value) {
if (value is! String) {
return ImageCodecStatus.none;
}
for (final status in ImageCodecStatus.values) {
if (status.value == value) {
return status;
}
}
return ImageCodecStatus.none;
}
/// Shape of the decoder's only input tensor: `y_hat`, float32,
/// `[1, 256, 16, 16]`. Static the export has no dynamic axes and no batching.
const List<int> kImageCodecLatentShape = <int>[1, 256, 16, 16];
/// Element count implied by [kImageCodecLatentShape] (65,536).
const int kImageCodecLatentElements = 256 * 16 * 16;
/// Graph input/output names in the promoted export. Used as a preference when
/// binding tensors; the backend falls back to the graph's sole input/output if
/// a future re-export renames them.
const String kImageCodecDecoderInputName = 'y_hat';
const String kImageCodecDecoderOutputName = 'image';
/// Rate point = which `AEIC_SE_ft*.pkl` the encoder was fine-tuned from.
///
/// **ft32 IS THE ONLY SHIPPING RATE POINT** ([kShippingAeicRatePoint]). One
/// checkpoint ships, so [aeicRatePointForUi] is constant and the UI has no
/// quality selector to honour. The other members are retained *only* because
/// their ordinals are already persisted in `image_codec_rate_point` and cross
/// the isolate boundary, and because the measured byte counts below are real
/// data worth keeping. Do not reorder, do not add a second shipping entry
/// without a second model in [imageCodecPresetModels].
///
/// NOTE ON THE WIRE VALUE. This enum's ordinal is NOT the on-air rate nibble.
/// The on-air chunk-0 metadata byte is defined by `ImageStreamMetadata` in
/// `lib/services/image_chunk_transport.dart` and carries
/// [ImageCodecRatePoint.index], because the transport only ever exposes UI rate
/// points. [AeicRatePointValue]'s `wireValue` is the *model-selection ordinal*:
/// it is what gets persisted in settings and what crosses the isolate boundary
/// to pick a checkpoint. Convert with [aeicRatePointForUi] /
/// [uiRatePointForAeic]; never assume they are the same integer.
///
/// Measured real rANS bitstream sizes at 512x512 over 26 images
/// (Kodak 24 + 2 custom). `ft2`/`ft4` have not been measured on this corpus.
enum AeicRatePoint { ft2, ft4, ft8, ft16, ft32 }
/// The one rate point this build encodes and decodes at.
///
/// ft32: mean 155.8 B, min 110 B, max 209 B over the 26-image corpus, i.e. 1-2
/// data chunks plus one XOR parity chunk. ft16 was dropped: it needs 2-3 data
/// chunks and leaves only 79 B of headroom under the 3-chunk ceiling.
const AeicRatePoint kShippingAeicRatePoint = AeicRatePoint.ft32;
extension AeicRatePointValue on AeicRatePoint {
int get wireValue => index;
/// Mean measured bitstream size in bytes, or `null` where unmeasured.
int? get meanBytes {
switch (this) {
case AeicRatePoint.ft2:
case AeicRatePoint.ft4:
return null;
case AeicRatePoint.ft8:
return 507;
case AeicRatePoint.ft16:
return 288;
case AeicRatePoint.ft32:
return 156;
}
}
/// Largest bitstream observed on the measurement corpus, or `null`.
int? get maxBytes {
switch (this) {
case AeicRatePoint.ft2:
case AeicRatePoint.ft4:
return null;
case AeicRatePoint.ft8:
return 732;
case AeicRatePoint.ft16:
return 409;
case AeicRatePoint.ft32:
return 209;
}
}
/// `'ft32 (~156 B)'`. Uses the enum's own `name` for the prefix — do not add
/// a `name` getter here, it would collide with `dart:core`'s `EnumName`.
String get label {
final mean = meanBytes;
return mean == null ? name : '$name (~$mean B)';
}
}
AeicRatePoint parseAeicRatePoint(int wireValue) {
if (wireValue < 0 || wireValue >= AeicRatePoint.values.length) {
return kShippingAeicRatePoint;
}
return AeicRatePoint.values[wireValue];
}
/// Maps the UI-level rate selector onto the codec's rate points.
///
/// ft32-only, so this is deliberately constant and ignores [rate]. It is kept
/// as a function (rather than inlining [kShippingAeicRatePoint] at the call
/// sites) so that adding a second checkpoint is a one-function change, and so
/// this file never names an [ImageCodecRatePoint] member that enum is owned by
/// `image_send_codec_binding.dart` and its membership is the UI's to shrink.
AeicRatePoint aeicRatePointForUi(ImageCodecRatePoint rate) =>
kShippingAeicRatePoint;
/// Inverse of [aeicRatePointForUi]. Constant for the same reason: every rate
/// point this build can produce is the standard one.
ImageCodecRatePoint uiRatePointForAeic(AeicRatePoint rate) =>
ImageCodecRatePoint.standard;
/// Bundle layout revision written into [ImageCodecModelRecord.bundleVersion].
///
/// 0 means "installed before the entropy bundle existed", i.e. a decoder-only
/// install that can render a latent but cannot encode or decode a bitstream.
/// Bump this only when the *set of files* a model needs changes; it is what
/// `ImageCodecService.needsModelUpgrade` keys off.
///
/// 1 -> 2: the bundle gained the **decode-side** entropy graph
/// (`aeic_entropy_decode_fp32_op17.onnx`). A version-1 install has the
/// send-side graph only, so it can encode but cannot decode a bitstream; the
/// remedy is a re-download, which is exactly what `needsModelUpgrade` asks for.
const int kImageCodecBundleVersion = 2;
/// Same shape and field order as `TranslationModelRecord`, plus the two fields
/// a multi-asset install needs.
class ImageCodecModelRecord {
final String id;
final String name;
final String sourceUrl;
/// Path of the **decoder graph** the file ONNX Runtime is handed. The other
/// assets are its siblings in the same directory; see [assetFileNames].
final String localPath;
final DateTime downloadedAt;
final int fileSizeBytes;
/// Every file that was installed for this model, by exact on-disk name.
///
/// Recorded rather than re-derived so the service can locate the entropy
/// graph and the CDF tables without guessing filenames, and so `deleteModel`
/// can remove all ~1.0 GB instead of just the 3 MB graph.
final List<String> assetFileNames;
/// What each installed file IS, keyed by its exact on-disk name.
///
/// Recorded at install time because [assetFileNames] alone stopped being
/// enough at bundle version 2: the bundle now holds THREE `.onnx` files
/// the synthesis decoder, the send-side entropy graph and the decode-side
/// entropy graph and no property of a filename distinguishes the last two.
/// Handing the decode-side export to the encoder fails at the first run;
/// handing the send-side one to the decoder would feed rANS the wrong
/// probabilities, which is the silent-corruption failure mode.
///
/// Empty for records written before this field existed. Consumers fall back
/// to the registry spec, and only then to a filename heuristic.
final Map<String, ImageCodecAssetRole> assetRoles;
/// [kImageCodecBundleVersion] at install time; 0 for a pre-bundle install.
final int bundleVersion;
const ImageCodecModelRecord({
required this.id,
required this.name,
required this.sourceUrl,
required this.localPath,
required this.downloadedAt,
required this.fileSizeBytes,
this.assetFileNames = const [],
this.assetRoles = const {},
this.bundleVersion = 0,
});
/// The installed file recorded as [role], or null when none was.
String? fileNameForRole(ImageCodecAssetRole role) {
for (final entry in assetRoles.entries) {
if (entry.value == role && assetFileNames.contains(entry.key)) {
return entry.key;
}
}
return null;
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'source_url': sourceUrl,
'local_path': localPath,
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
'file_size_bytes': fileSizeBytes,
'asset_file_names': assetFileNames,
// Role NAMES, not ordinals: appending a member to ImageCodecAssetRole
// must never re-label files already on disk.
'asset_roles': <String, String>{
for (final entry in assetRoles.entries) entry.key: entry.value.name,
},
'bundle_version': bundleVersion,
};
}
factory ImageCodecModelRecord.fromJson(Map<String, dynamic> json) {
final rawAssets = json['asset_file_names'];
final rawRoles = json['asset_roles'];
return ImageCodecModelRecord(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
sourceUrl: json['source_url'] as String? ?? '',
localPath: json['local_path'] as String? ?? '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
json['downloaded_at'] as int? ?? 0,
),
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
assetFileNames: rawAssets is List
? [
for (final entry in rawAssets)
if (entry is String && entry.isNotEmpty) entry,
]
: const [],
assetRoles: rawRoles is Map
? <String, ImageCodecAssetRole>{
for (final entry in rawRoles.entries)
if (entry.key is String && entry.value is String)
entry.key as String: ?parseImageCodecAssetRole(
entry.value as String,
),
}
: const {},
bundleVersion: json['bundle_version'] as int? ?? 0,
);
}
ImageCodecModelRecord copyWith({
String? id,
String? name,
String? sourceUrl,
String? localPath,
DateTime? downloadedAt,
int? fileSizeBytes,
List<String>? assetFileNames,
Map<String, ImageCodecAssetRole>? assetRoles,
int? bundleVersion,
}) {
return ImageCodecModelRecord(
id: id ?? this.id,
name: name ?? this.name,
sourceUrl: sourceUrl ?? this.sourceUrl,
localPath: localPath ?? this.localPath,
downloadedAt: downloadedAt ?? this.downloadedAt,
fileSizeBytes: fileSizeBytes ?? this.fileSizeBytes,
assetFileNames: assetFileNames ?? this.assetFileNames,
assetRoles: assetRoles ?? this.assetRoles,
bundleVersion: bundleVersion ?? this.bundleVersion,
);
}
}
String imageCodecModelFriendlyName(ImageCodecModelRecord model) {
for (final spec in imageCodecPresetModels) {
if (spec.id == model.id || spec.fileName == model.name) {
return spec.label;
}
}
final trimmed = model.name.trim();
if (trimmed.endsWith('.onnx')) {
return trimmed.substring(0, trimmed.length - 5);
}
return trimmed.isEmpty ? model.id : trimmed;
}
/// What a downloaded file *is*, so nothing has to infer it from position or
/// filename.
///
/// This exists because the bundle stopped being "graph, then its data sibling".
/// `assets.first` is a decoder graph today and would silently become the
/// entropy graph the moment anyone reordered the list; the ONNX session would
/// then load 67 MB of entropy model and fail on an input named `y_hat`.
enum ImageCodecAssetRole {
/// The synthesis graph handed to ORT: `y_hat -> image`.
decoderGraph,
/// `<decoderGraph>.data`. ORT resolves it by the literal filename recorded
/// inside the graph, so it MUST sit beside [decoderGraph] under that name.
decoderWeights,
/// The fp32 **send-side** entropy graph: one input `image [1,3,512,512]`,
/// outputs `z_q`, `yq0..3`, `sc0..3`. Kept fp32 deliberately see the
/// bit-exactness note in `image_codec_backend.dart`.
///
/// This is the ENCODE half only. It emits every stage at once, which is valid
/// because the encoder already knows `y`; a decoder cannot use it at all.
entropyGraph,
/// Reserved. The 67 MB entropy export is self-contained today and carries no
/// external-data sibling; the role exists so a future re-export that does is
/// a registry change rather than a schema change.
entropyWeights,
/// The quantised CDF tables the rANS coder indexes. A *wrong* table set
/// desynchronises rANS and yields a sharp, plausible, wrong image with no
/// error, which is why this asset must carry a SHA-256 before ship.
cdfTables,
/// The fp32 **decode-side** entropy graph, added in bundle version 2.
///
/// Decoding is inherently sequential the symbols of stage `i` must be
/// decoded before stage `i+1`'s context exists — so the receiver cannot use
/// the emit-everything-at-once [entropyGraph]. This export is the same
/// sub-networks behind an ONNX `If` on a `stage` selector, so the app can
/// call it five times per image:
///
/// inputs : `z_q [1,128,4,4]`, `base [1,256,16,16]`, `stage int32 [1]`
/// outputs: `base0`, `means`, `scales`, all `[1,256,16,16]`
///
/// `stage < 0` runs the hyper-synthesis branch and fills `base0`;
/// `stage 0..3` runs the context branch and fills `means`/`scales`, already
/// multiplied by that stage's mask. The branch not taken emits zeros.
///
/// Appended after [cdfTables] rather than beside [entropyGraph] so no
/// existing enum ordinal moves.
entropyDecodeGraph,
}
/// Parses a role written by [ImageCodecModelRecord.toJson], or null when the
/// string names no role this build knows.
///
/// Null rather than a default: a record from a NEWER build may name a role that
/// does not exist here, and silently filing it under some other role would be
/// worse than admitting the file's purpose is unknown.
ImageCodecAssetRole? parseImageCodecAssetRole(String value) {
for (final role in ImageCodecAssetRole.values) {
if (role.name == value) {
return role;
}
}
return null;
}
/// One downloadable file belonging to a model.
///
/// A model is not always one file. The promoted decoder is an ONNX graph plus an
/// external-weights sibling (`<name>.onnx.data`, referenced by relative filename
/// from inside the graph), and ONNX Runtime will not load the graph unless the
/// sibling sits next to it under exactly that name. Downloading only the 2.8 MB
/// graph produces an `ORT_INVALID_PROTOBUF`-class failure at session creation,
/// which is indistinguishable from a corrupt download hence [sha256].
class ImageCodecModelAsset {
/// Name the file must have on disk. Not derived from the URL: the external
/// data reference inside the graph is a literal filename.
final String fileName;
final String sourceUrl;
/// Expected size in bytes. Advisory the download re-reads it from the HEAD
/// response but it is what the UI shows before the first byte arrives.
final int sizeBytes;
/// Lowercase hex SHA-256 of the file, or `''` when no digest is known.
///
/// An empty value **skips verification**; see [hasChecksum]. That is the state
/// today, deliberately: nothing has been uploaded, so no digest of a
/// *published* file exists and a hard-failing guess would block the feature.
final String sha256;
/// What this file is for. See [ImageCodecAssetRole].
final ImageCodecAssetRole role;
const ImageCodecModelAsset({
required this.fileName,
required this.sourceUrl,
required this.sizeBytes,
required this.role,
this.sha256 = '',
});
bool get hasChecksum => sha256.length == 64;
}
/// A declared preset model: its identity plus every file it needs on disk.
///
/// Mirrors the role of `translationPresetModels` (translation_support.dart:117)
/// but is a *spec*, not an [ImageCodecModelRecord]. Records describe what is
/// actually on this device (path, mtime, real size); specs describe what could
/// be fetched. Conflating them is what made the translation stack unable to
/// express a two-file model.
class ImageCodecModelSpec {
final String id;
final String label;
/// Every file the model needs, tagged by [ImageCodecAssetRole]. Downloaded in
/// list order into one directory; the order is a download preference only
/// nothing resolves an asset by position.
final List<ImageCodecModelAsset> assets;
/// True while [assets] carry URLs that have never been fetched by anybody.
/// The UI must not offer a download for a spec with this set unless the user
/// has supplied their own URL. See the note on [imageCodecPresetModels].
final bool urlsArePlaceholders;
/// Which checkpoint these graphs and tables belong to.
///
/// Tables and graphs are per-checkpoint and are not interchangeable: decoding
/// an ft32 bitstream with ft16 tables desynchronises rANS. The bundle's rate
/// point is checked against the bitstream's chunk-0 metadata before a decode.
final AeicRatePoint ratePoint;
const ImageCodecModelSpec({
required this.id,
required this.label,
required this.assets,
this.ratePoint = kShippingAeicRatePoint,
this.urlsArePlaceholders = false,
});
/// The one asset with [role], or throws when the spec omits it.
ImageCodecModelAsset assetFor(ImageCodecAssetRole role) =>
assets.firstWhere((asset) => asset.role == role);
ImageCodecModelAsset? maybeAssetFor(ImageCodecAssetRole role) {
for (final asset in assets) {
if (asset.role == role) return asset;
}
return null;
}
/// The decoder graph the path handed to ONNX Runtime.
///
/// Defined by role, not by position. Kept under the old name so the settings
/// screen and [imageCodecModelFriendlyName] keep working.
ImageCodecModelAsset get graph => assetFor(ImageCodecAssetRole.decoderGraph);
String get fileName => graph.fileName;
int get totalSizeBytes =>
assets.fold<int>(0, (sum, asset) => sum + asset.sizeBytes);
/// True when this spec describes a bundle that can encode AND decode.
///
/// All five roles, not four: without [ImageCodecAssetRole.entropyDecodeGraph]
/// the install could send a picture and never open one.
bool get isComplete =>
maybeAssetFor(ImageCodecAssetRole.decoderGraph) != null &&
maybeAssetFor(ImageCodecAssetRole.decoderWeights) != null &&
maybeAssetFor(ImageCodecAssetRole.entropyGraph) != null &&
maybeAssetFor(ImageCodecAssetRole.entropyDecodeGraph) != null &&
maybeAssetFor(ImageCodecAssetRole.cdfTables) != null;
}
/// The set of on-disk paths a codec session needs, replacing the bare
/// `String modelPath` that used to cross the isolate boundary.
///
/// [entropyGraphPath], [entropyDecodeGraphPath] and [tablesPath] are nullable
/// because a build that was installed before the entropy bundle existed has
/// none of them, and that install must keep working for latent synthesis rather
/// than becoming unloadable.
class ImageCodecBundle {
/// `*.onnx`; its `*.onnx.data` sibling must be in the same directory under
/// the exact filename the graph records.
final String decoderGraphPath;
/// fp32 **send-side** entropy graph (`image -> z_q, yq*, sc*`), or null on a
/// pre-bundle install. Encoding needs this one and only this one.
final String? entropyGraphPath;
/// fp32 **decode-side** entropy graph (`z_q, base, stage -> base0, means,
/// scales`), or null on a bundle-version-1 (or pre-bundle) install.
///
/// Separate from [entropyGraphPath] because they are two different exports of
/// the same weights: the send side emits all four stages at once, which a
/// decoder cannot use, and a decoder needs the `If`-branched graph it can call
/// five times per image. See [ImageCodecAssetRole.entropyDecodeGraph].
final String? entropyDecodeGraphPath;
/// CDF tables binary, or null on a pre-bundle install.
final String? tablesPath;
/// Checkpoint the graphs and tables belong to.
final AeicRatePoint ratePoint;
const ImageCodecBundle({
required this.decoderGraphPath,
this.entropyGraphPath,
this.entropyDecodeGraphPath,
this.tablesPath,
this.ratePoint = kShippingAeicRatePoint,
});
/// True when this install can run the **send** half of the entropy path.
///
/// Deliberately does NOT require [entropyDecodeGraphPath]: a version-1 install
/// can still encode, and reporting it as unable to do anything would be wrong.
/// Use [supportsDecode] to gate a decode.
bool get isComplete => entropyGraphPath != null && tablesPath != null;
/// True when this install can also turn a bitstream back into a picture.
bool get supportsDecode => isComplete && entropyDecodeGraphPath != null;
@override
bool operator ==(Object other) =>
other is ImageCodecBundle &&
other.decoderGraphPath == decoderGraphPath &&
other.entropyGraphPath == entropyGraphPath &&
other.entropyDecodeGraphPath == entropyDecodeGraphPath &&
other.tablesPath == tablesPath &&
other.ratePoint == ratePoint;
@override
int get hashCode => Object.hash(
decoderGraphPath,
entropyGraphPath,
entropyDecodeGraphPath,
tablesPath,
ratePoint,
);
@override
String toString() =>
'ImageCodecBundle(decoder: $decoderGraphPath, entropy: $entropyGraphPath, '
'entropyDecode: $entropyDecodeGraphPath, tables: $tablesPath, '
'rate: ${ratePoint.name})';
}
/// Published bundle: https://huggingface.co/zjs81/aeic-se-onnx
///
/// URL shape matches `translation_support.dart`:
/// `https://huggingface.co/<repo>/resolve/main/<file>?download=true`.
///
/// Sizes and digests below are of the published bytes, verified against the
/// local exports under `/Users/Zach/Documents/mycode/aic`. The CDF digest is
/// not optional: a table set that disagrees with the checkpoint desynchronises
/// rANS and produces a sharp, plausible, WRONG image with no error anywhere.
///
/// EXACTLY ONE ENTRY, and it is ONE bundle rather than a send-only and a
/// receive-capable download: encode needs the entropy graph + tables, decode
/// needs those *and* the decoder, so splitting them only creates an install
/// that can do half the feature.
///
/// WHY `qdq_conv_pct` AND NOT `qdq_conv_pct_novae`. The `novae` variant leaves
/// the VAE decoder in fp32, and the VAE is the only part that runs at full
/// 512x512, so its activations dominate: 872 MB on disk and 2.99 GiB peak RSS
/// against 835 MB / 2.16 GiB here, for 0.17 dB against the ORIGINAL image.
/// 830 MB of phone RAM is the wrong price for that on the platform where RAM,
/// not quality, is the binding constraint.
final List<ImageCodecModelSpec> imageCodecPresetModels = [
const ImageCodecModelSpec(
id: 'aeic-se-ft32-bundle-v1',
label: 'AEIC-SE 512 image codec (ft32)',
ratePoint: AeicRatePoint.ft32,
urlsArePlaceholders: false,
assets: [
ImageCodecModelAsset(
role: ImageCodecAssetRole.decoderGraph,
fileName: 'aeic_decoder_qdq_conv_pct.onnx',
sourceUrl:
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_decoder_qdq_conv_pct.onnx?download=true',
sizeBytes: 3066597,
sha256:
'fa1ca65c52ecb9e1ec43c05ef792ac8b95ecab21dcb6ab89a825b4dad6a5a571',
),
ImageCodecModelAsset(
role: ImageCodecAssetRole.decoderWeights,
fileName: 'aeic_decoder_qdq_conv_pct.onnx.data',
sourceUrl:
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_decoder_qdq_conv_pct.onnx.data?download=true',
sizeBytes: 872896480,
sha256:
'f7714df0ec8cc495be1fb4bad3be0458c186c8a61d87b8487f2e8e6b84b8242a',
),
ImageCodecModelAsset(
role: ImageCodecAssetRole.entropyGraph,
fileName: 'aeic_entropy_side_fp32_op17.onnx',
sourceUrl:
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_entropy_side_fp32_op17.onnx?download=true',
sizeBytes: 67262167,
sha256:
'b7b55b0f6a8a02ec2e8f6e85820c064c741c870c226d276c78df45e83ca1a9d6',
),
// Local digest, for the uploader to confirm the bytes match:
// efcbbc4829a0029f487f17b7b52373c6af339d7f74a6417463981d0778d6d444
ImageCodecModelAsset(
role: ImageCodecAssetRole.entropyDecodeGraph,
fileName: 'aeic_entropy_decode_fp32_op17.onnx',
sourceUrl:
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_entropy_decode_fp32_op17.onnx?download=true',
sizeBytes: 60509540,
sha256:
'efcbbc4829a0029f487f17b7b52373c6af339d7f74a6417463981d0778d6d444',
),
ImageCodecModelAsset(
role: ImageCodecAssetRole.cdfTables,
fileName: 'aeic_cdf_ft32.bin',
sourceUrl:
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_cdf_ft32.bin?download=true',
sizeBytes: 813648,
sha256:
'4089fde2af16c340642a5c857be42f6d0f21caf71dd5b4f32d62efcd41c77bd5',
),
],
),
];
/// Total download for the one shipping bundle: 1,004,548,432 B (958.0 MiB).
///
/// Named so the pre-flight space check and the tests share one number instead
/// of each re-summing the asset list. Breakdown: 835 MiB decoder (graph +
/// external weights), 64.1 MiB send-side entropy graph, 57.7 MiB decode-side
/// entropy graph, 0.8 MiB CDF tables.
const int kImageCodecBundleTotalBytes = 1004548432;
/// Result of an encode or a decode.
class ImageCodecResult {
/// Encode result: the compressed payload only, with no chunk headers.
final Uint8List? bitstream;
/// Decode result: PNG-encoded RGB image.
final Uint8List? pngBytes;
/// Decode result: packed 8-bit RGB, `resolution * resolution * 3` bytes.
///
/// This is what the inference backend actually produces; [pngBytes] is an
/// optional re-encoding of it for widgets that want an `Image.memory`.
final Uint8List? rgbBytes;
final AeicRatePoint ratePoint;
/// Square edge length. 512 today and a hard floor the SD-Turbo UNet in the
/// decoder needs a 64x64 latent and collapses below it.
final int resolution;
final int durationMs;
final ImageCodecStatus status;
const ImageCodecResult({
required this.ratePoint,
required this.resolution,
required this.durationMs,
required this.status,
this.bitstream,
this.pngBytes,
this.rgbBytes,
});
}
class ImageCodecDownloadCancelled implements Exception {
const ImageCodecDownloadCancelled();
@override
String toString() => 'Download canceled.';
}
/// Raised by the codec session while the native inference seam is unimplemented.
class ImageCodecUnimplemented implements Exception {
final String detail;
const ImageCodecUnimplemented(this.detail);
@override
String toString() => 'Image codec backend not implemented: $detail';
}
/// Raised when a bitstream-level [encode]/[decode] is attempted but the entropy
/// path is not present in this build.
///
/// WHY THIS EXISTS AND WHY IT IS NOT A BUG TO FIX HERE. The shipping ONNX
/// artifact is the **synthesis half only**: `y_hat [1,256,16,16] -> image
/// [1,3,512,512]`. Turning bytes into `y_hat` (and back) needs two more things
/// that do not exist in Dart or in the shipped artifact:
///
/// 1. the entropy-side graph (`h_s`, `g_c` and the four adapters, kept in
/// fp32 `aic/onnx/aeic_entropy_side_fp32_op*.onnx`, 67 MB, not exported
/// into the shipped model), and
/// 2. the rANS range coder itself, which today exists only as the AEIC C++
/// extension (`aic/aeic/src/codec/MLCodec_rans*.so`).
///
/// Until both land, [ImageCodecBackend.decodeLatentToRgb] is the real, working
/// entry point and the bitstream methods throw this. Faking them is not an
/// option: an rANS desync produces a sharp, plausible, *wrong* image with no
/// error raised, so a stub that returns noise-shaped bytes would be
/// indistinguishable from the genuine failure mode this codec already has.
class ImageCodecEntropyPathMissing extends ImageCodecUnimplemented {
const ImageCodecEntropyPathMissing([
super.detail =
'the rANS entropy path (entropy-side graph + range coder) is not part '
'of this build; only latent->image synthesis is available',
]);
}
/// Raised when a bitstream operation is attempted against an install that
/// predates the entropy bundle.
///
/// Distinct from [ImageCodecEntropyPathMissing] on purpose. That one means the
/// *build* cannot do this and no user action helps; this one means the *files
/// on this device* are a decoder-only install and the remedy is a download.
/// Reporting the second as the first is what would strand every user who
/// installed the decoder-only build.
class ImageCodecBundleIncomplete extends ImageCodecUnimplemented {
const ImageCodecBundleIncomplete([
super.detail =
'the installed model predates the entropy bundle; re-download the '
'image codec model',
]);
}
/// Raised when a downloaded asset's SHA-256 does not match the expected digest.
class ImageCodecIntegrityFailure implements Exception {
final String fileName;
final String expected;
final String actual;
const ImageCodecIntegrityFailure({
required this.fileName,
required this.expected,
required this.actual,
});
@override
String toString() =>
'Checksum mismatch for $fileName: expected $expected, got $actual.';
}
/// Persisted image-codec preferences.
///
/// These five fields are destined for `AppSettings` (see the integration notes
/// for task B4, item 1); they live in their own value type until then so this
/// workstream adds no edits to `app_settings.dart`. The JSON keys already match
/// the `image_codec_*` names the AppSettings migration will use, so moving them
/// is a copy, not a rewrite.
class ImageCodecPreferences {
final bool enabled;
final String? selectedModelId;
final String? modelSourceUrl;
/// [AeicRatePoint.wireValue] of the composer's default rate point.
final int ratePoint;
final List<ImageCodecModelRecord> downloadedModels;
const ImageCodecPreferences({
this.enabled = false,
this.selectedModelId,
this.modelSourceUrl,
this.ratePoint = 4, // AeicRatePoint.ft32
this.downloadedModels = const [],
});
AeicRatePoint get aeicRatePoint => parseAeicRatePoint(ratePoint);
Map<String, dynamic> toJson() {
return {
'image_codec_enabled': enabled,
'image_codec_selected_model_id': selectedModelId,
'image_codec_model_source_url': modelSourceUrl,
'image_codec_rate_point': ratePoint,
'image_codec_downloaded_models': [
for (final model in downloadedModels) model.toJson(),
],
};
}
factory ImageCodecPreferences.fromJson(Map<String, dynamic> json) {
final rawModels = json['image_codec_downloaded_models'];
return ImageCodecPreferences(
enabled: json['image_codec_enabled'] as bool? ?? false,
selectedModelId: json['image_codec_selected_model_id'] as String?,
modelSourceUrl: json['image_codec_model_source_url'] as String?,
ratePoint: json['image_codec_rate_point'] as int? ?? 4,
downloadedModels: rawModels is List
? [
for (final entry in rawModels)
if (entry is Map<String, dynamic>)
ImageCodecModelRecord.fromJson(entry),
]
: const [],
);
}
static const Object _unset = Object();
ImageCodecPreferences copyWith({
bool? enabled,
Object? selectedModelId = _unset,
Object? modelSourceUrl = _unset,
int? ratePoint,
List<ImageCodecModelRecord>? downloadedModels,
}) {
return ImageCodecPreferences(
enabled: enabled ?? this.enabled,
selectedModelId: identical(selectedModelId, _unset)
? this.selectedModelId
: selectedModelId as String?,
modelSourceUrl: identical(modelSourceUrl, _unset)
? this.modelSourceUrl
: modelSourceUrl as String?,
ratePoint: ratePoint ?? this.ratePoint,
downloadedModels: downloadedModels ?? this.downloadedModels,
);
}
}
-2
View File
@@ -3,7 +3,6 @@ class PathRecord {
final int tripTimeMs; final int tripTimeMs;
final DateTime? timestamp; final DateTime? timestamp;
final bool wasFloodDiscovery; final bool wasFloodDiscovery;
final int byteCount;
final List<int> pathBytes; final List<int> pathBytes;
final int successCount; final int successCount;
final int failureCount; final int failureCount;
@@ -18,7 +17,6 @@ class PathRecord {
required this.successCount, required this.successCount,
required this.failureCount, required this.failureCount,
this.routeWeight = 1.0, this.routeWeight = 1.0,
this.byteCount = 0,
}); });
String get displayText => String get displayText =>
+177
View File
@@ -0,0 +1,177 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:latlong2/latlong.dart';
/// Timeline state for the packet-flow animation on the path map.
///
/// The packet travels each segment over [segmentMs] (scaled by [speed]),
/// then dwells at the reached hop for [dwellMs] so the hop visibly lights up.
/// Overlay layers listen to this controller directly; [activeSegment] only
/// fires when the segment index changes so list highlights rebuild cheaply.
class PathPlaybackController extends ChangeNotifier {
static const double segmentMs = 1100;
static const double dwellMs = 380;
static const List<double> speedSteps = [0.5, 1.0, 2.0];
late final Ticker _ticker;
List<LatLng> _points = const [];
double _timelineMs = 0;
Duration _lastTick = Duration.zero;
bool _playing = false;
bool _started = false;
double _speed = 1.0;
/// Segment currently being traveled (clamped to the last segment), or -1
/// while the animation has not been started listeners use this for
/// hop-list highlighting without rebuilding every tick.
final ValueNotifier<int> activeSegment = ValueNotifier(-1);
PathPlaybackController(TickerProvider vsync) {
_ticker = vsync.createTicker(_onTick);
}
List<LatLng> get points => _points;
bool get hasPath => _points.length >= 2;
int get segmentCount => hasPath ? _points.length - 1 : 0;
bool get playing => _playing;
double get speed => _speed;
/// True once the user has started or stepped the animation; the packet
/// overlay renders only in this state.
bool get started => _started;
double get _slotMs => segmentMs + dwellMs;
double get _totalMs => segmentCount * _slotMs;
bool get isComplete => hasPath && _timelineMs >= _totalMs;
int get currentSegment {
if (!hasPath) return 0;
return (_timelineMs / _slotMs).floor().clamp(0, segmentCount - 1);
}
/// Travel progress through [currentSegment]; 1.0 while dwelling at its end.
double get segmentProgress {
if (!hasPath) return 0;
final within = _timelineMs - currentSegment * _slotMs;
return (within / segmentMs).clamp(0.0, 1.0);
}
/// Dwell progress (0..1) at the reached hop, or null while traveling.
double? get dwellProgress {
if (!hasPath || isComplete) return null;
final within = _timelineMs - currentSegment * _slotMs;
if (within < segmentMs) return null;
return ((within - segmentMs) / dwellMs).clamp(0.0, 1.0);
}
/// Index of the point the packet has most recently reached.
int get reachedPointIndex {
if (!hasPath) return 0;
if (isComplete) return _points.length - 1;
return segmentProgress >= 1.0 ? currentSegment + 1 : currentSegment;
}
LatLng get position {
if (!hasPath) return const LatLng(0, 0);
final seg = currentSegment;
final a = _points[seg];
final b = _points[seg + 1];
final t = segmentProgress;
return LatLng(
a.latitude + (b.latitude - a.latitude) * t,
a.longitude + (b.longitude - a.longitude) * t,
);
}
/// Replaces the path and resets the animation to the start.
void setPath(List<LatLng> points) {
_ticker.stop();
_points = List.unmodifiable(points);
_timelineMs = 0;
_playing = false;
_started = false;
activeSegment.value = -1;
notifyListeners();
}
void play() {
if (!hasPath) return;
if (isComplete) _timelineMs = 0;
_started = true;
_playing = true;
activeSegment.value = currentSegment;
if (!_ticker.isActive) {
_lastTick = Duration.zero;
_ticker.start();
}
notifyListeners();
}
void pause() {
_ticker.stop();
_playing = false;
notifyListeners();
}
void togglePlay() => _playing ? pause() : play();
void replay() {
if (!hasPath) return;
_timelineMs = 0;
activeSegment.value = 0;
play();
}
/// Stops playback and hides the packet overlay.
void stop() {
_ticker.stop();
_playing = false;
_started = false;
_timelineMs = 0;
activeSegment.value = -1;
notifyListeners();
}
void stepForward() => _jumpToPoint(reachedPointIndex + 1);
void stepBack() => _jumpToPoint(reachedPointIndex - 1);
void cycleSpeed() {
final index = speedSteps.indexOf(_speed);
_speed = speedSteps[(index + 1) % speedSteps.length];
notifyListeners();
}
void _jumpToPoint(int index) {
if (!hasPath) return;
_ticker.stop();
_playing = false;
_started = true;
final clamped = index.clamp(0, _points.length - 1);
// Land at the start of the dwell window so the hop pulse plays.
_timelineMs = clamped == 0 ? 0 : (clamped - 1) * _slotMs + segmentMs;
activeSegment.value = currentSegment;
notifyListeners();
}
void _onTick(Duration elapsed) {
final dtMs = (elapsed - _lastTick).inMicroseconds / 1000.0;
_lastTick = elapsed;
_timelineMs = (_timelineMs + dtMs * _speed).clamp(0.0, _totalMs);
if (_timelineMs >= _totalMs) {
_ticker.stop();
_playing = false;
}
if (activeSegment.value != currentSegment) {
activeSegment.value = currentSegment;
}
notifyListeners();
}
@override
void dispose() {
_ticker.dispose();
activeSegment.dispose();
super.dispose();
}
}

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