pax_global_header00006660000000000000000000000064152105225570014516gustar00rootroot0000000000000052 comment=cdad6ea01809d9dfcf05b433c07102512f482f03 meshy/000077500000000000000000000000001521052255700122075ustar00rootroot00000000000000meshy/.gitattributes000066400000000000000000000004121521052255700150770ustar00rootroot00000000000000web/ export-ignore scripts/ export-ignore .forgejo/ export-ignore .pre-commit-config.yaml export-ignore .pre-commit-hooks.md export-ignore .woodpecker.yml export-ignore flake.nix export-ignore flake.lock export-ignore page.codeberg.sesivany.Meshy.json export-ignore meshy/.gitignore000066400000000000000000000001501521052255700141730ustar00rootroot00000000000000result .flatpak-builder repo *.flatpak *.mo *.po~ builddir-appimage dist/ .cache/appimage/ __pycache__/ meshy/.python-version000066400000000000000000000000051521052255700152070ustar00rootroot000000000000003.11 meshy/LICENSE000066400000000000000000001034301521052255700132150ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. “This License” refers to version 3 of the GNU General Public License. “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. A “covered work” means either the unmodified Program or a work based on the Program. To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. meshy Copyright (C) 2026 sesivany This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: meshy Copyright (C) 2026 sesivany This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . meshy/README.md000066400000000000000000000157171521052255700135010ustar00rootroot00000000000000# Meshy [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) Meshy is a GTK4/libadwaita client for [MeshCore](https://meshcore.io/). The goal is to provide the best Linux experience. The app is already fairly feature complete and stable, but note it is still in active development, so things may still significantly change. Feedback with missing features, ideas and bugs is welcome. Meshy screenshot Meshy screenshot ## Installation ### Flatpak repo 1. Add the repo: ``` flatpak remote-add --if-not-exists meshy https://meshy-app.org/meshy.flatpakrepo ``` 2. Install Meshy: ``` flatpak install meshy page.codeberg.sesivany.Meshy ``` Note that if you installed Meshy from the flatpak bundle (provided before), you have to uninstall it and install it from the repo to recieve updates in the future. ### Building from source #### Flatpak Install `flatpak-builder` and the GNOME SDK: ``` flatpak install flathub org.gnome.Sdk//50 org.gnome.Platform//50 ``` Build and install: ``` flatpak-builder --force-clean --user --install builddir page.codeberg.sesivany.Meshy.json ``` Or to create a `.flatpak` bundle: ``` flatpak-builder --force-clean --repo=repo builddir page.codeberg.sesivany.Meshy.json flatpak build-bundle repo meshy.flatpak page.codeberg.sesivany.Meshy master ``` All dependencies (including third-party Python packages and native libraries) are handled by the Flatpak manifest automatically. #### AppImage Meshy also includes a local AppImage build path intended for packaging tests and release automation. Build dependencies are the same as the native build, plus: * `linuxdeploy` (required for AppImage bundling; the script can download it automatically) * `curl` or `wget` if you want the script to download missing AppImage tools automatically Build a local AppImage: ``` uv run ./build-aux/appimage/build-appimage.sh --download-tools ``` This is the normal full build path. It writes a staged AppDir and the final AppImage to `dist/`. If `linuxdeploy` is already installed on your machine, you can skip the download step: ``` uv run ./build-aux/appimage/build-appimage.sh ``` When the script downloads `linuxdeploy` as an AppImage, it runs it with `APPIMAGE_EXTRACT_AND_RUN=1` so CI and other FUSE-less environments do not need `/dev/fuse`. The final artifact is named like `dist/Meshy-YYYYMMDD-x86_64.AppImage`. Useful variants: ``` # Stop after producing dist/AppDir for inspection uv run ./build-aux/appimage/build-appimage.sh --appdir-only --download-tools # Build an AppImage without QR scanner support uv run ./build-aux/appimage/build-appimage.sh --download-tools --no-qr ``` The generated AppImage uses the same packaging flow that CI uses, so local runs exercise the release path directly. #### Native build ##### Dependencies Build dependencies: * meson (>= 0.62.0) * ninja * python3 * glib2 (gio-2.0, glib-compile-resources, glib-compile-schemas) * gtk4 (gtk4-update-icon-cache) * desktop-file-utils (desktop-file-validate) * appstream (appstreamcli) * gettext (msgfmt) Runtime dependencies: * gtk4 * libadwaita * gstreamer * libshumate * geoclue2 * zbar (optional, for QR code scanning) * python3-gobject (PyGObject) * python3-pycryptodome (pip: `pycryptodome`) * python3-pyzbar (pip: `pyzbar`) (optional, for QR code scanning) * python3-pyserial * python3-segno (pip: `segno`) On Fedora: ``` sudo dnf install meson ninja-build python3 glib2-devel gtk4-devel libadwaita-devel \ gstreamer1-devel libshumate-devel geoclue2-devel zbar-devel \ python3-gobject python3-pyserial desktop-file-utils appstream gettext pip install pycryptodome pyzbar segno ``` On Debian/Ubuntu: ``` sudo apt install meson ninja-build python3 libglib2.0-dev libgtk-4-dev libadwaita-1-dev \ libgstreamer1.0-dev libshumate-dev libzbar-dev \ python3-gi python3-pycryptodome python3-pyzbar python3-serial python3-segno \ desktop-file-utils appstream gettext ``` ##### Build options | Option | Default | Description | |--------|---------|-------------| | `qr_scanner` | `true` | Enable QR code scanning support (requires zbar and pyzbar) | | `shortcuts_dialog` | `true` | Enable keyboard shortcuts dialog (requires libadwaita >= 1.8) | To build without QR code scanning (e.g. if zbar or pyzbar are not available on your distribution): ``` meson setup builddir --prefix=/usr -Dqr_scanner=false ``` The "Scan QR Code" option will be hidden from the UI when built without this feature. To build without the keyboard shortcuts dialog (e.g. on systems with libadwaita < 1.8 such as Debian Stable): ``` meson setup builddir --prefix=/usr -Dshortcuts_dialog=false ``` The "Keyboard Shortcuts" menu item will be hidden from the UI when built without this feature. ##### Build and install ``` meson setup builddir --prefix=/usr meson compile -C builddir sudo meson install -C builddir ``` ## Development ### Code Quality Tools The project uses automated code quality checks via [pre-commit](https://pre-commit.com/) hooks: **Setup** (one time): ```bash # Install pre-commit (Fedora) sudo dnf install pre-commit ruff # Or with pip pip install --user pre-commit ruff # Install git hooks pre-commit install ``` **What the hooks do**: - **Ruff linter** - Fast Python linter checking code style, imports, and common issues - **Ruff formatter** - Automatic code formatting for consistent style - **i18n check** - Validates that UI strings are marked translatable - **Standard checks** - Trailing whitespace, EOF fixes, YAML validation, merge conflict detection **Usage**: Hooks run automatically on every commit: ```bash git commit -m "Fix: something" # Hooks run automatically and may modify files ``` Manual run on all files: ```bash pre-commit run --all-files ``` Run just ruff: ```bash ruff check src/ ruff format src/ ``` See [.pre-commit-hooks.md](.pre-commit-hooks.md) for detailed documentation. ### Running Tests Currently the project uses manual testing via the GUI. Automated unit tests for core modules (protocol, crypto, storage) are planned for future development. ### Project Structure - `src/` - Python source code - `views/` - UI view components (chat, channels, contacts, map, device, settings) - `protocol.py` - MeshCore protocol implementation - `mesh_crypto.py` - Cryptographic operations - `storage.py` - SQLite database management - `models.py` - Data models - `data/` - GTK resources, UI files, icons, schemas - `po/` - Translation files - `build-aux/` - Build scripts (AppImage) ## Translations Meshy uses [Codeberg Translate](https://translate.codeberg.org/) (Weblate) for translations. If you'd like to help with translations, go check [existing ones](https://translate.codeberg.org/projects/meshy/application/). If your language is in the list, you can start translating as a registered user. If it isn't among existing translations, [file a ticket](https://codeberg.org/sesivany/meshy/issues/new) and request it. meshy/build-aux/000077500000000000000000000000001521052255700141015ustar00rootroot00000000000000meshy/build-aux/appimage/000077500000000000000000000000001521052255700156645ustar00rootroot00000000000000meshy/build-aux/appimage/AppRun000077500000000000000000000025711521052255700170240ustar00rootroot00000000000000#!/bin/sh set -eu APPDIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" export APPDIR export PATH="$APPDIR/usr/bin:$PATH" export PYTHONHOME="$APPDIR/usr" PYTHONPATH_ENTRIES="" for PYTHON_DIR in $(find "$APPDIR/usr/lib" -maxdepth 1 -type d -name 'python3.*' | sort -V -r); do for PYTHON_SUBDIR in site-packages dist-packages; do PYTHON_PATH="$PYTHON_DIR/$PYTHON_SUBDIR" if [ -d "$PYTHON_PATH" ]; then PYTHONPATH_ENTRIES="${PYTHONPATH_ENTRIES:+$PYTHONPATH_ENTRIES:}$PYTHON_PATH" fi done done export PYTHONPATH="$PYTHONPATH_ENTRIES${PYTHONPATH:+:$PYTHONPATH}" export LD_LIBRARY_PATH="$APPDIR/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" export GI_TYPELIB_PATH="$APPDIR/usr/lib/girepository-1.0${GI_TYPELIB_PATH:+:$GI_TYPELIB_PATH}" export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas" export XDG_DATA_DIRS="$APPDIR/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" if [ -d "$APPDIR/usr/lib/gstreamer-1.0" ]; then export GST_PLUGIN_SYSTEM_PATH="$APPDIR/usr/lib/gstreamer-1.0${GST_PLUGIN_SYSTEM_PATH:+:$GST_PLUGIN_SYSTEM_PATH}" fi if [ -f "$APPDIR/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]; then export GDK_PIXBUF_MODULEDIR="$APPDIR/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders" export GDK_PIXBUF_MODULE_FILE="$APPDIR/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" fi exec "$APPDIR/usr/bin/python3" "$APPDIR/usr/bin/meshy" "$@" meshy/build-aux/appimage/build-appimage.sh000077500000000000000000000175461521052255700211200ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later set -euo pipefail APP_ID="page.codeberg.sesivany.Meshy" APP_NAME="Meshy" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)" BUILD_DIR="$ROOT_DIR/builddir-appimage" DIST_DIR="$ROOT_DIR/dist" APPDIR="$DIST_DIR/AppDir" TOOLS_DIR="$ROOT_DIR/.cache/appimage" ARCH="${ARCH:-$(uname -m)}" WITH_QR=1 DOWNLOAD_TOOLS=0 APPDIR_ONLY=0 log_step() { printf '\n==> %s\n' "$1" } usage() { cat <<'EOF' Usage: ./build-aux/appimage/build-appimage.sh [options] Options: --appdir-only Build the staged AppDir but skip the final AppImage step --build-dir PATH Meson build directory to use --dist-dir PATH Output directory for AppDir and final AppImage --download-tools Download linuxdeploy into .cache/appimage --no-qr Build without QR scanner support -h, --help Show this help text EOF } while [ "$#" -gt 0 ]; do case "$1" in --appdir-only) APPDIR_ONLY=1 ;; --build-dir) BUILD_DIR="$2" shift ;; --dist-dir) DIST_DIR="$2" APPDIR="$DIST_DIR/AppDir" shift ;; --download-tools) DOWNLOAD_TOOLS=1 ;; --no-qr) WITH_QR=0 ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac shift done if [ "$ARCH" != "x86_64" ]; then echo "Only x86_64 AppImage builds are supported by this script right now." >&2 exit 1 fi mkdir -p "$DIST_DIR" "$TOOLS_DIR" require_tool() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing required tool: $1" >&2 exit 1 fi } download_tool() { local target="$1" local url="$2" if [ ! -x "$target" ]; then if command -v curl >/dev/null 2>&1; then curl -L "$url" -o "$target" elif command -v wget >/dev/null 2>&1; then wget -O "$target" "$url" else echo "Missing curl or wget; cannot download $url" >&2 exit 1 fi chmod +x "$target" fi } resolve_appimage_tool() { local binary_name="$1" local appimage_name="$2" local url="$3" local target="$TOOLS_DIR/$appimage_name" if command -v "$binary_name" >/dev/null 2>&1; then command -v "$binary_name" return fi if command -v "$appimage_name" >/dev/null 2>&1; then command -v "$appimage_name" return fi if [ -x "$target" ]; then echo "$target" return fi if [ "$DOWNLOAD_TOOLS" -eq 1 ]; then download_tool "$target" "$url" echo "$target" return fi echo "" } run_appimage_tool() { local tool="$1" shift if [[ "$tool" == *.AppImage ]]; then APPIMAGE_EXTRACT_AND_RUN=1 "$tool" "$@" else "$tool" "$@" fi } LINUXDEPLOY_BIN="$(resolve_appimage_tool "linuxdeploy" "linuxdeploy-x86_64.AppImage" "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage")" require_tool python3 require_tool meson require_tool pkg-config require_tool glib-compile-schemas BUILD_PYTHON="$(command -v python3)" SYSTEM_PYTHON="/usr/bin/python3" if [ ! -x "$SYSTEM_PYTHON" ]; then echo "Missing required system interpreter: $SYSTEM_PYTHON" >&2 exit 1 fi TYPELIB_DIR="$(pkg-config --variable=typelibdir gobject-introspection-1.0)" LDCONFIG_BIN="$(command -v ldconfig || true)" if [ -z "$LDCONFIG_BIN" ]; then for candidate in /sbin/ldconfig /usr/sbin/ldconfig; do if [ -x "$candidate" ]; then LDCONFIG_BIN="$candidate" break fi done fi if [ -z "$LDCONFIG_BIN" ]; then echo "Missing required tool: ldconfig" >&2 exit 1 fi if [ -z "$LINUXDEPLOY_BIN" ]; then echo "Missing linuxdeploy. Install it or rerun with --download-tools." >&2 exit 1 fi QROPT="-Dqr_scanner=true" if [ "$WITH_QR" -ne 1 ]; then QROPT="-Dqr_scanner=false" fi log_step "Preparing build directories" rm -rf "$APPDIR" "$BUILD_DIR" log_step "Configuring Meson" meson setup "$BUILD_DIR" --prefix=/usr --buildtype=release "$QROPT" log_step "Compiling project" meson compile -C "$BUILD_DIR" log_step "Installing into AppDir" DESTDIR="$APPDIR" meson install -C "$BUILD_DIR" log_step "Installing AppImage launcher and metadata" cp "$SCRIPT_DIR/AppRun" "$APPDIR/AppRun" chmod +x "$APPDIR/AppRun" ln -srf "$APPDIR/usr/share/applications/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" ln -srf "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" ln -srf "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/.DirIcon" SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas" log_step "Compiling GSettings schemas" glib-compile-schemas "$SCHEMA_DIR" PYTHON_VERSION="$("$SYSTEM_PYTHON" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" PYTHON_STDLIB="$("$SYSTEM_PYTHON" -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')" TARGET_PYTHON_DIR="$APPDIR/usr/lib/python$PYTHON_VERSION" TARGET_SITE_PACKAGES="$TARGET_PYTHON_DIR/site-packages" mkdir -p "$APPDIR/usr/bin" "$TARGET_SITE_PACKAGES" "$APPDIR/usr/lib/girepository-1.0" log_step "Copying Python runtime" cp -aL "$SYSTEM_PYTHON" "$APPDIR/usr/bin/python3" cp -a "$PYTHON_STDLIB" "$APPDIR/usr/lib/" copy_python_module() { local interpreter="$1" local module="$2" "$interpreter" - "$module" "$TARGET_SITE_PACKAGES" <<'PY' import importlib.util import pathlib import shutil import sys module = sys.argv[1] target_site = pathlib.Path(sys.argv[2]) spec = importlib.util.find_spec(module) if spec is None or spec.origin is None: print(f"Missing Python module {module}", file=sys.stderr) sys.exit(1) origin = pathlib.Path(spec.origin) if origin.name == "__init__.py": dest = target_site / module shutil.copytree(origin.parent, dest, dirs_exist_ok=True) else: dest = target_site / origin.name dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(origin, dest) PY } copy_python_module "$BUILD_PYTHON" "Crypto" copy_python_module "$BUILD_PYTHON" "serial" copy_python_module "$BUILD_PYTHON" "segno" copy_python_module "$SYSTEM_PYTHON" "gi" copy_python_module "$SYSTEM_PYTHON" "cairo" if [ "$WITH_QR" -eq 1 ]; then copy_python_module "$BUILD_PYTHON" "pyzbar" fi log_step "Collecting GObject introspection data" copy_typelib() { local typelib="$1" local src="$TYPELIB_DIR/$typelib.typelib" if [ -f "$src" ]; then cp -a "$src" "$APPDIR/usr/lib/girepository-1.0/" fi } copy_typelib "Gtk-4.0" copy_typelib "Adw-1" copy_typelib "Gio-2.0" copy_typelib "GLib-2.0" copy_typelib "GdkPixbuf-2.0" copy_typelib "Shumate-1.0" copy_typelib "Geoclue-2.0" copy_typelib "Gdk-4.0" copy_typelib "GObject-2.0" log_step "Bundling shared libraries with linuxdeploy" ARCH="$ARCH" run_appimage_tool "$LINUXDEPLOY_BIN" \ --appdir "$APPDIR" \ --executable "$APPDIR/usr/bin/python3" \ --desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \ --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_ID.png" VERSION="$(date +%Y%m%d)" APPIMAGE_PATH="$DIST_DIR/${APP_NAME}-${VERSION}-${ARCH}.AppImage" if [ "$APPDIR_ONLY" -eq 1 ]; then exit 0 fi log_step "Building final AppImage" (cd "$DIST_DIR" && \ ARCH="$ARCH" \ LDAI_OUTPUT="$(basename "$APPIMAGE_PATH")" \ run_appimage_tool "$LINUXDEPLOY_BIN" \ --appdir "$APPDIR" \ --executable "$APPDIR/usr/bin/python3" \ --desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \ --icon-file "$APPDIR/usr/share/icons/hicolor/256x256/apps/$APP_ID.png" \ --output appimage) log_step "Done" echo "AppImage written to $APPIMAGE_PATH" meshy/build-aux/flatpak/000077500000000000000000000000001521052255700155235ustar00rootroot00000000000000meshy/data/000077500000000000000000000000001521052255700131205ustar00rootroot00000000000000meshy/data/60-meshy-serial.rules000066400000000000000000000012671521052255700170270ustar00rootroot00000000000000# Meshy - MeshCore companion USB serial access # Install to /etc/udev/rules.d/ or /usr/lib/udev/rules.d/ # Then reload: sudo udevadm control --reload-rules && sudo udevadm trigger # # Grants access to USB CDC-ACM serial devices (ttyACM*) and USB-to-serial # adapters (ttyUSB*) commonly used by MeshCore companions (ESP32, NRF52, # RP2040, Seeed Wio, Heltec, etc.) # # Uses TAG+="uaccess" so only the logged-in console user gets access, # rather than making devices world-writable. # CDC-ACM devices (most MeshCore companions) SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", TAG+="uaccess" # FTDI/CP210x/CH340 USB-to-serial adapters (ttyUSB*) SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-9]*", TAG+="uaccess" meshy/data/gschemas.compiled000066400000000000000000000013451521052255700164330ustar00rootroot00000000000000GVariantX(XLX\_M\Hxpage.codeberg.sesivany.Meshy(  L{ vdj vs vL$$v8=8ٴ=vPUI.qUvhp}~ pvxf%! vfvvcustom-thememeshcore(s)window-width (i)window-heightX(i) window-maximized(b)tcp-companions(as)last-transportble(s).path/page/codeberg/sesivany/Meshy/scolor-schemesystem(s)last-device-address(s)last-device-name(s)meshy/data/gtk/000077500000000000000000000000001521052255700137055ustar00rootroot00000000000000meshy/data/gtk/help-overlay.ui000066400000000000000000000076611521052255700166650ustar00rootroot00000000000000 General Quit <Primary>q Keyboard Shortcuts <Primary>question Navigation Device <Primary>1 Contacts <Primary>2 Channels <Primary>3 Map <Primary>4 Settings <Primary>5 Messaging Search Contacts <Primary>f Focus Message Input <Primary>n Previous Contact/Channel <Alt>Up Next Contact/Channel <Alt>Down Previous Unread <Alt><Shift>Up Next Unread <Alt><Shift>Down meshy/data/icons/000077500000000000000000000000001521052255700142335ustar00rootroot00000000000000meshy/data/icons/hicolor/000077500000000000000000000000001521052255700156725ustar00rootroot00000000000000meshy/data/icons/hicolor/128x128/000077500000000000000000000000001521052255700166275ustar00rootroot00000000000000meshy/data/icons/hicolor/128x128/apps/000077500000000000000000000000001521052255700175725ustar00rootroot00000000000000meshy/data/icons/hicolor/128x128/apps/page.codeberg.sesivany.Meshy.png000066400000000000000000000527441521052255700257250ustar00rootroot00000000000000PNG  IHDR>aUIDATx\Gy7.Y$˶0!cz !7Z1`eMww7YW{Wݳ~ҩ`p?+W,Lŏ!\ǟk7녋j5[;1U:OZ1Zw*ekrYwlrSˉ _> g>oُVD[\i}M[D*uΘ5(.ME#<=|TT{-a2\.^N]p>CX=?@ UрXir&] \k52Q4a'2W+嚻]%{/Q'a;//獎Y31l9JV)ܢBB%bv DKKWTIaJ{$r] ߑ9EBCF+*DpQ߬k:ebkb;U?߸* \xyUj.oO{Ou7{ ^[]]#MLLt G|M|6lƍ'eAA۾jQTkyA @Dc*U%W BaTЖ"C/X[MJ!1,FΡ1 a"Ŗ3Xz?P(yHQN^f֞m_ʾ"0pҳ_F37x`o˴J%tb1N@1D 1-dL qiTIL"b%(ȭV!zRTdk7d=s4HgQc͊0q"(NJƶw?\t7Gcqu۫Z&pbXfڔYN0 FLIX+쩨`v2Pe_3=Lt^E,0S8Rmy1Wi zy;Ŕ7{' 3U_;gKW{;FWOGrZp;'?q>i͉Yث#^mF=aBT-@O_)Ip2 ;!zjISaa8~ _,Bv]tNE GQks`qL.ڼ {,'=ۆ眸w3AAdHgP6.#>$ `x1|Mz`"KEkJn Pi*! $|E%Z6#nA!-Z uArht*ov-4{h擎;ᔁOnspGg?'ۆܻ$;n% ) q3LØ̄p쇋(tJ%7"՛KgVȍ a / ~S&'"~*5BTV{3MU7G͞:05~5xae<ӿud){E 8NBtjE$B0I'ZB0: ZϿ !2+ !I&\^=:dhd{,gjLsHY+98hF7"dw=$wt`zYI.Э0}'O/C<,?>eBMvGERJFB!a"MJ٪F0ˊʘx+2џx i,8X.1LBBIh;5GȔf7g)Os<$\SqZh d,d̻";y&?Hs1,@RmCbq눅@r7mW>.=L+: {2:8ӴYcg( "Fb1=xH'=QᦀKc6HDIJYv"n=l248۩eU kaD&pr0ƨ\v=LjV/e_$dѵ˴8L.C0R|?>[#G[ gKr}7֊zċ?,վ߻8jmIq0uUTaiU:*bgkDKI$lU|,UOg1zZ3@' wbBo%=V*1\,c%L! nxcY'| 9T,?Q!hh=!J(C,,‰fXgLAB2{x0*tFhOUrK,RA›D-KDO-IėIV⢁Au,N+;;L18U/u<\ZH RY+N<ӂF.DߝCs>jEN.3cUQD-dB$  KD YGk ܹv b-SIVI4#.@|`E"(F.fZO9jl`qZ%kOdzԜ('SAXF,!h Wh&Oj XYMAͮq 9 2i5NYH~C?t:%$1$W8MmT]D1kes &[8m}@Xx꣞C9/r&Ƨ S;+NJ"@ smu3m;*bh.u;dv(2eŤiH6t^r! rV#q}Nr]|Jh]RbEr|[>rG[k%2|@b`akN3"Ny"Q ~Ch&8)#/e] cHǰT'}%0\3IjF$7Xdе'Qu,L$D@tScr*M M) 2b*(4Q% @}RgpVBD1Ap5*;NuTITױ'NNB\:b4Jd֦QׄP{!cj'i>c9сLsRlqFU$Ȥytgn f_?S6.Ahib$IS@*{hSn6`ۇ|n TZh\Bܖ !g%*GTX* RIC[`v)c/f酠>ر``=MD4nbvO(y)wQSDu)mr3'iWə1" !9I&x)XprqC&f=! 8}t=ѕA/EldHm(!0̿?l<Mت陽p9njaD-MVOMrׂ6qBsaB)e˿_c^9첸buWypFDy"4BIX)*.238ji8iaRPD)L!3@x=u4CcKDU,q m]&sĥ3ๆlE+fPԺMˍFbrԟJ;yIT17WZ|u˱|O\MI޹NvS3[&H͈a&',wM yLovv-tQӑ݋Mߺes~l׬qkUN4.C j28 홵^LsLLW_w?/`01y4Q3l |)6vsWN%_|g>S}6'CͿwΊ>\x1 @mJE)$MҨFGw$1mhRzt=\4Ӕ fǞރQc*91IwN/cG-+񝍮P0$H*td.XQpEdEjb&]6ڠi.ϚQa+_jM(rWm3Zć^{;V w]|馗`O}r"$r )"J;]@d5rh!ege99vpۿ'Ǟ/VS" 5ż(bb}( 68)wpPQ5  &z B1PD>1U arX2T_<݃3)?IgerK 01$x39 O$Vq^UV&qU6+mS7fm|xrZ(N-5׋YpT|]H|ZH"'%C2"-d!(%bƒVXAYT;,BN׎fDgĽff&%\ҧl]8)c2&fqY*cȯbv? (jyWyqPQPq%Rյysͣ|0!kau tz\w?O *., gF)T%^ :ჰ@mBvczl;vX0&FPAV!Iou|>OAQ$sLHW7@lRu),M|E樫wcN]كS[ĶNr֥'-LZ!ID8DD憢cWFnZbU=Nm⇫vU"U eRfD#$SoM6\!ĒQM;@Lb|*DP?}gDulX->5Ӣ|L7ˉp Ҙr .oVlT$أJ*:6'&sxx -hK9 iS`vNX|%kllApRP[CuIECB!AEsLXXlO pY_?5^!8i@Da1w2wVg)-4]k 4j]l&˥[1ZKX:|K:ܫǜ:@k&$*"rJI_x܎F]N> [JBEB,fY2>~ĸ[I5 rY3e>+f0J𒿻7ݽ/ it>=qN&ft'㵏zxӐr7L Nezj쫓ӿMj`@Gwv4?y&>2GY*4Ÿ܉-Fێ\u/dm XӅʈ͘{9~"H׹) `%T ]4(NQĆ3oĔ24/=Qz*7nTˠV#/;=e\}8ԇ= t'}Hߏ_hiWT,Sg5#wKWh)1飓JM٤ׯݎ5>+]чf Qtt)C c%-|Φ.deR R{BAD._ĩx0`cf_e\j˟Sdzgߺuh9=N}ӗ_.A_'Sl#6E<ӭUQ| BEcרF>CɯI3w?8anGy[lWIxԻ|:/ q`tIx6[XR=j3 d3 ^{$Pj7^qHDdmKbG/u㸥]zo؉_u|{E|I+Muc-Қ?v s16ߔ1Vm# S7e hxg20hԂ5g,w^<tҼk۰uprvu6ᩒʅt*Qm NB+p1|7gIDt]_5Nupe׵7xb˒3.Y6ǠT&e|y0[܁mxΙsr a=C a@Fjȋj$ɗ;$j@BBʉ#Al^]Ch#+F!M)os[eu,!zR] &-dVc^-4ܕ p}(+JܻsbǟЏo8,/2 O /u|\ZUP+]$z>ꕘ3|uxɿ@5υ7>iFy+wv2| m[銳 ,*dr BmGp#oɥQ^ĪzmG`xV@jC8F͏BL[hDAy*ie}A0ѲXB х]6=?sv$YAէ]u7mzs~\q1/cYN8*Ib8UJn سb? 8 w}uxnxOL~ zt?]u{pWan6I.fO S)(k9ʨS*j`2ƕ[D«bNMz1NM"rYװ 7jذmk껛n6N?f6ٵ~ܽyu]{hy*wB\tJDS2z<Պ߁wwe>W Ӛ0ۛ%>M>葭 [CG]Tq:e98,{ffe 2"!4YU; 4Fs|hm|KH&WʳT] o/WH^ME%9Vx*::gbq媝D]!U{t/M~(:}‰<~6*j&v3,]DQ'+غ7Jh{d_!iBo-2SM2rr8\>=V PEP qb'- aQ͔\5 4эO]@VGOfF=0ferDĀ~էMOCu=]ЉK8?+stCΛ' 1glփщg>*V Mv{.[R[´#& lyvۆ}L_ǂbr0SuVET ta53" 8}tP*(URNFߙd .s sywÒOtr7KGi[7_'9N;vC#P)hos{jU3CO!L5U_1߮{ g石=U4aa_>3pʢ!*^}^\7x{pU5噺g/9w11wr~/ ז/LW6лvLкXs32iO^ H` X+AQ @R fct;mlHe݃ۙd7X4"Қ#^zkݍ#kk'ƂFiư'=#wv oUU} 1 U0Jk{/mҚ볔}^B lSs[lE# z] qQ4Y"5U"ȐIQeI+jrvB/1ZFٍ3c/zܙ G7n !|bz*\!`5!.NX26! ꠽i@J'b "!H'Jߨ/P}ѝqIJ lr0Rg/݀ J4c11&@^?TDsԌ.<>p0 1s1Ku ObˤwaD;K/l }]όK*lłdEnM=l].kId7L @N\c'SiMXF]]BF>oyf8#l ŅF K9|](Z}ゖNd?gvL%3euڀ~ pV6<xӓy(˷Q5d7-+n8/fiAG{x$v)kbgfG>uiPKB? `aB&K K}{0,tM1K|c#ķqg@Y\ ?َѩ~ӗ=!>Щ<"k`3@\9LbՅnq5qH;}]k>e(>=eag657Uߙ漮MYBZD/ENC%8/ xˀ̏21kt(҈tjX`ݘ43':Gy"m5|{cOD,<ăl+~<~瑎]gzc[9}>V:x|k˩D!tݠL%g,@\mr&LC]Ȋ*׆GY+<](%-,\A"*u뫅š \&F dט؄t=Gny׎11XDܟݴvvS%_a([PvI 8~~ul\DI?:!*B MfO+&OK:@j0ɒtܺ[!8AR.~hyõG?5e'-3VxP6>!3jqM}K?K6m+71KEcQIjVED̀n}W8Zvc/+@agBO/a0Q`f0TziUK[Ȇ-_ d['kdlf &5$1"0>|xGۮ㲨#%8''/Y >F䩅{Fwy9n~6=tZGӧ{Gx'V8 x볗ѧ-@wBP+~iZ6SkW3;w;AG~ܟ:jk 䇹RM'0-\Bw!oU:8"V2c]A8h~̍9%!AQ >&$2L&Eup6k1" $V.no|:39lIGQ%k)H &N|沍8!tV {RSxŕ}{G-8nuVm$„/4^uLO=ub. 'pͣp,|%L =neರ$n/5XʼnQmmA+QFiI FphQ#4 8KX*{>=l *=}nu >OT@\U8yIxu"nw!JBQn%l(a*͢ \3XpC4X?Nq g ѥa|EZOa WO>~ ~ 1<N^zuĝo܂&BST} q^6Yx+y_Cu.[~oo$Џ0"2VmQfsق^h$$.݁5J}R{(oǟ~-d`2 P$¨)cBFD('p5mǮaIS0V70 6GM:3zCTW]ycƢ!WP5(2aNOiGah8 \X}@f{ kN (Ӳ$8S``URh)f %L/"rvZpDӡ6Jf'AicsZpJ``C= n2zE!ZS}A>3x܂|Xw"ѓgCLcFޡ*e-hNMw_;F s~p _-ZvH8)7[ aJr.f̣f rHb5oDk!D9. [L-&MM.@pv%Bcc4h"P0U FTd`¾ t`xE?ތw_Yw~i+w~2ӔEN*R LphMs{1c}һpź+!ñPv;drДZC|}a,.[HP\:..Xv^[NөCH>q[O]mbQ⭟ {\vrzw |lm5eLಕ:ai.>GF'_랃{ WNfn|J\x8-xQ] I(A)aXKzyI YULH-BC혋ɇi4LQ?%&گ'q1-E2}k.x<v5\W٪ B/ߍSTgӧ-E_==jqiW1>=X: _;P¶\kW{5|'u:V|()iSzZf\dR1?z9W=e6 T .xF> Md X0AH*Sw+QԦNO]7Wې$én9NpoMI RP(^N@<vOYCM6c3E3y/+O{Mk[OxypR ­ipU1W.3O-֒,r8V&,%*&|y3Tc(t[Y_~6?Mb$~PUJe2F1EkNh%?|V;o5/V+jA&N4&=r\3L4FC7TCH mHT1BKɌQo0m/ p"KwMZxcK|FW?߾}s1R'vI+ :}͘7={պf|HI&=S mg'ek !0ûhy͛ ?rO+},"%/;3BX諘{}XmȲTÒvAq:Vq&F?؉ G:s;{tY7&+KTQ3UyJ (ns)M*χTr>[F_-ӷߜA`r2bpT]7IXr.i~a_Bd04!mwDjNyӗr/z/ gϸ7A}7uE[K3 =aeqEzIf|mzجb3}Zt]{}bN2jHܰTz]&wk݃Sy`/@WG4q~Ϥ #uFMu-F6Y%l,q B?f-_܀{'ZX[}{,:[o?(|wĥ7-`ΑHѵ{#nkwݟ|9M}-wzD&Nz.ZRiT xܡ*x%kVW7G` naL\{($CJa!BE&&L O>v#善+صo'8PYۿs3Ŵi_Ncag]7>+#_o-w[i3= tqN[_۸x6B`!J*&.X{_EyWcR@%ɣWI]QC'cuz?/p[2?n?BAf=bؠ'~k4RL 3c0L =<i薜9E==WOn"t+儊0aemƚX-GϜ+LJQƵdN;k߰k=}br&HuR"ΪC߮!*j ܌䱬uxu8d`i1LiށW 8И~7gTmR.u TwN#;=2}>!󅷝ųҦ̑S@39TE1QG#exU߯q a|"[ej[{&G B@8mbؑ=4*xRwV_ItzjNFg"mP @sc9ብ==C3"IaCi@ޟ>'.ᕾJbaFh~1Y3qr/9\[ P59Ej?ll?]iv"R᫟Z9n-RSW 1`j=r#MT`0"%)n&JCɕD!,SxٳWbfϤZƊҗH5˹P6G(d IqOqIdNQkX~ux3LEμW&B*fvEz˼v6up/5A ]w/Z41Pc[Ë83;vmZ]eyQ)FqD[5 Gtvit01} S.ܖxIјbV瞲"kJcipffo.dz01;KeV_ {pҌBdPOWnlI!_|rwTNRj;]a"E0bYx4`Fd\.CY2 {XT2Cjx"–x)-?]iܙF&7Ne ƿsRCIѥ 6vf{ډ9jۿ5<}QQ5ƈCއn[RR,4.o o:9Y*wFJVbn݀R*-Z2){4f.W%?@jH%8#gcv 0ņXz@O֟$D=#ވ z,Wϳ9ѓ@B}95[:k̊e\N ߫z4Tmʙ7Zm9l-'5EU=3|c5 +]!%)͉6#>i=, NĮkC{~Ac6i/-?[_"iӘ̥11#$>'W_pj]V-nפzdxJ\ *Wׯw>̩Іsuߺtf66Ye Â58}O3YT~sch"3&"iSd?n&Ə&{P%Y l@`ZD=|^.~^\Z5:q|7lƾE JޥjZӻ$;ʦ0B<-!q>~G@4 5hHo?;j>P ![ h9 iT}i(NRx=ڭO-fk[ffK\RY?.UU_և׀ZQ ܎Kjx^Q'aL%F`y x;*1ڵʍ6 Zu`M`5@xҵC7hRe΄koeРҪ?}v{toM81YI!]_7kDH+ĖW0Tܘ BlMV$k\+p]{ODOzl ZzAf@k[$+)^.~X CZU*l1;k7>=EOx޻]Y󅉺~**(aND[ [k w% ʢy>~/{hUCԛLj,VѶ1-6CT^F_Oiy@1 c_~_܅1,_<ޮ(NMOnڊ?ԷbAEW͞"󢎄YN%Z)i;~}]3ԉsgpsa=nky_?Kb* DTVYthۼm`QCkNMyYxǯ\=fSi!KJ}-Sގ[!eO=;&.k4yQv&: hݜ$ a8]ӗ>+в9W+lbh6nه*\c^l,pYZWQA}y:d|nv@;GXҊ`_ 3FfL 5bYB2cRK$Q( N33ZWD ZmGtjQyȋh%Mt _v\48z'~=_3߭DB#jTW$V%liI&We3i7P 6a?kXfq$تj{~DAq3Fz I!'W\Zw I}虑Iֱ_oBʜ5&~-HPTxEMeC{:D-u"+VJlVSa'nh<4~Jz0_WHL z'9/ﻮ2)smt!@Q^Y"Hu>Q2X/aZLfwI 30Ը=av $ L(4d+T<nnܶ5ڬ!lXdA}FŴѯi)daT 滋Ǯ%7h:gx`qz[/&|́i W1M"|s,6ay2ଚH@W0qh[ ~-iԢ> ?Tb3K'A ɴуTM!(,+BZ#?iC/ Nr>6."gFDH|h\CQ4]Yw.=T:'U8`r%Ѵ^] mPRqFp2Z tUd7F»TWGbjr8gFgpqDh2ѼMtNn@7fM f&5s1F c*2U%b Z =} ș|pɦDU7 1< T妞Vg'L6;=HDIBRD$IF p"dQ]NQ&(P팍No"Dw iLr6Pe/hm$vUݶetuOv'60z:~Uw=C* .TPd d!K$-CDbDq26} h2m2!seWnP i;"k#`si|W¬>`ٸitMOrώpر{QLokS[E_5lHaKʑ 7Kw"]( L>"#}=V  D strkf>Ƀ$Er>v).0E7ů*gV`r>o Gpcrbysw/:{Yh/d"38jMdba\.b0." JzKȊFAvjjQ_T;T C *b)6g.yh N$r1Yfc/6NE@MܨL9=LqlhXneƒB+-LS{䀹ܷ]oI@GeI$UzrS S T $ߪQ֖V_<[RV8=;9 `m0kG+ 3REXwݴUrBs[\j̈́hUWXe d|Ï,W8Y6 {VNeW9ǖ&N {g1ruxxXxX zIENDB`meshy/data/icons/hicolor/256x256/000077500000000000000000000000001521052255700166335ustar00rootroot00000000000000meshy/data/icons/hicolor/256x256/apps/000077500000000000000000000000001521052255700175765ustar00rootroot00000000000000meshy/data/icons/hicolor/256x256/apps/page.codeberg.sesivany.Meshy.png000066400000000000000000002341551521052255700257270ustar00rootroot00000000000000PNG  IHDR\rf84IDATxeYU&}}^媮TM794 IŜD1`8q83PQI$uй+W={{+ lzs{o}k#z=z9x!!!!!!!!!!kϩ+}f7aS_ZK s!~X!vsa4ng灠9!v#*1HܡOkF 䔑ʵB 7猾+!_#\տr\Q?|QV[/ +ȘGgCP~R;|@RϤϗKyF3YR_^V?χ, `LCٙ.sT]/_nOЗ֧s/iԹu9~TOdsbw콫ɱCǎX9qoz(ݟg|=!옙߼{Ν۰Kv-n,vxaǖЍg+2UD ]V.JD`E(T ] oS:MiUUW ]|WU{Ιj?~(?{𾑆#u\WI)^Oһ{bbѩb\#V#Eg_UE]3յs|4#.C1ni})aqRqNLVOLIQ7߁c\{r޽7c <ʇ틛Ϲ [}j00e;i-O8.^Wz"l!PA* Y=ng꠪xWoh)%O^^"=f &u0wA*gO JlfC @uxD=TEqFwCWt|!4#2D2Xͳ1sTFN79UC{ɶnAVuu 2D]s]zQE3 u:qdr}ġGޱv㯿\37- /3۟6{\->6/Ob0 cY\/PI5者w CxU{ Ы鋒H7U JEi)zPIoy ^_Z Z.BKIˮ9MG} Q`JQik#%FG7~Բ݉Nk~ݐ`<:>}^V(4!Y >@c_׫Bo(*)c <*B5S>NnѸzm'{7\[_6#g c}~מK=o|Fmx"ͅ(פ,*0]`uZ];ЫUv*PǷ*8ŃUIpKLou>CP.8r2įb8d: !>ˆuoC  ԏ|UTQ cFv6،9L%[DU i(;EM0PFbF-Az-ۅlC#H}N% HH⿍g)h4*F!bXٙ1b ZY;{_>e7]s͟5+^ɯ`/pp%l;mZ".`28||V,?- AoUo@嬋 -ݣ 5x+SIszD0(]s;/tSNq6 G^c7(1 T۫R e <E"4y"+YF8Sܫ*Tn,<@ j~!,h@&f@>#d>l)r3[ǘMXw oy7_6^e<`leܟ{W.L36*?Ege&~vG|f(勌 0&h4aBp>kj!pdBw/,o][6.lz[a vgKgƇv=1޻{1z^qmI1-€Vcm$OXGEV5o q#ScfpH0bIjE@zw^^o`;, yg#LD\.leBKO_x<S CJ͵"H%hKhEoG8p@,.6o}γ|^l`y8_=~Yx`$R}.Z*2 .)[F¼׌K937 ?/f}c ,` d#=@NC'Qlq<вvL6C鏩a)INHhSTp̦ЃGkXqȜug9JC*dzoCHGmvD VٟyU+$A)Tv5Vl:@Qe~ B`<qƙ}?~h侏~msZ8ke]GW¾t,_qf?LX,ept\Pɤ9И0a$gMOБ5#LW!ЀGjKehؒAhᔙ"lF]?4C;֌D ğёt )]WB83gpf%<Ss2|!i$^";ٴG%*K8_zTQE5 'ʳDsB;$- hy0P8MnS] =Y2" ssn-0x!Aq>!G[G[Y20<}oK hkU׌ PPL?jEwh˖go۴}z`mݫw~8Az=`tNj;_=w/8'/#=٠ޔNbئVuT!m]fK_h0O ԏT5wB4N5h{d)jc}b2jf>ͅqJ"τ `E\S^3 uyI! Jjsؿ .> 8 9M=Y25'ϼCZB5ez|hC!4U>ktGR,mn޸!:+x"ya۰y<{ֶqwx^+ùuOv8p*%3t3M{t*T%‘uq!&xVLBLa S,&aE:K56>1ggdukn?ť9X;Ϋ:l>b3)9k5`A5/6Z@4#y >efF]6i`YOȭgC`2F1.+!gV-UʡnSSiaKyۣwqΡ]=F1/.4 xu4 9:I @1f&ęx6- *9QT &Vv"@1u< ` 5L%hrb p"plNsv 9=Ⱦ3(klmuq{XypWLn48lXH0(y`C37Ql)Ћvjyl+) '!,P#@-  7VHCZKrr3'Wڣٰaq|ՎQfnmOo6s~v@`XTbaiB mAe14(K-h}_s =懹 Qo OS̪f`CBL3vƣVz ilhoc?4̓}GP̦yضfr21cM%?*@3X! 84ܨ&' M1iD>67qh=CF(3qHrBy۞LAE%{gGX^813]?8<%tlo_{0Ĉ{j`X~63MC]T9~6>8 , wx"`P!%Ss,=; =<м|cÅH4~%o0b77l+\3 ueG9D{&˄ǫwם[# L #l 4g`jÑnFp2-SRo!}ULrv=!L9h6t[xDRo۵yg=}BMR\[q)ab e2csۊ>UůYqzC9G.>B(9ǔhl5gA*8 Z089n% ^ďd>uэb +7\ABhjcFA!^[~1t[=#;MWу_.D/ OL!z`g΀R֡Ќ4o<b:Ɲ\prw(@HB#PLum}8+٤AdByE*#FM!H-0i6!ĞCѕ``ġ{7t;ÍM2ؤ/E{ e߲9va!zZaFaG[u&#:aьqy WEfP;UzTl]m,*|'Jp<;6l0{4m|Ip߂ ]޻4ѱ yf+ Ӓ[SL?[>aޫm̡fXN =I{Cb +,F_j3ji?qM x\BhV9 ն^oxY 5w^\uUYl̈g @UUl[>cl-U${Oږ`yl Z/ ?pifMזgk1͍W?K6y9)a<8hk9vy["E[CtH.PumI|)Ru4MmSH-d} :*荓/p gAˣc^)8>g8MGZٕ*#,h]DtܤuM}Jm%z,/ ˋW נdk$A/ :.DVfߑh3("hO GC"f.ό5j!<[.`Z'n)atD `bBވ u0_֏ʖs5 SE^ &8(@ǀǤ\eWP/"=;tm0C{^gP8y85X#L,e=6F WH'~Yvn|ҿ4"2)n<`V2hKT`s(ēQƈdm^ Q "xD];NkӀ1ffg/>+]Y ">˯ٝWܹ=ޅ#`ퟦtn`QHؑWaf,e ՆZqÀu4 kg DC=-d}Y G=\<}PBC4V V4>*fsQՈ-t1eʃX]_kҪ[Јs/&~Sj׶&>ܸ3 Df5s6X /8^,QS 7[c3p7Δ/0"}xʴNLk[v 86'?|^UG~6y$̇28<2˚EBHf+/3SKbݗVZ)xyogYC.⹳/|\sΑ98k)1n0 fb 綳@W^PD-c^/YʁjyVhĔ?tX$#fĕxnV|*]B@,D=ܴ[| Too.u*FY.CF3 ~CmYzU*rBTqdJΡc+EM~}9Gx҆h, ypDƳ& NPw5 2J*\ۜ4|m4bø%#a)\zwO}gM賆{B8rpE!*O UdƼ&: &Y9HZl+ 5.1Dɹiuuh~ (M *pfydy6B{DiUNn"C0KJjDB P9ܐYpBcy&s"!~r{6;@.$RlY303& fTgͽ4]KAt}VC٤(TPnS{uRYj8Վ1xGN1Im; 7Uz&4m k{kF*42Ҝ/ip 8 CJyG}=k/;v{?g,m~QX<),5׫dr 7JX]O#溫 (U8&6w%Jz/75&;12Ya4%0;WQ&d%Y9iYia8|P x*lы T4_'[P Vd 1g>¯ϣ'WVVo~3eƒ kFtłiQmd|%ZLKY}hh:#tX7# zUkhJ𩡅 HbKs0Y2ܣDw>*\vX#ak X(!M4|\ e1$Z Sq%[V:AWj28R5~Zl[-csabrb*qrqda-ug\p.}ӷ3y}`E_8ް맘k:jBn7SA`ʖS.pdXi>Ix3lӪˢ%2 Q[g b\ĘQOv\_^9AU@@d9[ qGKqgM-q g 3_g`;0$T*Pd1kuǘ`'(t-te^~ f@#lR S[f$~yD 6ܽYק ZjDYRbs#ia\OPeH:PֳaU^W8|"cu4F~M>csOYܶ'pD⸔-R\'iK=B籢 1d]dJDRR 5e^oyAffKH)YI3/e- E "Q,ڈJ|>xƚSHJwv?7Ǔhyv+6Vdr1̜݈<'Zu)J=~xokYdcg s9*VWG M{\`GDb$zu2^TO= j2b*Л m0D,>?x1{?NYʎd^;πOZ ,IBSjQon-$r2%VZ N~1uzanI搇Qӏ<>J~I]~ . ^~aټ51VV놟)}M+1Jcϡy7NGB\_Bt&kAКtf*yZYaIkg.$s.H(aMaKAw Scν DUonkFf@d̊Y&Dr"!lV 9-40si>#0'qI ]c&hga@Ri4BPAu#a Ҧ2MQ 14Y* @n7m?ٹX0ڛP9}>Z)Cy3 ,*EH1σ5c4JҪ]><.6>F/`قFI*)qQFp-]S2[N 'b8'e٧e5ll^@D ҘM &+܁(Ɨ[+$j:z:n^ 3yvӎ4_oθ̆gL(L'Sj6'pFY۲`[3 Ǜp-~tFS=-- gL_s71i{/ g*cWN[-`*h9*s ט,=7PԛY8yɼxls@qKm,hf%eh\L3¶YхwVC |C 5hy,pbb:3f@ X.捆n]1n,m bX3 Rz9fȅkcrؓ`UP+.Z-ߵAhvӣ|UiNLdzs.OW&sܖ L+(o"ˏfWRwϠ2~ OUam TaLfyA oh#F:m\B^3N%)oy50W ?@;c 0q:Q #Qh_JxDN8,3BeR&)I IK5ٍb#,d4< +SJwt#/aGWBIrTz}25 p?3&7yX_əde)4j(qt3תVj3a 'OG杫 &!EizsZ13s;Km9sz ڊu*ݍIay4~(4o*lyq7#`FM$SY!)'_.>\O y$͘ZFB1뺛b(#螙UO װ]X'g>jOp6;a4'g6Jume7hA4DQR Q"迵f c4 Z"0΄nq˦؇p6s[nj)k@`h1,_NmUTA@AyX+Ouކ^fhÅT/h]?ɱa n*(!gF:)j9;mj #gf;WR +MMID|fL;X\ayO|.3&C T*24f 8bHbա9:C.9"jr(3DFc#@"܌4^ɸ-7"F2{됽pk'# LU*)VO 1Yyg4^m7<+ESRRͫl#QqmXASi)}nf.E((9W |i˖E@h},zBӘ 6;I(')b$i!!|}\Ar cpAV)V=X$8yG4baFؘLAσ],|й[;J}>M~N! NKml6O)MZ)5B7t 6΅-wV*p|/,Ha.O!8gazHKh=Hc>rE>ֆ%Vyy pL3lF ƵH+ߋOU~% oSY[W)͔O>CZ+orrw5ɚw18F/riF Em]WtAaqNET64%צ!AY#7H6,#NIf|!y65x$:= SXDMa{f`LvkQ#rT7lа4BV@`Gp;y[XجJUI(ٮ]qhn5eaV:LS!?vԦ9ErEUtR BCSr :ʦNC jmFhs%q1IPOYSJN ?fH:,[cDg&65:a+/ZqIz> `;U닢6dV9ڡ jfXƩ׬Azq0/l zJ L}YuFV~LY@ӱř@[Nx&);(j :MiCb3!4SspnkTǺ% ¶cL&꿭z++96 d;<'MT7ehIdS?,n^fP173;7(vcuqbGOfܱ ,+rUM1,a.ZkvGp4RSAEᇦ(X1O#22^ kЫʓU7RQGYAp~V nٹLb<ȫ$zl .ؔsIڎSܽ2% äXyzyXaEh䭆BڱiγDDkCC)& hD] at(Xˍnk+t%; aç 梖הZ=96Pwby`y\]N'^V8^w @shٖ;L(T`dϥ#8w1<'Ͼ(~֎ \ z|#Uڏn#X6V!Ck~`xq}Vt4fƸ, NNn%1 3Õ9Ԛ9HBn\{|ۗl jl[P4`?,6T BAjO!)9m~ I3")lVC C`{0F`\fE2x~f@_eŽ5L=zF >8ZQ0h9(™le83 MB9'ِŠ8,mbǐD~Z.|.W<.cf$qhj\ƆQ_U?(<'cyM}s)qEC+ txz#8Ȧ4Π5plfSZ=(}<#C:6;$ \mq[ٕJPC&KEgn>i2ǣoθ{dqmcfoyV|%[_u3p g {>#+( `Iܽ2R R0$D'HW.QԞC "&ffe^ A7[́Lԧ:m[._}cٺ|Z!$]5o_Kz4"-1#9Y4|@Ɖ1<YsZl!? Gk,"fhL%B:mP:&(,.:shp(%R$DYĨX&s/C6[[]1 d'kU{z@L4FǨ71'J|O[=o7槇edM [\}Vybcf =ofOibUNm~ H"q\ASz%9Oo<1n$1Z[q*eu| |y&f /cÌε5˯9kQM~WD}H$ /w,()ah܀LGS3ʢxqclÞ3w%r\!!CSi9 $ƋۺF@<ɺ0.~×=&J0QQoxr''dMzufEۭ9u0X}ΌL|,Ђ..U<~!i{uT?mdL3>jF9{>ᇾll8'HR0V@ {NCK7y竣c)i3 1QxX=ZP837?| #tjo&^NS΃kߖK=5m{ޡ cA2)q2W_sraBqj@2ڂU3Lt1i<7]>>P+׍>n3Z,֖#8&ɦu}o]3#جC&bzQl\Di4۾GC_0옭l bhv^kFhc^q\u1WKXS8za"@Si CzʃWOj^y\h(pvl! vJZA/0}D`}LŃ[}U]klb]'߻(7Ŕ:;^y |U8p06r/8.ĝ /?[ 2: hu\^mYQJR3x3nNeulj2jɳͫ;.Xˣ#7%+QfxB7cqz2l" QC'îiF$$<Ι+>\׬e n`DKHz7<~'6O-MM@PvG0 3 fȬB rAAJ?iCes4jO]6[v" "*004h-b`S XXGog rĩmqe'\ J_Y&T~O(=[`K?e0Y ]BQ{9MOh-5 gm: ¡Hh.דfWBMi`-2|h^B\}N[h?J!$u|R!'+vDt$.ôN fȒ]E ʅ("g&MuYɫxXo5 Eq5o)hCVZ&'NjiOB/P==*'{yJ jwᢨza'[ΧdHQAk[:wJ1_^-;c ) 7YK|c/a36|Q&mM4tMr!H/9f19G,ĺ"$4^37bv(&c8iΏ4l8O*qƀ# ("r:2^ L' 0]fZjg@_U X%lQl”G ZZ*[H͡4rdɟU]JX׫bcǮ->nQcOa]Ա}C3ѨC*%Qg *RXoͶdnHJJr#tF[O%# 2n!@ Zؽu1 (B ?Քp[7cqtO*12 u@ Wf:+lB۽љl($m"8 329uZ'pw~t6ah7jYNDS(RO.Pioneo<ܳe˨ֻ7Scicaf?y}<:cKMOXcޕh ZT{HzdGM:{Hb X,M2I]xnpV{6]uW20\ù /m4՘`{Qd )Sb6qܠ/Dl t!/'5,o:>9O5]C0=%Ht>L`%df3Z2e iTWJXPRT(ݶ 7$Gf4g]騔ٻύz^-Q)K̶͡7 u9u}~h# uiHԟOSj2& Ƣ󔸿citŒ-zmhӼ< fh\^ ,@h0 :7MG-`'*E-nmFMcd=и-UE89fdp#Zx3Lapƀ{fÃ,N]c12[ h16HVܰw&ң߅O1`h{r*k]Kv۴/ڜ~3m=POm$\Ӹl5(s-+d*`h(4|z v#Mubaw`'td,W{4:f /1P9bu״ m~$ӒkXo;¦na_T2%L}?zug DB ^zicZ#RߑFZdžLMƩeՠUj\ۚu<[ e@p",7so)3/;S֏h [%uW3ᚽ63SW5( ~@փaR[F7StNgp;A} lin1TM;3N GlĦ1\rSsfP&J',`̶6(6LDMQZBXWi| nwLxJfh_فZSsR. fP31h%qF;7voa$7ed5ÒdZE'8Efj7A?PeFZZ1-HGIu'a񮻰B4;:Cd`<@ɆkېM^hh?#ܸ.[`h1ml\Qt$lE4Aʡe'Ou!@32 DREM:uU`z&"L)ڤ"QD>he +R)tƈDimës?0N߭`[-itp:/Bu}wU2(Ӣ_S Up z`(0P Q4И2Z_ҡ ɇ?gu- ' %E.s+67^wܲq^h :f0X5*tt,{o'sHuN4t un:E 0, KW?/vUp?Y!<77=`a=ٟ݀d)ۑS̺ 4GoGVS !&hgQM[Ź~ffb\]?_y?qbU+VK(~&bs39,1hʅmW`8_+_S6%DzMZJhwN*r4zltޟa 0bFrЀdgpFnN>2+e.Vѕ9ٽ0y%M{SZ6jg"0Ra lG_PGZj;7xT>7r7䤗f%?uQYOTB$oʝS3cp_028ȶGXYQLPF\1WYi V ,T9$oi Mz"Z Nd^{pWv1$y* /}oik>Y; qΕBY]TjPF. *7:f8<0oKvq۴c!$Fn**O'1L[~Ŧcy h{ht BNB ]`.ʐ=nTTQbᚚlCVb2AiP/BRL_k6 VfJCaQ#f8|HttPRPB.5J@.P3.sP}bě@dΝ6O R54]Q^&Dd,Հ$G=='< ױ;1.; =5y=6,ЂADζ 7"d}#%[ɋf)TiN>wrR*@p~&0<ġITikqX:@9)MS]ƚV207+ >:Rw&1')Tv{T4@9ЅEC2Q{^ݨĚV-CZyf_!ާ-5ny߂\{My413kC|Ǔ'E,dx0)!<峹4Hp`i$ 1<LU#t0Je8kjWy@ؑ#oĘ,S/x$^bcPԔѢToн˷(;v첷ZvK7k;=@oRv57^nDX؄\+( #8rt(^E!!=gsğՈ@b,pW E_=qr!)"P?8urF[ΞgAV߰pR_A-F5o+p^(_ x<Ei*[m>ޝDHɽ*d4{RB, <Zs_w;Ɖ- J4-A vQ癍uHix9Ou^7ŃmҺݸf,ւ.ӕظOK(PH yy<NS/y0))6F+3YmvV14TD!YF&b[o&aT^}7=EgǿP22/'m?vҟ܀G_غPEX uWeywfC/ w]gӐgr|2a(gg۳^1D䍬 9_1_#X!J~Wߎe|ӷc8q2?~7aٱgq幛O3M1->2F ǑdbfUsJ8|#t) Xva| q".4:Q3\/4Zag~p @l?'*rp7aU#Io6+%퓹b$BNy`um FZ}srJ*0>]Ib9sZ9QTN3>8lv7r1͵:ؿFK3F3qy?oMG61VR.gr5ڨ'>jX=f\>s7_^ ^$ƹĭG&z'qGw᫞tv/$e|P(jf/#uٲ2#rQ2\ JVgK^(BXG4E !ȸz/8c-8rr:QzFd-x+9ՈՌcƞ̇śo6 pyd=.=c}O£)2U5UR` m? *(8]BBI3Yæhu4. r۩ּJI[vaYBAZ7P\ƝKoTR}Wދ|ɣ HOz.v4Jd^^_ނ?)b%.5 ϸ xś>ʗQY8gp3f%/!(N5NE]y NG|nd(O.ߍd5VԡAE}04Uji4(5M'@(Z+^ܨx,`!TǥLc.F%ewc-= ʞW`N^ގ@a5-Ks ѥAO]l"Ԫ2PPu^`L.x `V(Eچ)TR_Пp8sK{xb, \]MKxMxϾ whXaRs=W..)L وQC&'/sXUNTF#g2.D6KY;j}D#!XGqv3Euӏ@_Ա٧fgdWr ?Z!x2 9{PQ E8 Tư96RQ@tR(4#MXo\I;,zJKcPU]`J`aVn(,h%dΫX*ƗYj\9OikǑI_.|/7lDڿU;*/rCK::}՛1ttQB0*Z|V44)k}XClE0}Kym1:-Wl?N:9lN %YRn>sLS| ޸6 ~MO}Igo7 'BRh[|;^i.2#21[lVSԥl,êOѻW TYcH0C? pb [yg]ҽe'{|?{u8BqQ32~_?/y,6>--UƐlO-3W{R^hm BvT fZ {>R8OY\'tIs6pzĶjRXR)p2=V`'TЌEIТe mZYF)tdLmI#0دuu#[? Tb.vo3=yw[~УPq p|ͥF_y^=Y9d#R %a+~?tvܼ@QQEG^ rv(ikcp0IhB>,̤?'jD-W LQ0{K{JZ(Mf{O]x/^U0#PT1+x湳b#%±2k`)dnʨcw;"K"A G= 'GnaT6 tnS, }ayLDb=HE`YfaG7)<8BHRuY&:LXm哶#*ph,d٦S*ոH5v[0SR9qzEJ=:8} MU,?){M+x {2ۆ]Y`n:&k݁FűqZh,|oq}eVm̞ff4T'BMiϛ\ɰ Gf0-[c AL^ȾЭ~WoK OR`A-ڻS/}_߃bM~ 5cpV]~k4̼cVُ;ʑ rNQ PmXiɜ`Tft‚@C2*]ٍyחfMU2#RaW.ߓ>5bb{҃T=yiKeh$j~5u?1tH*fpe^Cc|uT*Jl`y^ pyNEG:l$;w89L A/|M0%%89uG~鵸)#wrb`?17^p\0woء'-)*rGv&G`ƣ]+l)M}8?kTij}/}➒az AT'#~7߂W$-?T/bv ^oeYHh\35"W2"r|4qm\sm\>3 #Ls ([5̸Γi*lCt -d x0)lhk[Ueλ"ќ= ` &[ c&;\/BV*6FhUeJ&G]b49Siv䓣^/o>x~MI1}6 %/b iBn>jq<3KCgF:\_k_үA ͵vR5UP>~1$b<̗{2` 8 V1=cۿ#u 9^m@LL]ن`'P-H L,my6iqpVALX) ҁ^HZe0w9$Jpm*} w}̻2~@MĿ/uWܖ Ŗ \(G5+%}Kynw8ޤTn=o]w\6^c868 TI\,:F*ZI0/B|| m#].d{֋M{zI|/upO×]RPeo< MJfuDJv*dbo9 +x֞T\c}N%mX+JrFV娱zfak}UO:x"F?}G7lD=$\"K/@C%ѻj^gnL:N3MZ?/-#HvЃ)Î=4E fZG1he3\{/ȆNl`P:[d(iTȥ&Z8duReͦD)zB&pmr+YV]1U98-42[w,xgw|Bwq^嫟F;k9R||a!.tĨY,C_R7ێ[M"b'/s憀'7.yZ)v S?_LҮx7uǝO>+g3=3_=$yAU*Hh1+&kio_q1w=mC)矽x9du@]tUk(;)U6'2ӗ3΃6{gP;~-k4-pz2M0C`K-vU,+x̥ۋNޣuxؑCܐvINMћIG⛹rJ?X{#A*XW.Zn;˫ſb(0fzEosgӸEme3xj)7ͦg.. /zll ~7߆p/̒ 3(xMi~-MϼC6,=Z⑗ռAWo@3Q`% [2(}7]I"` ̴"!YJV?;lQS1k.srD֚`1̚P0M_Y(ANL(QY4؛xVKTƜBkj*RSdDdrC1[a{M12JՖ_u5B"=M'q\}Ъnhl`}NJzﶣxnҚ&>;'-&H! W`2$:n5^^vS&%y?[b%ɘC܄!~57 Šp߃y.C?oĢIO<nҺJUZ^[)W^s6[ҡ˞z;}3EgQ7tcfLVPcA.rE[Ұ^f,9=Ћvz<9J@hZл.3ԳN뺓 ņ: *9t,! <ܤTve4_̅3XQ0ˀ@4P(qRÓlvV csz*}dc٬a$.rd++v_u\kr66_=0J^_X,<Lf6oZ sGml쪄g[_a(Vk77ѽ'?:c;6 Ƴz`e\5fjA:p{ƅ-y-\H3jv 8!f{-/FzI%6+l;0u Dn). #RjP<@_ {oQvL!~L)Pp:`60k)=ASPoSÐzU-UZ?=W Vr8X~`xc'W /} y-=I^MI1k>)gxٛ|TFC;2)_24ۧ=p& ')IfN,ygTC53E0=p#f?! RbrTF95#7!k:=ٶhY)Ε~1N/H.NϞ 5e.}hȸZ嗵Pc:V#b52QPo[9.=LIJzƏM&ZOBB Qhgr@+a /[J_Ǡ i/zeE'[Vy /TqW˅=*,-"}1?")0%GUe_q;uKa^XH@XэLjoXv)9܏U`N̦oy>l\V'*.*9(0~ 9x2Kg'Bnp^ś ZKn0u6Vy`C+/a6W-8`GSu7 Ȣ2M\ge+}<.rFa1+ e \.rQaMbOe՗mPa%J%-h>]39V/7֑Cpj@MXC#zcöiV*r^t\zݡV%Į\~Ӣ$x *M)l< d{dO,z)⾃Gg6 Hk5gr9!)(Kz%JZJeVv&={ɾ\A{%Hy2QN,)q gbCRӨP6[cd!MHܦp7v 1V4:;oW/ycf=2 hZ3<+?䓸en -C ;۟`62;g?Q oVS @//Hѐe{ڜ:!zXrN? FhA25ose5.y[O9؝GeAQCyE4~vv,w6Vg#nƚشᬭQUFC 8s<0KnIcK]*r4*+p2?.>j_LYC0"s9Sq<[`O?iiCixXHe{Se.bq#w+`$< K4=OQm#+$-mu ڏbDWֺOgEj˯WcrŹ R2<]ppIkj`e&)g𢧞]%]̄ԓ~~57X_:ZKѰnM-}o,7x3-,[:9 ֜(}ߔ)SR; B$;CEX<%l} O`jd] '`)Zmn@rt$ieZ9U$g@ƪUϞi} (}&ֿ- =Z0~BPL8o[' ,o}mwޏZ6+(yr6z? XIjלwKOUǟ6BBayO)Æy84zusu;0`DX7{&\ ^6\<\g-|pTXDfaN 0I^1pb\Q|,D|ɮԣl5WxJ,D8/e2褶9 %f^-zȚw9t!H $1(K}I2T,/`Ԝ .LA@}+b_g(j\;xҌoYe.3QbJYW_5r˳/x΅`NMCƦA\/ix?9T!]@@y~\O-":v_ /xޓĎ?QZA4@gQӈlj~AO5 >7ߣ/Fh.D1*NWѵ^gVdjp'V hR;%#bM#w&N4j{)-n`+q6iXO FZMrn51.fG'AzHfѧN&7I4 OCqߵ_.C w}dGᤣOqzk zZ{Pf=-~~|$MSISxw,f`n20TS39 d^u=!s (*/arrZ"횹_ eM,_=/}O%-p \׮T@aYtklv"=i>$GYArLo=ipǑ9{PJ/UFQѦS]g LcNj,k0hZ׃4 #gQQ$Ҫ[o0i˙2mZXC^رUdfmϮ=8̔ `q5cfIGw!4㣩z,)Ū&x_V&tw[ݓwb5fZZ6W  '3 XkRyZ3צj:3%GlcU֊oƽV9s""%rԤIc33E>t)a]Ȱ*;1} ]3ٔ3i\o#;S "X |j[e2]HbS V/G xp 쐞Q }~3iS j)kЬ5KPp'rGف1GA&.)vLa=c]wpmC*ϕcJJ KNa{pϡUXz}i!AO%P3R'glSBO7 X>>e\P#fx:\x_ #xs\8eM̙ vF>z*ku u1e,YpNz c4bE~I jmdwE澓&{F9kvNV@ E^?Zv"tI@<MLTa*\4Qԝ2RcF R7n}Œһy=&h 54ߟx,OwOq 2ǫԲۜTrf3á#) P4Ӌ/(HKxoLޫ, xSwv$ 1ڃE&nItzSyO|_{Ss4-ZLLRŏނ,0Uqn_ZasݳRД3qIg2)~*v1?hŊk3j WnC_B)@z=n> NHb-GҧLB 8ɞ1>5 Պp9YM vSN>)G7O@ X% x4 3#s ڰ[|L|Y3xx' s|I1]P8B!EXeCLx34.O޲W ϔ=",{~/c=P|}p˝]u ?ѻ*ᗾG4i;ōWZ/#͋!_t,k-7܊ht=-p~y⳩BZ9h!7Y`|\||\u}Z\{YZ)"CZ̥:yІ@ G82X䵢ԔGA(FUyj+7 h=5_ZM/1E5SVРt\-뭿}1bfe*3j:PcPW{OamM"rZ{;9Y撯& |R}i$a"SA(2zŗ_UғOx:^%f d֮r`>Cwb5nc*5Sx:W_e1fkZqo~~W07x[,c qdI/hi&$@uWF c![zQn/w?]rv 6MS3i3ׯ?b IFx-,w<[E6d'$TQ-^,-65٠~ r#5yxvMTT@Ņx"@؏_q2\ ̂֟&1w-Xk1azA u:{?{bpW\"Xr.jex FP=Z_.YEK")+a]Mo,${Y FcC8oxEX%XU]]c/}GRzdPf$LqecZXҎIS SQrFȪ>c36ò?``c͸;L-4ZR}>}5d! ߨq}Fg&y-|u۬%\Hlp+F{D!AT B팴AhL%K(%N&~*!GgVMh$ Gv^.)h|Z5ܧĔ-*m&\<}ꍴ~yixޏxRZ Jz;yGDTu;w纂"xJC!J OUNG >zC B&+3unKIF\yvكأ0^B\NA90ijheT=N( pFp xڶЎ ʆ[VvPz);X"y$+i2 Ĩ\ v!6ҿ=|0!AXG7EزD=z8w|,=ޅIj긮h''ky03-"*%O_gcWɳϗT`0Xpg~ yY{Ϗ_†ta]ε;&xʕ$$T'y|pc1ȣ%9vnf2v" T "Nhuz uw"*QrO՗>]MZj.CہC;±""*UdIA~IjN{ T7tf-s*㨂_I&&yD M"Y䢥?yL׳蠓D(qah!pكeߋոgҪi |Zfi}'q<%j pm(μg LZ8i뻴n]mK~ms/spyrK+ǿf~E^3d)~OTtFP%ٽnpw|w_Ȧ\U |@ ɠH6ݿc[h36/eݱI]\x̃>ٝV-x$n F*{P8z@Ϫ,#89-ht:_=TZ/ Nj̛ rӇ|2 ԏnPǡ“s L.le?p!&Lnmr%:Fzd9+F#U`U~@! '2|.Sv܍ٍTa(Cן'M?xqWﻭ(rm35ⴭS:G+rw߼/_~/^95"4ߓ/ۆ'g܋e*M=W^LJ@+ʴ@(8d`KAf:؞S=m;a&0] Zr]sZ2trO*zJE׾X |Mu d @YV̩fW XlܒW Nt:HD^cœQ$d%2uZԢ&Cʇy]S(ճNw*J-!Ι,,qFj|*kS~g\;֮r[\G.(@\[ltR(cȬ(~_^ S~Kb]0n~X\Zԝpz߂/`-I>\}އg]y6l^v{c v⍷:MP[?6o,Wx;0k6\yj̏"&0lDы-؀jj;eq Oһҭfǭwj jayW7WwMYׄA\ܔNꅧ,4JlP3 e1B V1jOQ3OuzGB<72Ҥce`) c 5hh617C4rSCnDׯVyإmTs7TYR6LV #o ;8 MaܸG2n zE HEPEJY,UfDE!gIjP^zHAY1'`tA#9k!\tED3yws'KE 1o'kBV499 `FB"di.ѫſG#vSHr TVpB-iCAD k֠Zwtwۤ|/:?HPzDDcEuҒkOGx}/~+^Z`tY`3KCywIN BlF Ò-8!5M1.Q`ODC'PP8[؏6 |0.f# S>NW 1u+l-a\Pp.D`! `?4\kn2N- ͟[%3088)7Zh"7ֲ.;0YKzʴ:sڵhL,pӭ{1sNi,m^[GW#FJhKdMej>wAV NE=w)厭.n^pΒ[,ǒwO=xrꮻbܺBpMGWm)/^[$㱜ulGnW]'=t<)<fmN%o+X_9+#$G^CFnɾT?xc$8ʓfHnv]ۗ\OՊ-F7.Y7.\ICa7y ݺe:ݭaumO@U ԏVJkWSWzuu^mz VmSp7{,QLpB[f[ %ͣ`6I}e_0Bn 6*X1#tsrJz$Ap%=e_*mCǏ᣷i!#kd(F՟蘲vE2o&9 *.^c.U@y 0(/{%J)pfn\~G֣<)㌭lKH;~z{ge?h̏k`R_K5˭ƽ^F twrd\S|zsNL`j?l2~+q`)FUImEIfkb^.]DK i5`ā݇z@j*׃$׊AY,HRdP6O#`QDA?k |2aC@]H2YSILW˶\m߁ø8-)m`;P}Ȏ46ǡ5ŵ",8Sޖ*gos'!F6Wcu:-P YD0FR5FʌEV^I5UbIvESKxn,׵`^Lt{٦MnZl[?~L\g:l%(` Ч)#*ډa?~ir@L6/WWemg\BݞX]PɰY& 3k%QS5 <VM$ج&k2Ǖ8w >_}*qn\0HZfL#Zr\@8nVo&_~Uҝ;E{'vIi2PFܪ<3aRe-zG9c̊ Ֆf58=5hd>ViҰ\6$3[pmLpl7Xb҈A@رuQhiZ{0/zIip!5TB=pЀHEE~G]֋azZ|n]1z7Xȇf8'⣯IHDyۄ9}0e|Ͻz-82ǧ{'*Qm/|]Opd-ɖ9 .C TD=,$},f񁏟 R6/|6ǯEK%"hŻeffw,$Y3P/u5d="/mH"D uY o ݛ>W*8 >,yso"P^cqhfU!y5S|S%łkwf:SA!nx֢M3{Ј&q$mۗ41ݍz<6X6e,_5;:対swܾ7|zf.D?a̖)jhDA&P,zgN@Sx٫xԍH|j?>p727wKT;kUT>/n܅&@31> eh<85浹ڽɠyi_)ҳWꭸM aOrSx8ץL% 1f @ n3ÀVG %4vRJu=mEU6%rjD͖/tpc>"e hKsB:ή@m]ݳ+p7ӉD*$^|r5`FtN4Lѐ(L5.ů}ί J6eL[t?~?~woAII_'(0||p̤zT3LIe+U^꾋n.ȌҌ P Vk&Ggܬ$1Wƻ Gg{K>ܷ)-jq,[s9~Egav҆+{;xZ D+ك<%!FBӃ*tǎg1QRS1e4.%]m '*;pS{Zu$*Lэ<=M U# G+"nu14`#1N#ЛD#N*ѠvK,zX '1YڶNd=7M,{I]zv!:x_:~`>czMZd5CwjUDTx{y+y0Rp4W~Wށ#a'6x7諒p+.ډ/<9,ʙ״h0diN>Y5WpxXD KS<{$T;.gG⽷n)_q|#y_ n42g=l_W 'D#f[?Hָj¾J4SR<MqM͋8䗬TfiPF\eT4= NdכkY{~F1YDS5zG^UX%Yeo'y `6yă)D+qv́M#Lei 㮬(W\,VvD=Ϙa٥m)XXpbD<̴bp}x>|왅+ 3ouɼD#l(_Z/{M8=_no~yX8=q$ȿ6>v6YG/t h-=fDznS+55^,c' x??z/_|8z(|1Ob^W%M8aB+I8qnlm l&%|c RhώBx9Uc[H VrO5t65ek^%Y>Ռ١%chjT,m 6NXV+Y;u+TUG$ b̈Ʊt z$9knm>t=FwYL퉉]g =$*L8. =𧞂'_ ce{3֟y]j(|3R[-{⃊\剏88[g"N=/ BRW e;3xks5HAibLwupoّuІCCE]$Dy`c 2NwcVK舆6jVZ~f՞@vUcCqތFD`>vq"!zzD.<ʴxz`F7dVU0%MOs/>ZC2Rf#eݜǠ6 dsYRbmREhU [s7U=$JN0=ne\uSw[z 2pR[PEEґ5fP0x׫hD 9C7ʄϽ՘y"3 3*ʏ4ȠH v*jߍ ؂"9$d"I*R\o~޳wヲoP"6uWo5|kkyf6ӴIߩCbĴ9}IQ=;HgVl2Z.J p-gD24y8\q)|cDb1>Cy(ZJ \<6'-2B{`X_oя&Ŗ xvGwxI&O8cw5[x8L(ɉiǐѾ@?2VH+?n;{3l}rhO4r @$;hUYxn:B3P'){kH}3S!h]M֊ri&;3/PIjqފz@ X} ] )W&'&y D2w^$ڋFIT - ŋ∢hۣh,n۵UB$+^uLX`Q@4Qx9zXT EiN Cx41^LJ$ަXCKɎ'0YLͿG5y0,'q҄TPU$8zW+RP%zJLZ)"ŹU\rZ*^۵{suo< Y.UR16 V|N=xON7)X7ڰv-&O߁_xU:>1x/ \#? iя}u/6oj;j(Kw1ΜOD\Ж̟fmѨYAdzz/Ps}/{ɄGN8ȄbTG ψ=qǰm>[,IгSA SQ,' E{}/d 9-e<מ˄3b~V=* ]]b%IJP`GD F ) cac%ET)[O37YomJF`)(Wbj0qBiX $yK?onį&lקeD2G9^ioNf X'O,n&0:#0Xc$E#M`VvxDH?h>~c]ze`13G8ɂtD3BV< @R嶓Bы5H8UfPcN4buqq!E`"r\(%f\f$Zs"lT1Wn)曑@lƓ)3~yGW`'a7_P\2㣼*ZLc͞hAž {p%BcL)ٙڱNN@h$GIJORhg (fIQE!+qtgSt@{Oj}Nqg0X 4vfPMXOyTUiII4I༳GV);uQ*ȃhP4(Dn^AB« 6.aჿD5~d!-*Scg:JhP(Č ='1rиqε>q:Deroٔ;ǎp( WT`5>boJb|7Z&M;5o;|.!ys\z<ʅItXRqK3GE V{nOog~<Nل&, o}{4 Y^+i/tXPB+}aq $R`9ܾ/{۩R͘ziSxx"awpb03 ),|nz Wc'%%1V(ÃJ$F d-獍1{YVhF."jxpzS]t"}W+ !zG'] 2 NܜsҪ:{qks+{vLD<(541v"%41H=^4qi3ReUh)r{sS5˚qI5 a#$ #57O})Dy]><uRo@ ̢@~uKx\tClN4pf9Ȏ3XA5RTYEAK$lg% ڛ+񦯕d}{_nܵALo,;K$VZ/!gC>5Oމ=uK_<&M#ֱ[VkIV]XLvpge/^s(шBfu ;g7''^.8#^m|)} N50V6s˰ЌRokU+`@h"pΚ嗞W<6 JR"XǓUiU|Fy ;=EVg9WBᄞ)uΚ[@|Z'HR/0S0cp+ТTPJƱ :DR8*Ǩ0/fKiFWCRr,S;| ofřK {t˾l(/1HKa5M?y8cO{p7r!Ko.&^+etr:SE lY+iyQ!Mb)ō/nkJ݅ ;!})x#@lHavdsTOzY  K|_i;p6eMUm> rӉH']q)BBm Ox+{٭Xhc^sa '1,p\s rHSp5J)se^~2ntSqek!"W1e}6O^tO%}u-;Mk]fp 29BoujT><_*=?3#(ß~V,a. 6 =,MqzM?K^ L" ĴvΓ>$饏l= visy"g-EVn#I3YNJTa\ј+8RD Yi4FK灡%c=ox>ejuuux_F@Õ>Q<2hFb qEuJvnh@WRqV%8o ~3O&@ZޑBAj(BC>sS]o~8O]#-%ߨb51ـ_L 4􃄌QH5oxUzQDSg'dݝknnkIWM}'wФOka 69WX% PC"3 7R x[ٛy ``>YOލ3G&\IPre_5;b9ueU8r0gF# 4<ԕ"BsrPnuͫ]~u`x&u +Hђ\'9]R䂚4+C*UD,>4{^:;$K|$n6B :G1=ۣ].>jTcH.D~^?sYLyqf:4bsF#_U bQZ37\۾k}A'sB7PǡEjDeC.u 15`Ǟls^jq MCy3ŐR1rU9DM-!Y2Uy&`O{$Y7vJcq#'yO;g/⷟Gw8' &ڼmWnIvk yfJ'p3x8_(0Zױ 7O5d!5` ޗ8!4 /}9TmTݱ7cٯIاHǦ^b"H IcЫ9Iw"R/G0Lm=?[LQSO<5x['r aU3b7-xF8y 6Vw cBAoJK8d͸NȽ򣜓(7OTs_tDQws)8i\:RwF8UQo~5 *AEueirA ND:D 84'[[@(Y cZ% rPb倄4bd0Qi#P:Xl꺙Yl(_\"n?g-|77mgC+4Y>q~E<.z ^+kӊpn;r tF/dZ#}N6VYHHWBDt @Z~Oʾ4f~7I]kF:;DZ0̽Fyr{,G$睅v%ۧbz֟ _ߗ4TP׍j80FahiP /E@w!!m'}uS3I6*0?~S'0+=Ï!9t;Y= Hʹ矾zi'DeP<k|-JWYdfźR>O;2Z#u\_$F9\9(rEJ $Wը=Yrep} QY Β3MY\|F3NL~ETJ0o˸w|zx8r}˘;?$Lw>7{vM[ q+ 56Kh )UD6NN9M$+D]iM/HA7JM]5„ƿIp1n n@`e9|s#ٖ5==˷&z.[KD~]MtG^W\y9vhtsEwǒ~Vt+v\^0o}c0>qXS`C^vG@x٘ڇغ06`M%|9бw,zPcxř>Ae2/p3;)IuyQ *{#DV,-Mg˙5 9g%l$|7cvC.qe'O,NM0Kq&u,3}O?IDb'N+66gW"W]ކ{+O/W 櫎FdL@T;\[\ J.?v~O!EV0k؟!>rEv vS1=[}CbQLcAwI?$"DKl's<3%w2 AWHڂ]{a\>C$G`f`;Ee3/XKR:jǝg=ڟd>؉c1E&To>d3 hcߌf)Op:7sj4( )7L =< Ń8Vxlx_bʳ&Y<)|1BfZm v[Áz[$F,c|s~de0rL!)\ i| V-i5x 4EV$.k, ǪFqs^BيVϙWʼn9< x/>t%~}FוF !KPp#jTB {.fP'^K'MPUVZ3PCOMJ8LP̯H89à D#)\z*db&β8-1S[ m~3o~Br)WXAE;Ir($~7ӟo˱sqmEQJ3fP bF-Z͟h3iBB;:\(J_ϼi]QM|b}("TأIGJWŹh2GbA Z_. @NA2#d/΍(<@g3mg[Wp%H !P9~c4mi9߃áӽoC ZCw )ű~"ŒrK!:hJ0̽';i)ÎqDS`Ć D\MXL"u(b F%8brF:6ЩL\(|<4(ȑ3Hwb 4@E/) td|P. {J2f AlMbl-J:R)/iCd7d<ʛʞj3fW,0)(O?OyO%n=w<3izkGPǕĐ{䁨Ƶ_ AyTk'M\*ʚ +1Q?ќ2w?)ǃSL,[ە[ Wq3V%J򵆍 p!q&u9$uQ;걹j8 8;8\r)xѳN&\oulB =GIIɭJbHv56W)]6 rǞǟCC,:wGrZ% ]@Tu8}]zzqSiOr>,L's 䞆D9vtzk ddW 6?Oڔe H^^7yOţ.8=S[EO=;s R@4<#Zz4Gb1&($} vk;u& X\ a\tXho&x-݄;|&;?ܳRȃ\]=֮ѰTu} ٯEOFnPą?#5ohM5'w[MiйޠROrٵlؔQRJ'4Xz^.e[WMHYB@g59Y;EV"/ܢ+pcj39j_Z:6CkD}>QsZ8m4˟!TcSSh\&׍xicJW_>-[ޗR NJ-YW&`aKCdKr)$n%Mduif<@au1.zi#_`yc×Dϱd\z \lYv>##*$cO+ к+ghiHqZ]h>:>5~p>,lCgKI#7beGûy"))2M0\w]63\r'>2wSMK~d2^v?trG?HC MNa{q0g0,SIfu7ͯ;6i\8F8e, >V+,'9<` ^Iξ p4eҺ)Lt8!HtLf(ΠhL^Ǟz1Spg!Y['tR!&C,̯8L5ٕ6zڂGEFlфHy'x2\ 5%a*9.C~QB4ylEE0=9{"Vv|'jΗϺ82X2Drq|HmN6 +mt^ՌILUel/l@{ʏZ; ɋ[Xl-8 A-kq 1BmIVBVC$~piaXn5WXCa FMdaKjWHQM!TZ33AjI}qRp$˽c*O券rhF%݊c i9\3m;גH-FAH#ka/Y[>|:~UHGJ0Z^*^yWA<2CaP։fjȍ[Xcc'gB5J-Iȫ_( ^=\/X?PzBJm.L+l2'aYcF GA"*̋^\ #(Uԑ':UCrJH@O3̙F t:#;pq"JdDgic:xJ1Fsoc/xv<8A{0H[^;]ÛjA^s/iv[>?+VfMOx|vrv+h* QzǗ1Wv'C/\*hh^]BB)ݺk \~ܹU*:jT4PeFTl5CCC PME(JKq@NO1dr` Ck]EP&݊Q l6+IR~-8`XEtIy<8&@ڕ(a^3ie'W6єOY[r[Ad7[nI˶>l* } W_eKL߉.y 3EGԗC%WlvCdD|ZOSPEă {s7s!꜓} >͘Q"'ƯV}eL#v*AGqͷc!HΔwrgk[J&J{DNz&(X\(e`KyEeY;@X1;\^jQLXr._-UvЇG,p08egLSe|q|1ͩLj3Õ%2QCi(gbviNl,ike}u{5Oa.ؙl. 1?rJB5vfIjJGSS* A;5C2*trB,#kޏ{wgE9ī՚aAY7ߡ=s˸j2,u~?Uk، aȲJ:Zέ֜C 1:J.ZwžC8s anW'?\23+֝y59QjERT"4c>1Sti%oc{i^ҕVmZw<:)sA6d9M/|31*qv'Ξ;+$۾q6ܳ?(u8rJl 'B_F30J$.1sI$98>Mxɏ?>A]#Y VwZt\A#m3NEH]i|ۜ3DD&6ޕ.̦?zbQ=|HʂrаJnc|x֓^K}uֺߍ1, #ȿJWrnHU Q " Q @I~PN}:m\ŗ?rJ34AICMrxi햴jOּmM;6;nag! S'sڲ}?>s>N>#DkSy? FyM{Z2jG4UgJt zȋ3z΃x : =JÖN^M1OceIza lGr5u| P|,[y!stD fLDQ RH$k|wlί,i^STET4kKﲯ$n'(E#f,T^Q'^lu,XTe_ű^Uh|fouE]+,ɧv圀$?,°8冞8hw3N8=žȵY etI-qE~>~{߿Zq4ԽhB5jj:4}.= ;bɟ0fftR@v=uHat؍8<^iėA>B܏HK4ed}?tIp 4fb'fktP$ ՚G"IƜ05@׺l\6+=Aylt7wFqYkAsI]L_^MSRMg%_|ӻ0& dtH&*Z/gP.&=cs1%t+*6$ zl_Z~,SX%\4D,0E i[~P%Ϣxc!G'9b qѩX~u]Yz( P^oyuݮ%"o^pgPKM HJ$j:͑$d`^xIҵ#lV#-:,W'PdBx6Z睱@a9 Iʴ5>".o;?mugv&uPTaR5 ʮ9*b]D)_-ަǒcp'{oۉcLәpCg⒣5S\eAhD>7%\= #G rF ڵw]x]r*- Ŋ[3/=>C?xz0<._E:ӆ6ߡ$隌dI/(hPXԼ`g|2t巒#/9vPG\\Jtj\̀st^?w:XL6f j/feA^ ɞHzfVZ4j5aҭ$!G}y+ҸS<+ml3. D7 z*̱hR~=c@:nد71\%~zE?޺+ |s2)怬1%&P_Htm7Fq֙8e]/m旰 vo~gmDwO^yNA[ %4 `rm*9O @"ϟ;-3 !K"p( ynU0ƕD(YpnǞ?ϘG16 ^Ŏ}\~] pF݌+#q@9Ybض*.<=w=YX阘 9G GmL$S&wM!gS3y]onN=^\޽}`7=eLڄQ/"^li,u\D0;pR=_k?ir;6]32LmZDH {H&,]:|JdIrˇEog9&,5[C"wqn\D^p#*1i2?2N %PWdΔxʋd|^$Qk5TnY \֨:7mX`h,msZXS7,]7Y~AZZیwF{]j{cQs}t Ui歂)!) [#i>r.͘v-`&UA* `>SdNJ;~s2)o8;DO DYIQnn>2B9崭 hqY@ƪIrsb=IB S`e97?/VV'vY5#\cE %%Ў^3= | j^1IAP7:A_DP<Ē%&B6IOҨ qz]~v6ɹJ4*xĩG0MI(cV"$FNG'Ug!8>:0+ڪrL9k}  NI<IAls#8W?DfQG>v}*gª,'V\u HI'3:]P|^ɋpkzލ ' 3!MbKizuJz5t69 QTE9ɞ4zk`1Wz2J}Ƭ)+F IK@q!"Z*xь:kTt石vm Sxmig_)TZ iYڷ9d )EW P2a( E;{[ZªV UC,`c%@(PR!WM ?vƠ~-^(L{+կxPxb}",t&eojy?c[,/m[~H a*>T @r#!^1D2!/3V;ı#<k'1\6}`PV+4 %-vnz(VT }X"4LS>>l*5c4bS;FsOr/?@\Pȝ|"dÉ(< q;PEơ$$iɫ~*6'iYMUEC Ðs>=n+ql3,C5zg_h7p:7e1G;G~O CcecQ¡,LOmuhc9:3izC9 +M|0DOԌ.m=*zn54:JE&sתvŅpq v0`e. CP rvREА bf)ٖl8̷9|rŽYgK=kFe?^T!a?YPE(^ f0#6T@6 Ft"ݙ6d'D!/*Aҡ'Ir񥀤m%Xd2a ڨ/:Ў i1kR@wY)K][abo2F?Q^ȉb YYMD)ڻ!(LK J." Q5rԩ0{9 (+8+HomUpSO9Iխ BVFVߎW+ge5!Np;{vHxuE9hCG' rHŘDK(guji]F=@ۦvQğ>P1ZPBTqV 2:pqqV+6m x9B="%Ԡ[^Z;h{s4F/㦹rЇP1v `~ޗf;iG!%.>եe[ N*]kbn%~3'R6Պ?N|luh<}d)J)ɴCA؆㸎[,a]}C'ߺnZ='S馚P4Y=pFfi\IA@LYB'|*@*J '4n:"$&M@:k,<YKyL&28Z |`43帄2cZ^a(k Q2-]a:A? h坱"O4v" u6z9ҟ ;m Ub yaf8(kT" 籠_E- .SL[#\եCVW|ytb\-X!V1煳ͧ(/|`ҵӳBy[VA4n :/GL5&NAkteXY>!9tnE#h H'ݕ#N eX~<rP Dp8uA0f& X0#-rVUϧ0-Ki K@# XuV_|T?Kql*y#utߐhL N!1c}@-War4{VE1,4!$ߟ,8zPE*hGPGe1 DiX+Vws/ ?86BroF\I AJCiViLNMo";VQG1 ̦³5KB-cu~U;@;>B " 6axNI$b!.J8|'/*D+#tv[w6*yq<ZfVŝQĸ}e,D(W`@4̚k%V7Qbʣ*@9vC/5!==C J`dcePiNQ ٺU|ZFf%"/vZ5c,JHIzԜ;[)j`8GDx*RBE_AE=Dafh$M]R )^.E\wzo4K]Q | h˚[bޙ KjD;W*4N4ʱڥ،g 6[4 xeBTҙu/ˆQHZqT6w*o: .b",T07N`'V>M3r )ھUzx̚I#ZG $kyXygP{\8PCRaAB/ft1?  -GS_G=Doe[ތHZ=`[9yqWyקq/ 8\" Gv~: D;TE/8$^dӰvl2:DcPN*W9W$ 7H+g* µ!ۙSNbj9g vGz |iHIIUMK)4vp0&La9|Zu >!RGh!> hhIeS-tbμ^#$*#P U<I0H8fy3 ޔP4F mX>!ތq1ˇn)™`SBNr*! {[]u?S":-պn q"!ᣑ-#%L)q1F9[[Y*Qǡ34QBB [-LhoeLzc y3꼂7pIJJ.> GOj*Hξ puzѪE)c00U ${=VG F rFy1=YAqOSA/|H mx[i qa?c^][keiW :F(_e 9jD䌑TEYs(dfIE#G4YDOyjxeN'+-O]k1( * Z s3#|VC$t݌9[$)7*IL&^q)B补!kݖjqe/kYVHX2 t$xüAL xKP}1BRΗ0ɼc97B<4"t_q>6g6Va!y'6 UC*d(Ϧ`954O': vnۿO~^g;xχGrM~ƫwA2QƩO4a'@)U*tG!# &W#! y}f(Jo5- FK@! 漂Ƃ^zHDDsV_=Xrr sk 'Ux?|%X/>0CA1!?fz?QU>)NQ^o€M)P W6QQ!2]z W& otBΐBт,d^TŅCz(J M#qϵMoMMm|Uh6E?VQ9kbIvP]%/V2J5:h@3Y(8J>"(h_|g3# cTY#Q4sCgA#/4 F&'d, q5armqa@mrbKf* 1Qr]Hq)oCE !ITzu6Y^@S\t+KqZ*-[k6i!c,Wԓ }g y'Lצ+^ryAj㽣0Gc.B3 3E//RNT (0 ,,/Nƌ( A 8#MB+%^&f^I(!3=졈!gV]TMĒ!ύCͬd@MCONxҺr'nD&7E)EH6 "A^43++&ڗVY_Y\ZgW@g}pj5 D|ERԈG1_HaX̤_bjSɼ 1;Kw 7zьeUّ͋ g- ԒOs-il>/0zAX Wf NQx346l04GU _Pazҙ6QjE18urƯnXcj#xqh QQiBKqD8] u+(-Fo.ɽ)gc/Z|2j)(sb!W? ~V0͌ګ2/ìsEIrāo[WܶˏQLkevb?cvzi?fV]&ȱY07GTROFH͞xu23ȳ>NlR. y,u#"&t:11;= dB^y Wjc^;rryIi0ipf 8͡Sl+#,W+ 3D%\RގCkf$gacP猂Q;P\*E40d$=ӓ̟)BV:% eM4'bff{ۿf5xTˍt[::!q,-EhZJ$I_Y|MgUrأ^ԲБ^g\*broSY`9yqEuDhT VFEc06us&-͉PEc"BE/=c{ީ_;cV-M벴՘ttfi3bEG,??A&c#&QS MH@J:5ls3Lhڣ"t⤟`߷ڿp0\7cgruF\38 ] [P!V+qZ_0(X MoʦWSe.K)9gz :Rִ I_=˰e:F%nvQYsKWkVdrnh]`@LDkJ8Hf(*2a~QCׄOPӽ2r (B5jo}oJ-3|)aE7$|<mh"|%*k,Q(~*(#0"-G֯bms7}n߰c+hDke޶_xMO5Q$"NYzH09gFaA,wD^DTCz[?f!v߹o+~r |m9oONGG#=,/Gc.[tjocEܐtEےը>FᡱHTFbrډvu((:oc$Xkd/B?13߃iw'LEmjQ̩ FV JښNU뢚XYXD9ޱĄ>|% GgL/U@YpیA&w EPd_IhZ^jl2!&i ;oUeHyxKrsr^ӵkzKVvsWҗ񽸾 _O߄C ;&\tG*H!@%҅VFF0Ky)P :"`a{y,:!!BW 'x=8PAQHI 2V`*!WT߃21-e=zFzK)J㔲F06.VĄC@,E!d'eߜ,+c .?Ͼ f<*My)r:V13X;w]l={u}@ܵ% ص@pxzjsBgΨ[]Ii@{._֬(jd6dP f+-${P% k0-l gK C*0%;|,B`S$W9Gu% ŕD$sXʙ*?E[o霃cPlX,|8+ +.QoLfzF{cخ> W`y!dP=[9 t4̉k&[L Cw]o|Ge߃{._a''=wu{5-*H\ ݫXB ѹc啖%V&B~0ۜ@2r˘WĊ2(Q3, $f'vl1Y^n=ZZ^(詘pBp"㔑&@{`(e }DkΦ]qd ͏ جvIѵ2sà=ky6oNiCWUZP (ԡHHTk'Rدl뺷N׃"5~]S3OtM$$J'NQ=(iUlj3&ntk]%|q'鄄ѡ\-EI%!e5X #ԌC0>,nh5/˜݄t!M5ְ\֮bߔ'`sP&(l>HJ. T^teę)SJb(偋 hz8վs+rV XJS1V~JU_c&u~opޘQmu}E0RDP$cAdLk]D)!EhW% ס2ћF[_Y5\&Q%='Ֆ@h#'%e%r;w_Ww>8̟U+.]|s]w" GNVh!!.=}T nYyJԮ234E#ڎaG%?9υ9cH9Zst~ՎRfob'f);+Kyi{4Ch> ?jdT,<㰄$i0Ysw31#/J\Sͣ̀fIdLTA"(U~h bП*dFm\?)?'VYݻ媷Z9쯯]kyi'0nK 7Mt##MF}׼/pr7-fQJUo&`Cq~?PMK 9l$ꀭx$#_ _Թ&q4<l䲈jETf0?C l+X"PL6L#X9Thń1sLWI,u@9j6fSn0adq;`vk\$ _{_޷yueN|XotltG\7 A?b2+ϗ[Wd?x1_d _m˳+vaޑnɝvh)cI+9 -8{Ecy!RGh:XVaUL5Kb!:_k7%"3#FW`;*WSX%GG[} цӔt;El"!)sS2FA0TX=.l=K-d& ;"aa_MQ@6r&jHd OpOE5Zs:nj!NKvn۾ݷ`޽lŎz g yȋ{ybWi+Fж 7hhɛ+!@.ZJ_E,@b"f k OZ# Nn-DQVʖm%-OERwkJ& Rj (ĊVNE[,lu .9\U-šСxMo{8 3@8ܳωt3{t+3, o8t, v,_z5"?pO_{'OyegGFqAItqZ'N5֖!(nEA>ݥПd8f\vyT{]Pvd2e^u= Yiy[S|!(r)D" 3k2u< +~~UH*ن`'#cNaʩM.-r ^۲-.A[q"#TdqdFug4Rq%":lt;WnٽpX=t?s﻽ F+LՌ:(BE$B(tCڬE(XW HxيSz 9xp~6-+T=xMQQCup:duؿ`]u}~hMeUdLSGa@gjOQ0tM}?QM:q5y~u6CK+_Y8w5 ?kp'6ƦNqd|LK{ѓ}3k:qug(Cuā~JG ?l)*PYґT&J#kᢕn.QrН:"yY6^р9jYB;DMUdaXpC0H" YHĢ$xVΕ sQ&jBC OnA{HU]m~ynV[ZY{`inİ|`a]sGN}9:7Aj3#:uxB{kJ0&;Hk:7Qj elظ  m2P/u&=+jsrgW5ɂL5=C bP'{(AІ,{Dv-Y& qV}QQ6j^׻ $gqsR˟Kx:B@swzQ}~l? r,k*5NZ\\'ix\~ǠII8B}Ҁ+~lZN/-x\m/.!۴|1U: -6os<2tOO>nuںЋM2j+f8T=E\EFg`u"vbXC!-:Ж"Kӱt9'I( KIM'&*KiVhj4~+kBn5<_!2Ȳ&Db}Y|$ٶ<%]9Kw!,M\7۶U pNuA4ĉ#i{[zZ_q 3霦,ytZu;JCZ;Cd FR$n÷]FJ3u$PjL7UuMjbל۱IDZ[6cP%Ӑ>'q $6aE IOzZޛ7]Έ'̻so}c~8GMg5-K'sB ͈uu Nǜb4G?s\8Ps螞9uD01љC23?{Ou}>uB5ϻ_̏ \OR*B yeBɏJB(aN9ODJxi'G7W/î5Gǣo ]O,%90QR23̀$kW ':z-hmi 鹤i<{4yuͬ^էsoGO5po/<[37($a!P#F9hbR IC+!){h1D%. zz 9sؘW q d2B@pژH\>Ctcdz`~,9(ݸW&J 9Qz5<ЛsfAfC8pSi58ј%LUӕp'_qI#^?Z ``(@J2,^7bhDIPb߼ #|p468$ew͸THћ^42b])ffb<}c'DIX(FZ!Rx gf<]/;}Ah:pТG!$S\-!vJW(7JcUŴ!4ٌHaVfH`NĥiZDH/a)<CsF耡-4EYh{w)1oP7ٕ]kM˛ j\k&P+FH-qyD9CF<4 M9N1wJ5^$g-j~"N˄I?3*QRGsEQkA{ 5r}OA5[@R!*]U9xJ_0%(1d 4NImg X4îS\Kk6 /рG!qq;i}JQ Fj;C<%hDž}ڦ(M51/L.(P]z$PM fx7U7bCıfr5%pPWCը|z}hm"J0^j|4+3G!| rl7pXӭqG3RZ7-P9 ՗lSk8dZ- K|h'^nǀ -р?$q~H7.(P}St#:z#^,ͅj ]VFY@$r2 R3i3dc{ƆBUU_/o=\"aϟtI;aZK{qlf=X_-Q+Z&aRMF?'߅c#ԊlŒV%bJn90&T88~>>L6l[qUKhppQ`#I-m#Aok(Wuۯ; ̀5Ɂݽm/U>w5LCB}I@)4>M23QС(Id6_w}* ?EsOy'ǘP0x-yU5C{JAaU+iQpt`bM Ӳɗd\TS%jd}?gl=a+0fz03E[sۄLQ>03} >\NJޘ@@͏^<r=Q̽2%HM@8#aQDDs>|Ld6<'È"Gǐy*|'Ȏu% cҮʺw6&@n%x]E fiFW:yW=iUYk{;ccǎ =+B +^ ;ƑInG: ȣ[IӮ{%+tIEg/B!@B ? 1D8{%(bϢm'KI6}%~'ؑK/fٳ3%4ĈId2eShx,?g1-Bdsu^px2|đn\[;^yDYS=BAim@ܤF 7) { ^UaΑ]6! z<;D.9'w~pNa}$H,IENDB`meshy/data/icons/hicolor/512x512/000077500000000000000000000000001521052255700166215ustar00rootroot00000000000000meshy/data/icons/hicolor/512x512/apps/000077500000000000000000000000001521052255700175645ustar00rootroot00000000000000meshy/data/icons/hicolor/512x512/apps/page.codeberg.sesivany.Meshy.png000066400000000000000000011147641521052255700257210ustar00rootroot00000000000000PNG  IHDRxIDATxyoYo~2˩2k-Qjɖ-ɭ@BeE"0vCap`AdBRzR7]]sV{w9{}߷eA:zu qo}kv]kwݵv]Cw]kw!v`wݵv^;v]kw}]kwݵ>ݵv]kv]kwkwݵvׇݵv]Cx]kw!v`wݵv^;v]kw}]kwݵ>ݵv]kv]kwkwݵvׇݵv]Cx]kw!v`wݵv^;v]kw}]kwݵ>ݵv]kv]kwᵱݵtNj_չrN/7gm35?bm3՗)jl?X>neu5ߛeZ^6,^ץZ[oLmˇwji/w[?lZ>?oϗ6~ւ~3},!l7?xvpC?|v;n-cl{|Y}zmϘWQcZ{^khp^ob~~uWp6by{msY~oK*91xv~֮hK m 港c?h|{x6h7saf/kOp1}/z~rfKeػ,ol_o uhw}/>]߻o"_3V.t2-x(s?v"-"K)2-:y[k qvٳ(Pz]62wvrzdQT$-/ryQ2FbhPuj`<e(\{q 8ېƾ-m:,2hX`Z_eۍTD{QX˶?l>n3b=<,m/s<%;yQR>FvGx7E>g^{p[67ڴxpF9;>(G7ZD?wx7nm5}s-+ދ^H0] wAԅ$d] ER,fC/uPwp a|``!Q BFv{G)}\#?B݆n@+ 8V%Iw?(}F#<莺^=3N1ZC9;~ Xzr{]&#OX>a\- lm5%V p|'o9ϴB p xƹ+B+ &h5vwgHB!տS.cLmJ=f &zJ#QҸvi՗ c2r~k~XG)'Y+ŗ}rf@Å;};u{vzo~g[88*7_W]kޣgp;?/:k7|bspݛO? 0&fŽiռ~_PCbNN.Oyzyvӓ~k~֛/|yz ow{ۯݎAx];^>{?='oNןk7 zd .7jn!A`BٰͧxQt!,VDlHG4 nrn% ˱n"B誅VMxI] ͫg WWBaMLB:6S ҧXUFz|(*Ń~.1N?ƍZ h2'q؃i̐}Y:v"@Ȉ0Ӽv&F*"1.yk߸^8gzKcKriՒ x[|Pɓo8VȖCg|^_`o8Md+•083%Ì5e-z|v;})~Ms6sdoO?FE$܂cODu"湁]s6rQt6-[oV./7.N|W^\ͯ'܁ww?[^?\'?ɣ>en-|j9Zh=TW/R hAU E"z}R2B1\,B{EBYt xKK^tpW<3LZBmP _`F&Ѻ.ť HGK `xP4^ +>bGm+/ ż]=)R#^p*׃j rmۅ9/3\jL 6ޏ\{"]U=rq擇'o/s썯];:_mw^;|'&?(6G7?sƭƝOoݺ.^ShޞBnUIYL}װzaqv ӳ:fbQ 皞+>fkR:֚E|δ8_(xЕ:Ԁ)e)d#2|-| pl(tRhl*Ub]ʔTWH;Ƹ1#mE> ,2JW@1;3*#4C~jv!y^zPb-*"Kpp-ҷ{;,` )WձX۞B:P#m1~ {6 8`(.@+x`^*fAG0*FBj+:ޤqEԖq(q;ܳ%bSjCHhi~ڄuv) yeM 21}l>6.IǗU!,l.yl-Yj2h>[!8.)*"i)"rZ1c6\@aF~W2p0&HwZtV_SD[#--LID׆l%1|``&JA2k=PA]>z>v7H0b.^w ]xZNՅ$2땩y LZˌ/Fz!FW(ydngFֶJU?R<́I&ܵO\FseCk\N7*?t?F_c@*.4J?(HYq} IɷYc5B}'bRۘb 7@ cgz(rQ6``Π؛m}b-vG=;o?|ʯ_*'/wխ`n~On?S{)Ǐ&k6\_ծa|WJFO.5KT_u}`R$gú0)[XNs|nÖvlCh)B9҆UڲK kT2ɖW&3]A{r=W|U> 1;@LdjLʪ9[$D[{*T*j.u\<,#@DMit%` W@&؋tA`AN#gM ǺL3lj +mn3~ϸ8\=)6ey۴K?.m:t>ڗy/k;\զ]?||?O?џڻăm;?~zR_f]=щmdQ[q1-ɦb #Щܢnn@`XL$ziI6Aj0=1n`^i ”l,$je(d"(8eA EI. 0Lz)@S~N+`|CkY@!ފboU< [̀G4-]avf(XV{W^m]>~@L4;r'ii ,9B)1=T>kɩ4G*vMECEuQB|au7sxU~}0]`u }ZYÍπVU(B*WF<p.Y`˨s,Gg&Ɗb fA")+ǠH^5wzp2Fg%p~ ,>;s{M9]pkpٴ#|xv4m=}ʣo/{׿}w^}^;şޯ?? O=<:QuÀ'S;="gG9"veI0#Gi%Hl|#`BPkqg4}FZ" "ts |('TSZW%T(SH>e5d3[G/VJt-6:2wvex [UK`Y(ʉJE ~2i—PDIFKQBnxm2ŀ('wiFFyꪺ^k +0ft)JR2^e[V\،ẉ1 6 <8{w?!B~*#R<֦o5f 銟06 ;$ְEK7S,M1+ӭ 8 ,obz ۸/XǾVAany?\\?{&8/.e6`5[G/}_;myu?}dGwS=zyNb.`K{!Z{N]ڣsU`uEԟ~e oWE):zVD@pR?B@9UO'| VjFCApE WD8+NTM%PiYDFJ4`{~< +O>*AP B?bO>RwFu#oTa2BZ# FC➣\R:YJw#/5jK:6fOa{4 /(0/pk kw2EodU,P+!7bK0&F l㷌ʏd ^֮[%!2JIZN\fJD Ēu3 1$,Č \y8! gp(V4%$\3 by:مdVKU@`]6ܨdgEkӊA!ʉ9< ,dZoO/:moaznvoA a{+_77ͭ׿뿾yٛn=w|O?.|oڳ:.tϿ\;O>SߵV d?OZP%֯TG|Xź&b?ϯK ;^w(|}3 qҬA!pоTf>ψJ9%:BŽX AguF d $#`0kG1D(aGrc((\WFVŚ+ RV}9>R09Ps2C_1E1X2Уbl+郀 V0QI`X֐;4樗SZrv=-l͈"k- PjXTLFYͧɂNRq>\.PpcUyG`~bn1&=-x kc/Z__<(s2޾hJvɱ.psnuw~^@2vQrg+__[k#^7_My|g/r1%HEMdXzx Sxi*w 7V.\yb`>HIEtV5a=xn-E~˰jNDy~,JR^G,f"Κ_C +3zNm} / U?`R*|t}8(س(X)airƌxxIp'T*)ԝ`_5`.mh,t8 ggU0ae dy鏞bkJbbfR uBjWFuАsV0qQH.XM5+"<Ϻh` ,D&R`աCEhVֹ@{7c55bvTjzHSA>2 ҥAX<5DjSF2dOE#ᚭd\(N{ dS)[dvgPmVm@?pq m=zkk?z~/o+^>?/whY>\,= {s{틠IL|ZnLrn6:þlܳ<.-QxBxڀf6,[ ?_mDCxLI)d"6_R45yߛ $|BnK`&~ZKZ voaȧatJ;c`P9tzeAH: fk` QatۺW,6*YƖ&,új28nt8@kLx#f}*їLVc[)^3K0)E$Z6Wr - w{}53KCq99fT>"濥u٬͔ܸv'&#Ռlp v:2ׇ*+/R3AvSg.wW2Ff^ڂCn%&?[w9q㧇Ov{^~4]kgw/|_/ӷmټ7NW;Z(߾s#[|]Zmġ< =Kk(-Ɣd9@PHP]洤[]@VeDSJ9Ԗn E{XϫVb0.P!8yԫڟ;7[=Q'zPBsXQbv)˙4mڤ i)i=MwOCgaIpG]8Tc;K4M9@C{aаnVu$w5 *^ @1q4sFЅ̵i̮ +PYsQU0LlζKZv* R*K P)( 'M(9,y?؍z nSr=+{ F']Y}ɬ;0sgHm]+03nXl(eN)hN.=8ZuLZD,‰*T6~͡p #ExS0,RxxrӇM Vt MGX2tʜl=VƏN,XIpq A1`BL7cgO{P2# :%[qOP.6TZCgScT"0,=iX+hrj:@A 0 7jXG Zg+Xs@؆}"MmmYVJ*k3\^On›cj) L h{'7ͨ`E;]iƉlLpa((TUT(GZb۠gT3$`3UsJؐua!| "@c`&yklzL'\Fb˴=ߖ=?~ލw~;nw?go];c/[O>yS_<϶;\Xo-4+/cZ@!+^`+VKZtƅ3&4|ʩM^[ ʟB)+(H'CKK1zW8GT3jTE]nS<[Pi J`.JB .Rʢhz#ӛDYˍZ~3 |I0OO%MWtMYmu57|TK0|NkuvB[n#Lšxֆb( \mTƠluXi"s{N5s KMd3Sc˱Hk՘cr.[y.$cYR35z[B4*22Un33 0bopuiC5P[`Haל/}+uO9:e&bRZ*מ%hp\vۧǟ's7>7O^O|g߼a%ǾYl򓓭ꉽ͑㯳rXؓAb +*Cju] "gn"ѱFkBT]䷖0rrVu`); Pi{p*rQl IF^SJ%fiQ|V8&-7w)CN2.MVzoZ sӒmM4@ERx&EMIFEej?c\Mϵql+R-)B?UkT!9SDJA @7; ru6WfC ז_ݰf_z@J_:Jk^ﶲm-bɷPXɾgfBXv[ I\;qOL\,,2LKK d z͚%w4B] A(j-eFˀھϸ7NƳʤa°'Ú2!f0k![<\%XD=Dzo]lcam"tgK<_:v{\ҵw^yW}~ju?'O{Glټُ[>wi<'K!ґ,@x$@r*RK8}沎ZҘL e8ᇔe&E*~P5E2XuTͤ e,ePY},pµ f赴C#CZߛC$!sц 2V &^I%Ωe*Mq>V0QW!̬ )`4rm clN e`0 Rn g}dɒBπ1!2HCA`Ep/=WWlig* 's}k dbJlTpޞxrIԚW#bWSm d &Zl)M#DKJ zI07It={^X6+EWz1J< vVT˚-Qk~|TC0f=7㠭=/} *K8%LhB@ŊdA0*e)H?n2i坑}$ZFO76҉0tY kTVhMX*=9Г[DX,qɊ1lCL1 ~"ߡE!X :;q)&AkEUdQ2]T$#V2 汭(2Q k-DQTE99iC`D37[N34- [Q^{Y+xD1gV;@3Y )fY'Iq,"ܨđ7l%V,+ @OrMpi:̉1jriQ^50L xMi ܠFV,s6Ex04[|6/.2~ϰz~zECk6pyp>@@lk( 1e(Y*$4 ^4*IQg8T}1ƑG?Zou1E!ɱCJSi)%@PSArLګU46o aN1@A7? &ǧ%$"(nPkW\+5ŴcW|vf) +6THA!˸^\T8ήӞ@pɧ{g|}o7^/=[Y=^({Ʃ;i}C=6,ƒ^="Q"ѷuʣ?`Ww/%*^|x*D>>|yFuؼWx Ar1JʊThz0S]#jVcIžT)2K* )qRVҰfbA^(^8+Ʊz NVꗎc>MN2Gmf%X qPJK 8j,ڃ FQ5CRYoHkP Tkكe_K֡[u]ӊdؑD6mG H'uP>`:1j^I I #h/V\*p){O~Kr=ק__d58/Ko÷.E|` q\W+:CҤakH ͨds*m/ĖoӢ'X4Yۃm4ZK-ab@FzSVM咱 dZ@Hh9NjOw:wTͮx1Kgx0-ΆLzW`SgtPu8"mXIf+ZبHLdBh;νJض1X 7U @ }Մgx:#ݸ5 4ָrfШțXfY;OzuaQ$ӚEYpo%뾍xa7UsV}]#hWbqL3咍r) :^ xBC @hi+5kpRH$b Q)Xҹ.`P1Z[R(F.M j2.z%;lG)ã[x̏~cuN?Zn| __\ٖ;f|-Ӝ,i"Mt{)]V2\f^4΍2 6RkZI@OVm>pAXZYY-udgР'9qi ĴaT,*4./^3~yyi{|' p٧_>=;~z͛[e[v/?ZfQ(2,lUæ]i&|54iACy8_&EݰcEQKf 6&+QDz"Ɖz)>Ei >Fqˠ23Z% .Q:B1QH>mMzѸ3 @$ZUV,S#tu D2G*x͆&&+t(I5}3ܐʥ4AP`QE/ {4<8thYRp] ['PMTL_IFrCalb?+/N8k@K{.`lTuB=tes,u4e)GYZ%ls-Qn}dQ YkYث\$%7י.$G.eAO| .`֚驖rir$D+V_n{Hl۴6m"p?xߺ?OϽ;é-#3{e{(i* ))xg*!߶%4%vnC+__^` %)P*r)%8"4&\(L^`uZ!a%ֲOγw8voY(p6YQo7`B1eYV{H'UO`jD3Kmٟm|m!-M"\s _GkN`/$,RJN% `3#Xx(f ADi<DBmyq]F59itAYb6Sg ;tt\Ȕ_=J>^*1b(Fg!\`Y[>uJBd,ɓzvĂ,q]i߄+NkYk%OXF .15=,r18`7SO 9 ]^ }j0- ~mp| Be=Գ}ν_kWݷv}`<~#>aS@ۛoG),R \ 6B4,:_|LgCxKcN++.z,;Rpǔ"ss 5@25Xͅb@R\Re(uέM/S8ƃDR>okC☥SS* Ԟ zEft: "@O;m|z_)mƣ(RTX'B%6Rn{w;o7t} w'} vލ3;ZdbCkzbYZ' 2ϻ_ ̃* q|Tr*Y}LpekDYa[*`ǐ>h8r&lT>ƍciȤ[(hoAF U!ɚUg9W“bt5M+!|R16[rq4 z?,آ@*GRLʅ4h K Q%@pWd57qt ȤV9SYI?9ΐf[4]w҉c + ⡬Dm}٨³%>,Am 1Znt|>YZ ;%<U0!F)h{. hT}/hZQ-X+4,_sߧ\r&Nb?>=RU}&F8;WSX7'3 Ryd Ah˜ X1aW 9#G], `=_y#}@xls?wk>]ͮٛ r` 2 R θyZuh \Bg!*NM*EB+ʃ_%/Ai!%Qĭ-(?Z؉1`%h)$C7,3񺒶D=OOpش.>IzZ0F !"(]}(1/C `4xN(lk+@WU\~+ _8VsjJg11ѕo])3戊VjPO\:Uv%55v-zdbҷ=y_Xc/W`L%A'qFAx"SUqok=.k5ڎZ{*TDAL0(6 n+z)fV29L vo6[clDvg6G[F܊1vCpxՕ r"ވW{L1T|aޑ݇W+?eȴW -h+6Is1Xט:6:<:/@ڝ۰3mڈ7Y :8c,hjd'T[ZŢ >P`J$"[`fy7 '`Y| @q4(sSOg븿^LrY2r>d(nzJN vb ';2щ<6=چ}=ՌUIEd*УifCr]uRAFK|~/!|z7݇L>z?^ ps?-r_e_wޚo?\&}BbX0GyDzYAEzn+woSxO+&)Tn) x(6{)}vCxjcӖ&gTUB0Q( ?vܜ !WqY2R5laP8 ktp BOey<>\q@Vudۭ9HƸ$@Mkkv! H*v{˹hz%b%-$IxQΗ mk])Ek ZmZ}m ֢y̷N׼dyFte6}n+MYqYjc/6I2iCLWK1RVā.X5%ȶ5`w&h/lȦJGO"(3H2 b.PULl*<<6ܣT,v(!,kOFF D8%ks2 y,X9^ wZg9z{?WOp?܋_sq0rg?ߺ_zd*SٚE4t4 Jyd̀f"% @3%WʙSø |n9D' :TU VUE- )eeIu,pV͉r+-Ȓ'|(4&$5 Ɂ.NΡY4Ğ Wht2a*Gj aH|ຖEooRbKoмekOnV3`eEUpz_XPκͲD5USyhtڑm2~6K_M,Ds9eeXM`whxzN'7ҕ`gS`k}eLC`GU͓c9vNvSXhΚ!y(X[hqܺKbã}/]?}ڽ'񍯿;ʧ~ޏl>3=Oltَslg'^ۡA]\9apCp@6*xJIk0w)lcW")'ՏTe(:@GJ)PB]VOʼnlF{& *C끍dtH)Yk 0R&"/ :2$|ҳCMNNdAV 5C!3EZC61"f&:LhH%,_QWd ` i@ߴIT6dZ`7+PhHs(9mtɧ$mgo(pjO%+C{6cR+e6%n SRmOWAx[OgZB`zq8!;t&ي#P| IYB[* kXc견 c BMH?NpI' '<RJZ?)bsMfB ;wp6F'88,ֲ4to Ҫ2:=-Z6t;)Bl&~ycN/loqv?:yϽ~އ}= >}NC.ƾ7m{Y0eZ-2RrgEdNMg4*t=YtV 所A$=R L[.$VA/T3/ܦ=LN:ry0*S P=H~@TjIV !&B 6*otVVx峽[%YdHۋy` RSٛ@k|ۄ,2y`Kem R‚&c!s-.B*CkI%n0 LV9Y3uΠU-͆oy<7E+F r ni9 hZ_Xxi9T|}8Bij,= Fw8\k`~nc-?]dʽ}26|7A7{Ȥ=ۨ'vL2T"wA3-(0&!~$0~tnȆjG'+` a ܾ}g{ɳKƹϮ-x*<'krl}{o}GF^USڎ^)Bjuj}9,:XnDґVeЙDƫBY=)m"H:wtWo)L! \ |^;ٮܿ fB͑!Pp`s.(M|p4*lR;D0$8o-PsyERmV4ï>0N.ot+A/2Lh+1bᘹ(E[r 8dK,Ͳ2ij\OiOךHnq y|Eq9Ti*!6 ?X8}kIO#P@Z U5qZf=]e ݩ*(jSVRht!7@԰âwna+yn(@ &_[kfa.̰S|0ˍC!WFV<;"b YXUA0Qb:omha |^y[?W|r/c/cw'?gzk6kg঵@ ̹0zZJK5-*!iIqKiT+EkNǍs(Ėf?ЕXt%ֆEfCANwubN5Z뎶VOT•s*HS8$l(;+ )U A$ [&[jwzp-e(Nǃ$=Yet<~T"Am n&ζ_uִ=gn(}(AmAr^r!zT 6XR(pDKr* )z_Uɸ 庫o4eI^%mHs}*,ә%[)J3ܲn2jdtS:\_ljiwHj= 9@Zq!) $98hO d ռn ֪$;lt_ձPlATcq~% eL7߸uoW~-{\;p?귟O?wRlNC ;;u{Շ8T?dW0qP%6P&1 #F-4o?W薐"ċ#0L:lf jѽ%K@ʬiEE|/剔PH}!8DaA H$(lRr L2JHNгo%)l)LEg JbCG _iu7CFäGIhĤ%PU}+Jk|7WD +%ŴhZ6nWSc3?qYS0\a+u &*巍+,ki82r X+Fdv]jlPj&jsb!0(&׫@LE֋q|Vlh Ԍ>8Z5a\+##[|G`$YL vI;^Z:Aa6=+wJE7GI>~L jkb#8S<9TyXi| bhz=vzxCDUkpSZ6v6 hGO 7M6@ yYq G _"=gO86>J?\lzk\07,b9 zٞ6lR9v)ѫXdc3 ˰(> +zuRւiYȔvy+$APp5SW6=1I[n\&-H.7Fh>(CD[JY!r~QHMT CF ۴J%'2BUoDY_g|< \B-Cf sQ6#/ 5U]Jd3t|OOD_8CyFm k6gʱwՋxNM&Wh(9Gc[9e~bV ƹ@F_(z,CiʲPpߒK&NKYk<OɈaU Dsٶ!15,* |=[)+)|3a|wI K2GZHkNrQ]Mmk LMR2q>2ppJ<:1l'1Y,ܼDmAj pgٴWnݺ7_o}Wz/ЍO}yѓzwLǞC;;A~zc \ M> %|XC16tfre{X$$ /8X$<5m`t=.yFoTEM셋2C`As63-|uMa]<5'1jMcx"".Ơ2HUr:0|!IJ T5@aB¹ aI -F7@Y~4rqq2FNmr.$CPH-\|a`HEP1[_E`2^sK\TF'{Bjy]Q2CXXRԜoQ!qPwq_+X*v\>b9gdhssu&x0O$F_ 3Z|~ɻՐ UW2Dh@X~yFOTzRb%EíWv5}$6%&Vk9q61~Ycf-@P|Vt싨Ӗ31 "C57ȌjF7Th@/ӴY\ўo:rn~ty+˿ޣ=?~/\<8~};y^_KRd&T%Ou M Dc=燼sTz6;S3ḣ T@H)P7R76T"MA?Vk4])6x|ލ1Hϸ$hkO7T%s(,fC\@YQ:vXy*©Fx1bƒg%+2pQr2o{aLr39AIXyrH|3-}_Ra~rZnٶڌ\MmGRMڣN.,˶:!KF>&0!plL nL5 :B@ B&A)p熲鷙h#1I1,`pb.H'f ơJeKޘM *;Lo16I#PYUd }f 325ODʰfGYe|%(urߠb^1^vY@sEqaid Ypx8LgM!ElNe{83֠3nsQ}:$֨֎9t}۫ova;[FO?}~q9{^mtO|z/sso[{ֿ kNo_laMBk+?A 5V\̓EҚ],uTDsG)1K G^gȩvy٢&NPYȊ(t5R"9F񪴜Ml-mq5 8Bڮfhk hqgc)&_nIl8c [t J Im _ f\q*P-%6xUHۜm7ʆgd4ڀb1&[BTQo-P dtzg< Gd ]NchQ&vn"P2]굼U>ylکU友B+Ɉi2k \Ө{jML1>HU!|(  JKa:.w}roTt23u(NZBpGFʔ ?2}ZILk<J'ra-os2,\)2P1TϖUiT/'FSalCk"L-أ=U+zEr1s%V^X%3  u5vtJN Z@4/e7Nf{t9tXы?h=|ÕϜ[?XflH2@GH=hXxc)Dd`j6of )Zp뙨uFx e|Tl$_Yhڗ"Ɠ 梎V+%Kr0_Ɛ-R>t8੤m3Y$mYy]Mkd0]HN`e(liH775.p~feJVQ8fX?%i\MN3 ,G^[*8iq9[4SבMʭ{@X &Og9Sh^~e6 ~p)Pm-Ӱ@qVT2j]jRgSƠ4Be4|Ç!KO7_b>*b>n ^(Dw7a {Z3kRǾ`iʵ3LXAmR @1;G[BLbA`TJΏKJ1Fb[5i>51T|5)Z0I>~ovzfOiz#T7X$p3?^nx<\kjo(4fmDT}r0<Mr7nJ\= I4 sL֗7Y0l\Җi/#BY f2JQPP@meҪh-|H &Й7Ϋ6ygFm7:-U@0)w fA\[(E>Q}®D32)7 "tZޟgS2Aˋ:M_L6e-%ƉRiZY3dr\A'KP!!SrGiu康٬R,T|[0+~\Pv%2\m6~t'Xs 3ˠEСHWLDΤNelq)F53g;isc4aHgy~9G3!)%eoTJc˵6N5,Sւ+@{W tjgE4Đ-Lk]/OFuDo&3 }mG'RY0ZΕ Yq|`/-|gX$PsO>vb!yTBUMˉTN2f_t"G ?';qI\ th" CeH: 7!8Gg k jmӲ z6 +R?Nw _{fEu 0%)\Td pê{O׊fp&Ar2sfVʱʿ(F#]FU]bd4Z# sAz:ƺ5Qu|;Hf-վbڨYs1ZEq43G{rM)92Ql7F8桬\Z%亜G#k:GXhB^=qҠr$C$W@ t`=FL[~llЊˈΞD\Rb2Nlf5+YwޙRĎF7+E˯Z]NC\i `7w*+㘃qKj! *.Li|*X6LBR+d`C1.l\ucJE4z! A˧%??v?9,@nZt<s @yǧ;O|}鋽{K;ZV꣠{((`Tv/!~@SJZ[KtY =faNKByղ\{F{'!r°Kmh E=/2bT$U ̄d.ig963x:*glo fH455{(L'! 4=(u5'x>Xs3h!cfwbhݓw;IIPK'.(“~}QJJ&K% NLgB|˛@ÞlRVZٍ@T;] (rLLC3K<*ėF[2*8McD4) ߔ7$xl?Vq5ѱ eeb0SelJ@o&04cH%eڜҞ 2͌{q@ȚɖSCiW*TC[9YEf(SDpeO 繬.a묲zthfqa1 " ؤL WZ"tRqFY%ӯD4o񪰖֐jǨokliٻHo^];ܷ2/Ү?zݞO_M>g>a':k׬nOSJ` TCR7gG,8w NK3 +񂽮|Q; .le%wJlsZF\ESP("¢^y~B[ԖdmрK!ґ IỰ+Pʖ ڨ(&oȩ"jPTRv ts56sq :pxkbC/> @qbmGMQqPp,SDH]qL TxX>́K3ƥwEckV1NkA֘?G)1<SK c۔ $snXU5Ѷ( g>Lqkdp3}UYjkcj>!Yؕg]bf11JOaj미Æ/ |uT>HiOm@qm#@z>y_=6krrdEUʘ ɡgۙkw0p }ɯ7~{\-Ãȳ?}w/}믾cgJhraqRƕ'QNR<Ϳ)bm[2p2S68lt|σ>/2!W [/h\J\YUiY>BRYHr[J9IJB)bTY.7W֩Y6Ҋȵq5-uRBʹ૒[T!K" ZR}BVxK+z :q"T2#^H*\* ΐL=xa BˬUL e)h’@(-&7r;!@ Tɵa2 2mu^Ϳ1]퍃~cG,r.?O~6^[o6!8^Ztfq *MDA{%"T4&0Ai!ee7"0d,W'+ą0 e y,DA ܯ \a)oV#qNW06!O,Rc9 RSMcn TZ$C?Y]>6L,Õ}%==%w19q$}"Ge)_<w^,pym.dx|'ƭ/~G˚wzϸ??2\mkvWLX3QQЈ ]qJMZEZ,1+!<~oH ?5!fCt' S) ]0 3{mU0K!8V 9]!B%(TRf N~nǾVZ庾/d?>1gr Ҝle<{o{$ xf Ԩ%Iw~Ͷ>% d;@J u,(>ei|O h~c q]?,J=,N +LnOv(qaV\j Vpm !:X9@V:aCGf AOX}r σҎ'J6qɔ]mNx+`E:EUh"ZQkMeۚ20-")W#0ªOʸRv Ǫ*mT& dM F2`[k&Vk$4#eNkY"AP.ߘ*3f,kBGߜ||oDN ߍIO6)fxĭ)2KR]E=OHVO 7=o) rs}HZV'%b<0dxZ:W4:)Dm՘ 3?m/lJw7-D0-eg<=bWM^/A>}2!8iIBpib\LDRE, k9Cr¡J^~OFQ@S ǣY p,QU`zQ d)+Dp+߲4x!*&)&2/5OmCTcO"&Du*'lF!e BH# gю2V,Pk5MQv2isnZN .*6EĚ@I^}vYd־_1f I[QD7qT(HbZY&YP;FWCĊ4{麩:F9[rdwNWG~izޕ=>6wxg68z8?,vF|5Eb6RX* Zf0/I-#kQ…sd0nL%eG[OiBg66NTGs^A.@[GP`CJE_V趍,h|˺Pt< BNeHS (},8'E 9+ >0ȳ"YVf"mczVL#tuZ:+JL{؇&ςo՚/K()$k"W%4IAovX΀>gbh.n 6"-6z ̵YtP|iTԲџf PNU;{ͯ*tDkRBqԦ<}b+"PrKD1P[*ROl{i--k/) ^cR4,d1^G)/L,5_ϝJe Xν-)\s^c"Rm4eWV{˜Xq V9!b,HZi6kUq>L}xq:2~K"`ZP! (u/&;=[[O}+G.^ p}/~>SX"p԰66<:-鄞N2h8Q07 vEXp!f%( Fm-LCAZR_r)9xfd.6Y'?,KBuS t7dP`:bt*+<6\\X,(ɆlA_-Ƞ=\%N^ki$8A+tcU48:A9QF40 @0ϲJ (0Zw0--* Օyܒs0+5Hj6fp|ze2-#6KIO6Q2E@H $@d;`Ib#k޲Bהߏ5C_C)@ UR #.=hl,J!Rl(V9NU7{,3Zř:=˒+QA.xY=O>*2ZW.z!p8*C>)|lEm$Cʙs]F1~s e5At0TfV$)Ք+! 6D& vl} ܱwz7n;?luw>3EepmGAQM,.M -.(E '; v])~6SbS"iabZfS[Z"] ]6Mg-fD*Gc2Smg'$kƫQB~-ow;af,q\"VEe]-c, 3,ٕ5N3}62 _lp*WXŀL,RD2iP}ttFL0-MxI)3S3M3ğ2WTvR7Q[M^7W;!_S*fUQNtc-dBgb6* @ǗqOPkP_3˻2mQ,Hp5I9֬?Ieʜ,M=YV.V2$s 8νW.G#0ЩPJ.[)6+Zqۍ/ip"`*.6%h+seMb#-7ts+N0`?wo2`8tYׁ ]}(7q3=NXa)}pa/ p|k7na{wlwnݾӛ{P/vAKo>:6̭1!tfRFRV46A%UPT%$d,=Т>V򈡄d!v pܕmIƴz"՝A43mB+Q\@Jwc&9ʯ507|PV u[m, (`5*lmZmT(.HgHNs&T2eo80bC'LVFu.HIɇK"Dqc4K~v|c-LP2 G<U4LrEѳAJ+r@91ݒP3]mцg\?׉|nQ}U%AQ`U{- H (f(3k#iib @)SQ&O /$3F>f SHzv~_ϓ EXG\T'6 }`P^Jֵg'Dk8{rVfAXR#D])^ɂvDrP2ZrȂni-{Z\Zr䅕Ͼ'+}(YT4r5bʱ(SO{@c?]ߚ7O>e|7=m9_e.t^@hsBs<@%6}80F.TהiMZX.KZVObz `><覅Qt.FZ-{}P E6ѶMO8VLCeMLz>XM,6o93$g@73ܗ6P>~B Z =VJ٩Kb@2Pe11_᳇B@f |q1WE[vو.mpE fM5.'R&VH٬ht"]ޙ^ Q5Isp_^_@Hb/ZLc!4P[S48NX$0Hn\,aK6(oDc-:6#~cX {=)9)[G3DgEM+d4)C@X2C kuSB1θG90@fRNyULd\(z] NVja;}V&\L!&8~߸ĊfG`a?Z(w@QBǻRH86ĺa-/; e'6OKk6Ǐ|8/ۭ-`QNddZ$NEGLH9Us/e`J▼u "eWsw&Rurh^4M;'&Ի{)`CcXJ":W ¡"@AU٨oxTC(P(U/,gV!-1X5]QmҘNJlk@683]4Sh5Yf8G$(H٪fFoRB.%N ngf t(]Ua$˪cYݖn&TcSoXc0zxCM4P2P3p^O"P׍~ S)Qu~1q2~F\ (Tx.x YH*NSAԎ;n޾ ;߶wz뷿7=\_دD^eQ/EE.9@E"5ZD⌢i)[ Pr8|SMP.Y":J!0Z:{`>P?@{ 0Έ}3˽ "eA[~im(Q!m--E>(ZN/jkʚB R+1s- +dCbJ(Ӳ8gD~(:IbCUUYgQ3Edz'8ԹNfI\*[L KK%e< sE2.ZFB'W L'{6Q ImE7Zb'w/E5ثb%"ކamk Λ Ig4Fn' 0¢s͡XH%h{[>XYME@6%n 0[j4ZAU=>!z՘*쥩T%#Рaf#t3]j,8| @E q@>oՖ}s2~(/JخVGdrV+B 斌V 9CYI1B Z P2mL~g|89R(r7ڹ.]i7Yk!v)gVUc$7u9k_<1\+b+FauvQ6L%ӆ0_ Mg1^ʂr9eX7-'u!>;6^;[4_P,EK6$jMt+|(WL;lu(%Fg +Q@|pD7&%6#NK+wmƸV5`柳w7 )JKTd+y H#F2@;ΆP(GU[gm ŠU+PӔic H˃T9a9ڴM!r0l6/_m-pF%CW Ŝ55P;`{|s &NbNX[ Y(+pofY[-]XPXyZ(&`d, Ӱ\Gfi6jF*/TXrdsFl6{'4 8$FVe 5ݦ4#"pՍ\o #p0M[Z3sb>lؠ^C)u#d3c)Y9涩;ސw7Av FcTt6 nhp<3E/|g@>eSYTEUO((PBVa9YZOMh#˒̎sMn^#l5SogRmT:|K/tbBZ`X)8Mu3N6AW! t\ݗ(1?QG) *PH Y ]+0P߽ؑn_52 e`kDOE~#SC1FWC.y3d}L竱$bE%i@@T#P&sXlZb[rlТO?m[ b~owzWѝg[e.bN yz9!:1J[.(@ 8"Di͛r 4F/Z{ߕ tuq]1)u>f Zp"*e&>y6(KnJ#`3+PRsP5-: ¿^E$ZnE3cݙ22d] 1lnn+>C|m)ć֔=%Jd%6c p^g',=Q(8VP1NK`mBUS )7 Qw~Ǟx/ٻp{9uwz۰L..?:3XJTŹhQT!r)>`a`( 43EJGɪ&|:R/V)z6Efq%d95ZŸʁt<[[SBjֳVB#UZPT&UB\}nbsJ6.@X܋Axҥ t* $F[JVRȗ2n W\S_I852Y{q-)2q03# ʞ06~&R-C~Z1&5W@dAf¸q*rYe=Pr፛ C .]xtm~q*LSCF0|ܢ$d!Z0 T O491~A [@XTZDS"$7"V;7Oր+ sB3@'JXƠBF7?T"и)U\{[uV J״*}UF%<!_L@nu:]AhOmLƘ ‘gCQ9f<}c&ZH̎A1*7nY $PLMF eA)Ҭdߚ*SI8dB(abg$oS$ bdGw\g"AQQh(+ArԻp0eQ6*- 5Lll))3UGftׅ +y* "4މM1S4JgfkC튉̶_ `] :}мXuq9 N뽸~gO~@tmϽZD@R1 Do*rq( StV]*"JRoYaD.hW˧ҡ\tݚJmjD4nr(I- rnOd@։"n͒E 8F|`fE6h)M3SfE`=tYD# 8Fg5W=#DLL"=  1@FSY[)qʂ! (Rmy0ԘؠOTx9-yQBpsrRA(NDz5 LV#;1D*(eW*󠊋x ;cӡjT>ϥQidã:ԎXCz 10pVƘ j vqW IAQ {Pd3C@.#xk~󡌦VL@2tpkj9,R71TLc 6OV=X%:ӝ#WF ._ܸѭo}ґ^VHSNNUI2&4{Pkaɀ`&Fmpc= 9e>WzYKAHAeE/BVT ʇx*ʆ:rgUXB^VSKhW> 3R$=rҸbyԡNUR!s & p4jΤm!ÖZu۪ڲ`԰6|q~z!Eq3԰01c)h Ĵ(SFy^2.xn$+LMnڪqb*1PJG‘VQ0(fpdq5ek]GzE=w)gV f$jw/O3LRV@qu&#q.VgR:-ϨBjDؑ;$E)R&5 e= r@$þċ62P%buP`4%P33 I}7e7$\:640ޘY`GfKEtgs&x|rE^"VӀ!Ƅ Knjkfs$K ,6`{ISyל܈b3|ybpn7=x`׻~'3e[ÑCqYsc0ҟ~.K -6&2T\mDvfb4PzRÂTmY6[Yl ޤV`e5[&Q"=YGPLp2P:V>R#mfZ39ݙ(cKZ1D|YD1V`k! f36?3oUܲTJ*X #^ |`T&[crr2C0MhZK Fv%ԽU@ 9U*"K&.\[vskkCT%#-*_syg_4>ʚnB˶Zk+w!sSs/H#Odb-v>q?6z^ՉX s,X?Ö񥊠φ @V>>貁09j#@ϋ̂29-ZПpF&U3t>߆Db*SˣvDlx)짙_ɲJmN(s붒m6xȨS0+e)2t%-Jl,eE 8Byѭūv^I׻TٻvLf0&Gfd-&jyy5w9US'Z𷺔T1rb+j꼄7Hq{=&5YVV٤ᘿ]m1$, DMh۹a5CWВEU` -<AIS:!StaFc_e sBcˠۥLԨXbLa[ {Yk`(|6>NH`b”f"bϩx"}Țb4a8܆26(8tQ.ّGyBwof:K܎ohoKeS=A J0A ;BUc~Kkueiؿ3 _}5rҺͬge̲9Übsеzmxȶ$2&t$[l~ dLPMbW̤PUG`ØkntLqE&r|^Wxw;MQe@./iq)).nVb+n4aeI'DtzVU7%zOQ)) @m`# Ngi$H!{ F\3Cp:e9ne_$XE-AjFA4yp)) W,f*ҳy"tgO]e@B O-M0dzA7@B A (oͭ3CAQB]<I=UXZWݍ2ku Jj) {&YM|l^´28huP0=' r2j}-e+_~6Yq=XXxrtG'ϔWoE~ֿ|=ڑ0 MȐj_NL'V$ dn#E롦f wE ,R@e`yB[fdh9TaR8H_?hk䒰?VmĶqtdOWnӎ4s5pWΞlܿORE#ׯhTe}v[^zSSZ@MgνN"6XFaq*k+}2omd&#ilF64z!rRuQo}7; EDq0FLh3 $&., *ڕ:gn1 OFn Jƒ'FW9walq^5mɳo$N؎qI,52l MFׄ*1/쥀}%^TE)UJcn F֒/3^ぬ22+V# T35cuzKo9Gcs,u wQCH FzeR 9%lx|1* ɌzӹBc^F*tƂΡho6"jW</gv;ǿZ6^_l hrd:UL%]QM ZN*:k$X#^Xly>Q"RR)g{kуD 78~7F;s6*;Z#T> 2_Ud&h_\C ͵F11V&e&3mt $11n?ŃzԉMK,-@vCK*QmG {GH*b5B,/d`ON_oD9Ug;('s;wpVã&฻\LagbR-M}QIT#x TQU-+"ic, .cQ[# 4r"dL@RJGGKVC6Sg<;'B`WdSܿ[t8&RRn](h}toPu\#' Nkl\$: LJSxPsO&z Q>ƆT` aJr,Wic@MԼNes/l_c ;se+Vpt䋴1mUV2 Uz1f̕#7pd5ѨfH L2^4*Q9b7Vh %i&6?ǁVvOo+OKB>>un4Re C*To^",eWUfrLِY)Nd>LQdžj`(Vr=>|e.oꠃ\Eftrʛ  lh5j,H=(&V+p Der@Jc°X(\\qW#JAc 7x%Ӱbs}d Sˣԅ2漂)9669ánx)m7ޒ%dnSx v&;ǩv:z/iu%Q.CB2o% faJ<ƞk^A݊Quc :`lkNz* O3G}T2EOz̗3P"P"At D1 dn%ɵ4Z^34q % qIwÓwp (F%?1ꭕٍBօehua4gvczT\si^n+;uÍhǶ~Q*\#h4BL,͛Q4_W'GF?o\{&nkMH˧aXȘ^( yB{}od(ׇmhrl*5A~t"-HuEgYK[w6{vJSav *ӑ؛m 1EDdTx2u"T^ѯn?V=Tus#0!,ޮfLYD!dRJ,qA:P7f1JTz,Yx iP>EۈޘEak/ T3 iuﰩ*zX0~;6u¥h&èHbN2י=a.FןDńho*¶I(IΡ(OEh1=oh;PEu&.NM4栿cC`gN<>i%!ڹΘ5p\- al mJ D# )e b`.D g)-K/_ `Dv gbIC1,B*Y~B$6HEah9%1 Å pW S1az81^[hUBI.Çj. m=*\< 0 d0 -Ns3e _:"*LWC`E&5$;+/,!7z*[q+-H #G.n%AeT N5 "Wʙ"z s\w9?k>nO-!s:>y9&+ɂ.zRakD0\!f(IτoD*xM@R S,nlgv'6G0ݏ.R]_"[HB>.eRrfaˌ2mI M5)rU5tJި !'D88 ѨZ.ߚFqԃMb\8a>7T'`Wɾ^wHwfB)PF'h!NOFNy7'd8ڱLb*e8?)lPb8CZ$q|c4([" İ\ɟͧR=:zh\Df];pN-f{"|'=ꦖ4*1u>VT+1TB sd;Ao @ZJϰDRRn(9  @ج߻:<[ Jsb X'= '>Of9fG Qs˪*#^PғޫݯJ*=]9#YG>7EG?U LMד AO߰7-JvuPhe/g,,m2Ƅd:ʰHK.sⵧgJd#ʶq.qR ٯ@IHcUɕ} ".Wj>61uľ]kE ) - 0;R gJETR@dsfY NSth܌F嬝{5 .*DnfST:ϼ90,dGy@eS1sQfH h, Lo'oC%9N&@^wspH4#;5; gob8u;y}6m fBR]!1o51*DH %/ACМ`.ܰ6-+%c|#Sԡ)Ӊ󀐏}zt 0zFl\8=lHNM撲 GeCy;G ہ-|zzף+6^Yp몥'm6NR-%ߚ_iힿ6ډ088jYcV=oCHݖ Or(fƉZ~? %(?My߳;7R' &(:jQMi* lCeʡTT3sf)Nnd?,_| Mq 3穐$R8( b5S+(L)r! 9rJEo4I-˵ӁAnxʓWcIW@qõu<lqo;m,^޷jW<㖀eN'5FZHYaoaf%c 2EcBԪxZ`J[>iu/=cw켽svùe%4Tk=uec>=p}29gp^-`Evx86GG2s1Ղۀ$ R $Ο)(_iO?b&P5QJubJQAuz@ewa4ãy3F+mjsfdq]@7o@X)0Zzު񼮶0`[||:XY62\M˓RX:z"}V)I*2 -~)@ @)\RJ^uQ+T~?D> ^Sj89*di'YTSg]?>P܌b;|EɮLܢH}LP&|xhԨ"%#`dȔh U_֎GDX :d&/lBLdFź7({ZJC^p"cѻU%HLYx D!E-sj2SMY2+4Re'8 Xp"qŃn/F{TWqvj @R1SZ4b-ߣY|hO];w;|7/ٷg`j?EO]fEf;ZcO3$(+ʩ٭I SH6(Vt]aH&*VM1n;=RHwр`u~Vne؆MjS k^΂P,Ky.}w-vn\kMJdq&T)T\[ٛ_{{s_߷OGvqB ߑ9Wu{khTî\z>dc5:T ( )LmuTfk~/rѦXɤO!e&"i|&(c+{S&-Z")ϞFhݰ~߆6As!YON}{ vB= iҪ8״6y dxK Zľ ɽ٣c3:)!5e~ nTo诖VWwpeɾ;7`eM&ΑF.#KlW=75CjW}+|ؾpt`O/  t5_/$RaWS!n@Dt3'f LXL9Vz5qU=hSuX"2x^[97cmS}cBXߑVgO/ڟ|9?{]/En X&GU:{npq_]3U.؝l|`_Ѻ@d?t%t7EFjdv̫Lli~Hn4@ (y%ln`ş 6NMW[z-/s$"uޢ Y季,F$S$P20uÏlm7u,>k&%M@$ZRb€fG Y` 8 l{>{oyC#hλ9Y0 * uy{˾kW67썵O cM=RB'Zp)(DtH1]EsA<"`l>) ˬQ~J-_UHf]v2hw4dc$B7nPVV*릆 ;.]>sV.dIZ`}pŭ@= 57\ oM OtQfr95B@XJL&|tScXQJ\ c͞+J99Ղ@`-Vv1RM ŠB%@Ilkv3Y>YU=|ͦÇ-/j=ow|v䊮gh(c;R3|4( ^Iw{_cx ]]ݹ?,O|x4ޢ h?vaHbKΧE;)c`slZ C" |X˜`Ț/Ea?6P۟➽-ݱk_gkXfҹY(L'=fԷcL'*e)j!cfgK岷 ÓRTc2LGTeD'd@ht*+yhGe- #Xf;mf6 O|y][TvQXu34=|_c@zpz^xOwczy8cm<|l篱_sK602_.ۛ|=,і]Sou ,k5z (R"AUI΍"B~IJ<|AX== }Uf14VfoLQT-sO3ռƋ$]@4Z2]z9hh41*W7adR,ΖUAI`%6\S$cؚ#3)4g)5c{xo6[$Maqg5E`r=뒎/=?U{ tw7ue !|5rS!0äZBu &u4P&X7@Sqi%H jMt|^n=j!Ū nLh#0-<>ʎ:U؜EY57?fo;^]@ w*]ZTl-00(7 i&'S;vp_^p5CWJtXD>hB o-;^fȀ"Hsm_$U)]{rzN4VvF$MEMJT>cV-ɨK1HWd{ND7Ϳ`q# =:vbR ;k=0MI!Jh\<55m隽ewn?kv 4 .YR.H>A#ӒKn.5wjg}h#D"RSY:$GCiL̈́/IMRJ;Lq~̱~TWi_B9낺 ӁͿ0l<,e"aa,46cnҶ{KT~n zYK?y^ǩ{uVgKŧ=)'~kpiK\ͶXe 𞺍;|O#ȡ<<:gt#+ѧ%gZT+EA$%IfFa@GT)2 %nPHTL6T wzń<,B9E 6۳v"*%SN݀ {=2 LV+~;'~~S_^K鑺a/)HqLϪ4*,BAm[LH|g3Irh9Y?8AAyF[ll>wMqVpaUR@!ʎs1\ e}tAeSyk 5+kF\EvGqN&Qx(EE4̹NPk"5JJgԭ3glk?-wKo<}K{-,uM>ȹ@G^yRtSaw&"/(N։oٶ1l;y:@7DSFfO%Q׷: g)+*VsVuRi;sp$2%*4t^3[_U[OoZ9q`s %W&₝vŹB)OSϛ'1USneS9qCeyXh0֌ha`P]kV>鋹U]h&A_Wm Kj|-+8gpHrKWƃ'7b'tR3ed줮Rlp:J6*=Byuf[}]g¥7! =QA=׈eC2 JP4ͥCnY| .}.S K`0X Q2{FU\9S/Oj4 8Odǫ ntPURu(Z Vk(1NɊQ:WFh*<6vǻb_qv5<]+Se` _sjpG} jfx7>}=r#Ke! @ ^`dDY:Z)+{pj.wp̢Tw(@3%XO}XڒDs9@*¥w:`sӣRA$VqOPtv lXӢB@M5CAύVX!&Wt5 'T<:zۺ"vC'wwXl3M3x(YaPI6+Y)s5tE@2Gf*ikєEA,sSo;:*Ia,G3س|4ON|ӮZUޛO\#O Gu}˙%׈J ́i@v K3I ,nmH5V=i9@e23nE*L"Sdp/|C m@.(Q ߩ3Ez^o߭TG؍:.u %"iNq`DzV#E_C9u{hhE4['qJO~ML BĪpY3JȵWlڞ&A7[SsE7(inncSu]GeZl*ibwi.h YCs/ٟ;vuB-姻~xfuZ%Ѳ<,ﮗ~}ڑ"#9K Fj4ygLMO8IA ݨu ZBJ{VLtEr?YtD$hp7m1݃0Vc<\Q.cuq*F v1kF3,-#fx`.\q-7wm4C ƈPc}BJ *vPs!{wfoň.tB`qr_X۵OӖ LC[UU7tnY@sB7Kc,zT͉*~zAHc)͂!Ɏ"GzWjm]/GhuL[oЂ|]i&D xgky2,xlڷSx*8^b %9C3 #P!((kw 0@Jo*& /=;R _/J[_?CoU0X#EQ m#g*)UA6Ȍ ZҜD7;]o]m=.+fj a#y:CY]XfaȎL+)ߔ"_Ƭd&A ͉lFeXMTcҌy06FsyʤUs<{&D‹+Ex~\LhADu0O1\5[]{p%~4yweAvUP&~؁rgU_5]Y^8U{k.C7غ<ѵmGrcKC|܆%8UnlҎr6ק\1FچzK;M/ ^"g򱝭OwMPUf|020m?ȣ>oq[[Ugp2cbfP!Ko=DGjN`3eҏ5av#*tc]V7;J|& Pʧ./0jcNSd[Ѡhz\\&6`9 q0LzqzhƒjrsW..+^k w5)X/ٟoW Tc~} Xxe}AQiZt؟z]-7۹~w:c|L&QI||2S)/YT5oDpĊ 0#4I`廀z ڡ<>GVQ}J\81#JhyRJ"`MQHx3:/.1m>UjU亇Q{00  PU\^u6Fᆠ7LP[zЕ5)83`{YO0!LѨT`]"$ZesT93%^5xdbc Ie.WLդn KǿN{۷| oʓQY>W 4*m%Xy㕷g҈YK8;|+lW-s"N2{rC4|/D* 27(?{݌l MB5R h&WMKo"eCRnBi7λ>їE)CaY.RZQDw$Sb7q.~+GYvsUY'Hǚ3Bșe :nh)y w,cI(sw|1D 4:2i=;ٟ޻my+083-iD5DR9p*>jG_XI2Z`x/%<2N4UJДNHy!X^L;woZſaQ!(YȶMv:엷svǒ/-_m:wG(< yv/ye:d^UOIUA;8zj xnPu{H &28+)^9fԽ% QaeoS9LR?+2ν"f7‭zM,Apkpln[NSh|Wj[iRK {OwIŖ9x~q:S`JE̶'6gQ7QPJQ\U.q=N̬z5O 8`LӢZ1lIh\4gLu1^wh6$EӸC䨼hn2k}ϛ/], yC.Ǵ٨^k >+y@2)>61 2&o{.iv<\uP1d܂dyEt# $ åtR SY=~vÌt}rbg# =E:< abzb:,6I_{!agz(?T@Nrqs>e2VG u.vtHZJ/(Py?j!&PN5Nt|oBFc~E3U Кt]S 5qՍz ^ dk{SԮU{hFsw}Q>J~k/3:_ZE^:7}˝vk58RZcHVHvJL>?"E= WɥYR!aϏ,kͥp*L `*/ZYF0J噈&t p ˮ(mqA\Y 4蠆}Hi@V:!o.{ C]Ua|zM=b&&`iyrDk"%oZw5MosDƭЍ EH?=hopz8և㷾&`Ii,AgYw[]47@%ɣ-=-lBƬc#^ԟ K)VEdZSc ;H GUHy eL}G%@jU-8ә)Guҗ72)8U|2!8-7 ϧX-&VVɘО uG]#$j!W'S/, " Wp97̐Kvc{^x˝3Ddam=/ JO%'TDb(ko\pV#&T&hhyEd4lVpsɑJWZƨSrr0/:G ]]zCS "ɨͩ$TLqd#{iw%kܘqfւPkx !z42CóKf^2V1˿Y>|Gh%tFvMۛC>i\F\^K<*h ˢ,;q ,POwFFy>=&|=m{v~V @~/=f@N%[SrC.JHo:N5JRafSv$ E Rd&$Eo(=9 mh IfZ`zhplbuN2^s$70׺HU+莄bڏB.! ?\ou=ðva];n| z1<jNx4 U,XɕZ\AeN>5q\Zc?`-h!X*(AQT*x2ۮ^d%k6R7W8DH|yq3 jkZM-D륣bc@^챐VlV&-Pz7A7Dҩ"]=UG1\%yD"i4@LsPgrzKjm-^Ke4`Ϫ/o-Vsxb}"2o iڗzٺ[?`}<[ do@|%b*. n|מ0O-\Bl6zm*U*g7o1}1&^Ulߊ~I{$V(O c UPH dr\J@s l^dypPCe0B&[]eymdv&p:dӖIy5)l]l=H@Ծ(2*#W\ ph4p!ɶdy;zg܉O胆}-؟@Q+k|nrl#T%#yNȋZAs`e` ^J h/-y+aU0A,uZdOQju\Ghe_U \"%$*%* ̗εĈz\IzC'%V]{nM"gpj4ksaʲ9wDDȔ;AmFI]Fyb̢_Yr{>N-p=?rArS'Br'`!Gf+!RhT erJ mh4V{4gf^AK ox ~ oPErf* T<χJ^lK"_t~gSfϧ rNC(Rl~m7ٙu-G?/'Bv eA?PP$KO҆eESs$ɣC -f Odz+6f)q2 ] F u:pš7C[kpp՚*ED@##\24"-0`Ȼ OA%UPW yWFr6'}_d|]p=?=_9aeۙƃ8{Οd91]B=TZ$1S'{mXWe3\L4,{Ƚ8@ R+j>׈`8QՐ2[a䮗SpIلsv5Vrל4DTΓiimoN<!޹aJ"ն4u +,0Jߟajj< ew)Bj1INKCrVQM[`R0F ؁G`zNdX[ֿяg}НՏ5~%6d@5ږ[șҤ54cH0rvhQgku[ѥ "z>֐nhI HlUEU}ȉWDw6eZt 3At'EsN4@.^[ DbH%ڑ65NUǑw``=]x B,+yDU ) '[+JRxBlR82Rz: ]"ʴ@{ $%94P%F*(O 1}mm~g =z-8K-urǦ6{gʐQPaɠ@Z!WI>Ĺ}\gXp%mE5G~Av]][>5P2~Fq~l88% Q\s=I];[5α?i ES(li@b6^1y>˫6ȍuNEB#s#ق6^&,YO$AѕxĨRVx%d2 7LNNˈB|3ye.9F=N 4;4Jg? $qRJlZsh~\#;%헉5ݵBWaU3\Co0f5nQsf8z*3ğIj9od ̉) P #3JDЎ]#R^ז z!z]O=׵SmP4V&xh;X/Cj CTr ~.")6lVP L t=tܴ@c|4N p]D$dܙh wP̬dH3J(kߥDP'%HF" RK} ^j[nQEjԨ 2JHBFf&6ҺVaFtdѻ)hB /H1O\${\xpK;dß&r$lr,:\$ VJCF;a*1FڶEʲoʷz1i=3/Y,jQv2d*?i-Mc{Wq;~POQЦ)BP#*̏x mB]6W0*+ 3OcHYd>TDűUJGN07B@G(NtPan͏~.*+BΔttH^UѾQ|m ] }1N=?ٴiTΟƍR`5@\{8sgD-i*] hxpSRô@0–XH3 ̦0%)Qֱ‰cEz(ĿHz}ziA(:=Ƹ`'z]u w4uS+7N*DUoh[D+V~iD2 s =ޛT4("h!~ "z!a\k‚+5;`p E!q%hDJqv~@c˖'Wy 0޵$CGfW,Fq ^1p|M|T=6/<ktG/S/IST{̽&{-V:s s.Yha3A%@[ؒ 8 Utp~AVM V!7G͋Jj5$8_h&IטVI^L;i2/c_jB:(`<2pQz.[G [e*tr%=1 6q\)wSquO0%35 ڒG{69}"=sǞJgwj?5jkAUJT_X= |$Xta(G=i~ĺr@Su!*"7Ir(F<ɳPTS7T%EkqqEgNqj%Knu]q"*9U 2mҪ) oCf2@Ao1@,F=:զzUQJ&@Ĩ@*r<y~O¯lffi=}TE'"J6KOMW=vPrZ^'s-^KBBl9DfONt f!/hxb4i<0Ka(K!*:,Bb<; >UA(-JAuC ֟}_fAŇh>']dWtoP0h]U *2 VsTNSx:#< lѰ7CE ~s_=yK 5{jtX>(II:'$.ןko+Yz|lqXg HoXuEݒE\)/@RбrLÏC 7: Gxsַ%: ] xv3b!QɆ3m:VdV&K@Rgf5es df9$~:In +xfȉ򠌌W@!B^-ۚ#BI;Ĺ95WB:"]|R je85ZWM.Vw$ݚaڕty@Y<4řSΊYV*kZS82[k%l c, KгaD^L:Z7|LmWG^*+Ch984H)YVW ^ۗ5*p0b?YNV|@C!?~1'?e4DbdBuD>*^P H+:բ\S3C*CKȎ&^VH@W<;&f4c@1'>E}_[ȓSásmOBy&Γ{07ҙ i0U#~Z'Zz04JEC*qEԱEIi>/Q3 NȬr57{`;]Wb][+]+6@`09qLpS/GI(EI-֯VFԪR1D4mw?ʲ:S-XUk4NUI|j'v#b+A؈誂 GB[xd-@|N)Su=3*ݐ$,R?ѲAu̍Q#KWx>5VqT7-bu6 T%m\+\ ziN槝Cܲ]?ԟ̮ɿЊ|-^/Ҩ SeN5–bGĔLV 3?>3URΆSDRtž? 4Ekc13BR{j\R 2("FsfJJ "ElFjy(hЋb$6FRuҟJnU@8A^ɽ)}zzna.Hjl܊p3"xh6Ӟ}kؕЉ=!{? &Y j^Mt0У+%zO= ^=C.#YhlЛh'ewVn|zJUsMs>"2w\qaӁ=Yx1/#wl9JȓȿOXʣU8JcO $F>N )ET'4=䁜ק i =JoZ rģQ&M<ꦂkIFq4"G}QN`v=74Y. ' fMJU=xA FX_RcCi;ls>G.UGlsf50׳緮5Gj p>l )Rd=֬y' ",t-mNX3 xf1?]ׅ$u.@ +IFRJG8 (MBMtZ նr)VRϷ\z#e6i{mVGQPK0 H%%mc)’K>Gm5m57lW}T"My)L8ٞ8J|uq-@͈0VOEUO|*.`@'3=XYߪ9F%"=Bm(%=0QS +a2TJ{*:D¼T0˽^c?'MTjuEbkSUp}yQw{]!LG g427jbR*,DOܸҵיI4 PQ7UUFbt#7мr˕vK'qf}+TMQn=a&+@/9ڔFşyvh\ٳ|Ҳ.J{pa-y;wpO`otCZ7Oٞm?gЂ3==QY:54C J`OY=;xf'x5068Aq.grZ饁MF׫H*X8ĉQ8H%3ءUl"ڢCTxSiMoEØ sҪ)#5BKc*S7^Q Cj!vY\ 9zFge>Sm+%Y /<‰u@ljyk_b79;ueUStֳؑvBn<5\A~n'`&<_*G4^7{Ÿ}ߥo|z)ˆHF&-1?4Zoaɷ=Q@#`85PA)J -}&47 Dq7f=%T]V72;S4UÁ})"TbGf{QLBJd SI]@r]܏ ;((qy0L%cl\V0Y%8u[ąyJ*M  MKgHܤLfclQ{LK).t<*v '*m1JR#xnQ&BBBq@KI1-Rƹ.V}"p@WSoxB֙#M y Njyj{/|te@sЈj Fv|*+Nfqt~vn|~wQϋna.g~R̤euyCgRuf5;BB4h6dƉc^h{Er~}.$BQ AX k^ժ &L2~%<7s?ǣVY!$__`K3#̟FA2j50|JʃlanӢӁ<` ^ϐ-3b|g9Z kQ^sfGva=!^^'{ w<7oےko"Ff:SwHp'u{@L&H8j'%*)Q\G4=Z@V4fSz:梄Z$ %BD:d}: eXOKx40MХ9=TT叨\ղ ڠ(PPRI}ByL"!T(_z`gYsM ]ZQ &WуĬQs$HK-LcP0#=G>rh[)Pa,a1Wi/ Ꮦ?u?'.ٱ]'nٮzIa|)*˽ԥPb?}ʖ^ᘱƦhxǛlg %9!w j8p*W .VAWSdX꒠c.QMʐ!=#ROqjัO*QV"ֈpjb#`wBOb3m0y.P%6B/MGf>}+#'}BObCT?q}oWoQϦYUbW7{5#H|o0Cb *N #CxqHn"aYwn_x]=8󋜦% +Z_d )9N?bO=m{ƾ#DGp7Wxfgzshk)Z2FCgV[\ّ5w%2H"j^iU>{M"f4 rJi (\[70˲L7[CeLɎbːбP*rfUXm\{Z̔O;hTH c/26>l/}ɍCGѸ]d+ w3C]laC$)Aܫӟwx}㲦s &t9p̊+h#S&U3"tp|]KQT=&]V`VȋJNT O8UԖ(fVdcqPĽdK>HA}VJ8fo{[i9r9I5qSg2̰9x77 -o= SZ$Vo󙹸+_ ev&R`N}Eczl_R_z3׈@ryv#+s߀s~SE^ (3"pQzj[(beqrr+y +?"p@Ҙ_&xAg/%[G$2u~4 SBE&Lga{Y:!@`Y E-k:@l⽔9Qml]O~Cx\v =o~GѶG iTr,AQ06^30ٰ7TR" 4c^2Bqfz7H/­v `alX͓*1Q"D6\jOqj .)CCA8qeR#u=$hDSD ͷ|䉡09[0f*xp W@Cx!H,e@SH) E`3 ^ ! gVIB6%+7c)CYW ?o Wf22_ˇ+h5uT+҉O^:OD?.C?fa~bz^zpKkmX'3 YH$ 1}/=YPJrX~:b Yax1K}GYNDR3.O%ґQkSN(Z੦IįR~%QMCT a[ERboU5|Bռ쬥ZDry۾ڙkO;DctHoek˛ˏ>`sfIIUaCwJɯ")w> nn4s%2u7,军Vjݎ+HZ^JogTr \ h0o{BC}H woG(7y#;ǩ\ Ǖ^{r<ځCP!*2/^؍eQז=}0ǾtOtKfݽZMSm4O$jY:5ּqlfmNzK6JCK ^Z+yv}UWPQM 8-)ș/ `ap<&3a]GYU {E}?,sF*g}b>ZSuрZZb#i*?kT$I}ۘVY=̯,VJyT%jBҏlƯdJfff ب+H67dUa*$a 4 EB.rl}_=|a#/ބZzxa)_/l?{~ݹtpd{[)YT8t 5ҕF 9G`98`eR*k@]~cAILdû'3S<tK06Gfr2•.=BX5)y2s A^F=imP?qLFdnaDboz(2֓ݺ[o]oֳYdjvf?3ez=\k?]o>IJK񏧠<=VҁaDgcJ0Nn&"hSrt~.6:  \W*Y IҖ$#fI:.3RvʠGba"`hT*,)j*2OKN*eE5S}ų)ygnƀO:1'MD p$QRpg)-S%tr e~ d+/^q)#ǦܓESS*VG3RR|G{ƀ rֲ }Ӻo[7o|?X.7xe̐ -FqtW֕*Ҽ{TZ_Ӓ8Dț^fvv._E &D׮\rB)3BEU'MQsg0sUnpC M3Ha K7i7V{vRⳆ©R(+u, +au^a}(vxvYG|2AJЖR<g$VnlʼxGD+5h=+O? @4:`bom.?6{[_m욝mUB*%yKrz_0:mr>s=pp)'D+j\^㙦nܠWBwQa^K 6Ph]G[`2*+zSXܚB ϰrWc qv.P+ЩwhPgS+JM&D3k脊|*t ,MwO7ȍ/%F:zucO_9v٣@&ujj,c>)0"`ö='|)n&v^Qx'\h.X)9l{Yx_K=rMi-2J(O Jh$~T0A|wΝ6Wci,zcչZ!*x֓Za&쇼8#d~0m0ehJ2!wOE `t !t|?Ӿf'rԇǶj;O}neg´sv&YsӅz: Ρ(]DhH(w ^H7㺓;//~~~[l5s6tnOibT;AL!@58 e.$Vז"H 9I5豴jDѨk NgKd׀{C y42_{q]ڵGKa{15M7}]ΤFB+fW8Pt%WR\""_!hO Ms:̋Oa6,G"^W%Of*U`j/=)2 ­4A#dف'R4=wl;x B1 il '!jFXrYԗ/b0z;PhG/'8EPMnF{C}csgnXADs{+^y>'\C+C'CQ.԰]|!9!=^%Ƀ %1ߝ$("K&;Ud3EΟfBWOxgTTfӔeRsģ r՟ {gFi׏m(_m@JM)t}!0c-~x_ͬfUfX}EC 4;--wj-9(1G:bXѿQb1qDv?͕W| vnzυu 굫|6ZHyJXT-N5H=nz9Gf4ƞVƟf>:F NHoL$t@aR %ZTjF?w^M hr4ADMH>]~vxiܒZnF%~~:LX@Q 4XR>q{ Σ(Տ?6zyYwm^|+C@Vk6&@eEŢ4խOT$Oy]cd+8#_iHkeu8;g(-4-5V R;7d_'4}T Г PŔ0 E [W,CW%_8bUD7Mx $Aw|6/k__ݰtXԹỎkk3ٓe>K`Gv!_͐I'r-)thv3Nb/K#'vsW46srrJgP\#^?;!"%>ֈx2OYi\NKLj7HhLlkh0Bgg1@kjɜu9>Vj B6)c>. gvlN9Wo]~^.=nyύ/o]DsaKdw0ٚQ5n`> .dsLH " J(rMaC3 Lٰy}d]W* ?|ƃ ]L9]w+s67o}KO|.ao R.ĶM1Mf2=[{{Q[أۍ7߳b:Y7=}4'},F_j퓟r{|ɚEݺ!y Q_FF/BDW/Q5d8 Jy7J1!KTXއZF> C\~1w%% Ics;x -wІs+۩\]|J?f{A^A,̭ uEρ"Wdf}g^}M ԸqLr\Q^9O1gãMseD<_}7|m>ǩ’ػ=n}V{S|m ?}W$N,&gœF2nP!5@BڶKiM0 'yXJE@oW*BƓEG#cg#2g!ƃs\ne(T-1IA$W孒洅U٣݈ZSfV;vɋ|zr_ߵ}/ v`oK9X{"8u:7OΉKAyܧ?g0t~n^~KL(|yEKY[vVկe>庄v| 7dSaĨ>jHjW` !^Eb{-$v6ȉꓵ2_qg[RbJcnsffFhkoʵɂVC(e}nYbƙp$#ry;iG.{|zdi$ŬlW9p[W6Zb?ޥ3k+~=o_eLG"-upk= ̛pH[{\;ѫ?ab?}9;[B׿~D0Ձi]Y;X7W Lfʜ{x3❐ekE}[);wY&rt<hUcߘHQ^B&5S9U\vƆVt?gw?Ȯ>=ow=j?s{_;/vէ02ny0j~<\x9BƉQ J©[ITx\[9dB,I+42A|30A4Iк+ظ?u #~=J bD:>޸><<^ X/OW׼>kN> rЕ(ϺM.veq].ԟsVI4tA%^vmM~>|/^oևϫ_}{qSATB c&;7={.ݯ>̕Q;*roCV'H.L Z01bٛI47deM1p{_AVR>*O[HK=mkD78R(ojfؙW"KmSE y\Ƽi2Z{kɏ{{Ux?8Xq6oniMD4q;Ah6߿l_J/ôgK'j{[ZrB8n9蟜'{ƞ*+;'uFvd7# ^k 1ov]ٻͮԽe_vͭg/ dQPOZFBb2-12 cpC݌hC*k&|NȦ&[C:JGGY6L1IԤYEJ 03Aձ&^Ҷ[ޱv̮-)/c_/^N_: &`1UAʹ5ص %}%(%;wD-tJçnB%w/ܸpBDnV}2 )!Z كٜvkT{髋?Gy>?iw?eFCt1e;_(eH7~x KcTaJ12³/ޗa^O/f5adX|6h^C\x'p^c(2\ko0f]7țg5SN}ﴚ) ;<10[hӦ'Sg@7Pܑ)zCD8N=aO']PlkTr+P%rJOm/y|vtlW.kgmjW+#ѩڼgWW~yѓvUkiviL0>tMnwJsvg_c:j_8$&8tb۽pۓ;y]n]rfR719RJG =0fea?b. xh41P#ַF m]e%ѶB4_0bu~ϝK 5jz5@.3#qk)EaP̘>W9/g%zl/sT $6IS*(J2t**8(U)iŘ`. 'G9,z)OZ-KQYo4w"PR,.}u-ʊd:T{-TCc.?]BhYM֒mHQ4sLeQ#7X6z  ?xΗ}WZiJrܺ .%~^q dW7ķ\ܵo_kNݱ>k׎O~CvV"cezy̲of?ٵiJQ}K0Um.vKUb &w#`pPg9-;9ȋ2ՖB@w= ʁ=V#TzCAzlGPr}*mk6F8'k.zkdɁ!T~]͇6=s1o6; jRb{Wf(K7zm_wV/k`?ڻxz,F/艖 ^_Rk_dw>`vd Zv߭ %}XZ[ٷfݔ`OxFOqɕ0:`MʩwAJ:az/W :ntCr"LB|3t[f*vAϬc 2{:B 5j R)YC4jFRv, 6/3K"a8^АY #}dL l.7 #Rg@`P-JW4)l7TX:+6q]W첩JV(6#K\_|q>~>|=6mG;խB}{?c_~h1KîX7C #І^~ۖ,J%a/U[oUBf{j5 R9zD9@ꢴ=w saLr]狥B+ݘj$ZǪPo/sg8:c³~! U`{^[>jc) <|}wteb\p׿1_q1; O#LIy}wbہ~n'6ǻ0~)_{GW2ܵ9̈́`1q>l#BI٫cjY$/}=#^^a 7@!"]+Q\ nq5\ih}3V!ׁ5=fW=3E ~@Ve-LX 3\Rr9,q>#"KA#͋ޖV]X %R8ʗX0ʈ ȴ 88پwt4+PMG?Ȃh_NN6NlNGP1X]sUAN]1[逇:7"+o؍nsv\?EqC;F'ֳ#vRzm)7׽&R1TJ(#02E q>ηga :g/Eo0eH$cTc*%,b&ܲL$xx`¶z ұX;3A瑞yh2z`-iQ\q8yrZ32`GKTL8Npfv0( y8SQ@ )|A ifR s1WB9}f'gxNqf}Zwe f`H|QŸ~s? >T˩3JKdjtz" 3L8m) t3yU|%,{9j~(r\Gf~8O<6x{Y^^8Rl 7pU{1?~0 "Ajeo9|;N^i8nqrG, |Gqv7A39RX.s5q5 k;.]Tai#-ɨil_#bGH@+XSTA[&T~Hs]2 Pw(Khި__٪upk6;"'fSMص4;UWΠ詯i@w8oTRVwD{3빇"S/?Ȏ3[sr64fTIbrYIW \-b͗cԌrۗj0B*ǑZ68Y>"UyaB!x U HJC+G!qQzE,"$@-q?DJZ+Y@Rrk@P@mfc]R)ĭlcR-hThyH3 ^3ۜ5=;=Nvn{ #~ǫ] }aF,hꠤhf˸.}Xyմ 9$8z,%˟3׵iYWr6S,Nz73 J'L9th95*L UϖӍPNsZ-H|MI =e2f*.M{^, ~~"ymT7^hI1 QkۀDuKXm?J;m~Όxc+k mX=gJq3*7h WqW6!`6jW# ˌKk\ih;fߗ0ģ܊Ӛ:nƫ B.'^ws9Z]bd + egvԠ.*KbۣdnaNyk:Jɱ:up `:ĕaq \9[Ҥd/ \I>8O~6'X0a´^FtFDŵq4➍ڕr%PIa(k# eialJJ٦^U0e>L9Tg!To\q~ko~gaXEJ5L"@$$6#>{OѼ6lL %J/`U:ft܄]rfTXݖBYYé\37iu4 +kwn G)=;:RS] Kp<7Tsc3䫧aXHf(j8l!gjko6.уd8Ak%] @ºN5M{Ӕ/C\gky7P{F n ͵WY#X0hڵdν{>\mPk(wr-IUlщ{ +Ng8 FUJ3`@H<~z6JofO1Md ]θ u3Ef`>0̈+aS=P];O߈<)ۛ1wz׻l!ܠ\ή(N}>̏G>Z.uAø+c"qYsl .hF R$g~ӑO<}6zMX'7]%4%H)'j;`R xQأUF%-ivy(6UK}Dh6]ߪvA <%DG'4eA` "x֎3u^,(]cei\Yܧ* gxp?eکܴ6^aȳmrG^uivQH_46H%4apDSrx`Eɺzt4±FϡV.JgDqdFEKYd4"e?Gw\6||tbhz=&5Vb٤@^gpݵ덒X͟ءT==<^ҚjZIkGO^<YɻKeTDΣz\#7|  Cq`1Uléo>9џK}{B -dn~^_\3ӱ6 :&%̙vڀEX*+f;ҤvߔSթ3}, o\ `XɫZLh_7ӯCpkv%a߼:"N'Dj.`6 vPV.˱Q~G^W, , X h;ؿBM_(_"._@i%ە+s_ě^~ 3a!xmt(tR9Oߎ{V:mZdU4R#b+bg[#SqϣDZ6M/@Ϝ=ҮR;J< V#:l :`)TF [fٝ(9vc$ܾsC01 .wޙvGs2Y#_+Fu<jD䬯(bjLU08͟e{V(,ummd^G!cRCqrj˶M ",rcV.RG|$- R_E.$'Z~5PLYJ Pmv2W;eb'=kϵh1.pt;7 2;mx8.bN8=rx?tXyz;k:S+ȬH31J۟y_4<jTф8t0~:-vݪq~l9& WNPMaG^];KTxh`tFȔR9V;Pm2A?+ys]fۢo9}ΑEx6*TlSpt (2eq`jugQ/϶QQ\g^pb3GB5 0=/Q~R~=_/|qlR8gӽ65W`A!B[+A[wkڝΨ#<cJVW6@؀N ^| yggn$SVb6XP(z8;߅XEq\r} 3[S3o@8SZKht ka<) 4lvA 4s.Ltz9acR}igoBӗ.F5ԎqK>"FV .Klއdu=|>^WP'3m4Wy٧aTgAuOLg,(j^ K] J{ [dVJnN Si>̏z\툭JY;f YmUgʚxz'cz4EQL4vD\5E~;р5[lNV}f|e5Bʬk V7/>^1<_0%1`>>#>'a1\u{3j 1`:BjM/mJKЁqNM'伅I2<0$FqxV?ۀ FHe^Z}ϥ`cx\AĜ3#Ar7RMD6!LQMip=g[m%%Fh mÈ#؉8|إ_n0mF>[Ud6l }x3j0C= ܸgq<сjb 67{3[Ɖ)tD,)#mm[Юa^߾{Cm:TJ7g[`,|\MC\jrP  FԊܘQ8Zm 6*p*`t2A ZgO`QThLLzhK&ɵ AF?=ïƍn׊9ez6f/ bektn8~_&nقDS35-Uj~K^= %-N.dM_/9w}ȋ kQ73G|To&86qpf6cNi3v+,//i]85F h@1 HΕU .BLLAbTMcق>8= qnǭCȒ#åufD*شkup[6m[J#N94V/d?Jk卬cH݂1'-]J>`́;E3qaՉ^[.;h'A3]m2ZHҁp f4ɎH`DT`a*{!;1"#݄zj|W$灛p5ɑ՗k"6<+)rjQF/451,!zuӕr H2ձv:N G?@Fkc3JJ^o~*,1P5Rd$,o~ z&E}F+ZqQuc|wǩia 5\ب`I?sA[˼ĕk!/c r|mbYq+k!.["Wj6e 㱴gvC]WRt:wfFQ򂒊} d1o4:.>^._92%'32+T?| >訰L*y Lυ]+4Sg%{u`2 홒g᫧ 0^.*h=F `lW~H''kwB޿gFdJeEFWUo)%2S؛$3jC+R'NE Д jC v]ۺ>zRI%n&dڹo`dޞ> 9UKxq#v 4{YK"d.$ի95EN_؅uaݬ@+ 奶yFH#l7>WT<Jͨ˥6+ =#J(,M10x&į8aq3W4 * )`B\cDt[ɻqpC=o[GBҋ7kj,DZ*O9tF㴨M6.>u9 *1~/zUnfl Qd(if0rdZq XEѯcj<4:.h:)]%w^m'0_IE53π#aPGAץ:_~gV^HӍɶ^-k9e<>8*rTGєLT'dim]:lC|˒h/㇞y%.`TLEyZ@o1>wa|XنGOѪUc:ch_ VprP)0|IPfu%2/O^}('+uG8+35=rj&:o-L!\2'Ta^"(o?E.6AVXZv= -nʲ <)2TZhiϸmXTK3w+5uz4K7gx#-I-"~2nk`D6ձ[@k޺%(YQl΃?uIim/.vKcIKgv)*)Ʃ $X11 Ek"SsI<*o\gv<`78BezAF,`E=rฤ2nxZa6f+ݓ}_aN]ΜwF a6 mX+gS$Mivà Q GEV~ sw ReP g磢O-1Ij5@Uީ >IcfW.~/{fmHG֙;UѢE`+! F)c` )r[,\=Bw\?[Ӡ3nv?^]pT6X/(y>:v:Y"J4Pr0}\;F{x{dQw4J-`x)5i7=m;:2T1jgWMDzbh67(2>%|/}qmˡdTMmǫb7 L`X$&3s?^$NXK(քirɷm(i3W|9_{˓uM;'0u`|y lS $iQ e|1.eLs,ߊxe`zK-DuǑm j ["˙^E{ o|nVXE<̣ Dz$87?t;o-.磴X~d9M3xMpG-D݊H?pϣ';_֥.Mjj왩#s"## C7B@ PkdA5r }J- v]L9׵6^K|@ۄZmIVMx"me~ hBvӵ3y=|̠کwAmM\]cs8o'ʬ'VhPX+4Eg-j^rvCe*D$BV#35!3!kв:o^flf^@ldP%r_'{DŽȕل'ϵH(yOx%D\oS(kQGL׵t(v:j:!*Sp6<>t |GK7wv1xR!۞vpZZ9i3+y&V2T3l {wJ*'@& sʼ_ pӥO]ǰ%C mVC˗5g-u@6,ɜY8d/zt7 P:KV?Ͼ G=&ijHTg%TD,'k?ޅᇰ6ګ(zAUMxT/ѿGV+.%i%}ggϽ`qSx`r,W]>(wXJ \϶"\mmˠ'۸Y=jCꤼׁݻo 4O(FXϣ[tGw74dZˌ}zV37e a:lTvAoFu  $!>'TſG}bTiAȝn 2 j W: [<;]>eצ/~PfޣaR7 cs(DsLM/U~'fTd˵7-3k3uZ3#g^RNUx uFݸⶬ%]0*\RO篙XW+UM^erj:jC`jTվ:r^pqj+ޗ#ettlM&\>vڛMu\wFdGV57Qw?LR Z=6Փ?cg8,OqM@E?2pW] ō;ރIlNjZ5r֮{'L*,]m1-q $bXr>Sf냿'Z] ~\L ths +w:8 GNMp^UΓ/wom#hA'3YOj .\uQ5l6ga$zf>$hxB@4P8s@0T'dtAbK/Ҳ3ԇ1%H-efKZaM,ʖssoUnӜq twY5uJNX43=(kpuɱ'sd(VoOtNEH>eo߼LWB:MAPKVdRCgJv5$8VB-pAV[޻sڥ-TLh9 k{,su$CPy1%lQ\+/o~J1L}MX$j+4r|ÅXܿLt\ǞuF978(@}udOг8s)7&?K(Z 4AdVJkRV^+#m +{{U1Jn*(P1-h{RPMzMu \+~n0M *8)\$s8Q~Jg攣D BHpI<>!8D)HbPr-e.0QO`P} ]l:V3nY\`K50Q[KPVZl9-B ,U=W(El_k;v̐-NGkd $}1 M2[ƙh*^G3,mWzg%]$0x?LhSjwAєӲ<>h/ފ[oн7ER5 :x.M,o>t֨euz]q⚵NV_dcRS,OٲՁIsc.quy,2j:_5ivBf,]}N15l87^(YR2a` fnhOկ]wL.tdD"G*HU;PvĤ_G^7܌=K,;@;X)ޅ6 t YWǵd1o04^c<ı>hjÍ˱˭T 4J.tK쑫m44ZKA_ #P[?gbj yx It5Ơ+=*52-MheqCHi'8Z{$ZPG/ʓY}HXOTsLtސ=Fǐ)iԈJwPlGXo jdaֲqj kwضYIi ;%ȍrCpDk*`iz gz}$p(RH ,RrGh$@1=Ơe.ܒ}۱RMFR O>k7:+sCPXEʠ Ph#{EY~x?=[ "ҿlb\ Oۇ?s8LiAzB/|c^6cwXgŧo{D͢8|_{*a||a^?uvxs`QfMts u:i!v{`9 Ͽ2,Mk~EK:$ZȐ4ټXS*e>&ľH_h)fyCGNiO fuW^zQS柽L_4Sq/4,Eru`0X^[֥t,^3cd="jsn 'DfVCJ=n솦l,̴~?G f3hkS$M(v'$p_Bm;sE9h3-lYH )unڜ9z7Hlyor֯z4TUYLao*jaqx2oT"V1` xy8j^oE̍q@O{@p,x0lLGJ`.02͙8n <R goB12yް>^['Y f.ؿsF'>~ ah3{zchx-C>SiR$*ynu^kAf/\^a~qĩ"NJVY`0-dJ}7.)odՌm\x[ ۱#R\-KoEŖ^gY5>IvMt45 k3rS./֞"g0ʺ%|iͼd: $^ZLBMWZ)),<~O\1%i6,9i9^g<Wc}~eMh/GM+}m-Egq獒?W \|42Ij$Y>ӟ _:t9ٺ rmz/ $5*F߷93'U(.؅I^NF;/d>N4S| ]O;C BzUw&=V,U`&nec& 5+(^`^(VnI1Fj9wtSkxx 9׏ɪ8.%*z/#ܺ(M]؍ RZaҚ;{BѼ >tEavF֢jn,Z- rфL?Y Ց=m: p'RsAFj-T.!4KLá@ <-#ѷ!рcG,Ϟ{S;HK$ZX2K8").= |4fHJsx3s.,``Jj "MaFWh96Plf+L.̭?nZNE1nr6Bt"3bsh8:p Z4cK$퉞5ՠbY@Dҏ|פf9IUggQŗ[8ژf-eئەk?WݍyNan H"%SU>@kIz|Fd[\Ů׼F?ʗH_ =xx o}W˚NFn~axk`{e:$ sӪr4 o$3Z`޽K80/" T(SSޛY5kz"ۼ EY [-V5:ÊC54Vj`! lE=Z1b`LShj&0۰$@ofZD;)lm] @A]LG4 9z70hXS!)]pJ2eƚRA*–O9+ M  0cG"!$2VWiy827Սzz؂evgPѻ9_:Os!Vc`k4þ xހn^"(s{P0AeF,y9mY"O} gPlfZaԛsMOW 7I٦nThn.KZ?G-xLJ #aØoKd<ƶvy ,m}؈۔R88:'uǃ&:7fFz",^kpqI,v`ιȹjqΟLJ^&ADGGQ&Xt|shczXW18+S-&Nsyl2zМ)MfA;cƜÓOd.~\ _-hCŝ&#Pŀ8F,k? =Ck]L#C&s^j|=YI¨k8?Z\G>ouvuLZ=2FmZQE>vTfڱ2ؘZu=eZ(?u~)1$ <0:Ș7_ `RZd]m<i;9Y46%:MZJz/Vb% l"[#Ǔ>TOP" X,.0EVqYVUft'4n?c=kq/%@sZ7Fβ^FZ8tqn*2(Wlfki`yO rfwB bRj} kܧZqк2>{ 5MC\F`(sWTog%3 ʔ]{0z˼wE7ܰM0)"7~ 2e֚+JGqe~/yӱk jbG+k>m]T_;掫;eΌ7ĭ`*jC$T`$x-7O'tE+,z|{{ͰNk/ݻxOO+t0uZyK&l#+NFzhy]&xrmS烸Fm^K̐TS 6gǏƋ.t8ujLъnC"XEU,GW"fs0Oc>.NlzW1 k彊{ r.t}4Zs+NMxss ڷ5G-7-ER>k6]xof#MIhmӍF=^ȮnqS;^K:gazxr|*p$P (pk%/iɶx-uvNv>wS lNP@8Ǵ J7T`'KeD1Zj9sAQmHdUgz]BУ7HPPAEi5#CМ$ܞ$f~vRDT<,t2_ k g?⛑w:V1+ߓeOQr`<#ւDX5&#\mƅK;׏CL?u)*)f/Fڷ(Y la{pN)\MAu`F D\6b4U:g~\\([okiMX)SCr\En';xZg"1azcDEPcR{4-5\t#OG|i>`G^W,8_2Z?jCkuLq'\rw6kaϽmyd<}t({:.}םw=ZxAPZG=jzV BՎ>{ǏOx䓋ËVׅ9*㲜|F5oVpBAʍѻ"㑂)Y+YJ:fX='Э(%Fw2`qc*S;"6FKxi\yRSm#&O^*pFKZ=_WYWrLX}\NLOV}evccf$qAS)cyBh^4~z}u+}KqgJ F{ʱ"wENq}+bwBaW9[Ѩ(vc":X9ֱk0l}ۗzfk!EIUcum_cjbϠmy h hzF"{ZuzzYs}EAB/d8Qw8)p|٣NE= -tf6ɾOdFTq#y5t Tz2s']5E 0y=s5S{h†:Q[^f%+g+h- SaJh33 2B*P)&`֡GGcds*TI ԕѵd%;^=.\w/"sX `p w?p9&8dKMJvjwG굲7Q@P$2,ŗ|{sInSݏ<oxNĽ uB|m #wVw~qR6cxgގ_|-}9me}4(@o4h_z~+ǑﲖDq>*赓]<{%lgg>c}M\;J:.b<~+:XYYÇΖ4)1leW9]L1N~@ɖ]+O=w`BVGg缕YS1okub$\8ϴwUS>~H6)dfR3܉;('靑aaeh%qߵVg)lΰ5sLvzlSja6EˬrpM"3XA2"5V>"tK|ZSO:C\ZXX 䱜X_͹o#*3MJn Gٍ7Bu/b(ǣlfKxMxUGNf.Z:{\+ 0BE=`_,b!OB}CRXOrGrBp:z HWAS&IrcV ,Zb@gPڽQk..ے[21Y,U܈J~yFzFhZS T'5hᱪT'?ӿ߀>\Lj;|?LT,okį~Ta<氢zԚj!gh,vWXkw)u6ڱQmW@8jZ?v9Dx,@>GpۑqFRf{4nAbTוk2;Ke_̈ g.}Ջ)}’HʂWmC=7}}j%$:mn5gh3;Кé>qU!}9Nh>\}C U1VR%=hp.V"`(6gj=h[MJ2.QԌw y nNspWXQ,toB;nb=PD>dS6gMYlZVnbP-G;4w_= E gPѷ#?r} 3ۋhJ>}TrsGglk_ ;/#x$-pIg.hlfs$b5{;E>]Kl˝,}<3e<˴ &jOL۞2agTGX)bS)kzIEƄa+9z5経mn)cMYI;C&\Axи9/ymU%R+;*d.#} -lheje? 8,h)LGuq@,&CBIh@{=Gz<ez}297Ȼ[LߚB=#`jhͪJ;]4]Bm7P[,<t![[1x$ `p0 ~Y\VT-R_.[6mC+WycQ~6WcA;ћ:4s :ٮ䤹h@, B f. o\#񤞪6:(]; l/,2_fO&󫥆_kG)O JU"Gɟ{{^TrV~Cv,*=xxyGKT׎}#K#tpbL3;C8PnyPLm;LFazHOCȬFY~\#W= ٮ~E0򻟿B9n"eDY?IO+}Z[{QB\Yê{W~ X]5Ř`2*;w,Zځ 7X6Ejѵ lm{ \T0[6iCsURSw\9;[g &X:AJ_/1ݷ%:j:cy*L"*k?kzzBt|[Gjup (E3~pѕ"+:Wt*vԩ=i(UAY\Jf}Oɣ_aeGehQsnl՘vumTliڧʊ(~`F%UC&garUT-HXq(!#A7pTwj:1{.Nji39w7[bH0LZu=7 "3T2a9)SXD:ud|R_W/m'jX-2(m1#3:Dni "h8]a۷g瞃u@'yBh EjHlQf5b5zjN*VU#$5#3*#Z&<̥ɀNۖyUcn$Ůvpڒ(\ˊN]]=Q{Xkڢd(Dhu!4}V( ( ͪa/_5 {"EkO>fv&|(﹭|D @Z1A'=f^H"4g,ʛ^~= Ob׽ J9ʱmO 'Ϙ7-tg2~.f}=6AiJ،Κ{Պtji4F`ֱ0Z"1ws<ƃ\݉Ղ>ɯ3#l,WZXDuZf<%Qg'2i-}ueMo t|?ln qro#u2'f5n pKOŶoe-p;U{?v-`q$m-fDỾj̧:Ym]4fc+֧fM m p[B8~/y>>{Lqz鐣>;EӤ|Raf1bDuk⢂#YX`˫T&"xj֤|+@,u ?~~IEV}#=f+3mAhqm6bOXbP z} 08+kƾ˹W{/DR7&2`~ /Xe$eȦ>[i\v]Fdm+8퐽]+l o'A=prKev4s] p5F g"_{-hWk~ׯj4# %5|uG@CODÏ,?ūjԳmliFXyqYieߣ@a+750^;4e8+@Z_}1FR*UeI5Զ>μ-<Ǵ/ӆ5H i-gn=f UCjyf\>x<:vڡ-gئӻD;3WgpT nu҇O??Zn{x/7<_^EAQ 8@ όWg`i0B7tcaSx-<; HYRW/{eKѫF$֙"CS@},:C+ &x'ʞVա9ZMwܿo/銦=dH%V[&vs'ʕ3թ)X^ςr{2;!ۚruёscZ SkdڐfxWQspP~#u&˃ֽl4P1ֶ4T/V)ޢ12cA%hٜVc9a**3[;O\4*"R֎6Ѕ2̞zbTr497&9{"ө'wgnj;m~)"ySqmgeU{] [c<1:)ls.ڷGK"Ǚ#SUF!۞rJfDlqtQs[٦!\Y+<Ҷ#UѬ=mb߬r'v_ZGRek|kiݾPܴrKڎ} 0q`=ũu Mv#w[{kMѠ_scZ١~y|<u-s(5$@5u_))tzO'7Ǹ'?kxX/`՝YoM0{,.'֫\*Ѧ^AKiƚDJ77;Wt!.#q?d/=?Q^]R\:d)ʤkMH4<+ B;agsKmu^+> *]9W"Ϙ|zSq{)ZX\#Pۗ˯FaV2)Z^xY{:B64id'Al&x?I{oC(a4g )@iJ&{>z5MA7.\2 KeLXeuAi`M x?  "3]/|x,CyxoP 7um4@ ʱ'_EXY :M16μ}dD奊 ݢm EԶ9sIC8a3#M7 zq9X)vi90/RuLlGM&2ڤ\Al_M %RY}fnOhV/E'>(dctt'u^t9\ͼPԔבFzLA9LmKn.>Tj^T>6M$t'< wlfqjQ11^':mڡTYkMk}&|j#Ԅuf;c9XSnnHKaX)q^3U &t'yO+4CP썩UiԮUT}Ռi9N|w#ݝW}FjJ{q޼_;؅~xu-\asx֢4P(eQk+Se@]q|?n|aejS\A-_aசF_ S_Tа&x3*ng s?a\4l綕kM$'skGs2ÄȆi8tpP^ӈo\?"/kzp>"y߻> &HɵQ徵8:I5 ["M&kzIZI;1"8rgs8 &"H`y~kKIF"` aUMGUf:,̀PNMOP JBe-5 6G&"*Np Uc2ys ,+hkDL] UP$SCjysNz,FyR5w9B62-}-2I9Vl(kS<|Ubm%q;V_\JtQy̏kBzη*:&MzP+*RNݤpf l.- ha=I3ʌԤX6CJl uKR`Z~޹CW#רȱUT̜f{T"2Q3O碝#4 BiH|,nyۭx };|xjF~/寺:\<>I:ylM|o9a1j8 ([/_a$Re m^I7^*No!vϽuOu[]o7{%EPY;ow~/E-==q(u'KI,5@pjg*3`/C,yVx+H+=l_ X.r̋% uF:q %y p:;U(< E> < x<]cGNa@8xG=ʺ*kZ"C3Ijfhyj hTrhӪl-tۓؓ{84GHUO`YSpD,J+G*+jGV~-QgҐm@ہ$!LTREf9<`3r)^})cf\;0ʊ?:/Fhn@UiIgγ~nHq9xJBQsU y{=GŢ^/2E;S:(>3l% $ˋީ $`+gJDD^y]Z-@fȊ/LV@c$tw9Nt+ en_8~]q aW&؜n&g83wǧsX%ͣ [/S/ԎWNd>wZhMѲ9$;iH@ԏ%4/Gsa0BHZs)?/fo*n^Y) Ԇzjs_O!ysP/`u^rh,3u&#[˖n  Ncv1/I/  xIkvL Lj|߄zglV: fVUCe#VUǹ~70VS)ɼ.T^L3⿖C魟d7%F͹IOzҙ)}]jD˥~f>=7TU7!̐)u zT'aH޺Ac=9/`K~L`LSXZ>UWQz_xŎ/kWx0:$KHY[ڟweF #;hvZ{#A$FC52DJ{spiͳWd 2Ř{fwLŃЌ5 GF4HR. xmtTD4'][q?f`67g{w(8R= :=1ueJL}WUkht{q 2^-|RF+Bd^ҫJG%m.>%L`c,X _Ϥ!`볣.E[~vrFd`n+BᣵMpj{]}s^¾Wʬ>"cUUQS6'o{ݠD棫>Ͻn&@[-<̎$\Gz[&:61Wm=u)j t#vm#likCk0B鎘Oa@ d%?9käk[db4:." Kr 4 6*(eek`c*@&Zdj { 欼dR5-lڦn.SrCUjdSfx6fU>XQ=n4?*4,[! ~`YՐM[="RTT|Ȓ/V >qn#1n?ח!)۔َB,D1YN[ZW  4H$2hX!aa25寸`K̅~=O1r;WѭO_gSEl"Ü6kdqR0{5ٚOV ԓPͨZIpf*f {<ꋬH8-\ bV6sb%m>@ZN;9:y( Ml.9=htXAx} @QDF;qp)蚖dj_%Q x|5 bʣTr (f٘gُ1PO:Cz>[#X[,3 sQR4kB@*+𜴕&ax\c6 RmdrtNZ[qvq:wշSgzl+}2Dij6E󦿁vFƮJ"k.P&@'թ\ a2o K}eZRLg6,m.5f~ܾ:Ns]of@4 INi 63$KCr=G_qo?ҎV uU\f7JM8 ;)/Xw4aCo2|ۗ`o99hGVVE|P+7ҽ9@+5cU;4=M& 5zЫE Bȟv\|7q{v, b 8C'j,@F8]g1A#dJ[`uw@?2[\P壱zUg6_<}>.f0l~`^vj_SMfNzm Lѝ]peҹ# 1Ͱst/F̏2ǵ#<“>޲?$9o=*x󞆫m`{ ,5ZM%N&4v6p7]YYղ)c%Rэze\rss65yڍ!1fAZƛ؂|pwT#fe͜ѣ9өTPf}Ɂ唧NN#Z qث*Y@6iБpm77ڞ %;jŅu0bP ojSIznAgM65亡͞ô' y@߶| 75}>@_7jWsb^/Z(^١#( XmQw qCEG=qNM#v}%Q n$ӈqQ3Ҙz/ Qn-i+WV}g>7M-~Þ>g[ Y_z]WXPZ=:L(Lڗ=Bj܌,R;53$^cцB 5Fbc~S] Y^m ۄ)IPզc.U,j+whbBuR\p^H8"];wt,0'SgK,Uo4[u{A#4J>zPOk r#,sB(f# ɚ ɴ-btVo^˨% \ cۀD#e3|N{GOIS9]EBynuw[ I_Txܷ 4dE}<~1 [ՁGv4#㑜LIK"' ma$rEX)L`3Xk>nEUX'ݜО6q#3:H%b\ jBSpep uђH#U5<tGcLaݷ|E. ױv vGlNF#0B:'Ԧ$aq|=kP ?7\[äer1ʌy|9)02E7)x+:~m+7hix.7CSDC~&b5UXe$)IZncabIAq=xϜ(lV,̋FJJźen}dw>xJ1El !%zN݁2 0@BAWg%p1Y6wV*lU3N|'Ws;,#gS-IȃK*0.U:m[}4h%pViy"YL`K퍒L+KF|:BUTrR*2 BeM>1`ֱ4۷AHpcv@yfYW}49o:oYBh4&]e)5O5V9G4,oElVep}z|?~_݁?+Ď EDk]U&t <:xKe=~˪#yS"gKli*P@ѣ ?@"Ege :J b:'X3Ft,\` wAg.(VsDh{:\ylN%]԰mQ*X XI!<?QԿaf#4v{سkv Żo_rY/*=\ ZRM"ML4`eC186cO=~f*6#ܥwao9=gxbu9|MK`3j.j1tl*)2MFf#݄9e7&ڞX>p3yEp\9$H\m(ԟKԁPv "*u l@펗O(}!k"Abdkؐm w u@ Bckƴm 8%2׽P,ȶNY?;;c*k7ѹC|703cvA]Owt^\Û?"imvbb4[S9[f7zƗGV2V#N̕钛VIvLl{EyгtM wlPA`x*VżG4 J9w ,jsv.Ԝ(l[HX~ y>smya [+j刺;݇1I"K)\Lex'N}F 5alb9XR{#ub$[OFaAmqvj)lh&7BҘFbBr]BU`YZ҆k87x~xTRll-G, qbGX$lHon@zܜ`h  *U^ :V'NCXI qɮ1 sF6 hY}U,͙dte摹S灖U,;Oh{N@B_XʺJk?,xq!4+QrbOFZ՘Ά5 cs4Q5-rqz^G7#tYO0oҵ.wqg$WV^1AŢi:moiH Z' r9_4keXj&=g>q:sr=j ެ #S\خW( &n?RJ Ց0+9.Ֆ}ԅ8/ae |n%>TяB&ySq?vOq:MXZ,m nBиuJ-ߕWtd̶' F:| 7eBv)QOK{&*4P2)R|%dORVĞ%2G3jːʱJPLK-Qvx/ߣjko@7ӣ q1_ṕQzTdj >'gpkɉX̫x ^p1ʮfk%xb{ X~?jtx{*Q7')RMh!1@fΛ)ȣN"lYQQb˄zm*xеVg'a׸*b7Iը7 V.Iu.DQq<ھXk7"6&s< >~wqPrOxٖ0]OGz+M52#&\uUo7[F˞=2%6 8rz>Uǿ롒uiP(8]\?YX~$Wu&lsB0Ex aP!"@VD]1he^ E5A&-oxJ}~ s6wJMl+hم* S@AOlsL:mwb\w e/Yftޅϔ?,5i\0RgN` lfc"_3,M> yzߞ@}j=4+G띐eNɐy;zeS}V{dUaC>prg8)͌:ba=IѲhITQ!-&y;aF۷t2^턃M9uG|AT mC&W?'[{yΚ3[aP\oεi]͇kĀ_ xZOP.oz8׏(tw^LxDŁH-&Ǫ] (Jlh="7&߲]R lITi l!V2^l KdžAK:\䊵5B UzӉ[`$j|v4o`rmɨpY/g5ONJ}oޘ39f4͋DP1_h[{-]\\N#?1&Ȣ j#XLdDkā]sX禤Fb.:Ryg߇][O_v`Xyi=9{Cwd z6q^g`ZQB/4'[?-[WL:Vbe}RZg`~&}UW @gI2MZ?ן8s9ZKQd ||6lnۼ$.2 `/|U Q}HbG2#h]lOiuizRYMǤq Г EэGbDh8 Z1^ɔr{]Ÿ rMi}[ /c`w2 B-Rѹv2kZSsj@-֩w%߆UؿǢ`(]kCb%_{c]޼O0!z-a14ZA:;`6'stt]H?&2'!dq9|cm&]Ȼ!0c`G%ᏽLgh\Iz8w fF[=p:&Z@ҁ wb2V;9\/ɑ7{3)[@3jE.{Tҧ5(@-,P %n\𥼎7 \')mm=(5T\^u}ncSLGs ˻\i,+mZ{GuagiOT(H?^k}J.xϡ8~Hxuva,Li=p鶨`&@JXu}&lBl&M&^\]SAXڛ%PP\~'rlmGLď@`r58wtYQ%=m PfMKs E#i :]{6AV\Xڎs8:q~;#zR*TY8SzFRtp ӶP2e4ob4)7iYl5r(&^mk)U\f1 EfgN9M*6+49KB#T C$hիO,SxU5<=> fe7,nt^x'u1?^ iܱVG~iX)3S2Q,ƻo' c q0 [|鋮%k%ZZSD&Kn̲=3:bYSJNciۗ 7_[} o' ;k]Zay7+k}ϘOuSt2KCf&$?,eX.ߏ80h .v *=]쀬W$CMjZ|̕2Z~{'ֵ"nXel R풊gY^fa_?Ǡص78Μtܧ4Dyܘ@Yޏ9|tmkskҋͯ~&Dyn)>0w @)Ğ`uʽK$֋sQNUf jarc"d SXGK=o #T?"s0}7@l Шϵ{K (ɓe5NO:+uÙ ۺ={3Y,3,)smZR#àދk(0rvJߘrdR[qmٹ3pR*`M=u6'L#yTHH\#a(c\Y{t3'YF? H:(C;MԾ1s;^OsɅg4aqʹ/u4f ~Ž?񌹈溊i@x 8QABli{O4 {D=`=ۂ ]QZ.eT)tMxHdE=`D-f$:Β;w2P37Fʒƞ1ԋBvT~a8!䮷]\knw'{M 6-<؎#6x>F 9& ExHfL7= s2&"v! 9⧊71|oh{R)Dܕgq_oJ!PZ{J)`nz}Ysm SYzc_?ͣ,/T UFrr΢:'jy$?fjbLw]4C&޵?^]xbQpmk'{b\UH('Z=NygPAq1nct]nqN=;Ч?`ﹿ)lM;P g:o2E\+" c L` ؛ :+r!&QOĮ8)vD.,YuKA30]Fb/Yʙ\@W˥#*^E3_a>*j77{X+d/eGDSs:;6DqջBOt2 vFV^-iz'Qnb]=.t},"֢4{1^Fdo=6vzXn$цV_AI/_ ͯD9@MyzmcMRߍ-Aq+/LP:[E\EE b%ۃ'Sb2EuHڭF%tOm=o> &o׎ҟ/QVXd$U5Pr_$/Oێg^#w|iu(f%IkIǺ*߿GgΔf ۖ2 u*VNy"l}9;p= e 9<)N >n|mY_q",tI{OlLzdUt[hcU4}kF+cX$V2d:TpLozglF* l^}Ll % @+0vau[86`,8 N}, Y&^Pd#lp̑"9uhSs39w Nsє[r#7C:]s|Rw.Qo Z for %jJ:A*f8Z gte>DBrwUY*^bؒѽ;4†,]|ʡ*[ ~#G[_ãsPv *6vz.6oJn-0`:T~Sxc/*?HbS>۰a-RVTGs+[TKzޜL4נלOϓ, Fσ )l78(֦v5pۜ\|l##̙Ao6LfQv'YF}hCz!T`  cl`Ә/y8+Va#^zfEF Z:恎a{mV6+<5@H5eǑO.!,{ܙ+rdA``JB$gN4̚KĊAiBnXoXt}ZiP 4js}Tu(UWo#lkWֈB i+_r|A7( jc3~(Vvd޳QF;شEoJ%Y%2:/6c1s3sx:Z; ڻ@@@&gb:KUؤ;j^9nUGâe>R&4}|k?LE }t;> EdNϧ({eN";InvkjRǻ龬%3%X/V·}_>4Q.8Z eX (^f]Oy cյZ h8`^ўat^r@(#k8* =kTId +>ҏ~9Me$%ڠjl֫Lw6U\)nݻJz#biyͺڜ{VYMޤqa)LPmMH-_dM?XT/Ț~2;~r){Lй/7]tMJ9~7FJ$C=ZDm{@WMq3U;l!z.T_ȓyvjۄ<-rFœJ }l.Щ#|KCDk۽yy6Jgm`S(J#e%$ZʨpBYWĎmKը7KQD^&i9EfǣwTpMa'rK JlིWTh4"BKF(JUgi*-,@p!ѯ T׀_,Oc{8U_c٣;if'hQhή3zUe#q<~Z9yj )/+DL͒)4Uw_iAi|oą`U/`N2-Ik'5]¦xoֲeWFa{b[XpڥLstX9(ʡh= >#gh{2R-_ %xd FZ-~lʺXy{]Fc 4a ^\#I fbˢfcieT+ҙP["[o>4չ#`:呚Yhf^jL ղ ݍZPKlXVn S VuyQx\d"@t%͎A0oȱM].݆Ip.?:TG9gl"k}h@쪭&>d8I ڮ+Y%jsa@t Š)RTg"4K?6lȚȐ\0k \K* ÆXcU@&SgYYfpv}"[Ff57{햿FD]g`mѐGbI%B"_ݗ er/1MOs2b8eܷ='D?z ˽]q鞟|>|?'z:rpYkʧ't:5:~-geU 97UN]]VK-V I%Pl?18al &ll> #X @BYrV+:n:g5ǘkj$?n{k5c9~q^p-eYCSv8W؋IeMjdoc3h z8Wې2~^49ͶK+wvfX@%/O1ޫL\gYqJ@K+[ b>nNB?4fm :UvvmLv~\eJ=%U$z-ul3p X S9z-d^9`@A2"9% ?(xd,_sV+Rh>(xAtFATSpC@eJdYkv~sEh*8xX1 lJ.;e x9( GRXM+VCX5}R_^%3%aP WؚWWJ:IURW`sCCɣYfTdž:2'r>u/L nG5DY3X᫧ A@TFe:nd ƠPcsYF7EՑk^v3\dGOZa >KVItej6rwnoiq Ǒ,.TߪEB!}4'IY>C2KHyB.sW+`v_Y_Eq9.^=XΔ@.Mo֠I:<0۞7OxГO:{ -}cI9[UyeǑl>ڽk]0zi΂1=m i%s뤸vaIItgQU^^A9cc2@clTv \LpBdS(t]{}8Q =s0lX'n W8:¾:9K0Ѝ-J˒e_GpVFŃ{p tyӕx޳3:圃PZKAhX ]d|uWN</k΅N8t aD~FKdN?zk++S:n9?$)RyYۯб݋~ŋ2ar|3qap L٠oL? '4m4Gɻ^(ZPhf3ϵy% )J2A@Q7[rV} Rܵ'h@oZ}Hkb-RLrspkGzϪKEhRhwߵCдT3773QT%oy.*dJܧ;O2g![CoC;ÆrrU"v}G´͌}>J&rB@VghɛxvK6Uԡ,z48GQFhߊy\SFVf?I*o\K&cq?oϸ?qZ -ذOJWrv ;@?aM?F}^iP61J(k̡z7S<׾&6q l5/2"VVO܍VYD mOYfF̩;Jbz{j?uSP:[Zhyna^Kjr5 vt%%u]Oΐ wne:ص@&ɁfbHNj+f.RgLu4KwB{>7Y;@W]6{\iۏ=3k-mRl :{p22shMPq80$ÉͭЯ' m sk|B](nl" dʀy`!%LG"ͧn'(U}JZBϞHNf"6BR_x(΍t̛8el8<& -!ڱq+8/_Žm[ ѝXŒy azr;a:"baΕ-Ր(8'kw;BC}SA%J]*H9;3[95}qǏD@,RvvNVՄh:uZeU;P6s`*q J"c[k(闲j Q6S>U8NԂvr"Nb`cT|zyg= 6"H3e;|5p\x|@=fL]$է>z5~lRAp5*)톹@%&7L5ʐxq'g2ѠhcW(dnVsΨR:w{RgP_LfrpρmWF ӧR$i!֯QgÜw^k 0 M49jۃ3ybLdgh^[1v ܇Q8@l%zA"ESf (z}a TXt%W,]Tya7*37zbE[un{y o>}oQ 6 jQfHe)^khI*4=:~FFK LlhBPek%HY§?g7猲[_ϼRwWOm͸j"} v/b:Yd>7R=e`@w>u?Yq)KqL`com:8GT'?\R @S']h1ޓC9Arڣs&Fʙ588r:UnY^1NJi2Pzly;?5(\ ,a ٮg1²h֗">,yհ*:GUH^=rE6.gk6 >Jݺ>r7S/YN'QWS WRAShi@fUYe?*BDĮN>-!Kc7d{<=zGҘ%( U7Cܹ`x]40}Q*Hc̾6tDR lګ};G; $88lH oρjS=|4X0yk 3BeBtMLC m_vf8:(z *; 𼁱\yjE,)U-ڬiRc/*zB.[״\mx1ܧ og+\FȗO2U)X[@OR;(qG`IU@z2{ϟTo 8mn.XhCu6%Ͻ LRviKP9-EE$+ă8U䛤kzEԿ2ɯ_y90~$Vji_瑮–E,NMgFy1sT0Å~]4yomtX3ZNj"7өo:.~ *;(IWAh(QLet}`&hPD 2 @5 (0'wD^A30u6sà'A'#-R)k;f@۽.8XEtjaHK7\?&bތ~jPi/8ˑ\DKIm#u'^$PN~t"zQLex_Jneq| %EwS.݆׿:,3Y5uGTfʘ^8)qe|G^A8B9O-/7?NWzO:1H-*䦥*%b4p%{j.|Qװ{v@tn> mtqG :{)}<DN_~97+}KutQ:He]x5^ud DDW?O ER fF݉g{;PQK_[qI %j(v `p}D-C |4"rWyT ȿ51l  8O܅ul)淪)6 -uջ™ߌ티RWO1W7A6Ӆ܁yqRWrJ0\ʈ \X[wz{~<@r5YI Dژj k/=ͦA B2T15 yu4P)?5 `j 潧+2\ UpΪ.M%}nhqq3GJ_o5gnyߢj5I9CƲ+oN!欂GxqpVi+[0ZkrK FN!+]SmX;B. `EJ|f.U>0x(R@ehD}|mJcWoاtVMHWUAT\AmL`R!|(xjqFp]HW˞s hdM~D D5'H៭sc/Z{u tFs|BWAE֯`huY Y̨JH5kd:ΆwG&]75{=MHxɚ~ > ܇,,8RS@O$D\l[+mc.]X]c=]04V̽X*p3zYnj :ͱC@6 1{}ѡ 95 dknpaz J+\\DDdRyai'.^:.!C^zfC&I+M;Kmc>cYpZNoj͸e\j L{[ n#yd0 Q<'wVH׵a2GܨNDiMër%ع‚4,t~n=;)Ff;3u)08Lx !+.ʃQD^ti79M ]EolY{+Af9?l.޻>U8)Ûkt__ %0{sq4k{~ie7$T@sSUUU!˧>lN>;;wӓJ9ǫcΞ>c; \S|0NuJu~~ӱ{ 4;5uٕ;\~ޏyKEe/L٘z&uF{spU݊Ewc0b&BV((xP}&#-b8?S)c)< ֣ȹ:XU5F/7!`PfSzncVp('ܷn "b~uf=<מUW^_A--` PfOl`8 pP^X"F| ω9`tR!uoX`.i\SgC*sp U02L%܌$?[M[[ "-}7g<saE[ ~@~BRjX7200+;rVDN%Ѿi_fS $EH2 iOREMܠӞ 8|jUklbT\IZoqkZD5 0+׻ث3&UVg),mc.4qhÖ)'>{/w ڋ=J,P9" vv KV^zdP[3k"_XᙌQpߡ;}~ R|[8;n@iMhaY{;>%%M0Ymd1NvCO]_<ͅƲX2ʽJJW- L UQlt ]s<dP޷PV>߬?fduK<|䌜Z"ATYK/*g{I5 @cFA]($?Zӭf:|ϥs@Czru<@^rW 5 uL2DwoX>Q:̴1ŞZGPZYqQ HYDfyV>F-H`#vӫ4wR[i2cOi3؆>DQw Fc(#%zf su}]yk0TrUUJ𓼭r g=xyr11HYs78cT ;7Xvh2)H 8u9Wz 7(aNwB^Sa?EOmo4*uNcc^=_[ 8 OYg(o(蔹HE{AE\s<;X^)g~(KmyQl2hkt_8X=;8e{ug/cW:V4-d>LqVQ׬+wCqbcjn^8^~맱xt'`:Jrtk~ d D "6'W+ha5Lie2 0gWDuPdڜmq`S0X_TVm3UYkzg*p% Ul\k8b gEU+𽞌13e VoC7n,ݝ{>!fk gժ.o,nT5ucA }J.3R sv\EFeҳ 5RCJ]( '8*զ lR%0`sΆ?"漓puP^=f+y[M{TWrxJUl=~n\w`;-ODG8ϣ،:%͇HNW ثQ9cD)KJmH=lD6Y>E֖{ =Yh`%(|˟|tUs,3ppQ̑AМl'Bcw5*q`F1|tM1nd&p*H,~0@OlGj->o*9q@H`nN:wV3͚\W A pPyU;tOb]ʛ$Q4(&`l%Q~h;(MLh泦g\%HKr8w!Gg/JZTڗJ(FDpX[S "Vύ2) Uf4(QEJЊ.Y]+p(WZv ?st*-L%a2iٮ#k, MȗZy$Xc~kf"> ZIs|B#T{>(u[(kʞ5rv~լ{1,]+S|+IPWud\z}7 ~[7_|ՕK|8M,vk!4 ?QHgғ~jkjd|mSo69]uQ55nQ_Rm1's4^z\skA^[>x|skٷ٩bCua0, FDZT{1`ZF1FKJ )}R%Ƕs<Â۴)mO7<Dv鍃Ofmz -[pS `^+2m;Eƭr@#|TG @cEv, m{y==9g,ߖD ,h*իqB=k7Z<~޷ԍ."UT 7BY*TXG9J~}Ɨ\Ԛ]qDϸq?mEOƜyPžY]C'fKQ(kNa(zf7ė: vȕg x%w"Ǡn0L:{ou52o-nm( @ #m|yX*ް"ۍvxMR\5|FY0O^L]`K.i!y&֦[>Òe_|3$Brn^ YE6 *SnÄ^;I)ivG3k$^,S_Gs#2*Kedk0B_;)T?Ӟ8-i;ɟq!72Tx^~0ýrT_# Hu<G2Jw`fsU ^ԍ >7 Ds&V$dlN7(ǍZngoqfo?3xQXW|Xjz=X3UkX0~/>[siŮRpu=?zk_vT>o7I• v.FOg6;^m=NX0)o/y5ػcY "T|gя݁OP^jFUu6aDK1N\0fA>Z-;F]lN,LCMDnX2H@sgѠBD71^ޒ{FX,{ A;I5E10P=sevӪq4q #_( hAi}f4kZ׾dhWgQc ?RLz$[I+pZפ4)០F@)OyݾهPx[[Hi6  eګ&|eTPnsh?'}-Aq%k̪ڰV/OC>/} ܰ(Œ;ʺ@_֯UKg L,L\7|9z~Z8hS2J[d#z4u( 3&8 lUuϨ>D ^dBXX퍢~mp:k<قW]\9ʚڀKƙ,td}5 1ʡ({Mr"$6'9 qZMDBpАQGrK5u85\oV7N4 0u@ ,H7Q#K٩.CЀ $ m]W<#[ќpQ0,L+G{1cԶ?wS޳vw'qcxf|oĶTPzr}6Sqc^j@ǸB૿Y$_a}Fz]ɍա76x%"T"[ |~䯼7.>RubګcxU{c$v P2Z/r|'>s>]r%D5Lg0)t[9[z7ƜE1QQlTꚆT94FHL7(;uc]6s-J켣$Pd) :xB@o ހXAa+Npȅ+LLsv#%}~xRehg F}$0$E_[XEOlk= @N<ꕒ[.C Oqωr[c,Q:/'wq0;$p:y?g*3:hu;#g-=#trx٣YyyԮ楇rCg(6U%#4 s뀈67hEM&!c(oD{{?9r?!$.b2dOٕqe+xih6g%zݏ"MVm/U]yot &f *=<璇\~xt8aL5&1P$imlDfeZyݐe<>G 0hMjXm5Oڱn,^o (s)jgf7=ط%5cḲ|֠,;rr`'Mg[J/DQdԺ9[X׽%08)*[Na@:4KSc* q`gzqᣵ. Oc%N3ܱ7):s ލ9̒YrZQ2`n 3%MkOp[NSh`3;6`z:~m/⭷'q\zZS7#C]n94E+{cw>y [J#I)_G>u7n}MURgGoE_ M윧ʞ؜,kJ7*n-\/A`Lr^M nF@;o)W;U-v6ݮ!0nh,oήX n;aBeeLo z]zC $Tedvv>%ڞk(2Wn ^B,}K5zSOǴcO%O=rU+.Ydw㛀|Su >n5qqA^ tF#XKҨ\T%KSy*gUBJE-1N"U,27=O2!t< =YY~JVDo!3|y4F؆65" Qل ,Lm*aJ`N ;lZW^Q(]#7(z|b|BgOusy'O(s,; AW^b 3ʱDSenp]PObWF{Rv( :U.Z).,ؐn~w`RܽTpk.Ъ//.̓o؁7q%Y(lc73:ܜϋ-zrCo |> |oE)T1Y[g<{)s/@Fe#>p!X%?_Cz4C[~C?ξwՌKM{j*b|#K/΍u [nβPH,GTz {,hbc9qvXq5!T(/ҙԳ(^Vrf|pUqj" [љQz1A35-㉵GiOٮgjȞguW)`&-SCy;LO-e!0ȴdTqa/,MQZáv58ʡɾg^gh-4&OSI;{f͹PV+#(w]p0EzuC˸CY8 Rq*`[$DX.d9`60akO %Ao 51'-ij nXiYrBy$Uٝ6E=B:Y}-ܤ{=|nKQqd4?&n3nzkZ378^Ur#o l댃ߗ^w ,׸ yᔬV6dm ѽe!Ϛ Ib" \UZK%C_wy햯葝i% W^y]/-B_{Mw4Cۂ>$Z-F?u'y܃f^~~#aaaNxՇ7&o nʑ2u$Lx 껓uJe̍GXKb>wއ=XFrps<ۧ=vpD2=.>n~>ea fjv7Ry)Cb7#](0%u8@gbԇ? ZLY98ntV 纵[H'JIcm;\Me֦(=i6GiE|w>r(s@5)|Ձ]u,(ѱy13-w`5|/=s󡽗=klgB{P30kr֠ C6t\Y 6%l5 ,AZ\A|ÕQʪ2kdnƦޣ:X!+)lKU)wN9ӑӾ#/_z `+//;.]0*sm bVBBg;vömc\E3=8r#gws;^Esh6S~VF|e||mY`{ $\I v Ef+4ܴ&)y{iuu%Ҧı>OwFcS:gԙAg2Zl0%O^}Mʜ`NxgYT?8DZl@l]@%\)pV }!dσι釐L6Zݱƍ Yƥ`ea=e{8l:WhgrcؿgvO ʛOyBk 6a{͙7 +s!}W@Dh;xlvLdbgH)硯k iGтf|B ̶!/3im64Od3\])5 'C x @Ila&\>ϛ=-'R_NEuٗ?,:00yGz}O&2q(ŽƥBI&6&vNA9!Tݬk}:׌vH:E4Nyh0 [Zg[X94́V/=P͢R[9} Pˣg$ĈAE&dZ8BwcjexDƶTrbh"#kJS2+&3lOn}Iğ8V|p6u/ffYwG}nYZu^)SOٌ͍ʤk>?M|[_UgY>Z E5oq]X-_%%}2@2,Kc孖?сlUniW&rI1;v{rsj us  ~TQG#AB?0 3+P дJZ0$@G-dkA[{&>guI˹w.b0.kێ,`tj*HӐ:çs&1'ajt^6 IF :PCȾ7Pqmn6Qz$RF;͊B8SBɶ }N:Qjؘ[զZx4w/`Fvow '+VkYϺ]\//@Fh%S~_ݘXǏƽ=;:h>, M hwN0Js/0ν{Ob]:~6ŤMA\K@ʎyS|*_UQ$Q8dA䁅+sXH6 0hONZνhf_l6/D6cnz!1RA ;9&V'ޱ#h<)<,ڿDbIl݁^U)|ػj,ta*i낌!YG"IxbsRlÒZIuKg s8k FKj`Rϟ3<򱏗g_mصu ՙ=19N':=/QBٯ, .\l-oڏ鴶Al RXkZU'[Do1ǿoMRBK}(nmHd о+7[Pn%w]g P5vdCnCY7|)سr=G$9D qx v5)>ޚN !Y;24N)RMFx~7e9зa:!r9 qӮY?hyfyY;:_NR",[doȭI$,VA.Y%vs[؂ 1ju%Ye 5tICR^f+݇'!a^4.+"VE{u#&]ZW7AχcA5DL4 pzI8fU/j/[6&NBP"4]S1Bb6A|EfLǺ-5I Gcd' &<@k!ZAA/%Pq5*UG1=főDk١Epop'8ǦB^ezE1QLpʫ</mWAHb#|o"KOS28jٟbh> N.z5(8Ek|dy&:Tq`VJW@P؀d'*t'f/}9]GZI;~uS:^& %(04Ц%]SmÂ˹>8=iãain1FZ+?{'O<\¤`iYHI} ;uh3Y㾳o4RhyԮ8% 6=cybh)_K[@̡@Dui:p4 tb{xֈNvI v\q ѤA5֤ɓ6&O d !p9:tL kIva n)uUYs^ b !ֺm0Uؠˀ;@2öK)'R@;b}Liaue3JY/~]4PeHrn{k M= \bмJˇ-L 2ûD/09j nչyȶצa:֟䔒 i;6'F| c贜YKĦba%;uĨ]/f[;URJafZwQ8sJȃ楂@ dE:ѵQ0ڃnS!"U~?x)rsա͊Cܜ>ym&lΝ*b-/qGW1FsagŻc["؝^2]grC7aƪp70&[4V]9"Ww.;EVP}Cuٗy k'  ^ w uOtdKq+Xǃ:/ ă8 0K'[w ZrǗ,L kLSϗW³,3{)`dNۖNOVJ* h#Vd <90gt$O)tF}[dI yg)~~tFў~^Ovsed, #> dܦUpz h`3X}vȖ@gPkѩ[~QD" Sq]o#*gtgQ ~)"EP$ڈG/]f@M瑂^]+J`[HIL68I;u9 $zΰ}YM+y`F88*05?)PJN @u-C8$X^w~hG $5Pg Q l>@zt.m3XO45TЩ]gr kw}![w/t5\$/u%<[I?z'xj/fTn0n'ƮQ6_k@sM!WiνRFSY U桦̙.uttݔ%wW3-nb @Žw~Gpd5,`LڔypW1c8iOuxRq|28 >#a=EןLY{qژCn:FW wQ7Fwoٹe~S&s{f#yn (ֿX]P٥8rsaB[ˌ@-jBIU ˫@:yS):Y)`mds Hx\5A揶n ݷS#`bKd:v` ̓۳lmiAz;`7͐WTew@:Y*87Dž~]< Т4RAC=crH8A^ chppVͩQ{/j2% 7d= I%)0#  :Ju2K 4ysoH-%cz\}VڶHT IUOăWi1Q ht&W6Tu4-`[7- жY֨>PK.uORJh(٧\Mj4ia~n?][ʪlځ`,a̐i]kș9~?_ƩV3jFl>#%r2 yY['] lsvǤ-?7 7p&zM~(rtv3,[oXH箮mbs6ڍ;xp` ,M#0}67f<߂;W+ cey2Vik7ڞrHHg_ V輤4,oerRH00`M qo{p^aY xhITgP~G*4A,z7ؿEמKmWY:/Y/l\o)$܍sg.Zt SxwGŅy]4 (tfNbu4z76: {p3u@޽Qi]hXHHd{s3>JjimHn<;uXQ!)K,AHOuo!7bZ+h9<7~'MߍC{2ZqVOn?'1E-N R^#ǣk5hCC4ge@'upN͜QD-oAPG }qA%e aѨlgo=@YUesj*e/US^tf| 宲M09le33ԩ؛Km4o|'.l\|( '2Zz۪|l)nG)Pbho:g*JזDZ[@j@iMCä#`ԙ2HFʢ3Yw%-WwSsK̲<ӕޣҦ5j])nB3o؃?ݰ-c5T"6f A,Ɔ8`ϾiT38dg۟[Z7_s=-gj;'ъ?Ú,9],dr55$ ϩU D+oQYhs3DgoBNkVg@ nm3'd'}8~odCϹ t\cXx K1ޓ? ^^CmR?xʳޱ肽.`bvRqzH ۫ZQ}f7L 5`@qLHNf?:k 449p=mv+p. q{S*3eIMYrn}6)0G&Cѣ]-|_u?Yxӯ`㣘gpe+:=tKv.GCPBn_U90耫rzCzM^RiMW[DaGۨdHe774ëg'lIChӼiM7jiu=~G^[ف_*h`(842:F q_{?JQ)/:&"K/*ݵry6]g=6cۤD̤gWtZ41l57pMWX_~5B!K( :pkY^ ?W^W ,oŴ_+W^TEsk *dR%b"9Zg4!3ITs٧4r_=L5DKT_d=t9;GvE=ٳ-Yz_'#;RoV-i8Rj$6ӂ:A\aQr)@Ceݓ=.8xB+B*UYlOV#sz;<>idL j<מ.R~j0ȨÖRXhk{v.oo_~>tl{ hhHv#ѝƒ- SM3T8i$ƫE׹:n'cDfUEs=.I}X895kgV0;PֿN֙8(uN&(4GbHQ.W缮gv2n7ЄTMu uO6r" (u0ucTy|m"b?7vmĠ ';pPQ[AF7:`9(OmW08D譛j;xWpdNL$ ЩF* kYuZTD[F4ƺn.k+!1n{O{JYd^='h Bї;∊ GF@lf2-Ecy0nLȇ 5[ʆOhHI8(R8,PB>Wq`498FOѝ%^Pm7ХmT].I?ypU JQ05l{eQ=Ţi)7D:o!5yXա\p- p-LX_v .Je~Ł}[m  FJEQڶmcW>5xMs{nL*EMEzbx |$E6 shjح].ڱ}S1hj.UMvu2uUIz>:ڥɩ9>zPtnUNj$ȑN" O~󉽐& TÔgx{uyAwB[5>q9[JxK]v ALPkXDbzdc!{4thу1AFWLUul<{. q"g5 ]1\'AI3P\ɼ} $\@2h3;섅o,̃!lvp҃rSh/9OO@ +gw!HR>IX%gj8QʩPBz)S'>sHJW׻ʍq oxbb8Z]x~7P}RF+hyImy mN#$ngړɩV)̌zޘ K'6kۑ%k V/Gv ؇Nu,gquOҤTOQ4P/=>N#;oIƞńv ~nvm BfWrr|,f9A& nԃDwou9d;DPu ɉDBI3Z2𓸈f ,FӉY_y`1S],o7YJX:)vKo MT9z40O;g#9v|D/9Hhw}*T%Uގ]1Z>OX;m8{:xl]Gl^ciync3w.yt,8fk -G;ty0ti,*E*P9<Lwl zLCe5r&d-=fdHmu.E_(Paϕ2A )^~l ͖F?vĂG~rjNl2n9=N"8{wހ۞v v.VGRD3B!?Y`kdQz_U5 z}K qw\^x_VM0nZ5%xR bۣq>`H!+'i")];sR"@pgn|&50)fq }eؿkKE#Σ%h2O3|u {|8VI,OV](s^ޡԏDahyRQ_$:,f 1 LYY+Z@U0ֿW&(7)˰z FAvC~ ز_PӟE :c.~`c-Vb> [a:cI|ưZeJeߌ-(gm:U'%%aWax:(KÇ`u3A(^VfOuu5AY xHo=*<`x𸀻^+tn壴^jmƳؿgWo3"`/ks)|C-n? `mXtUn -дX, Y͖е;ybh = eaHghg̈:`~G~/+L͒ч"4wKpHGa,=Lzpi0x^?, E9SP9b R maH 7@lToU R? @#Ϛ!(8hC}+ Й/u7beKNh7$,O}ˏk#$ -Bkh:-:%x;veT%IůsͣMP!}h@"G/(\\(gy(9SG7_D(_P/GOh|3_~~M*&2q"j/^u^5ҍxg7"Mw0w׆{$)-0=h0cHC^*2@{7ڛXLg-i}Gyp> 6;|O,=5ܡU1FeK5R.߅7~ak%]sՓR@ݰzמ zq{RS187:(4Ic8qIIɟ&iX6gV>e\a>hgrњImyX/FbijF4&x)O΁D{x^Iq8d>Zrqd % >7=B(#{lϤaYR;G^͖ q(Y1e'̪^EXIU=TUc@+뛀Ɯֈt@2INeCsQ>;d D;rk #(1 F9QEo9ļP"]}s='fk0uͪy3kSUlAɼilö$?>uwF!jZDLjuP$EoKZb& ?N2C5\HRFKrt;h ^ a?NTy ߂.!!РV.SaQ ˑ8~?_ <A)Wپ| ,bG{3aGKsqQO4(,^dO3* *!zϜB3 3Z9a+4_]Sx募 W'S[΀~?~Z0F |X['%&UJ7-9.&^:7DGAPNm+(PI ƍs% dn-s%KlbZ upR if+s dV5# /0B/Gэ]lKTx]D WtĒ+ƣW*{ZA0:Zn nN)1&ˣXM ';us%Hxk>\Lj'y#3@~nSCI~=Fc0P2aa0QoG9&֖YF&OyFNX)Rr)2Hu$nAg("lܰZVh@$Rc<-L0)R;r\|}>cygipjOg3>C-,zf'ZC &%:k Wū h@L Rj,a`lhNGfnzfnEg#gs3غ}Ȕ_=%cq~/zn}UXNC;J)n ݇ױF?x,}kĤwujޜOS-SR[ԗ-dBO^3{|e)߬%6swA;g4pbRul;-6|hdCPoqaxê^j}J3<%X&I> JX^'4+5`Q"HUrjL4F3_|mi٥Lkp3w~Le!Q+Zr\ѵ-HQVB[20 @~İQMUk"ć;k>pekʀ[n S(-O%G8YB[G'~} sjK<*4D`Òw{m7+}= MF&Uui3 @^"dT7?rdzəA$NF65OY,^h%!2B/ARtRT/FO6c^{voή,m.96VGdۏ`AUP*S(vcedh":Uh h)@+t-MX-ܘ݌N7`iGNw[4+ -69Ap'n).e/,E0 Yg"2v+jkqZ~bDM̕tY~h:6ٝGGEz( Sih;zݳzH&T5P6LE97=JcB?c Uػ>Whw]5}Qq"^Q䨗}JR[ƹogqr5'N*h)0H^ܒ?ŸQyF"V cH9Iu:~`9la,>. Odr˃ې_4y9šlk_}\V)i\@*2'OͿ=o3!-o݇ލO .Fٯ5GA;v Ե* b;0,F9ʍWby*A&@aLS6m!08ZigAփWH`gOMzy)$EZmm$601liI\3s5Cr6eN USŬz(>v) ФP~UeV&X[8)ʿ{'_7fj!6f_tviqt_ _y O`Zm/4nҵnRtnkT? !]6>3b꼛\ܦ4N5Iΐkg9*e: OUΉ۶v|ߢ)]' fKzRvt\'0`ye4gijX<G~gTgy)fPa_w kS0@RV)q]d VKl ,@ug9J 8 3aE?OzRr[h:;V5z ?$ƢL?id]| rs>vht^VG&^3ѧNFu&e3F/ Ctj]WL:_.">zg5,p,4pQGE>raym,rtiXchENs j~)(*)rjK[~}M,*[?8'vO@`" `ށ'0\sQi8:޶[V,?y:35lpvoϼRXiɓ3©cZs)sfvhbP}&4屯 e԰)SBU꿇J אy;p{M,h$^kK.[[^xxy퍲VOKy%Ͳq u-oLzy8kb_Β9tPa 7ߊ+),@3+X+S@eW9S L 3MS#p.TD|Ȍ1{w'ֱMlY]~γn;<}궓Uu\gɶ$>ٝN%%񼯹ʇQMZ`Ek5I<`[)wjB6Ӕ{~$~JlZ6fAR;y(Vlu}βJ12tN%%Q CME 8[<,Np_O8!N/lYdݩA@l9i`^EѐEG a'wʙS?,zή=? G0#Zvy"S a5RJ"8g6$M?g NkdmS[/&)y1NY% eT5wJ3,]4AR~_@7pIGno6\wV,>oWx%1vCp{4~}GKÉLtVTsqos?Iwu#\묁/+y3%ݴKUb1gNt,4ʑ76s_r^yuF} J4ƠC}Y0XmHs55ZZxMOB.EXezVvp(=<}{LOQLV bCUil {V]L(_}I$.cal9Jɴ1e 7T3f,ײ3M´ ZNj~ zf7|ϑJr׆S2vrW\AY+9.xlx2w 6D)y_NNyl^EoI'q3V-RԌʬSbb NHP 22j33Pٚ ԱK>oGC&tXvˈZ^^?Q|!GRऻԺAO~nWz?^k,C>+d ZP>8TE+U Y_~4 ߹|}>Nz٦vMdb+J/,vIXaYrzl_ozFq:'˳]ԍANFk"ڤ¦9vQ(”`L |W"U5"nUFי9^P{Tϭ}cřl<ɼd5cz&AFFdEFOx ?=OB9&1u MNPNRmu)^|eVzb"8-tF"ՍQ;}O֧34qǟ債gkg7){?α.H4|iHYF1sҝp:Ips1׷*W׹#Q5CL0.-Mܸ}/yӱ}1g7̽Pwdw?x{0_IۛFK> he?4LuHTA6ܷSØw}mO1-(O{K%ʟr쫗?:ٵ~=B+!8%'T|3Fg?/,v>sbo6[rcq1MR|<7swbU9Nu(nž۳3p,%'U`,mHnTܫEH86TF V;B8|zzѕ ,O- ڔ,^O]Aid\Oӽd1I.蚲ZHI~ ڷPSzVTA:_.<h *B&h7fQ$0oq?}shUJ`?i$G#"v ïL%oA%sm&q\"Gg[)jBʁ5Qߟ#ͬ3p@.æk-+3S%D[s8pnfqm_;Y@c'Oui~y2VKGm.m*ZE!1ZZQ5.2t Ikjjiznj2.MI:[X:w/~oß~sw *ϟ>cZ>wxq8#N|XHcElߧxp1T؍} 8n B0QϹ󅆞?^lt:3A<5ٌQAƭϞnik-"NV0Y߇>BƼ,U)wѪBig-ՖU"8Eihpݑ?!گ\j')}7Ϣ3:b<0MԎS2 sOfkۂuVaYқ^.wgOojbqd^>_Y{mC"ke |t!6F М % hЂ)y]i[241҈bŊhDPTy4O rlnȣg?e]ؘ\V/[{O@8JEYQ)''*m$h4Sy mLAopkC`{w^Ғb&DIV:d6(ySCvm (\e{ɨV0tYȜY!Yϛ84DSY荛SjEIenqd//PՀňfgb? olE_=ԲTi߂z k\UjUu5p:ſ}';:Y;L j]:&vbB\kwue %i4~e8HY*Msf~Y|B9c],돜E"5J}h Il'z/CЃ(m)znM&#И8b@-It+ewy-5KF3OV)qʸ M,^A:I ܩgݚ:mRkkRj"4?g(6+5ӫ6y0/u@UH2_ۜ? ,C QRdMnLuFnYh[g=y$^xHi&8]e5L AjAEShF^zz'?yMV=2`p(r!XvȦ͛} oڭa(zQ?b¨jU(_=^UjS]O:0(c]Ӭ>ڸT4 16MrSC)ZWS}3Uxjf K5SK\3:لs{%o|bq*@8˝%;aVڈ=COw{Nf_CE8.=鳡2Z>xhFOؑ&{N ̌$0=~|cB/@:AWR.ӄ>͓FV&\j{Vvar9~s=տ7fl$(Ԍ!WW;6>5y'{PlV[ڮuatAPX;*` }d?oŇNqnVzw!uem)Re޹7~wu/vbNQꎴ܃@ѫ]z$[ @6(ݷ7s W<1t:ru-j#Ow ΞFmXLr=[;ܴ2`3U4eϮw'Ҟrݚ Qo&6q]-qg#U 8Y&hf`P?gBA }~Z8/bo"A"CJо4A)bt᣽,fc.rJ:3w0dd84n8mBkMeQ|2G|(cr4r2Au gECԼV%WT>Yxv2A=;No?(k@0k2; QHb90`ZB5 4Nz֍{uyers|ΡRa9/f:Pl~Z:-cj{W:q ${#+57/6ay#_ďkqݾe(>z!p]ag3 X'ܽGx'X {Qg_줜&Aב$w×6:is-cXYBc$lhny%x4Kj=cmJe΍r'!GzMyMAR#qʦDZx/+R)vM*Na|A\Z2aJ76S˳h\7>`NMfeWyz?ypVD/S't`4lؾ?ǾZl;w͙XzVhxg9&(QH}yihѹDs ؽ5Y{801`^OZȶMuY{MƖ^),3tC rlH|SroO 9v9pcӭGQYis VZ[Y1t{& e/}PX.hbW-EF'dLAH͛?,…~]4P=<<%ChqNrJDNӛN;9xD Dʌ%}ހ޲g6g.FԪ@9u]׆znQlkG +r(ceGKRu6q5;]TaGh5Vqvf@1zx h#9q¸Ɛ;Bfщ%WgUBK௽2{.YK`'u&:*vsNG|G3?{x]%<[K*.Iѭ]G2b!w@kXG˚<Ҿ,]A9')͓͡:%m_WKob}*^LyaGQYlѤ M{g}|WUcWy>ggYtNLVaR)X|QL͋LjD}ʡ%:N+U@E}=#:d>g*%}Ch0 ٳg:#vsϵ*M iȀVpއ ᕮEAQO^ 2qRN r)4[pβeRͰ{g,q&ȴV6d tC>܅q_ ĩF:*o,PHa1î0~{?]_˶O\s=-;+?vxƏG0`V;Ug\Uc5p 4NK滫58tW>Qq9ދggVFhHӟ;3drn{ZCUk*^!υRv!`ag$@ICzk;O-Q 5u)`WyaJ@0=j( u?ؼhX*~Uqzr7Ưjs@ [\uf4\;_ox:~o/=e(ײA!Ϋs0MMhA=X;z&4zjΆژA{?r`R&]',e=p|uϣO"0W_؅)ylxbOZ)ci۰zaEA@*WURΔ.[*Dcٽ W8yk RTM,=3rtCØv@p׎B.'ai$4=/[IU{iEԭ;r]JD멲!u3[M(LC}jk8!Pn^BF+^gTqu8kKeƒ-(FȱS̗v2m{9 ʼĂ;AHP3Is291(rsXuk9JUtq-,CGӯ#tAF}:[\Pr9lF_㡍}{ʞFrj}+mMYF =w! !jSyˇ1LFtgIo+^5tKa>Nt,@#} W;kOr+6W o)'Z?BUK ^®-I*f/nmt8޷cYd(oid蓦Ar:6״ 2%PPoq? K( FNX[۳uK Jܸp;Y,Mtf"n zsZyFQl YF͋0BN'|#`~P{$mw; q\Rg(,J}c-N:)ЙR3uͶ<y=e|+u"Ai߷CvA՘͆Jf˞};b y]7 ϗq_h% XFTww%%5`ߝ~oiS4R>dn+)wn,eќmѬ|Qd98ބxuR V}9&}?8>.Lw++)L붗d(\~XO~EF/eYe>od HRu(z{H6:<2TYq]JltU;;m$.ǽSנ,kX ~|U^TBsIs!V ̚ȫsz]aw{.)R4so7pELg9h3i"0ŀp'%>cTWG4~I:,ZZOiNKMnq9lNY;|5N{R̾#Y2>)){P@JK?WU/|v-B;کא6='fampule 0g`-:,4f^R~AWݎYL=QlU &vD܂iamN42Έ%C'%;=g{Nh2WʔzHs*(6"׌(RR>o{Ӱ,pu=]Wxw戽yA-3KiBS]s݋{ R?Rm-5,gr^O.)){he8 |ZYW*L^ 8-M,.V/nZnk'21@`jgfzOD:mslQ9N)z>΀- ~{b0g*2O"Jbj|Bw}~sPvߺR;]Yc[AnHy%KS*IYPp@00Zwlv}{ŞNq>]?q_Oh7Zpsj ݹq[ /  eԣK#kg29tF]҄R9i}$)Q;*SI_rmvO>-4DF9{gM.CIM,,}>Ʌl;Vu$G~N6:4t$spa_m'Z1ZN8 VhZL Oja2>˱\ <`=9_7Ư4ֺKl,M:`^N&N)pnqoog=iZΌΐS+ZCse^xV,s8BIb:?\Abvc \&Bi7C}tipl^J"kHd =2(@3B,SNM] ;%xtG[!PڨLJݢiɂXgZoعʢuEsTt"h}L=^l̃* c LvLOpsOR"%+-'ADlB䓤xV{3/z QR`Ī h rQZ`완&s55}H&ZRMgLRUTThI僨 xӤICzE(j"jFF>o):'V_. f -ޟuٿ}oΔTjLJ揂RMdcHy-A.'EsZѥ$ڵ5Yf]lYW۰vUkjzOk@W'.m4VvUNg 箎 mB ʦ1c\t6浫Fո8?c8UbǎwycU_O]<y{c`G)K5&/Ec|&&|hHDذ@ J`~o{{^k]kyO~yߧeuI4%Ṹ_B' !Դze8#2g^SM%8*lc (8R^{ye`t4v5if.&Ri ߿|VDXYy63F(C]m{L#6 zҿy~fSX׉(61* *7sjԑKMڴrZ\|"גC Z[3<+D g8k@si)z^Mrj*5h!:dTL-!/3x+ ~$lF`2K3 7nl 0T$92D}!PiG(pHaJbbG+'T$*(DVH.tZE"6lwFK;K-+GӽTYk`p*,0hǃ4z7A kY[Emi>g}_+Dž_7XOnTF LB,.Oݹ??{x'fݮ{/lAhWRus̀ELI:]qj:I6 VV,j}a|s`c=Jhu+U@_<rqR$LÚCijzTEwk}roMI!ig kY!/6,r1+,$t{ Ͽ?Kw~ban dT͍}\Qw(2:CSe/@7(hVxKD2,n}@<`6{ U6-{pR֜w{z:ZhdqF͘l1."Zf{͠4v6h|Qn} e&??=?I&{0>@dX9۝YܵkpVzXXi?TGdtw](DZ_*Py޵FhW TA*#@M)q]5Gը1:dP@>TjGB߻<8n`U7s,r:0`)3LG`͍]ݐx:hѰp{oeؓΫuسg#h*r'>..-4{Y_t"ZWs[-g>Ne>'Hv'uU|6@ۥUǽSÜa`!R]=z: qc|jY2әV6Z{e.,{ك_kxބiX.c1}-ZA[)"ޢfeHSz0Ƙ+(Y VvX{[}x2^wO㿼ؿi܆CP?KZIػ፯ޗt? KTlp#wal_+x^ODNKy\4FADÇzhy\vr1qjTi[ xp[^sMj<haa{+i1h+;tCQ՚k@mjUKiG>v l%~H F z9K_{M.7T 4l)jk,Q(xηb*T&&]?*toXT68B+0k>}'=H&FR}EAIp؜y)k*X8^#[c,@,3GzOK8]wcx߸֒O©Lơ ^ xTe;_q~~5pFGB:bfZZ ,=UiZ&cGh2**(:Y-F\Ń@fKy~6j T]CSPш+߾'LL32^b]PK|P 0bU}RvzPLgl*L|y|Ck0f xE7f:f|jVC+8cB(j+81 f4C8شĂsUlNzJ-DӲ^"41 %&\x*}Q%xOt쾌>f/V0L”Kh/V{ {=:>șYFBg@\8HB`~Nz&؄* ㌝SxKĮ MO)hk&(9fw?DӅE5F=< vɠ~R:2Sۻ"L/J>? '- W'QJh}[bތ{<=T+A1.wy o~!v4#{= gwee TZQ-kjM8ґ7]I9#Mkqlem,Mu||#HIf,x }}}EJ/$30\KfU*V r4YtK'>ÿo7q\{Sc0ؗxrؾsx#^yaCL0RQskoZrޞ ږw 3t/tƦ#=>q 3Bti 6xU!o뚑{eԪ;W&7mzAj;˗x]ԀkpndOLr F_2#<*6c\-_gRNv7?l.QQ wu,3m؄ K> c1$*BnjdlqEIGstSutC~g:`@OvV#Cwm8XGv\3-Ҭ2ȯ#ehl$,Y f+/*O+8k5՗Qj1Lqgnz-(-Fzab DoU]$p@n`˰G`rJHDztȊAIv '0U6~q=OBU7{9B'LݷvMxG0TU iq2k`)h:Zy+Ҡ;AǮyk}X(yByWhm-jM˗gq23zNl:][͖d^GX]h/^СLJ߃.c zmȘϖ tʵʎ}(Mqqjy)ש?T8ڼ!8:nxԿ8) q[LE\I 1Zl5(őjRz/8ңZy:MtQ2b[ >T%M veHRz)qU]@[b[l]n5kŶhK Li*#8֦Ph"&WO;p/j7z!TZ^u]Jf>p_Ŀ/^BDX0#C(o`/}jz7-9p|_O /[d*# V{#E:JmWqշ*5"êoV41k2F-,#)l0("PfXL1[~7?#3k1m;?%"l’)s*~oML6,JvS)NapGkb&q9b& `,Sm y]t^{kȉtc X- v;X s@[MBc#5ذJ@=Q`T 5Zݼx!cA7XQ(1hճX0>[*3jj3RUg&Y;cj[M­Q*&";]B 5(eȩʠRkjyڹjXI0`G:Z14ir-%j?PN^Ȥ2#57w)ͯ,WV(q%\@ 9:{,JHVd2<5R9iNO!D$A3d&XO()I[Owx Ͼtv-d { 'z'>w'Lqoy]6=/H7郠e`k3QXnє#kPAO 4QD1'S9`( (cM#"yZ})>p|%{^)L#LpIrϺ||`1-5ewI:[`{3˃i+c7zxs(\<A:2n#]ŷ^\[2V%%s`f]7W#TfOYX35(fzJ&s Az-8`T,uz2oHh- OidQPqرaB.z^ R<5"vQc&GioεP:M:\q?)&{f˰wN^SuWX۾4pGLXZáj KЮF&'FHSh L40Pڌ>Tq;O{m=݌#JdX,+VVǡlҲY-[[]g8k x[3* &fy[daJXYf`_V*H O-jUx5(+ /ΪEF; a97rsE.rfd \DSi:؂, :`t Y/9gsTT(ݏ=C%n&W}+D#c-XiKbhD%OFQqYc1?s\z+^Z!}j01DuSlP֏'??O*>Gu یC=#!dG JZLTکM}, m"AN֊\PƁn]6,cV.F\\HuF5w~S?>j!:x럏ɢf63lR$=5 rc2D PKѪu3u2 hm>Epς(b|k#gTQ{^4S^,;@W.̈Z=`U&tK`x4~9Sөe.[61y)qz5J԰`o + |Nu [S6{db*DwѶiϔhq.ŗ(=Y.\֢] 1SL*0.6yfK 8ҺtTg/sf\zQ7*>Wk*d N[LlP<IY4,[v_Ũ7"=F+xZ%h{ܚwhA:l%q=S۱: $|}M 8Yں ?W^}oE:,Esmr҂2Ԩa#Տ?&?_Au~…^Hʘ8QdfC薂-U:6q67Q5\X/ 2 K3|k8O@/ |U5_\aYT(YځzN l rj.0gkAPZu5p_˰%[1wf3~#(l4`@:Ǟq3%.AsnM&rG x`Y.꺡f!GJm˳,GxX(fR)Kñ4jh_R셷~eo\2CSӠkGK : Y\A2jװ04z(nAjiIL3"Φe}[G k;Xe hLOK',k3Vײq%'V(jL2ybb9ʰ;iLII Xʃ<-.26 |z7{>tMIogx֕;ڟZ_({yilPa ?/s_,Kpt6& +>-x^ vO(]U4}R iCl ~>G6񴊾Qђ%0eUF)@jucOBUkt40Ѫ#hI 3lu gEJYsYG {KIahO u_w 2bIj%t Զ\7^J D]Cgxobɔs/%=.ͯ^K SioG-SoA3 ֚ؖ0먨]pvı Ay{:*vqAb9h;3A 3)N5VIjlq7sosϿ=C[% ! .+8bGyKvEGW9,jhG#e_t'0k投N;(F^ aM f~z9ƶ3d֥ L-~3 ]]x6W^+"3X Gij1 K.* ~g#mgzf/!r8gaJq04Qz<伣/T p4E ")s\UD'oϛ7 L>)="LFƊδ{T7w)ӷ\5XϘG}+`Ջ_2%%7y#6HgP7bѮHwO֬mEK9N6ttNj)ysR) #!* @`ᑠJ+(a^t`bbEGSqϊ?pף~fLbsx-f`IԚT˙F@*` gipfjC0p:O 0ceYV#I.)q 7c)@`2g(q ѻS\wxs1Y>Z  o]xThe2Mo1&Z"nm[x+ _wEH};} ~iJ a12𩁍!X9Lv,0AooS# Jq+TGw}iUZ͂F-0V/߾?6E!;6TlM}5xY3 쫁XYT) lik}|(0A;dlevx<,&`d^?_y,n>Qz1l]JZjd2 #EZpJyd_ٽq]2-FLex 3f-x]iAyOcj,[6O3s53?) Je2!PQlT!_Gg2M}΋dUDEk2[X1MӍ~gS㞹iKLf!6lNG Gv:AbFw;2Ҟ]<нtzVut|k1pԙẀ'i6Ϟ2#E$rR+3jqC^ube dmOes91B`7@bn٣ Qz_M+^$9 %-r3:voBR\n{`~ '`c:R>SL B^!{)T4+; ozy"A֏@U?|InNx}OS`?cق6Һ?&B`A\@5IohCdNtŅM(@l:~>uד84L#-HIgd-y|%K8GXA[Q 2ݤMdi"MͲOYc5ge-cM` m-cL5nL>uu.@e 55i$~F\:F{̯FSCS/łU3# \zMi{MVZP{NjUDOe{6vK[!{F`Ȏё \"'udC 7 x%;eIii{|}(sۡV97JX}""=33)h6bTS5?}gWZfxF9P[@1'ꤲBw7)b#G yKvFc<~qn$[J)9xI6å( ܱNhFS [>Ci*홀VHE֘ @dzڡ}^Q0:8>b w{lm [AD.hA'*Z*,m89fFZkK~ {YYF+ Ql&C&ICDb@ Z{g\raHU.{c *ؙB63@"is^6`Q3 n\ۺC,j,B Kfщui{y4@*Tƀ,->~O_2^=~_l}}{>vԧiEBTk)%7^ΣЂ↘Mu=cmcFKղ 2A WM~Lʩ/ 015(ܑ_U|}.޳Vb[ t5N3eGo?G*w;Y-TH6VVD Q&Hxs$A7|V@@gޏ^OI[wF*GWL&'SڲĢEǞ= fJYP2.4JxKW^k+^f.@ b\6<5s00^rM/w:[^/Yun\Ԩ3 VTKW3SW-HqWydBbώtø= QlH{a,SnyZ7?6OC \gV,A(5VrxM_n݇u&<Pxc `1W?ʹz&ZkT']rrup6~9lOn7!K!OFG.,hEW)ߍ=0aml~=+?[w*n0ٝXܹVEPOK4 p{πm2Q==HO@QzO%[BiDȆ-~-T}t- ^E}`[SG,D\Fg;6En@;е SÃUyZ|[wkD&:6ˮf@`,w[f \;>c'gdaCάT'joban|>fa] -S-3^#g δsj&2 5=Ft dˤܙ%fa79»#H\FCAǵ['刿-_m`G{FnHJ;3R⻏[q?ܚUUMފܗ_rT:xKw9o@,}afK\W8յi2@<ԲԼu2 UoJ=вZn`LV^c4n\#{CU1 ߣ42/dS8w/FkK7 fS#  %# >t0"d0jX#E9Uĥ yYISg[|&[ᰦTC۫Wʎ~ ]rBk WS9yĆnNO_҅1TͮД-^~ұB%|/vl.r7fAG3>s}? 0_ܭj;_/Aw$5ʍd})n{2d߶54Nc&,D_Kreøqlz`oR%ۉ_-<1_7<B-j^'YZ !g84:h2TT~?_~}bd` '*"[ & 01PW`,e l%mr֖fdė2Ϭit$j_o/Gpx@fNTd8Ke )7gcXu:vhe"#d%5I g8{.89j89-~bUz^h;\QafA6gL=lL +*#^ u*f,O%Ae?.?MU"v\bmiֲ>Gn`cU]d!^1T rBt;οr|/k}7Wt0]~w?G|Aw +徖 i+neepP7xg~̏ޙKjH)C54O+QSM1ոhϴY;Jj1β$xo}\APu#a{VϽ!+l,\W9U& x(z5X9zUo>ccFi:lQsUiX*iz*Ӣ$3]@(VUlq/Vu*l@Y(Rǯ}8ovVE؝TZ|>{K3y+kZă;qy68nq 86Phr$|:AcK }:V=k0AcJ<栄֬[i0oVmJiƅt0Qc Rn- ,qE&Ng8k Td{rK $40MraH$e&ZNkG[,R௎{1y+as v ,-V\rcDqvD6OgXWAdfL1<̩ꢧKHѲ=}*1M\sܱ# 0j{Gր"5Q% H\b*Ж$Q[XHp|^밫c\],+'{xǿ(:ݮY.sY[V86XVq|=ZNOصctXV g IWlc쭴Ef="0x0 8֬R j&Dښ8>.W'~faT4/N# )k]_UeG <^ֺcPuէa*--VזԢaT{^ ӰYUUS;(` Wirő=I{kQ8DƄk\T %l1v͙@ ~U(Gx:wX\GRI3AE'et Rus ~U{`C\OFa8+_pw]m<^X-hrl hZwH|\Mݨ:3]%򌫫c07Rᒙ l 6D/'t`51EX\ F)V=BQeЃAhL~s nIazc$x~;@7;k$H s# RttwIy9V1W0**=Rbqz)[T-ZT?p-C'?x+# HY}j7 fĊ*rXA3 ֺ_YǑQmĂ- o3*3&A+dT蟧);Ќ+k @*oT~Z_)f9IRbQ^-:Z~r%oZ|Goz5ˠCes1盟qR^g4 zb5N*W  =׿$iG 7?\2V\GCn6dԠm0VOi*Oc;+}r+OבzOHɀ4H.saeFZSaw>UR:AE ɝ7vARp}y}prNǡY}3}=єr2`Ս&b&[[ZR@ rJ `[0 lR);K C.sMHff  p23Y=mٵG#??/x2"Zf׍Qs5ghkvذw(HGLq =Ng(ZL((g e|ًQTj;2'@6kMΚd <GWGlaMO}2GbAH{qv0=LHnZo}Y;Buˏ@`xE][_v!\0"^,o[EiG":q\:Q&o],nՔ;& dukceFX~V?ܵAGe1 cW)ڮT.܈Y#eC Fϖ'OΟ|L`2f@1yhRIYDx9%kc)mK23FTx(*m9(/矷S[A/dىO*fEnȴ^g/cH=Se[3Pc`#孃S,.Lxƥi=ﱯֽ:X 3M<%V h@3s#>\uyyF3^jЏJO L4y-3]}t*ӨP솒xt96,ZjXCnѷb-cn]VT}[Y bz>az]Eºuֱ撽=d/&6B**sS2Gx#Z*9=D-a y?.7iY߅`BClAa6^#?G7'l X7Vm;λni+ux|^ T<d,::)ymZ dAE AJ+3z6P͝LƂSVW tUӣCqMֹQ̓n'F*Q D鈐kBo!lcFMQ/Jg5VړQD9KҭCA)>õRl??!09sbi~6U "֙PHה%0Rk[}km1p[{O0$OQ@e*仙*;swl+l\_t>.8ٻK+ s =碂Y ׷x' h>Y9֚3R^ѩRInֱ+>ߟ[^$u sڑ׆Z5E n;~}8/~ l#Maν N~4%&;EmCKO.o SfT3 ƀ$>v,^`qzq<~8(Z_>b1{&8_~oJ~u X%tvIQ< 8G_Y;mSvXrՕ:23;ߠ?ֱexl[+K?2WUlܫ@ e;qek%FD4GT[xg]:A.^wWu_\W,ILed%.o~˰ܛsXR7Z?%%lҝ f2.YD}w.&l rў%?8^084Ł'У}3Iɮ 80F˄NƦ(BxSWC2lcXP]9\Vc: Av]<`]SoW-;4OS6Ռ8; f3ԛC[BU<^HKKn9c5/+)lGA;W )d sl-؎ՑNb ҠJq7C 8HM(k:m!L==v_|<9a%\ie2I0 #n)TS0|Jfq r`ג0$5o-6M?kG쟞WӢ^[5QȔKG_-Zcu smO&xg83f{Ғϙ{ ˚P6|3ߓeW!$HWb\cMi^y^B\)~xk^ըK8'A )w(ۿqK;Wcd>L֔TG沬ll7> V&]QFA͜;D5ltXH_&K`Q7{hki mk.;9џNS W v)OƁCp{<2ۃnGa 0غHV$hKW+h3}蛑a*[\0PnDgoP07e;+s?s%\G}j OV[R˄ƥ+X l=0`df 6Q>SǙO4䱴|+"693&ZM)/ؒ9I/ Q!/xa@`ͧkj9ψٯsx|LY(YtyX@EVA\~-/:],X6.BY#sk0jTPx}_t5vЃJ뚱bM?sw+=>^kv%-+G~~?OZ^0)c b4 xڑ_ٳ0œҀ9uNҒOMQ"MfFey}@ǣ}ɱkeC"0&`*r0EtT2]Tˍ?q0^p"Bz̦a\yص ^es$nUi]r.n=ѻ鴾G EXOރO_<Dnֵ kyC̹3>i>̓ՕZ~{Q"#i7 (rpeFFV~gǡGeW_Eits#N^\ w|6MǴs"o:v^ {ހ=R Бo`VK=l51:s8,9\9c\qr/8=Z߀JTƬ V*yg 81_nИv/ &)?h |UQG$D2˰kr^ ߣٙdTCV@ТqI@ENGȩ~ V+ȱ2 cݪK k kqEKڝDf428 ]U7zx9oXOaQW؏w7/;U9lP(COL~K _Ԝ蠭|YM Y39`j0{ݦ@e +mD O{g{Mʀ 4-[h,CP-Yg6ӮT^LtUHžz~/yTluU0W0Zuo=]{iާNƖj;b5%eU(­ .MDX<_JRͺtyXb0g6@kctNQVlO5erWq)Q'drzcRU'z]U@BaZ`Dھ+Q{pK÷}׍Q [J>x 诿u7 W:ZAsA-3TY?"2MNLt"/.(K)pó`tA<AؿL3>|0ҧ6\1@Z kfE%3%ܪp0uzEv_8[Yc~Y(w(y]N:#!Ac'D ۖv]uMv!߅͍Ya 0BJR u_BnM?' `AbsOCAoU@Diq&;wS|k^u\B}K%{9u{;/f*ͽm%^glyC:65ő4bwl}{_/{eM;B+6ydT 2-֋U)u% 8>p=O/7>zq;$'N6؂= ÅVGʁwz(`cC?Gpsu-m˻F˻( 2*7>`PDSoP'Mt{W=ngn`0gL:2^*};h`$eiJ. y(ڣf.ລѠ*sKifĪ='{YQͥ_aI[m7p+;oWR' >oݯ-8&mu͏_n~hUtvq<},\! Q0PnRxP*$3 `F*c \C\POA6O"8<ŧ&Ϳ?P c:[eU9,\z/a^\{P#zoׯiJ\X0L%d<hEs,vPИo2as{&@&ÔEn m{9h9MCT`_o@A5nHwa>zDZvc_"1^uv]y U-+1[S*KHhծ8y0)5lmfZM)`++!3)˟EgJ \p[O+"yɎ4ҸzmZ(cG܏}|Eٌ 1!ΖM[2&?!6VxSHab 5R ٹݫZ3*!+n&:QҬ -F{Fm K*R+!scF?Z6 @qnje> |lϿ\|ի!|"}M8^"{kCIՃ|zhZum z(uj"{N7pVbTu&0 麗:[wo`GjT=' ^3,,& x:>w.xP+ 6gBQ2[-,_z<"sYh@q^dM6a34SM-#.\woΊ,W]<8>Zu3g rwm vXlJ.=i9ݦV_[z?}} e3BGUCM=bSӶ KSJzz;Ь}]KFB %p|˟ =ZмDOT?e-Zan~vfhx0U ia6ɵYs< `$ݥ5a 8zjp"6=4ʀ@Ms C IW8p }x߹|ؽ(f|V1|NL6Z3->wml00W_Gn,:aqM!~FVk>u__0g!yY$8C6z-GBy&R)4M@b4Tv.J|c7~ZԞ+|m yHp6dnG2i\MP'O|E̽l Nmg#eG&>蝇;3x }ۗw_y,@/-ml" u F'E H\w}ZD<,mt&`&b ;'yƷKq;{^tJRC!~9侧n Jnxe=??CᏱs'6gȴ } }ߑE~hPrZ'6ZLLU>kM"`S0*/*VJae`/Ե vCZOwމ֊v Ň?p#^e rFcjpSPkTg((beiua?'7GIт~2Y@DZ*`42g[ B/U>y)T<Y93p7oֻݛw4a)L PV`0:_x,Fw,,>qWQ˴fɽHejh >lϻdȩ%Ɏ~dG*P 0}0u½=9?f$AgCNd1=p]oZlNMlmOh `6e8 5xߺ(g"yW늤x\ؾG[4k( 266-}K&qB>AW!W?ab:SKs!jCBHz~6B-Zt-hxGk+ޭ"|_=K bmKz{~OwlWOokr)7ly_~ WTZy5''Y? ևq"~8w2aV_pU7}A$ny7gHA,\gwAU >4Y:-X/ƥ- oLSrL, g1-GikOuh8%,~˯ B@R@ꌺiؚd_7[p^>c7aO<| ?n Sh~B$(-;M#|u:in$\„ 3(Q+vVZ?O~ez/)T.}ڰN'P4{i+!fu;JnyE0/7}&^먋+5dí>5-gVf^-//QPR^C?"rzZieKI,4W?-<<8޺1bկfuG]!WE]Hy $<4fO)Ҩg;[)qg,GHUiTB2LP/T빼tp}qt+4uhpbG?M(lOZfE-FY _|>Fk[#ŅBq\:1p]}qpa9]fl|c xKoUcJX4u[Yܣ~kq|a[4bιF߾kf@J}SZĭ&4 ]Ƶ #3?Bۤd`UG*,FaйQr5הI;7==v.f矯EQB"?#U'pE`AGmӏp_ߖ6]45$KՌ;Fze 7PsW)T ek#A#=ȦY*ڷ KjC"`Lx ggU唫3f إ0%V4~%-/xn,>Y btqk4 lRo"*qpy{4#yL@]법xW{@C> FI.I#Ԙmfj"oG %fkip> h:ȱц@=9O5ݍ{ЭsBFұo*?k-jݷCošTWh)@N}<lv`Dt^rExM+h=4"s8C(_h5'; xG&+(q~lҲf0M=Z.L[Yqqnyg7팦>]G|5E5kN>S1R\wRњphN1{m(3%h.zbwOP۹JZȦ܄V6f:cLhzgJO Ig=8C:~_{%.xEt Gׄ8zQ~l<k${}d9H񗀿 ?sY+En \h/h&53 lݚ⎞^ǜ0֔j."#Ӡ9 `y w\:,teh o hZwY:8; uskg2{3Y?0kħo?R8j_k%lӨuLysޛʪ}cW޼ Y"]k%_yM2Xkff2":OVXhedμWdo!&|}n[o'KS4C`@h0PjW^+~e?NY?G>vgrOnwsng|Q~0u,sV֯j!66ұ"akhpeW^;{x_Cj$],4({4[OC0c[O5D3E<=3yg 5 ` >)Q/Jy;Jy0Pd)e~/%We\H2@6H)3ns76"Xff%3-0gŷo/Uł4˿nF a}\:|t1S/ f|Ӓqm(h4 <0YE**xG5-aLg `#Q67wL*xT:iJcn$957 QهT5ikPQY .&)m͛,09rW9WC oH9 *K pAJyLia w0gE }AhT9Ź;F C֖6v.t%ýC~YxaXU.Z)y > &c؛]`7ڡon@nb ƒ޻kV JT"0ʼ '|0X-KW7x +aMkG tÚLbX9V)x-X5=0 U=HGN2τ~LjFkT2IM;dN*,No2&㉌Y^K6uTOԃ1+~BF; W^x{~+/ OKMcݯ0"9 ׅa" YӔkXmcƲ2ԞXJ= *h%mŕcUU] )~5Y?Ydw,uuS3j4[){XATF>LQ F&h=ez-ڄ% j{/CSM亳9*k`#*2ly[\q,ĨbnCFNS$ݮ,)=:Yل K*XEuh>0Wu_7 85SXyjJQ=;G 3g6S!;) Z Y785WZ$-hG4y>c6v7dͬ)JʘX 5~i?xm>Vv`xO{Mٝ. 4Qy7U(ݮs1Fl@0̘cz-V^E0WӺԽԧϩuՎ̀FDZo?4 *@! VumtBg2c~=+QYl-lOIj+)j=ϰ[Ee,spUɏZh@>.us+x `aO "lL-wqSt~Z݁c["O43- (3ǰJLqcN2u@0w/[(g&,`!,C8YqjKЌZ\uI";kѵ aDOHIFV:oX-1ٴE2YyvU0ԾkuT_`d V8 ㋻iɚv% .sxK:i>$|ųqXmq, T*aXvuAuJ1qNsbf36[yf} eZ<Ե J g8{.*o2hHY@M#f6 ^d{#OI:+⽠$yX4JcvXAeKyʚS@ψ~#;V v 4#-+Ȯk}17i;(7d{lZQzֈQz,cSy\6` ^P!5.XI`z[#a'-^DP,I^EKe/|VrYISqpi@Ӝm:u]OHN'Yx'^X) BYh:̙@jeU` ͡ڱ lfU|XlQ>[ G3mo4k.u=϶ K(LO[v3^V˶'e 1ltyb x wҋv@9`D x]j9X ` đZWGPɈcʹ~KbEwԵȚԾ]0"c)cj~IG yƏa >+΍>q}g8Zot?P.V>ьHd#|D"82.QkrVKC@0A:RfZ$odu{cIzjK&/އ<&cN0L٦?n9!܅bw&xlW} giK:F[|G~X=^C bX=͔pH -70^Q6`‰>@hAF\ZlH`YFg"JӚKjEXDsZU!َF Y$x<eL0{bGz@<}J[drlw@A8`e 3v yX^OttiϤ# )4ĿaX9*shbQޖ sc rc(#sŲkJ]'}=R\'}X/z<ʌHtz bP͍*YtNNe}2}V+lϝxrOd5~i*u{ٯ bW6(EvMq/־IqS: _cBgA^0V|./+qŀ;u 𘣑xHΙح0¬TK`%jP0RK:Ӏm)], 0+MHkE4鏜Cj8k O6rȭG8hEeP ؐţIi"e^&Ӣ4k Q,W߶edn#FEyK) =^Ƒ",,^o%'#]O?-mʶ}u ,p|/-O0\D\Ԛ3]keL::FE K * smJqu`v*kF1PLy fT \0 ՜Ul΅}KEqÕ 9=ʙ/Ds@{cۉ\akkKdJ{`mԁwϹ~؎ԬFkFPOX:l.;,`5^tƸf6gT} ]Yi``}=I[4D-\[rώ[&U'gށtLOWH^iKa4^.Yˮ15[P* dMɔki̓d*A}s&XʦK&ق ƫT7BpTT>*}`3r5 NkdM*$Ik ( B`vהLk1U:R@ ђdOܼZD*="iuۙإ(~Z_jtyl]e_gfeZ` Iy]Og8<v+q{a:˵hZfTim9eKH<Ӝ*Tc|Z@upߞCjA*ނ܏ojK"O\r]%`jfe-+v=Kص:1^2X)>8I쵹w'o33gP PcظqoiփTk2\UTBEjeXjͯUV3Jؔ47/7s)^ۈMFc6;2R7X`qj uzFeB 6PO٨Bk`[Y/A] }w-wx嗨18JwEk̏SՕek;0peߔs!Abۻ{Ϲ`~|░8 3zVcrM-\]Κ(ld -IT\eceFߣ3HZ2k0H-6sti4ԾdV9Zp A If]7A㪴BGPwus4vWzsN\J7fAWc;0xz[6?o+t,QZ1/{^gk4ƭw~p?iWQj;R(ZթY/فG0T4ZsF5>et=;F\Q-9L+ԧ UfmLK(#ev,/hWGR9 +'x렩mJaD4,_;ߺcKd}fK šN{#3>e&sI>snkE#8Us< "p xZ4=\R?LK<'E%ސqFrƙǓOJpr'~u%;K e|pX2B_FeS$S⽍p7<־"dv:ܲGsXC:_#@ cHB]u[Uu E )9X3e${ cFփWװ,bk_naIfW@Ic4}2r_:@: ā21CFanյVp,8@vQUŠ+KP @F V0^ȼ u҈ZRq87@]7G!h1Qm4W @4 mwd"&U;aN <*[x58@NZ/7UOWT~ޜ׭ S_O8$?i|>Lw ˃1~Osh1?@˙V?ӾES{Uϩ~p9$!)0|R?* u}bCm8./s}~FaL6s]K+*#z]F@?Y=9HXonJn" i3@-s*&.笷@.qs"[F?fDžYU^l3y@T]ЎV -L 6Kr)bRCxQQ8eF cP9P`4qD|d_+C ^v}sYh$Mw.}=5Gݥ{_XZXwcUȊu,dzՊ,-;jq:q;3^67Spl<1CJ|CU[fpX(0/w蜶o&Dr-2@"V)P3[+Zd ==S𰶹];wd nl“ 3m%Nr2U0Ԋa؛rɃg&C}m/oQ\ 0(c?Txn g(z 􊎾[ seA-%(0P j2$gcݸO0]$lnjq'W'?Is}i1ȑ+mmGp87j \w(Hs ԯxև>u誱&29Ry0dśR ԋ,YY(0bPpճ{JlnbK-́qHf w5cǓź#8Sѧ#Q}xCo%m$^V82 PeBGfxSt@hj΢Bbӑ|1 :d<)([W8a<Ŧ; '"w9s)]'Rc0Uqs p3c.l-ŝduNSZŠUMDch%Lg{;Ц*:pJU!s"W1Sj4Z }>ټ3O'9mJYCNGͽV]`c0SI@e uNi u-exXMe߉2m|Mgp(7NOoW2l?a~N"d #:8ސRRLu=`%nj2G zxL졲ѧAݞFޫb23 ީfhEI%aAʃ$9 )v3 ::Φx5UP&u`&1#&Zn2q{--/|"=$֧cm~ JDV{j.yM5t\Qf s=ux: lئdjQa7\IRKjqontdhmS5qUrm g[7:kfkkaK՘MAj;Y Hk 7 4ӈ96WQ[_mN@J,:8iuX׼@ٌ'O:p䆅 L y~MX]CJN3xpm9Xh ؠ5Il p0\Ap`c¼>ao amõmΎ\xg²+|VwUٚ=dU,5p` R9kXB#QA>Kl33[&gV{$辌/01iųmkQ֊jfi C UԲI"rruir 5P/M]t%̓Z׹ W6i:zpi=-46P j>O l~9'gnmpN;%^CW9+t1/Z>0 ,@d_.ΝYi)p:,[Ds54GB?5;;\Zy~>>O>9,=T4? ԏSi: 'R1ik M+ gjNSg-ܧCo۽H-kuID?qKT4qw<52ؔ E9NiAdoo&R icjs)K]f&3ؼ*LC Fq<&-P;bv͉?Q` ;qj]?(`2q 2$S Vd {sO A>\e[qCes#+a '!v4G֧ ٬_ڛqɾ)YɅ*{N. ֯FFͮ|fɯ?|W^ J-*.(Ij Jx-k@!%KqǴ6 ube '~)?=~5{^|q(LA([8qHٿCC[_]w޵"lgT)ظYl| :Yv>PbPKSU"6 ȼ37[hUA}VALe@Fw+j WG =(:GC9t+SN Ce{:ى2C`ԀsZ]2 /qqR]~䯾Lsm~cGuC m9/8qlChsJ?YZ|ozM4c c=Q+Ib$^ZčE^"}^Yٓ _: ){pX?/טF,] և 8$hd"^)f3*ʵc)H 2l4c@h]V 7ތ{`H2axZ_?^)~:DR,Lv ˹qփ #7hacdjxL8⃀ՂOjNM &h|X~<ͥ]XTa0,a9_ꓣEMTb-sǀ![)uӔyrDxM9j vǞ CcT`ATvbVj$vwQ KqIjF.f8ԏqطk倫EVw+/&<1uY.Hu8]wؚĒw=,>n쎽{LV7iq&3wz]fD룴* kr-Ǽ~A`d|}n*^-l4$|{گ}Mc܁)握xz&OvS77ûai\v ~i^?7kmW{h޾4nedlNO γrtg+466"Ps1 Mz=SgPx: >^;%cqXBߕғ!Y#?ܬvQoz]t̅Ƨ@VwP*ҳEM"r 9g:[=>^]ŧ;}۷?!ʮmQnTH>ɻqd-z/m2cMig_C6@UUۊAR,~6{F@!( ̲&+皱3~4d;|49I J 񘇺ۉCE `hflՍ[>v<~(vўcTŴm5 ڵOXMv'[偞8xuF m9jၔ<*ׁh+ҔI{dxUی7@-)̡9bf10J<ԝ A$WYeH ȺM!(ՀLC3Vܪ_XÅ ݇ġ5LNPx塬z5sS|y׳}^Ïç^Gmc .+smܒs mSoZS2j<]$#~ylmj;k~qcG1s }bp̣i0 cMX+><(+QI6Csَd[zABc'"pA0YIyo1,Qk#W d*y@ `$hApkf"e\=y>efI@ڻg\lC3BJem04( 5;XX,6rMF@+l: sta `#ΏY ujv;.賦6LYZg?{7˕Ъ5{!4VWi)UUZr(O>Z(`P' T 4*ؾ +c 3L[uLeTǒJ2wÁeq)V,5 h Ruv&Z>\Z4L^Dr\cLfЧ-e>y? ^WGh?V{70/;N=hqGiq0@U`ĝ.CK̛K~Xvw`,UXx<1lh@Tϡ]QADdyZ4O2#e̶=8{1#1XQL4%33Q8XbQlh\5ϊ/͌d7ѐjWtB +`>Wk-v%.E5@L2ԒwKݎ][XڷJ#Exo_ss2p6"nSa=O(iw?;,cV&7>uwͺ fWEZ` J6YR Rv; 0[툟Vd^FBPARf" Yh-;"`IcWN#1ԧW f*}{f!|J` <__-1fdm3OW|5.s;)|,2F;̪ȩi4,Qvue49>XWQ-IMiUɃ Zx9W tԲ1xR4r ķ2n 4f`\>B[|`[3g,}e9œCF2[eޘ5-&[b84uZBcV,-kMh,sp&ylX]e2d/fnTVg:=58{uz^w-l by-z"5Xl aNZ6 K-ÖFR=5 4B:YHe0+5x+:;]bBg@8rn-7Ȯ-x9sG[CbUzXpdXZX)Ҵ2BͩX eshUG*W\V.8\m(p!SNMjt(FchMEQBRJMqS8R֩mzbd!*5k'u!IV'5wuMhz Nܬd`NU~>3 jGB~zЦE?830(K$ H9uMPGH6 -S]FkNF.c|4Ҝ֣{n*D%2r k|2+YoK>9|kn(kfcv}QWBB/Kf~>ah\,\/+}*gRQy6Xw~-&0~2AYiqE^[Y/u֐6L #015Iⷾ#,;Wza]O+}{WʮMBe3je8&Kxv ܺT:! fyM]DA>\EUM?Z]? G|&'v^glJ>o'7N*uY^QyWi#B_De.(]J8J/ϗ~=k$TFƱ{b1 `0Ź|G(͆EBոG [#}f 9ؽ@(2;2m t1ũצyrP/Q6%jEXtVllYc135A~#fo3$nXY7#ge zB@7ؼmjk`&׋ˀbNƚfk 6Dztu5@u=cNׇ;yo|r~_y?[~\4obarNf{P7 DOn}dŒ|sp4H^% 0&Ή%鴷ܿ_iWeÀҚ*nFy,ƻC(M_ R3tf`)YY{!{x8;(t9Z:]e?О=6cV:MV5O4mZ+s.}T>DHErjYW8\k)clpm~d rFVqň6نG@?0d#Fe(TWd~ocY?h; n 8gԁ;O-Hr2|wW]KZ@ 34 J*J][Ӎ "-iȣ"p+GPAM2BX 4u16O\]c},(D(?A,O9gR0$XF(:gW ̌TGUlr]gmrWw'V񏰹au+ jZ sz3;(^:*"/"{Fcci$g3,{LU9PfPh Yb 0Ze!u\_)W0 `B֜TV;UE!p!@k`2:p5`]TY:{VHwc]fzQ7NLH-9/@l7g((S:?}+@41ꆚZ9BG.^A'йݹPlGܑ5Vs[%`M 2KpdlNx oƟmNq6(l("BHTI T'J'H"*MR&guocV˓},F`+(bĨ!K3 mC$a?4j=ΒYhh>g pԟͺ?>t"m*)Se>?Q@NݐZ AޘHZ2n,+06GЕXK5 ZƦ0kh_L_3 hb=KVd9#"`]=u3պSJ@o^/y3|{0V@yr6`06kõsN9BY$&.ђFE/VZ„(YЩb֞u@:w\_,0<ޫӜIѢO` d+'O nSln{f3idڋ,~j?㝀8-}" Wah%fv߫DPʲqzVPML'9YY4krH;Z_llQ4Rأ+Ja&ӑFb=l_c H[&ܐ,J?S 1 ;Bh((\șQ6{磸sDje64lh8b>ߢ#:dZ&VJT9ntTn8Vekll50ȒT6,m~y&L{* O7BZGxh愌wTK_`]xpUPӶ3\3OeTeA<#w [9 XP^=3RpC-%GV:o̙<]Cv-Q bU@179s:dtH_yy6Rz=%uRdt ȌȰ'}YWY7?X\^w y]Щd锿z:c%U,4?zЍn.]BI50 /4P('0+V?JandIeȇls!ԊC(V[[[Sq—25 H)7j'ЂKZV zf`:S7,Ww?&F-;)9U؂X\_mگ~G{s?09%4!~bA6ج3hXYQ"p߸ÊRqS0":R:?QYU薂lhćL1EM(4pw`Ԟ!X( 6c< iyкVwR0j *V(-ap[)` UGTdE2!e0т^_GY|{ߟpN-`5 w' Ü+i΀[yI:LHOtc fzxmNF^+GǤ3z> n5kUQ0Ec56E~YIفk`횱Lא0m0֨[@#=gW kJ]$$>fpckjI,+Pv{GxZyCP;MyUG7a6 ~߄[,`#Řs:bnayތdl) 4?qeH0 :5k8hۆ8jjU3yܛ1r揳h$&bڒ-79gZ^|f܍YTʯFrB-kyTbp(iuȹZ^"R Ah7B\(0 ;?~.Mamj-߳)i.YCeZT]{deY]?^<X)vn. u~ u#XίСn:OP\Z )o P>5$wIWAyܣ{5fX6]fu7O%y;4sחYS*O8ʹ;G?{]akUfwM5%KdP,Y"E'0 @$@VYER˦&[R"G$$S Iq0)M9@6CUw\]s7={<疆@owzs{a^Vcn;]ێCK@_4Ά.+O p6 h);RqReSoQ <[d1$92m?G?o/]3TЫVզr^k H载>JP=Om;C!Pa]^W%< Ѫtc67!pwOEc}a^Y*#t]qv&Ͳv(ܤYe0 DTcld6i 1@(ܰ{3_x_f_r(8o5v__Eq& 3)-x?1?cD1v F.D#B<0ʔ7!)5w7 fL5OPag&e eM1dC\XpfO<`Ơ/i821GU1_{]WdbW U{ ;P`Xݽ7_O1{xv`6$|o`I8\kw!f29"мK:y79Yw7gʟH*w/yMv,)CcڏeWKvd w)l=e w:ꞍA]yFExDXf{׀5l&8=x7co/=2WaAܕG [EVMծ26 Y]m_g/ݸvIBW65bxX|l`)ͼM(<  uKͩ$A:W`b7oEtwbdU@Wg cnH s!B<9N˜>o#$)yu.y}^Pbg1篱WI Pxe.v8;>ڹt6XʺOnΜ;CXCfpҟWN2ΊV7UhgXPFBi#Q!ӈ![kl]wQ FaylӂQ2|-9^T#M/7ۡȲcm1?ew;_/_&_ |{VŇ[ܰWΏyX?V%=ys]xtN5R x'˃qG \Wpj̔㔿i;gtR@eoF =~|b"PN^1<^Q ߅)u~VF6؊?}z+< vFO w?h-_>>n_7NGsc7'޺uo,mEWeor׬0jvُ/؏Oh#Y!Hc~ l CTcVǠ!gt 6= XܞP\p)LqDҲۿkuǞ{nLeSp8ƟS57UOմؑcu*g:|PJi2,C*6N ) c8T{Z }C;}Sߴ';}#.<(kBbꕜҵg-7 UHHLr4֞ƫ>-8C66߲ޝLXOsYwLmғˤj+( dfeF(ma,TrP8drXV5 N;5 1b'Eδ(fBxҳu'`Biv9 Ih !#Pg*D6G`2Tc__nr_/}ٽI\n 415^$ s>J"eP/RF_\Vt-RLblBoR^[rΞۥ ަBx ɀF8H|cMbϱq0F1_]؝Mi8\hZw݁%;I GcN5jŨiS?)O/; ,,#I8' /svV&c6m|̯yV ( Q̮v~^{0@Y΍n$K;K7~-~nr߼ܦbS珎Y[{f7+b{#h&ġ <@OE\yDMצH5o33{q\!< ޒz!l;ݮXZF]_)f~_\K൰}J]å9ټ̓}+O>=[r?͐FΈLɋjZ5V;xX , 0 z:?qGkQ@!ﷲ?IDs6G.Io; L!&u̸f0kr^ݿ=v=+~f\;@nOOå :t2(u弛eŒy ه gÓw٣9M0=C#dpٺ @v&g\%SYk-Wg^>4}OV{snpAiw8"eduḷg?1^W`?c='>TI=2/{O\?]?9 hѐW J6*8x_Iquj>dS_{c?ь2[K\]p+IsMnwypVfUpF>\OS_xC'Oq4EŸ"?鏌(!OC0mwlzpm|z<=TYciڧZs>Oó7jN,+65!cTEtOgXED2PCW_/n2~w;]teLXH}hΊ4~_$:dO}{^9n`1zXmsL!Z[NV `K }jfSC>r lk0vsO7&\D;^~}_+o}wnE̍{d-vY:uh"W7x|/v}7 1Z%B\+bT6vBg!Q)$zs/~kȾո-}[Qc ѓzdmwptlA0dwq1R>Tde%+q 1 j$bh6qhcgԵ،Hfo=O}طq2;\ZzήPx\)#8|uQ}^yjq{q}$4,pvmDU`d`h^sqHvY IM%Zh!L{jZw}N9r~CJڍn$vmΞ3վůy;7/|c./ 5eSlrνE-Z9cm,>3) 8>h1b'rgZE|[KMa5K{>Qf]}?i{|{)q'?M>R_w<ۜ_nsy6{pnv_?>հ%OSV5$'0׳SǑq9 lNz]j- % ,W{3$hzR0s\|cs)yZ)PqDUrP (%A!(v0fcCQ9Ç䅡BLܶJ66lmK<׍s^g[z]]m+[^=ϖ/bYhTΑlr屍;6jܓz{*nygZGG5F@@.P 2" }{ .ln ʜɈ:·q&Tf j gƕaLJRZ)d1!6n씷1i| y/?\'~KO=ew8'6>*#Dt32 rLUJ$kuS_~__6yټN''fGCʍƝxwy捕LUh2j V}Ǐ#b0e63/~N5I -^}[Nϝ "Lb*G̝Uy t2$wj_G~7iOlxܾy lo'طۏrz֍;񻏶=Q8_Yꒉ]4,PC[PeAA EJ==禃a?9RKe`M] `!&82 af8)pxM{vۘ?򣟰_ُ{;nׯG=Z cV W}?.OH6ɗ>ߴ~_I޷o/뵇>#kPtwVm>,O3SefK6IzQmj pʰ;g xt2uvDb6s`0 ̆Y;J`HI=bTn:I,ڷ:?zho>g\Ob=^ؔ7)Ia u½p0P*oq0KGBrW7'C|=k'޻72jOћ;PhY7m&5Ve2\9->Pl3HYM\ "hMC]eʽi\1&j0~sAoex@AҦJqDbu !3ae/~"l؂\nu\w'o+'7Φ#9:nb~==_{~g?g?o{#Ec9_˵F?S2ldmzh<7J1isgMY(2̛J|ޘl;+3{ ] 4yk/nel *Eh]PTDF9 0JŸkZA "%oP%{aU[ÅHWٿ;;=}b'8dG,QmWXyh`_ʋO}};hlx͈P8"5p&YdVaa$Hofp K1NC,P p$=xJw2r5b텱i8aJ '}N Ù'>_GkouwD_ۼ{5c˽bcPK .69M':r -eEt]֖L'~wIEA jTTE>ɧ{`w)6H0P1O:|;{|c͛vƍlWW~7xۇgw?nj4햽..܌ghjS2QTtGXڝ~8Bt4{%J=>6PGgkLvObAƥN6*T796>=|xab-1vmgKٞeԱT u#L '!7C9BIrl2XJg<4X|:W%="uOr H"OX3U wt3^56IxH>^Z D(K39] 3о{`pG8v|֎SƷhvۍ-tvvǞo&/mvvgQ[4mTՆ[/ɪ ce Z>,#I&Ddj1bzȼƚ\ot3/yM,/|I*\餳ڢYѼ4iPDžXąMT^Pdg y ,QG{90W644hh] z76#twq6oss{lصavC696-|%Їp4~ 1,&S XMaA ;oJ߱n{;t#[` ŜZNYN>Oۅe ܷh7x᩟3O}^W t!~7+;%9yɊU"T_iP HCs%=e pvz6 Tڍ3EkRd6g%.tMFFNeBt(>P.F'bCk_}o Xӓ񻣤&.7yvmY46003Zn1*)kSs1% +n|8p~ E2v)s_'nJZ!l,Txž`}8FE.o#)2$`hcKxvxj2/&(6Q,+cRP!뗦A8 wf$_&'珆lvo/]l߿|d uˤ,eηfNe&B8 ?,BJIgwg.vqq5)FzxàfkTj@`+^-,4yeW/D(m{c֎A^qEnjk,>'pn먳CVU;x Y8Pl'RyC?}cĔZ{2؃@gLGcM 3q᳌Mx.mGl<)P@m8y} 8;)3|}zR>d2ʮQO7)f`7ѽ}R;2l)^t Cs,R[y */\wnV$ m :YqolR ym5[p| Z ~\euAK>i}$[gteܲt LD6ZQh3;5L>"a:woAv6!G =*z](?RXRxCĜF_bH$Nv"/=g*s%A!pYo[x_fRYѐyWn*JvB%U:z: PmOfUa 2e=&Ox8BFFYH;p]O0wvSN 9B\5v;QEєaX;M^;u%q)VbvA gUgYOn_nܿnu<_Obͽ5qeSl*X5'%us!cNM=eƫ0 y*NM$Z~^B%Ӫ tXVZB ̾N>⫣~Sn6; u≜_)F(ߘh:U I=o| G1:epa%8d0AM3W%O 017o=rQ8{PHeM$+Y1XQ2[+<Ɗ,/B8>uך@*+F,lx5SKIeo01XfsA ]ٮx:Qv4JyQQU} 쭓1jN %t2M@` rd,m9WCFt.116 Z:Ñ94VU953tHłoR֦$*K7iGpޞ_B[и(RhSjNѥU 鹙@Dب4v%`\ϴI4Fu-2!]l>)zND8T夘G='/C E()V>ܩD4NXOтfӷH} YHPX$-yBAz sҖюϗSsuKH`S6f`;@:b@! ْ`* h0 C׾VC8 mNz5v42Kd42c8:jLZ(kS%+40PLRT-5iHo%د2md`Qe)zTa-@}b2E&$1N?&Lו n՘G7."9' <_| ?A~A+1~nUٍU wV{0nqq = (5/#9~X3lYgMf8@aq/a1z/g޻cBCaxVUݬڀN4fP6fP D#4GQ?:THrɘު8P9خX82H+̹aq(*qSgWB*ڌ532=+=⬭>3j@A8NG)V'р"_Ez?§',0+C*^.dLO@M^Y}Ƭ-ϗqҪ5YFW4  Y͝r]]$q7y{/Gzkq<I ظJ;@K.,ȏ:ivάo_;B%^R9ˬ7O1=}ͬqnH:* eX5x&AA;,G% nXόw)>,g2ΜFv!R8LeC"[U2v6(Us43ދ%IAQ6Hcn ʩ' zF|$[[61<R6IS /S8cr| 4,5EiMaʗԦLvgDAuAe721eЮ2Ur6&R>csosΪ4="*ҟ2Np{7&Cx=dKigxI@ǖ]!oSI'ƽ]a\PFQ1v]>GEI%}L5 Q+ ʦޢNP+ƧN%BrV9F@ò>6YE{D1g }̟,v/Ɨ:S[sS뵎baA CZ9: @?9\lȭ\cQ#%Cy-}c2*7& ט0ru.;[^W<<_Ť.nAp#A$׿6ο?}+hڼv# ,AU&B낢9l- 0N#O \V> FcK㜢ސx2窏×0>F9i\AR [W=h/U]rZ qWRPi)kKQ02(C`z"Lh^X `n *: (Ck%hPl УT~zЋ!9vڼ.>xR>JEMPۯs \0%QJE&<&(۪܆U $Q.8xCҔlQ -|{Uv ^!`8+@ pYP{"B/Иh"bɡ3fv2F`p G aAGyq01 d!|F4#h. FwfZX6h5:#K$$ |LHlO#}D)Adu/\KuXe|N*O^6DENpa1\ ;n)v~T0N[t̏z~ұW 9ݔk>,Km<}3B ;1Uդ xTMEcHmh4z530lv1/{`j;C^j2'm3iAq-&OLn¨p7"^"ZSCf嬩=sdb-g3S1BDRBܱ cM-jɠ6he^C+C9 g%FbnSj4("SѸdb>RɄDdԠWGI7&N6DBy7c2mO_\|wK_3l{a9fyw9vR|\&)?԰tE+`hMK92gh)(!_XRVxm&VHI#V٨}8^4ҖO9=m3mLxGL!0*Lgܽ-N,`A8Zhlz!5+V( h\pi/VdZz̰B鉡%qV E^o#!od]qCc S&bXV9bD;C2 {6S 'd P }!pLԙIa0K9Tf <;kb:kz4b蘌u[-X"Y1H>PJOb1 ˛1!ɩ )/Ų336ӄaW&iij2BiNZ(Aw>xqیPqNB{׌ؤ7w`Xvr^Eg4@j.71'6u\UAĶp$Sg_p(M]0)|cFGj?v)X(V%@lY!k S9mu&JY Ng>CuӑX zqngOكO]=86@NNQ۞CQdJp.#R:G|$Y-qiw6(ܮ6ZW}@LT%q.\`^M7-AQ {gqWyŲ3V#$RIkT`2 ǔ Χ/i:%(RuP4z 4!$v*Y ͈֘r\Sz5:P:s#*@ h$ d.VU^j DֻbƪzcC5>uڨ*#8Luy!|`qBh%?gl4 !ʣ^9iD UJ\ +~iW*!e'1@Ḿ6Q<+`%3 Ud-=1od<,uI*> 渼 [ ?ё25`5-i8KywνgnK~'u 6zfn0OJϩtSƥJڱx Y_N``9\HY "E]\ }7Iat9W#wF83.b*ZZ1Fdit`hbERnEyÓxi,ư8,@UVV$ͽ)@Z|`g"gs`AؗpKE/T1L[ 3r-CcA=#(6ӭJ:ysSuh2˛kT:Ay&CaV #e~ODS!1~ۮ@&̘3g=UwL?:{s5{MMGv`p )o`5! (Ӗ L)?dah<7.}wQcb $ C 16# Ie8p9:;hfQ#|d!C=Q: o<־|L`B|&2r Zw` 81'#+jUU\Vƽ=&J#C~ m&a@LT8̨_va+ 1q"1/WrU_'@Z;ūϞqpZNg쵓[Qv-Sll#[+J,[ ^T(%QqB%)'+8Mto!;@;75z*%a1kƍ ,"?] DL#,'F,D B)gӉ :HPnPSe]}!`MWJyeӧ78w*F+@η: rh=muR12kSx|boŠǏ ,`\C!e>|$>ӹ%0a^#|U #ql{>eɠu\ccWY]ĝV=K0ʗPZb8c,ceJ:+gm銞F q; paw?H7 ;{2w=v㥟N3: w:(V.jNqsLEl K(^S:8*sXʊmFX)8 *Tmw mRu 0yi(K*36+WIqRzC,vs~TJ I/`섃`0H"@E``1Hˍn,@i<މ}aYdM y ݈LjΡ>)h9l@]N UKi)NSsLpV.pFwNEGš:40TxXyS(Uᯕ #W.#KR$*.ؽ+,SfO mbߍ}fAh 9 +?`rp@ y/cyGlv*z(FvIS%fkc;{ejA0{6p8rn87+ iVh)={%lHV"|$@S~+EґҠ#*7YZp2r D,$+g |Jod ItG{Tc3n;{ϴ~i{_oX`a8K$c+90qN~ݘ-뤣F]e #07/ +ʗRe;v(nw ƳnTbH L(̔:X&4z8́v*_%Y[c֩붔p)c(6X 7zH S ==rZ{SLsiq}$SrlOKcA##϶M czL44髼{ eR*oh U %F`8[U9QJq!MyR?`\\C5aǂ*.G%->QՔ7N[)?:U3 {+yb6,l:LF }OSjAg{/pXvVN="#kmYSM8h\|@%@ !Z LXEe2KE{Gc835_X[( O%{x*I]EJL!ov2hVz+H.tTmGq51 %t"h:b |U~q?꜃Iwz2rSѕ5'z>8{Nv5pGRj#n.}*H:36T|2&ceÓjK^(3&md~a0J*p #,;㯩Pƙ܌UH7v30p w(ɘqj<ldT&c~wjn%6Ҁȓq(c|j#8%vid pK+K:9yo4 )kjtJǰJˀ4,=q;$z_%8AѲCCHNb3 ^<{M '& |5SPb/wjS&̪ ]6%́JřigR=ƯHl|qС`IMZ֜4ءĺ+)HA&Z$\Bux&(KnDpQe<'dq۶X2RѢSc , |ƽ.Z̜e'PM&٭s-, w Sxᱹ{+Tv<8ATU:W{^{|/ۧ7B]>B* f*UN@S3:$xV+"g=3lqaLsrQ'ֵ̽ݸ{^7gzwu~a @=xK18gyɒR }! Yjʼnvg5f6I%zӃg8+Ʌ&b҉QDqU3(5_xf 9-*m|p$U ]~ʋ5VXy@X19k~ItI4=gF92 g*m}U<5O'Gɕ$j d\7cc6PH,cVa)'ã٭\L.#jV* c\A]+CWrFɍ3^_ve{^o(●gw_or.lrjRn=pbaڣ;pPtƊXeSfvɢ>ךA.滣vJ+! kLh"6D"l,~Hbt`ӍkC)Bo 'ap[*X#uaeS'm|+7W?ݓmU6~qbnޖO(!+IYBelYZu"z hpgRHHJt K ^⤁31Τl*?-0$#!Sp ;7 :ɀYg?R||a\ -Pθ˥'VyΘr*+*mb+֦4<%- Hlz] :|xetqyHtnh4QuQc}W_}WprƵiP%.BbN0Nxha 8P#  GJLd-뮖s$QzGNX( AWڣqaXƸ)^^i/h J`JƠ4*ߜ NMR J WTM˒ֵNџ,cNZP*dzDzR \gcȓ|1~I0@= {5fGx͝ئLFCc$zF^vfk͍s {h&cYL{J%(6Rj~,P gq_{Q9+%* I (BZ axWX|p;oY }=hV51'ƿ@H8>T)bpMY` ؎{$jL u3&}ds`*`T6BhKaVƹ2:}~+Fi(A?a8p.ݸ>%Yn_yC\|ޠCzz~xd׶}f1@%d  ^ ,(@B rIOʸC?gǑY3+9+P9U9 6c4JTKÍ#L+&/o_9' gslA iۢQCQzlef^ vFk0ⲨFc=*&'IO uI6ģ6KIEgi.PK{~`|>mg9:AdCaoj6\{d3qzJXl*)RX^䠬xSI7 ݦ_C H H*d=:ekoU2(+l-Mkk%ma8_/4vUP ,!a:ɉ`dHu7ogG)<w^A%J "=yB4.p4q\ Lٔ]1 isnXճz } pt;3 CQcU1d$>$$Ly؏rM/  NVڔؙ$h7oYdʣ/~u`N?q7m.6i_2Ce,)/D@+: b-BefCꏕThN\SYSiP@f=Ea;"reP;v? Z/4>RAurX> wF6|cWz RI(_Za*LujSfEK|ȑIKccí"QLU XG;+KFXEpb7aM\uN@(\uov8[=0dF{ad8Fy*OCWV52;ZfGQxM6hfگV%+:⍽6JFiu y)3CG(/=Èʷ&K:cpzH&"&}V,:\JI!y `K&Nӆ to%eHB 5*p\ױd=fsU8,uA[0KH ?fhXN<@dL07SaO^K!xOzSG;/mۜmsvZʔi1;u)v,eeFYgSh?Q3tDwXUj/oĩHNZxej5wf2f1Jyt,7zPٙ!,!igaG:ٛꅊY^16(U9"_IE}HȿĎkjj6QS.A*fu նZ[LĔXdw1'Iw|w)Ƽ Qv0+%hT̖.yTu %_Xd% :Ϗb+8~Ԭr5;\1 :eNׇ|On%Ή#ȃ6haMvŠh3. O3v&!hZ .܍Y1eo"sBd0". 8T&bW IW=+f!)"t6NՓ/'{csYLp}Ca >JvghR[g `ké H"g|c6]yɟᗏ_x {_oh# OG7|_eکSڊ4vW *bYdl*w1JVY˸nxR#)Rru&3yFb*qD hE2bj[y>)hFd9&9n:4Gkh+,u^M龺'aT 24r|ͤaOXM֚w1)Gޤ%iܗΣ'.Fiy:Zo9oݢ,SIhRI07+mfHq ɐARxuQ\y(o\Ce*Ay )UHI2KɰO&ٮ1M'代:$vJZdNA J1 ˱Vr1y s%:j1us}͒Y叐ѾKE}+FN&?Cɪqq2[&vj\8u=E:e'FljًxJ5wC4žxe7 Щ?[n]ۜ/}O>y/zS0WK?sppgZ 'zza9X&TETBˈ)GcS!f͘CԢ(kyJVq+Z1If(f2)o)Lͮm9MH̜OT> Q V| .1B,/}\3-0 SvDWhq<"`=cbb[3CK&fW1KyG%~Vys+ ^:V^n2BXGo/#n4,fuP/6 GkeAa'儤NsYH4ӝ l?YdS9GS:-kH'ǚ2UZ jS8Yqzm-}\vQͧ4֘8O/j5'N:P-=j[\uG}^S9^ݞi.c*'p <ִkݮ|f~61>0͏2ji& ,r>$4<~FJd3)`1 r(ΑJiF5҅5O>-J.h3 ںlWy{ 8K_yƐRo^\'P1F9SBw_lX~U^ [+F*$0)Q`dzBtl KpYk|I99=26z;x`F |^u;|rc[z  nu *5"wZ1sM\rRE;h`vXuUZʏBrج>0 &hk)7WFdEӣkzʻ;M;awMG qH[={̹ ~ =:sN*Zz*WAghT=㔃׆~ώ|Λ 2xs?Ч1҈v:uͅkQ zD_\muAgXg(mwzt&L{w/;}^ϖ,]ivng/}W{_oc_q:>80!IAoby MgO.R&! 3KI(`I-z]ƕ="XgDDϨrr/C#1 , % Ư.۵2j 4(6e&?["M蛮D;ceg 7r3ÛufR En]0'3hb5_m.@nAsu"-8CDwN<\(#_K˒Qn4h8Hovb`t3KmtF$ -Q)U M㓔-2@'ZT)}*߁;#M8mM' 鶽k2GY^7H 4bC-㻅z4:'h1>WC2EY3oV9鍺jec mE"2 #{H z6jd L,-c݃rG $3i{miJSjxK>K:~|*lpC9|j)X&S<^;LJv O?ӟ+7M pW~7nma?htQ?3:Jn:=^eHxDѠY1rnyJ˅0q{ˍ<04LdSflcJ<$ \~g`jCn.9)TKcYh.tH 2 ʃG%&4O^]'Pu:}|ywDɩENNgL1RYҦ?O %;rGa]v@5 C0qd)M@dc NNe'@,l\S |vBbvXl嶫dd<ך3BgueKpٸj"DW yطz08=\nK1Sw&~M[ لM8GCT=<޵|PUq,CMXR#cBY G;OQGlYs (Ky:]>S.yHC$sPlF:6B+v0R{;6i9 ☇ S苗W/ҩ{7v,:^[o j 2 P7SKa+ Pb}b+[_ ͗7Ml 㔤[ pVi9Vs*\^L:R;]McMZlBB̕Q5WY%dIYoy>LDߔhSv`=#狽{ŚeP#N%cx)MخΥiwf:(Q1aL]{G2RWs->b#eףjíwd0(go@J Ir,LQUSӁ0$N=C@[ 挋ajN_Za{FQ6j)RbׇjNke`l0,AޓgLtb4Kc}~}7=ً/}'Mzs1_ۦ“f+2~ ބz'Ҝrۘj4vUP75Gˬ?]^Zg 9ۚBѳ92\ pyD8C}T`&^G$V̊M.xu(I熈s*7KbMQ[R2h;J+; }yB8qƲV,~0ЭD@YRc0e@G31P~S{zRq(.mQQ+[j/,=w:eut ^v1x^]5psѫwIv +.켞(;xeQ:[FH y;NJ绚P!(Oo<$cV|RMQ23sT ش9Kk}^7;.߷W{?ooכyt|t<޸w.~P, Hq!$ɹAדn.Zr-:{G+3 'q3d9v{tT^TqE)!T2f7sЫ U2'Xry1ޯ| 2%,Ch|]nDͬg#bKcLLyߜo9Aϻ*@6'I 2B0A0H aiI=Ӄιy,|Xޠb)M3"}T{AAF;o-; !R'`4 Әc>nzmaqƺsy*}򵉠ȳ\iqĥj[%9" Gzi*gy'͊tJeSu@|#SXΊ lZDI^JFv:3JZlz1ҙ~u)'4,U}lkUU8THD/91__jqOWo@4 .59/JmAj_Vu>?<+ ڧ'}Ńƾnei#7=TxlOl8A ) [s]G!hy^HzYY p Pp$T|gY^!,݀PYy cNؘ~`kbG%}F}JFԍj& gBtMwS@t8}0MLŜy( e|F+IdZ_yE5< 1?-)\:Sܬv+oa\V_]3XH6Jc5t1u^MOCDX `~Ʌ<&6hZSd:b3 DjRVJJkO_W zwv<=Ly[S^ޤ&e>-滿O*Vrl1 yNq4@&X2]_k3'ß`@MI ''UM$*=H8+l3ᕇ g !qq9]k:] 7O 2z<6 rv rFD 2+ƭ`2ӟdV YSP@ӣ\VHfSrv ^2r>AW8+\Lɢ.CJD&biQsFi2ҕ/9ٙ[vx .Цc=KHk 2ujYʀX5 yYk[ǯK_^o_ٛʻ"~l闛e:9+ƥ]ѣ:U2聸}V"C6k񞩘YDŢQ2263=(j:@ A蕁HtkhU"^J)̨PVxeOQ\N8bꍰN܈6"fug(n;F0jhcˑ?)tc};B1̎/~̉ e<A^ޛ}(|&/ֽ˸qdd4}i+YSw*qV.ɸ*Y2}`p㥱P6R2/]^9T;OJ@qѺMh2#3Wynv!5a5Mӄn>.l)|#dJsy_w)IBSWy%h룸myl橏,/|fe7--Vrl w _vzz.-cT'҉k'JEW|Y;7QJr96} LN!𳋅St* )`Oဤ3 &0e c}ypftgІbZyZuc)g?|܂P fbJ5xxzJB kt}T@ip:ߛ^:jՅiPhW<#ﳢAsUC_vrV$S}<|wCIig^J5dlY|U^ f4%=j}Rz40o*J2.W'x%M pML㍘.0P+{`RT5-h,աN&<چ]I{ˣU▱WW. ՎuFCU=b#91EQ'A}+5C+iLY*s|Q{ aUVZ~\ $Ћ>ɄQGA*t (os"btoKO-ƉXwstnUr?v=lAZ~ulhQ5?tY]oa'vO7?|ބ75w뛏.-oN;9vH驝M'PXJzEK+#GC^j/R%X{MRBم.X"7 @ȵOpCQǎڪef4Z^^wW*I$:4 +==NQ1JeViTa2#P>؂w6+0,c5"É Iv^Ϙ! PDžlW9)k('xnc lGNmh7Fl]ok\~g/>Mz~~yӓ[o;Mtzf}|"sOP|iئtRDcS t/DE"IcdhPХ:6. w^FLOt:Ibsfa0 NBSas>vChHoua-)@yjA1mxETM=62ِI)ϒ9ai fqnf}8?JaPQ gJ'_qnwDN  R2A~e]36Y8|Y2(}1SNZ#7'M\OsJ$K̭֚C>c*s*դwL`h7=Q`-s<=`'6 f8d hFs4B+{4@O:;‚^sW8Buha)fe6AV `s 1TH +ƔYwܛPtMLEjjqv]ݯ}G.<5'MzKYyvݾf(ɉ__Ra2̊vIB]^U%)SƘAZPAὝF h4E Hl$cf bq:U..`W<:+ߢno{C< Z}Luz"pl g2mtԚ;}*Tը_ PQ$+y5 L$@!vnUuzA`~OބBpdNw5#i|oYvo]'VWNZr/ZRWcɷ$бmg7k*4q[|NG AjJw}_%N {֩ݼ~pa˗/{_~:7-/ڭoi[oۻOrShk..w46c*DƤtz!AgC1)*/.|M0װeg#'n;`(Sޙ+ kq°G(|+PWE&#YCy"=TC:>3U r,#IiQvN&TR5ǟݦQbE`p hI9$m},eg3(-,@wΕdUy=|"Q#à ҧ xyFy)FcY]NC %@,[MIpVJ D:ˁ!=hTڡ㞁h(roK[oS/o}^z|ݹŗ?tip8㦨6E* &|nnﳄJgC':]';;`v5b*Fm_4*%dëvf3oqU{u)$t5:\F](oB6W`DͷsvF6;e04f9Lg37檀\OOA6~ Ty6$?+͜T'VĬ=ybڅ,k(Q_m)ٴtҠCZ%IeJS;@eT^޺+nI“ q3i(+btxpr+ݞ2,imtg>?&Z P3Vsɨ+=Bjs=\Ԓ45Q d&ԝ ,֜!^1={0R_c A_QS7U벓-^+y~tޚ+94a;wm]ٗ>݋x7-6{/n=]dMNOkcK/+;(n#b*3+;7#K yYxT)m*k|Hw&=r^d٬d*oppqFFr's`ʠJZ4zr9@\vgŶi B $}^}Ε(![͗5CZ-J3Hr BS p \H'AYa|^Qg@Df:!P!"t;&nw!VU$Sz+J,uͨtwtsOͬ$U`RLЍ(楯bĆ)ECuב6УWW @ARTD+Kb2`̸89O"x(m2q0^ -12qa/ I`SEʀNAń% j:naԮq0hA 0<uTeꪚ~OWLGVr89CF#Ӱaǟa7m,f~Kgl~;xt{<h |8prGֺގ>ᭌ#v(UׄN Ȯsݵzb|xPtpy#80om*W ~ d5W4d/IWj쫢rB5hdNnIJ '#*k*42UJJ֕V23_Fy-uGfHH`}L^T\/a3E;_gS&AS{w}&V)M[]T scmJ`.է>SH!]-s?px!Y6e/=h7vɰ? \4D@ R/}y|s5c}Wn:n/vj7U{_W?E^o9}}nYN;zg߼jɉÒL+Tb Q)^g΋׶q) u2b_Ċbla8l*f%e3~͂]/RrR1<50QNRturovؤ}x3K b*C k<R-Fcq/2K߭픺 :'JFZu=d!F6*L%\qXWf橛@fPOA~\(˛}}&<8ķa$SvmxLy(qRvW[ b0XQ9]Ͻ>n\.Y&kyT"̐*I,2SoXa}@'m3eY20hX $JNEV #l5)t1M_sƜ 20e: `3ԣX+6*GRB53"› VDwn/r * :60 enC/y~vv8'`prg\Ӹ fkuע ͂>3LI1ƔXKe(^l jrc}q6A>PNA/! E:rW&\ V8͛PAu2 rL̦;/B.(2z枣hqvnLDL,Vs6)lVb_xEɓ6YSK뛚>ʾ(M?B6f8 "$L+1!1W`L),z oR_uP \w1h{g?/|xg^oM`{=|_m_xwݺ<~;Aѧ3)x-ԦHU({%Z-J_@l&Tr@M}<]0&UgP_}Rx:]>P͎ P*MW ?:&h#h/UWGUf|7%8vz1-\8]0-u\ʀq"iql#g\k^Hei{g^cjvsX/ocdLfe1Q|ҳ 44⸼>% ݄$vqEbJC~_@`^AP+Q{t=)uxGG _^ͦxNъ _MReyP0B+ ̌NJO?%Mͭ*6D8 Ec"L^d`6I綺AsVZc%LNyR@٢g5<(ˋ]gcRvB9^ldszXJ.Ϋ4= . n٧{MPLPn@+3 h9.cY譠$ĸJmbf@$B.|M t,'@ge2HV 7-.{G󙹿|gJ)wWcMWg`)ŏV1Lsxn*ad8AUcUM@ FPԾb26$ (l4-x,Mɗ0cM} O enTBj}x!\BK:2N`G{:xޅ*F^*m X*<@+`=gj5vwU^ΏymӖ+*;V]^ωs,n uNc~Yb%Õ[DV;FqgsZ4Ұyy,PR dI $,5%en[^!@1!NڌrX,pD|&*ZԹsgs=v|<<ݯ??Wޢ<ڝ|^mۏԲ`qp%TAh^HjNXg*ҦkS\lntq53fyiHӉMFB.Gq:87qy\{娖fe6&ۗirq*K~RwT#1 mGmzAm;Ke #CnF]LE Pp@Nj8Z; gVp6,:zbTbYa+zyI9̦ۗ%)(82/@X,ݽ/}!ND]ϽP*. \sDgw}Wn!sL0رRcV Q N|ǽCv}>Ya+< GQ5#8!Чk D=Oй}C s [PV:f{;[=>k~o~||cm?rioo 0^wNWz=6eFmL(H ߉^${̸R*~ F^ԊU0|L6߫3 h7mZh,o*켦9Nݵ\g3JNV&CUJ1ox*3r z24 ˵]-k_<_JׅPO8&BR1#3X Lqʟt*1ibSiP.$;g*9H.p,s2Ja#QgeF䐑"6\/|rBdL̰82:dMZ{bÐJgLJD n7qXkexLQ y3#\>/LnUl/U,%՞1P;<ՌM+J|&NmIJaxk]Å)XV m6 ' <,mg .*Hiw6Ho6`NMC^hqkgOG~ϣO_ix]>zz۟zƵǯX6]YxXNbtOiY )`cL\ I P*wY^VPDg(#8^3T%id 8J,6 3?Q(4R 0PSAB~M7A\\>YnA^= Bc6SײS6s0meSԼ_1=Ly)̌ '9soZyZ-nC>e,8/Sf6^^]GGi ߙ /뾧|fݬæ93 t.=ηV;b>'AjsTȬ,RcQT\{^FӸ-v2]}ݍZ,YglY̎ 5t9=Scaޫn&wn];wnZ(Tn\) 7nIlU+,0\fP/:gHB#$kąL]C|Au"BT\'3q+n+NXŮL8Y*8ȟi~D-<#Pb;1!JilϴS> i*RqD; ]}Hz4>X6yn`]GC (}mLe2gq$ QALB(ct;H1KG`m$gfK-g퀟xkM7o~~G;?|B'v|.^zɝM[ ǜuZjA ~$^җ턺FM(*zJN]H*9ZJ_/!(@zSlNFr#X""UR \=z'q|}kl T  2/g>^CY RƁM war5Xrݠi8P*(?`$@*$J1DUyU@: [ S?U d-kꩤ*K( B%$Bsp|F3t˭>fGsDA9!2"ƫ:9A܀26g}|e$N1ٺ6]pQn5ٓ(Es 0RkR#R&kPנ˄rN˹[0K|C~. $Y+&;l8_+xbC.\rw! ALmb؜6AN{w%eKvz<{W?|w>|N;[z1{y[Iaݹ>E)F,҅B0#-@|h1_s!ԄoqԋH[bR[$.sX H Yh$q1-ޡ wˢR&i}t DPKd')\9 0#%y64|q??1M’aktac˅dNR%x9'HA'%="% TjXl gJVr3qsCI <#݌)`nS~|HvZO@RyWs! L=Q%, 0;Dؗ:p@!3aR!E5$vo|O#A(-o H"̡o8 /p-c`Y=`׷0]OK04J0 P1M5΍@:=evXuցqykh,%u1Fd4'=nZz%zp׿?|㇏>|n<}7rWpbVf?]Gi!<(]eV@:Fe<&OR%݊|aѢK5"A}~H6K3}!8 -.пNd , + ɘK(]A5^)XA%])RJ;@ LDӚU ICS?J0x,tT( \/ixÌ~o2 !./-x33_@ơ2 AJqM+;?o>ƍVFcNR`EI ӃD,,6LRЛ"t:Z{p~8Jש.X^H3-0UA NIvaĺf"uQ`鄡Nϟ'穻lQDi;)/u_Ke2saK8hN(P#(nQW cuH1j_ b@qB>iX 'WS nit wp9~m'OY hOSŚXd vp` 1{wL $k·PާhZD8T$S W6 UPTy"(4Riu3ص+\. rX*}z6 (?Pc߃HuEXD>cKt@kFaPerb ^ڢr!r׿'R7ޯR?ݜOUcBeX/e\$[nZ┷}4 }Ҽ܅uL<sJM {jE{b+FC ݆>ﴶ#o,B*ֲ "5 @C"ESY:ŭ HfV 1 %l ũd3-^  ^ΙT^vX1`/EDUϤ6 %9Q:uv`F? NK2aD\d`QRYMiO[/UH?z`55S,gK@<lb߿'@ŒjúН+nz/l N>;@+1~ s)i9rlUޗrr^.~}*vk@k7^x+Z?yuR:Gm 6xZ]܌u?/AU5b5^ҰL(7G6TB%n 4/Y<@ 4Q⶯xDBOtҵFa`t.z|D_ BMez{QL%EKh?^\ځ?OP )BQ0>!8 [41Z&[^ї &$ҫ1eq %v J3נ?4ylt j7MҊnN3 ~dwUDMsdL>&9wPnʄDkXx:{+2lH=!>8ć!I@!aPdERC|1ι P(!(=s 60*qQ?d&=Y|F0}-69j hoC}fdUeV|,K?\7}n'*{ey/gNO6^.7 m' . (o-}eDDAsT=ȅ+򂍕]} 7J,x;|H+_iKyN3W(3ҁvJ}GlfVC`]a?GG N@L,2/`S,/hBZO9ēa !Z1vf€NCxpq_K;$%z-U}ÆLc p4XDZ怬'&1 1\;< tTWOJ'A)HD~C` | U$%J BA -=xF! YZvֳ-_\N&zA,+}2LJ8د>`m` z1$y '({x.Qp Sd՗ʲI|#ㅜ eyxt|? |Whsxo>y}+?,ۻ'UOЬ4zeYGm F2ʹ+X$2=3,"a L^.4M n_: Ӯ5΀:*5i:D%?Տ9+܃y@Ch$ 2OŔ<_IaeFNOp: @z5N<"(}ۿJ%wg?VO&IxVWs:u.Lqj529@~GuHapJKq><9}x9|8 # @2 ?PcbQ<_" 9ᷯqȰ z1c*:Br lVk O,=gt영ieZ:G3R5.ȔLpzYY2ļ3 4JA9f4X>Dt!P۾ʓők{7X;6&ɽ[٬F 7>/׏ƛ_;xO=]}Un[p)˷Ϸo_|<^nOF@ۋj~neP#%L܌-}ǻ #: ڔ>QL e>W$<2C'Pƚ7KHZޤȿw$6銄>;И1aF (r1cslpm*=HCgH×I񼚅y&kTK9%F톬cpg*YCPN,ab}<^D:  lܨr;84֠ĜOׄj6NE(He1*~(dߟ“|M]ūlxr;C'w%HHhx쇹T$G! I-=i@S~JSˎI}޺Ƿ Ќ>\KpsqppyzoNyPd,0x>Ggww՟une{9:|PK?׾_~W[ڭlח;o.~2߹l/2$vcxآh a}u oOd3  KaGcG CT+)vФ&LC*KWL^DŤ)¤`t?cWjN›2ha( Z@B+aBY"3(f]ĭitE|)x:JA,m VӂzsG/o/%,հDS< MJ{{hSgN]mƒM/d9Lդ俾u˜M!R|׻/#]F cԾ\ Z8sD2$nk?@Y,>ތ1]h1 xjB1,['|?Sւ$GN/Xp1o ?I96h OX3M}7D +I =tBLYg6O>.AªjLpB{vF"]-R}ݯ_y;? v Ю՛O׫]\?fTuWLX&ݬr8"pH>hL-HƁi pB\S(Wf%F 7H(>~k`pO[f$MŞ,`4  >p=ýrU-ҫ -F(ޣL|iC4^h>{SaA --JXO%{+9k`o 4ZbdM &F+?Z8]2A?R`ژDzesrDK(4Jr; FO k OƜ &@`9'^za]LYvGs S,{K ܈o1,{J 'EJOpu<`ᣖv(=(\ZC `>":2-`YQ>*-ԡWL{AP]XJo~=)CJ Pz!6,Yw"(Г~ tTX:O+ pX &ܱB3ks!S KAa l_'BP|@uĀBLhQ(%8*|m 5 ѫҶvaGXh+u=%kf2%b0xzq`䊥S njPeн$AxW(k9q%P9dhD|dBVpaAPfgkܤIm̋ELGe͒Q[0 Rױ۶ߪ`wlerxSuTޗEo^>wFJBN_^ُ]<{~^u >rҾjF"]6im !*tMEe~tեO,B7:Shy#59zj~P $(}Gs&f/hNjwx~3>R}wN%"/YB/q<¢zO4r3I7""H4r#VzW"%PݾmƑ[Rw57[gSY{q/XrXfܾVb„4oil8M_;ڏ)я+wsVO$4hA!$u&К4Pdk Y1|1!qr27nj~k+A{Err%fQ Thb e}P>xa" h#Kk0XxB6=C;=ʲ0S'YۏwsE{ jw%b.1x2 . 먑6y;r{p `0\dΧ2w@ORPk@ܞM795Q6VH+O~_~ۗm3M;|;O\~Gw1QjVDhUJj ?Ʌ $js >ân(iXi%v/lk)B7V~e)YWMhP?]\GO)t5PT}h մ'@ajWp"SnXY&3жgV_/KNPX/ Tnޯ5ɮ9F kWD3=J,8T@Ig{AP0\] `mM'@u%kצ0e5&yo C b XFa!kPCPzHsK}\CMi[d)ŤPX+VP,&HT,k}8(`jxG8 29^.؎@?uupA2Cx+߻UjGeՃl˥Ǐգ_|ݧ}Z?v揾Ts~MKW_~U9yʿ݋;߱Ne7'v)\mJ6YpCTnhNRM]A]2 -86EB4ͽ}&XQ}-VA&rBpM cdꊋ 64IA^ 7> D `-}3s5BYc83hx ?]G'  wD_ӄqK_F`k,7H'<,e+7[;^gneE,md|, A)t"T3R5ngNli>TA`QUD1✽abajȘu%02XnG"K|g(OpФ4ά#F-wt(uqF Bߟ=Xl/Q{l @:2|ذ ( }_K 82QZz+pFb@װf9q+7hpEX4QN rXrѣeJՓo>|Kx?5,~(l ~;>ؾu_msmd}-˲ u* 6W*3PXF8W6+9pPWSy-pM1 )#xÚNKgśBd>^JXA㎓`eB5pM\,eP/ijW һeG[Yӗ//|sE/fw޻xmui.K[VZT%zrQnX(Pe+?z;q=`ܭ_0aт0 L&{P)cA֎P rtņ\"2tcϓ2F*@'dʋ|@_s`3"W 1Je F2']!V]/E.2g)+"V.)rJr Rɸtf>E(Wml^R $ N̍4. %Sz\c  1V nd= ?1rp@}/x%+ƪ: peA-Qi:tk01rg4o~/3[?&\ QW06r51.Vf<Զ@iM@`X۟GYֿוXT=ϟ7r94K̊_w i=y޺Z1|y?wWoɷ2j,@XWdc X~Vqp<͚l"i9(^Ox ͍:nzlHw{`2O` i*PMO3AMqDxf,2KX%fɉj2愬QG)<#kN@+ib j(b^&DsP6>2U QS `:0UϾ?bUCp W΂0'a1!nf'L߀/3-xoFj6y%B#IP5'JPnພ 4k8J V!稬' >5(RT@t׉Hk88sFSj8 @+ pҋ" sl[g_dEπ#  ?Ӥ` ͯn|a(d\g傽`ŇX3OgYY.i4jۈV^sQRkE̩'2^;[?u8>k=\Ϳ8+ߦ6 m}c9,}r[ٷ~zBZd_@cTWu-ؔ׀8948cx,n!/"k黀 )E" X# (\H0ʯtNL:\0q6VsE"["/OC!0.d)qU␡ x34&u4EPLYR MWg@o>OZLY.c->#@z'nqtJ e+|/  5]վߞ~$ӡXGXRWWֺRp.;佯>}Oߜ?KBϞ 5+?c-,{1layW'}ɽO[^W:5WA㸬 u :\"+zkՈ<~2X =-~i4k>s K(Ȣi݁Ƞ]XP)I1ʠ+RAWN,QF  h{)1QrքMtfZ)o_D-VZe#,VaO0hϚp tZqpgtEWtJgӱqfT-Pp5 WX$4A+$~p=$X?%KkTBIa!#c=w 2(ԯ]{H\ R&`Gys;f𻥽^;G՝?ھݓ4lݺy7z] XmX12LlF7 "j98rX*^կnna]P+2ǙQ\|W56ˁTL P~J۟!CN g)T0 [(bwLU;,UVxY]cOP>I= Wul.,U#.Y1Vt-,# sGIv#1 :CF}Z31 T)+k@$e8T'$kWSxc;HkϢ^:\1WQ׼ (u+Yy ) -%oEY38NRTGJkg}6cSRVpɰܜԝLYUTtQAEFfv( US-P@^gʛ*9*+ n@82݅C¾?H7"Oib"=gs#'ȒYtJ | !ĮttBQ0vٟBD'?x75tY,hRލkO`T܁.l 8j ׅÊԆnSȆimA{?aƱٓ~$&s #+’VcB: P0 tf$XPV7ֿD*[Hx:L(Cw?^ŕ2::% g{LN[V[#f$> p-)CY%)1U[C^0=g[7 J2-,P-?Qw/N˒7p}.7Ogc\=ꢌ$eNoY.i3=|uu9_Keʱ *` ?4}J%lI*XV`hNV/\&Ь¦VzÏr+]'چEEW]?SJ7v hP _ntTABhE+ 8ҳ(F[)^}}v;Hmh9"*{σ Dn1" 1R(>|;m,/R3ڇjC%fq`}-HmB#' ,r}θFm>-tکlu.Y숩(B<-qD?bI~YX;(jwgwP/Q '@"~WX(HuS: aG')2qM9u C [G=p%cmV[q{3J߯ZRhji- 3IUbZE|P{ꕪP;@͚L/hXZ.؃:%*7=OS nTpBY{X6٬aވpoP,o h{# ~%0^O g'1)M栓zHV?ʸ &䇎" jN%v*旭Z 㱐!嬇[O|TXgJ)υ0THgs=Bf6S:yRO`Դw kFߏY_f n=ud% tϊzGHC b.FQ7Pl8 impx`q]*,*a\w >m mls]Ug\j=o=8x_o_կeX;tСԷwy4jjf]~ Too$L062,-ފ_;Y&%иl4λ)~ Y/Yy*iq~ɒ9:F4CU']pSr%q7r-Uݣ|(smsͷPs)Qآ65~jALU+YUEяmVi3W͢;VOa>Q_TQuM (K\nd}g%yxZikJx,V\&uee\r[}MS/rsz][Uvύ+>SB?]ʳe]'Wu܈\E~ym7\7Vh=Yz7ucMtyg#zKrUwXؘ4U_9@ʦ~fQ•"u[5aARmVX +7-:N.6Ǻ=)W?k?UW8Ssgmnsv [mnsv 6mnsms66mnsna6mn`ns6[f0mns-l36mnsv 6mnsms66mnsna6mn`ns6[f0mns-l36mnsv 6mnsms66mnsna6mn`ns6[f0mns-l36 {@_IENDB`meshy/data/icons/hicolor/64x64/000077500000000000000000000000001521052255700164655ustar00rootroot00000000000000meshy/data/icons/hicolor/64x64/apps/000077500000000000000000000000001521052255700174305ustar00rootroot00000000000000meshy/data/icons/hicolor/64x64/apps/page.codeberg.sesivany.Meshy.png000066400000000000000000000144731521052255700255600ustar00rootroot00000000000000PNG  IHDR@@iqIDATx[yUŕ޷7 (,nD!AY/hq$8Ɍ:lAbc$?wb@\  44 4u{zk}̟RUsTw; <K9<sLcyz~{OOsjNsr<K y"iOt]_ߗW޻殷%{;ͻr圜t}ߐ/h`R #B˨/4x '`< yw55ɹu?dGMɑkD_綇z}NPC,)gM Tp./tsr<͛'ABxa4yfD7}!j"-m{pLbr>s73ο5ͪNʏCB l ߔ;#Z]2WטAILC;, gat,Z#,7 'GB(jS?|Kl9rםΞz^*G X  aY&% uO̾gCyc~Sט yRWmيݩ??4m*gaȴzyY ׃.aPX u9b+y&l}(bܝ ޶釫y$?O7/Q6nmiw "[8Y!I V}k=8@׍70X!.y*3g==OMUκ8YptT=3[ Bs&rs`Lʡ6aD Y u{}5}H+培OyؗKvv-̪(shLH q٭:;]r80L}; <˵HgcC.g4L7&1V՝e:d)߮O bgڳbg̾}DXSyۻy L+8i_ 'W)哞hz_QJ"2}r`;Z!3VEDe֒P4>A9iEu>ȦǛ/Yaj0Wo63!C듦%ZðxK@\K0܁0Z"Ym&rjUBdCk|2zqNmovϞt $횉MlELʓyz|p^-VmȫXX >qN*'\)F}dG s[U 7<quTK<|}*i"݄XFpct/mXP]Qwcuk0Uua5>> 7p"0Ht!D]pܤrbr1mr7u*<ߜDwD=V^iE.4.c(5V nͽ=},i7W 0ٛƽw㪳j(k*MUsD  S : C\"7Q,d"Dps'Qjx@`J@eY)S:3u~͒U ދ~VX-Xx&Ӓ fK֒&}q糝8gvv"'$" 6`/MMMnù7@>TCW> 9=!>Ýaj<~M-ӣt!qcfg}fܥ0`ųAc#.vkRPJs? mRa$@i*įԂKn~{dD-V<҉OވP &T6J4u Ͼc7_AKM[+ycDlUIL[ƣ,9 / +H("{ 뺰 \]Q-ش+O̵zO0gЀOܸ2DAJt.Uծ5sRSbvդ{*qMm٭ɓB"-7=;6#W øPNPhJf%; sUȱT4/ftr0t8kًXlO Ҹ'$PvM&ϛTzŮ2a68_m9G\d03ZIfÜv{QDdRvv@>szUs`ˀU@K̚x|;JOFe[cwel̨iL_%^]~&8Zm-ʱ7?D,QwR8v.ywZfHם8vD”L$D4q<2N˜_]cNGNG[G;%p7㕭pX5M^$QEù4RwM&فD*KΨCxv3R_|`oj\ 3TAr: Mć(2;ѳ!N˂"=sumd3S;[)qԖ7ˈ*3$ N[_6}?e*>)[ *\`/v{˯sDDӉ$r9z)I 7LYwl\,G.Dâ#a a^nQS)--H1U7w`LTp&aSR=W4]0d$\@-p:~)~m%&>)Kk&c2avSmQI!Ȑ<=UkIܹ 勧77 !X |UJ+2zgU< n.>vQ0Z7wk͊IBg<2Ob>.]cV~x&WPbd Eȓ )Ql}SqAtTECͿD0DՊ[X=kvT@Ȯ ٬]v$j5}g~F)#zqm^Ƚj"\CX νnVD ]i|\Xݙ{U@XQAifsBgp5*INT`B-,9q w2uV9*Rؓ7eƕ!,IL oz)F8\Oς.Aq)0 Zc&h9Af.p%Zf? c۲0,o+U T03˂uEzzR+*t6] c`>VWPj` CLn_& .kT$0wELDeb4;uuQ';d]hŒͦ]mվ7ڈZ{ D qbj|%L-rU {{@_~Q߅bgkN}a*@f #OÔxcG$'\bڢ%xQ 0i֓~?Q mb^D+HOiQ 1D\<]o?nݾ(ZD;MD3Ìn0KjEV]숙"?ȗŢk+*\Q%ݺ|eho)0N5S); RjdZP)H9鿲D\bO0/kDGT7}pOyĀH Fv뻶ܔ?].?w<O2RGIENDB`meshy/data/icons/scalable/000077500000000000000000000000001521052255700160015ustar00rootroot00000000000000meshy/data/icons/scalable/actions/000077500000000000000000000000001521052255700174415ustar00rootroot00000000000000meshy/data/icons/scalable/actions/avatar-default-symbolic.svg000066400000000000000000000006501521052255700247020ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-100-symbolic.svg000066400000000000000000000014561521052255700250640ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-20-symbolic.svg000066400000000000000000000014561521052255700250050ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-30-symbolic.svg000066400000000000000000000015401521052255700250000ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-50-symbolic.svg000066400000000000000000000014561521052255700250100ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-60-symbolic.svg000066400000000000000000000014621521052255700250060ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-70-symbolic.svg000066400000000000000000000014621521052255700250070ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-80-symbolic.svg000066400000000000000000000014621521052255700250100ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-level-low-symbolic.svg000066400000000000000000000020011521052255700253500ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/battery-symbolic.svg000066400000000000000000000015341521052255700234560ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/bluetooth-active-symbolic.svg000066400000000000000000000020721521052255700252600ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/bluetooth-disabled-symbolic.svg000066400000000000000000000021101521052255700255450ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/bluetooth-symbolic.svg000066400000000000000000000020721521052255700240070ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/broadcast-flood-symbolic.svg000066400000000000000000000026311521052255700250460ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/broadcast-symbolic.svg000066400000000000000000000027121521052255700237450ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/charge-symbolic.svg000066400000000000000000000013351521052255700232340ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/check-plain-symbolic.svg000066400000000000000000000013361521052255700241620ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/circle-crossed-symbolic.svg000066400000000000000000000011401521052255700246760ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/computer-symbolic.svg000066400000000000000000000010371521052255700236400ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/document-open-symbolic.svg000066400000000000000000000031221521052255700245540ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/document-save-symbolic.svg000066400000000000000000000011661521052255700245570ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/drive-harddisk-symbolic.svg000066400000000000000000000012141521052255700246770ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/drive-removable-media-symbolic.svg000066400000000000000000000011251521052255700261400ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/edit-clear-all-symbolic.svg000066400000000000000000000023131521052255700245570ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/edit-clear-symbolic.svg000066400000000000000000000025701521052255700240160ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/edit-copy-symbolic.svg000066400000000000000000000013751521052255700237040ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/edit-delete-symbolic.svg000066400000000000000000000020021521052255700241600ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/edit-find-symbolic.svg000066400000000000000000000011161521052255700236430ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/emblem-system-symbolic.svg000066400000000000000000000112001521052255700245560ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/face-smile-symbolic.svg000066400000000000000000000006531521052255700240120ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/filter-symbolic.svg000066400000000000000000000011731521052255700232700ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/find-location-symbolic.svg000066400000000000000000000024501521052255700245300ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/go-down-symbolic.svg000066400000000000000000000011171521052255700233530ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/go-next-symbolic.svg000066400000000000000000000011761521052255700233670ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/hashtag-symbolic.svg000066400000000000000000000005331521052255700234210ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/list-add-symbolic.svg000066400000000000000000000003441521052255700235030ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/logs-symbolic.svg000066400000000000000000000024511521052255700227470ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/mail-send-symbolic.svg000066400000000000000000000017401521052255700236540ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/map-symbolic.svg000066400000000000000000000003651521052255700225620ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/network-offline-symbolic.svg000066400000000000000000000033561521052255700251210ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/network-server-symbolic.svg000066400000000000000000000030701521052255700247760ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/network-transmit-receive-symbolic.svg000066400000000000000000000021201521052255700267440ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/network-wireless-symbolic.svg000066400000000000000000000031211521052255700253220ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/network-workgroup-symbolic.svg000066400000000000000000000046351521052255700255370ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/object-select-symbolic.svg000066400000000000000000000013471521052255700245310ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/open-menu-symbolic.svg000066400000000000000000000004721521052255700237070ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/padlock2-symbolic.svg000066400000000000000000000007031521052255700235000ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/phone-old-symbolic.svg000066400000000000000000000011651521052255700236710ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/podcast-symbolic.svg000066400000000000000000000037761521052255700234530ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/preferences-other-symbolic.svg000066400000000000000000000057631521052255700254340ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/preferences-system-symbolic.svg000066400000000000000000000037451521052255700256350ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/qr-code-scanner-symbolic.svg000066400000000000000000000017441521052255700247700ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/region-symbolic.svg000066400000000000000000000015301521052255700232630ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/share-alt-symbolic.svg000066400000000000000000000020101521052255700236520ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/sidebar-show-right-symbolic.svg000066400000000000000000000012001521052255700254740ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/sidebar-show-symbolic.svg000066400000000000000000000011761521052255700243750ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/starred-symbolic.svg000066400000000000000000000013171521052255700234470ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-1-symbolic.svg000066400000000000000000000013471521052255700247270ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-2-symbolic.svg000066400000000000000000000014251521052255700247250ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-3-symbolic.svg000066400000000000000000000014251521052255700247260ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-4-symbolic.svg000066400000000000000000000014261521052255700247300ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-5-symbolic.svg000066400000000000000000000013761521052255700247350ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/strength-bars-none-symbolic.svg000066400000000000000000000033141521052255700255220ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/system-lock-screen-symbolic.svg000066400000000000000000000007111521052255700255270ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/system-reboot-symbolic.svg000066400000000000000000000016621521052255700246220ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/system-run-symbolic.svg000066400000000000000000000126421521052255700241340ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/system-search-symbolic.svg000066400000000000000000000011411521052255700245650ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/system-users-symbolic.svg000066400000000000000000000020571521052255700244700ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/user-available-symbolic.svg000066400000000000000000000005111521052255700246720ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/user-home-symbolic.svg000066400000000000000000000016421521052255700237100ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/user-trash-symbolic.svg000066400000000000000000000017611521052255700241030ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/utilities-terminal-symbolic.svg000066400000000000000000000027541521052255700256350ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/view-more-symbolic.svg000066400000000000000000000007631521052255700237210ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/view-refresh-symbolic.svg000066400000000000000000000021361521052255700244110ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/view-sort-descending-symbolic.svg000066400000000000000000000020051521052255700260360ustar00rootroot00000000000000 meshy/data/icons/scalable/actions/weather-clear-symbolic.svg000066400000000000000000000045201521052255700245250ustar00rootroot00000000000000 meshy/data/meson.build000066400000000000000000000043361521052255700152700ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later desktop_file = i18n.merge_file( input: 'page.codeberg.sesivany.Meshy.desktop.in', output: 'page.codeberg.sesivany.Meshy.desktop', type: 'desktop', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'applications', ) desktop_utils = find_program('desktop-file-validate', required: false) if desktop_utils.found() test('Validate desktop file', desktop_utils, args: [desktop_file]) endif appstream_file = i18n.merge_file( input: 'page.codeberg.sesivany.Meshy.metainfo.xml.in', output: 'page.codeberg.sesivany.Meshy.metainfo.xml', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'metainfo', ) appstreamcli = find_program('appstreamcli', required: false) if appstreamcli.found() test('Validate appstream file', appstreamcli, args: ['validate', '--no-net', '--explain', appstream_file]) endif install_data( 'page.codeberg.sesivany.Meshy.gschema.xml', install_dir: get_option('datadir') / 'glib-2.0' / 'schemas', ) compile_schemas = find_program('glib-compile-schemas', required: false) if compile_schemas.found() test('Validate schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()]) endif gresource_conf = configuration_data() if get_option('shortcuts_dialog') gresource_conf.set('HELP_OVERLAY_ENTRY', 'gtk/help-overlay.ui') else gresource_conf.set('HELP_OVERLAY_ENTRY', '') endif gresource_xml = configure_file( input: 'page.codeberg.sesivany.Meshy.gresource.xml.in', output: 'page.codeberg.sesivany.Meshy.gresource.xml', configuration: gresource_conf, ) gnome.compile_resources('meshy', gresource_xml, source_dir: meson.current_source_dir(), gresource_bundle: true, install: true, install_dir: pkgdatadir, ) install_data( '60-meshy-serial.rules', install_dir: get_option('datadir') / 'meshy', ) foreach size : ['512x512', '256x256', '128x128', '64x64', '48x48'] install_data( 'icons' / 'hicolor' / size / 'apps' / 'page.codeberg.sesivany.Meshy.png', install_dir: get_option('datadir') / 'icons' / 'hicolor' / size / 'apps', ) endforeach meshy/data/page.codeberg.sesivany.Meshy.desktop.in000066400000000000000000000005501521052255700225310ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later [Desktop Entry] Name=Meshy Comment=MeshCore mesh network client Exec=meshy %u Icon=page.codeberg.sesivany.Meshy Terminal=false Type=Application Categories=Network;Chat; Keywords=mesh;lora;radio;chat; MimeType=x-scheme-handler/meshcore; meshy/data/page.codeberg.sesivany.Meshy.gresource000066400000000000000000003667551521052255700224770ustar00rootroot00000000000000GVariant(M  !##$$$%%&(,-//111334446777889:;=??@@AADEEFHIKKK)_v y v Nq ?NLX<9H<vX@r@LA$v\zvL!."L!vh!F/F/v`/11 "1v2[9"[9vp9 ?Ԯ% ?v(?@L@vADaDvEW,H"WvWdYddvdHgS?HgvhgInF<Inv`nxr1xrvr u, uv u x xv(x|)|v|óvbv c4ucvηv>Tvmr^"mvs sv\5[v(-+-L488vXITIvhe"e vpM*LL[vRvR"Rvh> 5q>v>0A$0AL4A8AN8AvXAG#G%v(GK_2"K vKsY2sYvYa\fa\vx\$rf{$rv@rt1tvtv&}9vLvvһvvvK}#IK}vh}~VC~v~̂-jP̂!vY9ovY&7Yvp^O(/^v2N;2vPr]&rvԖԵԖLؖܖ60ܖvvКAJhAv`3,3vPWDTWvp|i4"| Lt?C LoS"v8Wgv8"\ /" L,02Au0vHCv#´)#v@PfPvh v8{@}v^%vqhߞ"v'vcomputer-symbolic.svg (uuay)battery-symbolic.svgV (uuay)actions/68$(IA=>D 4&E5B7)0HJ# 2;+<:,-F1G .3find-location-symbolic.svg" (uuay)Meshy/'"utilities-terminal-symbolic.svg (uuay)document-open-symbolic.svgL (uuay)channel-chat-widget.ui (uuay)filter-symbolic.svg{ (uuay)connection-view.uiK (uuay)channels-view.ui Create a Private Channelchannel.create-privateJoin a Private Channelchannel.join-privateJoin the Public Channelchannel.join-publicJoin a Hashtag Channelchannel.join-hashtag (uuay)face-smile-symbolic.svg (uuay)user-trash-symbolic.svg (uuay)emblem-system-symbolic.svgz (uuay)contacts-view.ui Allcontacts.filter-allFavouritescontacts.filter-favoritesUserscontacts.filter-usersRepeaterscontacts.filter-repeatersRoom Serverscontacts.filter-roomsSensorscontacts.filter-sensors
A–Zcontacts.sort-azHeard Recentlycontacts.sort-heardLatest Messagescontacts.sort-messages
Favorites Firstcontacts.favorites-first
(uuay)document-save-symbolic.svgp (uuay)network-offline-symbolic.svg (uuay)share-alt-symbolic.svg (uuay)sidebar-show-right-symbolic.svgc (uuay)charge-symbolic.svg (uuay)view-refresh-symbolic.svgX (uuay)bluetooth-symbolic.svg4 (uuay)preferences-system-symbolic.svg (uuay)weather-clear-symbolic.svg3 (uuay)open-menu-symbolic.svg (uuay)broadcast-symbolic.svg (uuay)edit-clear-all-symbolic.svg (uuay)settings-view.ui (uuay)edit-clear-symbolic.svgr (uuay)map-symbolic.svg (uuay)icons/?object-select-symbolic.svg (uuay)preferences-other-symbolic.svg (uuay)window.ui
Disconnectwin.disconnectKeyboard Shortcutswin.show-help-overlayAbout Meshyapp.aboutQuitapp.quit
(uuay)ui/ * K%!podcast-symbolic.svg (uuay)bluetooth-disabled-symbolic.svgB (uuay)repeater-view.uiI (uuay)go-next-symbolic.svgx (uuay)gtk/Lbroadcast-flood-symbolic.svg (uuay)network-transmit-receive-symbolic.svgJ (uuay)chat-view.ui (uuay)starred-symbolic.svg (uuay)system-run-symbolic.svg (uuay)system-search-symbolic.svg[ (uuay)view-more-symbolic.svg (uuay)page/Cnetwork-wireless-symbolic.svgK (uuay)user-available-symbolic.svgC (uuay)qr-code-scanner-symbolic.svg (uuay)view-sort-descending-symbolic.svg (uuay)go-down-symbolic.svgI (uuay)list-add-symbolic.svg (uuay)avatar-default-symbolic.svg (uuay)network-server-symbolic.svg (uuay)bluetooth-active-symbolic.svg4 (uuay)//system-reboot-symbolic.svg (uuay)sidebar-show-symbolic.svga (uuay)system-lock-screen-symbolic.svg (uuay)edit-copy-symbolic.svg (uuay)edit-delete-symbolic.svg (uuay)scalable/sesivany/drive-removable-media-symbolic.svgO (uuay)mail-send-symbolic.svg (uuay)codeberg/@edit-find-symbolic.svgH (uuay)hashtag-symbolic.svg[ (uuay)system-users-symbolic.svg (uuay)user-home-symbolic.svg (uuay)network-workgroup-symbolic.svg (uuay)drive-harddisk-symbolic.svg (uuay)padlock2-symbolic.svg (uuay)device-view.ui (uuay)help-overlay.ui- GeneralQuit<Primary>qKeyboard Shortcuts<Primary>questionNavigationDevice<Primary>1Contacts<Primary>2Channels<Primary>3Map<Primary>4Settings<Primary>5MessagingSearch Contacts<Primary>fFocus Message Input<Primary>nPrevious Contact/Channel<Alt>UpNext Contact/Channel<Alt>DownPrevious Unread<Alt><Shift>UpNext Unread<Alt><Shift>Down (uuay)meshy/data/page.codeberg.sesivany.Meshy.gresource.xml.in000066400000000000000000000304241521052255700236600ustar00rootroot00000000000000 icons/scalable/actions/avatar-default-symbolic.svg icons/scalable/actions/battery-level-100-symbolic.svg icons/scalable/actions/battery-level-80-symbolic.svg icons/scalable/actions/battery-level-70-symbolic.svg icons/scalable/actions/battery-level-60-symbolic.svg icons/scalable/actions/battery-level-50-symbolic.svg icons/scalable/actions/battery-level-30-symbolic.svg icons/scalable/actions/battery-level-20-symbolic.svg icons/scalable/actions/battery-level-low-symbolic.svg icons/scalable/actions/battery-symbolic.svg icons/scalable/actions/bluetooth-active-symbolic.svg icons/scalable/actions/bluetooth-disabled-symbolic.svg icons/scalable/actions/bluetooth-symbolic.svg icons/scalable/actions/broadcast-flood-symbolic.svg icons/scalable/actions/broadcast-symbolic.svg icons/scalable/actions/charge-symbolic.svg icons/scalable/actions/check-plain-symbolic.svg icons/scalable/actions/circle-crossed-symbolic.svg icons/scalable/actions/computer-symbolic.svg icons/scalable/actions/document-open-symbolic.svg icons/scalable/actions/document-save-symbolic.svg icons/scalable/actions/drive-harddisk-symbolic.svg icons/scalable/actions/drive-removable-media-symbolic.svg icons/scalable/actions/edit-clear-all-symbolic.svg icons/scalable/actions/edit-clear-symbolic.svg icons/scalable/actions/edit-copy-symbolic.svg icons/scalable/actions/edit-delete-symbolic.svg icons/scalable/actions/edit-find-symbolic.svg icons/scalable/actions/emblem-system-symbolic.svg icons/scalable/actions/face-smile-symbolic.svg icons/scalable/actions/filter-symbolic.svg icons/scalable/actions/find-location-symbolic.svg icons/scalable/actions/go-down-symbolic.svg icons/scalable/actions/hashtag-symbolic.svg icons/scalable/actions/go-next-symbolic.svg icons/scalable/actions/list-add-symbolic.svg icons/scalable/actions/logs-symbolic.svg icons/scalable/actions/mail-send-symbolic.svg icons/scalable/actions/map-symbolic.svg icons/scalable/actions/network-offline-symbolic.svg icons/scalable/actions/network-server-symbolic.svg icons/scalable/actions/network-transmit-receive-symbolic.svg icons/scalable/actions/network-wireless-symbolic.svg icons/scalable/actions/network-workgroup-symbolic.svg icons/scalable/actions/object-select-symbolic.svg icons/scalable/actions/open-menu-symbolic.svg icons/scalable/actions/padlock2-symbolic.svg icons/scalable/actions/phone-old-symbolic.svg icons/scalable/actions/podcast-symbolic.svg icons/scalable/actions/preferences-other-symbolic.svg icons/scalable/actions/preferences-system-symbolic.svg icons/scalable/actions/qr-code-scanner-symbolic.svg icons/scalable/actions/region-symbolic.svg icons/scalable/actions/share-alt-symbolic.svg icons/scalable/actions/sidebar-show-right-symbolic.svg icons/scalable/actions/sidebar-show-symbolic.svg icons/scalable/actions/starred-symbolic.svg icons/scalable/actions/strength-bars-1-symbolic.svg icons/scalable/actions/strength-bars-2-symbolic.svg icons/scalable/actions/strength-bars-3-symbolic.svg icons/scalable/actions/strength-bars-4-symbolic.svg icons/scalable/actions/strength-bars-5-symbolic.svg icons/scalable/actions/strength-bars-none-symbolic.svg icons/scalable/actions/system-lock-screen-symbolic.svg icons/scalable/actions/system-reboot-symbolic.svg icons/scalable/actions/system-run-symbolic.svg icons/scalable/actions/system-search-symbolic.svg icons/scalable/actions/system-users-symbolic.svg icons/scalable/actions/user-available-symbolic.svg icons/scalable/actions/user-home-symbolic.svg icons/scalable/actions/user-trash-symbolic.svg icons/scalable/actions/utilities-terminal-symbolic.svg icons/scalable/actions/view-more-symbolic.svg icons/scalable/actions/view-refresh-symbolic.svg icons/scalable/actions/view-sort-descending-symbolic.svg icons/scalable/actions/weather-clear-symbolic.svg @HELP_OVERLAY_ENTRY@ ui/device-view.ui ui/connection-view.ui ui/settings-view.ui ui/repeater-view.ui ui/window.ui ui/contacts-view.ui ui/chat-view.ui ui/channel-chat-widget.ui ui/channels-view.ui ui/device-actions.ui ui/discover-dialog.ui ui/share-contact-dialog.ui ui/rx-log-dialog.ui ui/settings-sidebar.ui ui/theme-chooser-dialog.ui meshy/data/page.codeberg.sesivany.Meshy.gschema.xml000066400000000000000000000045321521052255700226650ustar00rootroot00000000000000 800 Window width 600 Window height false Window maximized '' Last connected device address '' Last connected device name 'ble' Last transport type (ble, usb or tcp) [] Saved TCP/WiFi companions (name|host:port) 'system' Color scheme preference Options: system, light, dark, custom 'meshcore' Custom theme name 0 Default channel notification level 0=All, 1=Mentions, 2=None true Run in background Keep running after closing the window to receive messages true Autostart Automatically start when you log in 'messages' Channel list sort order Options: messages, az meshy/data/page.codeberg.sesivany.Meshy.metainfo.xml.in000066400000000000000000000055061521052255700234670ustar00rootroot00000000000000 page.codeberg.sesivany.Meshy CC0-1.0 GPL-3.0-or-later Meshy MeshCore mesh network client

Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your companion device over Bluetooth to send and receive encrypted messages, manage contacts, configure channels, and monitor your mesh network.

Features:

  • Bluetooth LE and USB serial connectivity
  • End-to-end encrypted messaging
  • Public, private, and hashtag channels
  • Contact management with location tracking
  • Interactive map view with route tracing
  • Device and repeater management
  • QR code scanning for quick contact exchange
Jiri Eischmann page.codeberg.sesivany.Meshy.desktop https://codeberg.org/sesivany/meshy https://codeberg.org/sesivany/meshy/issues https://codeberg.org/sesivany/meshy https://meshy-app.org/screenshot1.png Channel chats https://meshy-app.org/screenshot2.png Repeater management https://meshy-app.org/screenshot3-dark.png Settings - Dark Style https://meshy-app.org/screenshot4.png Map view showing contact locations intense intense 360 pointing keyboard touch

First stable release of Meshy.

#f6f5f4 #77767b
meshy/data/ui/000077500000000000000000000000001521052255700135355ustar00rootroot00000000000000meshy/data/ui/channel-chat-widget.ui000066400000000000000000000150621521052255700177060ustar00rootroot00000000000000 meshy/data/ui/channels-view.ui000066400000000000000000000053651521052255700166500ustar00rootroot00000000000000 Create a Private Channel channel.create-private Join a Private Channel channel.join-private Join the Public Channel channel.join-public Join a Hashtag Channel channel.join-hashtag meshy/data/ui/chat-view.ui000066400000000000000000000150671521052255700157740ustar00rootroot00000000000000 meshy/data/ui/connection-view.ui000066400000000000000000000070241521052255700172060ustar00rootroot00000000000000 meshy/data/ui/contacts-view.ui000066400000000000000000000040221521052255700166600ustar00rootroot00000000000000 meshy/data/ui/device-actions.ui000066400000000000000000000127501521052255700167760ustar00rootroot00000000000000 Trace Path Trace a route and show signal quality per hop trace_btn share-alt-symbolic center Discover Nearby Nodes Scan the network for nearby nodes discover_btn edit-find-symbolic center Rx Log View received radio packets rx_log_btn logs-symbolic center Reboot Device reboot_btn system-reboot-symbolic center meshy/data/ui/device-view.ui000066400000000000000000000345051521052255700163120ustar00rootroot00000000000000 meshy/data/ui/discover-dialog.ui000066400000000000000000000067501521052255700171570ustar00rootroot00000000000000 Discover Contacts 420 520 system-search-symbolic Search filter-symbolic Filter by type view-sort-descending-symbolic Sort by vertical Search... 8 8 6 false true true No contacts 4 4 meshy/data/ui/repeater-view.ui000066400000000000000000001264401521052255700166620ustar00rootroot00000000000000 meshy/data/ui/rx-log-dialog.ui000066400000000000000000000042171521052255700165450ustar00rootroot00000000000000 Rx Log 480 560 edit-clear-all-symbolic Clear log true vertical 8 8 8 8 none logs-symbolic No Packets Received radio packets will appear here meshy/data/ui/settings-sidebar.ui000066400000000000000000000052731521052255700173520ustar00rootroot00000000000000 12 12 8 Backup Save configuration and messages to a file backup_btn document-save-symbolic center Restore Restore from a backup file restore_btn document-open-symbolic center true 12 12 12 Factory Reset Erase all data on the companion factory_reset_btn edit-clear-all-symbolic center meshy/data/ui/settings-view.ui000066400000000000000000000541311521052255700167100ustar00rootroot00000000000000 meshy/data/ui/share-contact-dialog.ui000066400000000000000000000050711521052255700200670ustar00rootroot00000000000000 Share Contact 420 520 system-search-symbolic Search vertical Search... 8 8 6 false true true No contacts 4 4 meshy/data/ui/theme-chooser-dialog.ui000066400000000000000000000037131521052255700200770ustar00rootroot00000000000000 meshy/data/ui/window.ui000066400000000000000000000417271521052255700154160ustar00rootroot00000000000000
Disconnect win.disconnect Keyboard Shortcuts win.show-help-overlay About Meshy win.about Quit win.quit
Keyboard Shortcuts win.show-help-overlay About Meshy win.about Quit win.quit
meshy/meson.build000066400000000000000000000027441521052255700143600ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later project('meshy', version: '26.06', meson_version: '>= 0.62.0', ) i18n = import('i18n') gnome = import('gnome') python = import('python') py_installation = python.find_installation('python3') # Check required Python modules _pip_names = {'Crypto': 'pycryptodome', 'serial': 'pyserial'} _required_modules = ['Crypto', 'serial'] if get_option('qr_scanner') _required_modules += ['pyzbar'] endif foreach mod : _required_modules if run_command(py_installation, '-c', 'import @0@'.format(mod), check: false).returncode() != 0 _pip = _pip_names.get(mod, mod) warning('Python module "@0@" not found. Install it via pip: pip install @0@'.format(mod, _pip)) endif endforeach APPLICATION_ID = 'page.codeberg.sesivany.Meshy' pkgdatadir = get_option('prefix') / get_option('datadir') / 'meshy' conf = configuration_data() conf.set('PYTHON', py_installation.full_path()) conf.set('VERSION', meson.project_version()) conf.set('APPLICATION_ID', APPLICATION_ID) conf.set('localedir', get_option('prefix') / get_option('localedir')) conf.set('pkgdatadir', pkgdatadir) conf.set('QR_SCANNER_ENABLED', get_option('qr_scanner').to_string()) conf.set('SHORTCUTS_DIALOG_ENABLED', get_option('shortcuts_dialog').to_string()) subdir('data') subdir('src') subdir('po') gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true, ) meshy/meson_options.txt000066400000000000000000000006021521052255700156420ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later option('qr_scanner', type: 'boolean', value: true, description: 'Enable QR code scanning support (requires pyzbar and zbar)') option('shortcuts_dialog', type: 'boolean', value: true, description: 'Enable keyboard shortcuts dialog (requires libadwaita >= 1.8)') meshy/po/000077500000000000000000000000001521052255700126255ustar00rootroot00000000000000meshy/po/LINGUAS000066400000000000000000000000601521052255700136460ustar00rootroot00000000000000cs da de es et fi fr hu it lt lv nl pl pt sk sv meshy/po/POTFILES000066400000000000000000000015421521052255700137770ustar00rootroot00000000000000data/page.codeberg.sesivany.Meshy.desktop.in data/page.codeberg.sesivany.Meshy.metainfo.xml.in data/gtk/help-overlay.ui data/ui/window.ui data/ui/device-view.ui data/ui/settings-view.ui data/ui/connection-view.ui data/ui/contacts-view.ui data/ui/chat-view.ui data/ui/channels-view.ui data/ui/channel-chat-widget.ui data/ui/repeater-view.ui data/ui/device-actions.ui data/ui/rx-log-dialog.ui data/ui/settings-sidebar.ui data/ui/discover-dialog.ui data/ui/share-contact-dialog.ui data/ui/theme-chooser-dialog.ui src/application.py src/connection_controller.py src/frame_handler.py src/message_controller.py src/window.py src/qr_scanner.py src/views/__init__.py src/views/connection_view.py src/views/contacts_view.py src/views/channels_view.py src/views/device_view.py src/views/settings_view.py src/views/repeater_view.py src/views/chat_view.py src/views/map_view.py meshy/po/cs.po000066400000000000000000002401561521052255700136020ustar00rootroot00000000000000# Czech translations for meshy package. # Copyright (C) 2026 THE meshy'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # Automatically generated, 2026. # Jiří Eischmann , 2026. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: 2026-06-05 08:16+0000\n" "Last-Translator: sesivany \n" "Language-Team: Czech \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 2026.5\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "Klient sítě mesh MeshCore" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "mesh;lora;rádio;chat;síť;" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy je klient pro zařízení sítě mesh MeshCore LoRa. Připojte se ke svému " "companionu přes Bluetooth a posílejte a přijímejte šifrované zprávy, " "spravujte kontakty, nastavujte kanály a sledujte svou síť mesh." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Funkce:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Připojení přes Bluetooth LE a USB sériový port" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Šifrování zpráv mezi koncovými body" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "Veřejné, soukromé a hashtagové kanály" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "Správa kontaktů se sledováním polohy" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "Interaktivní zobrazení mapy s trasováním cest" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "Správa zařízení a repeaterů" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "Skenování QR kódů pro rychlou výměnu kontaktů" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "Jiří Eischmann" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "Zobrazení kontaktů se zprávami" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "Seznam kanálů s různými typy kanálů" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "Zobrazení mapy s polohami kontaktů" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "Informace o zařízení a nastavení" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "Obecné" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "Ukončit" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "Klávesové zkratky" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "Navigace" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "Zařízení" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "Kontakty" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "Kanály" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "Mapa" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "Nastavení" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "Zprávy" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "Hledat kontakty" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "Přepnutí na psaní zprávy" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "Předchozí kontakt/kanál" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "Další kontakt/kanál" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "Předchozí nepřečtená" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "Další nepřečtená" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "Odpojit" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "O aplikaci Meshy" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "Připojit" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "Nabídka" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "Připojování…" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "Zobrazit navigaci" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "Nepřipojeno" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "Stav" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "Odpojeno" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "Informace o zařízení" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "Název uzlu" #: data/ui/device-view.ui:53 msgid "Board" msgstr "Deska" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "Firmware" #: data/ui/device-view.ui:63 msgid "Update" msgstr "Aktualizovat" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "Otevřít flasher firmwaru" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "Veřejný klíč" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "Telemetrie" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "Vyžádat telemetrii" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "Poloha" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "Synchronizace…" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "Zobrazit na mapě" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "Baterie a úložiště" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "Baterie" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "Úložiště" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "Konfigurace rádia" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "Frekvence" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "Šířka pásma" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "Faktor rozprostření" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "Kódovací poměr" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "Vysílací výkon" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "Výchozí velikost hashe cesty" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "Statistiky" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "Doba běhu" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "Fronta zpráv" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "Úroveň šumu" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "Poslední RSSI" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "Poslední SNR" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "Doba vysílání" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "Pakety" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "Flood" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "Přímé" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "Obnovit statistiky" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "Aplikace" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "Nastavte chování a vzhled aplikace" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "Styl" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "Zvolte vzhled aplikace" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "Podle systému" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "Světlý" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "Tmavý" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "Barevné téma" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "MeshCore Dark" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "Oznámení kanálů" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "Výchozí úroveň oznámení pro všechny kanály" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "Všechny zprávy" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "Pouze zmínky" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "Žádné" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "Běžet na pozadí" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "Přijímejte zprávy i po zavření okna" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "Automatický start" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "Po přihlášení automaticky spustit" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "Rádio" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "Parametry rádia LoRa a regionální předvolby" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "Regionální preset" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "Konfigurace presetu" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "Frekvence, šířka pásma, faktor rozprostření, kódovací poměr" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "Frekvence (MHz)" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "Šířka pásma (kHz)" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "Režim přeposílání" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "Fungovat jako přenosný repeater na off-grid frekvenci" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "Vysílací výkon (dBm)" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "Advert" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "Nastavte, jak se váš uzel ohlašuje na síti" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "Název zařízení" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "Zahrnout polohu v advertu" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "Vysílat vaši polohu ostatním uzlům" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "Směrování a zprávy" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "Nastavení trasování a doručování zpráv" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "1 bajt (max 64 skoků)" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "2 bajty (max 32 skoků)" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "3 bajty (max 21 skoků)" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "Potvrzení přímých zpráv" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "Počet ACKů odeslaných při přijetí přímé zprávy" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "Nastavte polohu companiona přes GPS nebo ručně" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "GPS companiona" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "Zapnout GPS na companionu; poloha se bude aktualizovat automaticky" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "Nastavit polohu ručně" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "Zvolte jiný způsob nastavení polohy, bude fixní" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "Zeměpisná šířka" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "Zeměpisná délka" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "Použít moji polohu" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "Nastavit podle polohy tohoto počítače" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "Vybrat na mapě" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "Zvolte polohu na mapě" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "Automatické přidávání kontaktů" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "Automaticky přidávat uzly zjištěné z advertů" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "Nastavení automatického přidávání" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "Typy zařízení, politika přepisu, limit hopů" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "Chatové uzly" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "Repeatery" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "Room servery" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "Senzory" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "Přepsat nejstarší při plné kapacitě" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "Limit hopů" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "0–63, ponechte prázdné pro žádný limit" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "Soukromí telemetrie" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "Určete, co ostatní mohou dotazovat z vašeho zařízení" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "Baterie a senzory" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "Baterie, napětí, teplota, proud" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "Souřadnice GPS" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "Prostředí" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "Vlhkost, tlak, kvalita vzduchu" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "Regiony" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "Omezit dosah floodu u zpráv kanálů na konkrétní regiony" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "Přidat oblast (např. cz-jmk)" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "Výchozí oblast" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "Všechny flood pakety budou omezeny na tento region, pokud je nastavena" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "Bluetooth" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" "Po změně PINu restartujte zařízení, odpárujte jej a znovu spárujte. " "Nastavení 0 nastaví PIN na výchozí hodnotu." #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "Párovací PIN" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "Připojte se ke companionu a začněte používat Meshy." #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "Spárovat s novým companionem" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "USB sériové" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "Wi-Fi / TCP" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "Přidat TCP companiona" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "Hledat…" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "Žádné kontakty" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "Přidat kontakt" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "Vyberte kontakt" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "Vyberte kontakt ze seznamu a začněte chatovat" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "↑ Nové zprávy" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "Napište zprávu…" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "Vytvořit soukromý kanál" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "Připojit se k soukromému kanálu" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "Připojit se k veřejnému kanálu" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "Připojit se ke kanálu s hashtagem" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "Žádné kanály" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "Přidat kanál" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "Vyberte kanál" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "Vyberte kanál ze seznamu" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "Zpráva do kanálu…" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "Systém" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "Hodiny při přihlášení" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "Synchronizovat" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "Resetovat a restartovat" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "Chybové události" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "Doba vysílání TX" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "Doba vysílání RX" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "Využití kanálu" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "Střída signálu" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "Odesláno" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "Přijato" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "Chyby na příjmu" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "Duplikáty" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "Obnovit" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "Vyžádat telemetrii" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "CLI" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "Příkaz CLI…" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "Sousedé" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "Načíst další" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "Zobrazit na mapě" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "Řízení přístupu" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "Přidat" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "Načítání regionů…" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "Zkusit znovu" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "Výchozí omezující region" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" "Pakety šířící se floodem budou omezeny na zadaný region. Pouze repeatery " "povolující tento region budou pakety s omezeným dosahem posílat dál. " "Ponechte prázdné pro šíření bez omezení." #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "Použít" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "Základní" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "Načíst" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "Uložit" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "Název" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "Opakování paketů" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "Hesla" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "Heslo správce" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "Heslo hosta" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "Advert" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "Místní interval (min)" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "Interval floodu (hod)" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "Nebezpečná zóna" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "Restartovat repeater" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "Vyberte repeater" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "Odhlásit se" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "Odeslat advert" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "Zero hop" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "Vysílat lokálně" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "Poslat floodem" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "Vysílat napříč sítí mesh" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "Do schránky" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "Kopírovat vlastní kontaktní údaje" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "Zobrazit QR kód" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "Aby vás ostatní mohli naskenovat a přidat" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "Trasovat cestu" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "Trasovat cestu a zobrazit kvalitu signálu na každém skoku" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "Objevit blízké uzly" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "Prohledat síť pro blízké uzly" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "Rx Log" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "Zobrazit přijaté rádiové pakety" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "Restartovat zařízení" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "Vymazat log" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "Žádné pakety" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "Přijaté rádiové pakety se zobrazí zde" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "Záloha" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "Uložit nastavení a zprávy do souboru" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "Obnovit" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "Obnovit ze souboru zálohy" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "Obnovení továrního nastavení" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "Vymazat všechna data na companionu" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "Objevit kontakty" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "Hledat" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "Filtrovat podle typu" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "Řadit podle" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "Sdílet kontakt" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "Vyberte téma" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "Chat" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "Repeater" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "Room" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "Senzor" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "Neznámý" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" "Název: {name}\n" "Typ: {type}" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" "Typ: {type}\n" "Klíč: {key}…" #: src/application.py:101 msgid "Import Contact" msgstr "Importovat kontakt" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "Kontakt {}" #: src/application.py:105 msgid "imported" msgstr "naimportován" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "Neplatný odkaz meshcore://" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "Neplatný tajný klíč kanálu v odkazu" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "Neplatná délka tajného klíče kanálu" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" "Název: {name}\n" "Typ: Soukromý" #: src/application.py:130 msgid "Unnamed" msgstr "Nepojmenovaný" #: src/application.py:146 msgid "Invalid public key in link" msgstr "Neplatný veřejný klíč v odkazu" #: src/application.py:149 msgid "Invalid public key length" msgstr "Neplatná délka veřejného klíče" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "{} je již ve vašem seznamu" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "Kontakt" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "Přidat kontakt typu {}" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" "Název: {name}\n" "Veřejný klíč: {key}…" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "Kontakt {} přidán" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "Nerozpoznaný odkaz meshcore://" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "Zrušit" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "Zprávy se přijímají na pozadí" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "GTK klient pro zařízení sítě mesh MeshCore LoRa" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "Symbolické ikony Adwaita" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "Připojování…" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "Hledat zařízení" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "Hledání zařízení MeshCore…" #: src/connection_controller.py:140 msgid "Scan" msgstr "Hledat" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "Hledání…" #: src/connection_controller.py:263 msgid "Stop" msgstr "Zastavit" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "Synchronizace…" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "Zařízení neodpovědělo. Nemusí podporovat tento typ spojení." #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "Opětovné připojení ({current}/{total})…" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "Synchronizace kanálů" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "Při odpojení jste obdrželi {} zpráv(y)." #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "Chyba zařízení: {}" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "Synchronizace kontaktů" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "{}ms (pozdě)" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "{}ms (pozdě)" #: src/frame_handler.py:352 msgid "late" msgstr "pozdě" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "Hodiny zařízení byly {}min pozadu — synchronizovány" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "Hodiny zařízení jsou {}min napřed oproti systému" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "Kanál {}" #: src/frame_handler.py:886 msgid "Someone" msgstr "Někdo" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "{sender}: {text}" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "Já" #: src/message_controller.py:171 msgid "flood" msgstr "flood" #: src/message_controller.py:173 msgid "direct" msgstr "přímá" #: src/message_controller.py:175 msgid "path" msgstr "cesta" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "– {route}" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "{attempt}/{total} – {route}" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "čekání {:.1f}s (rádio zaneprázdněno)" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "Filtr" #: src/window.py:473 msgid "Sort" msgstr "Řadit" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "Akce" #: src/window.py:536 msgid "Channel" msgstr "Kanál" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "Záloha a obnova" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "{name} ({path})" #: src/window.py:687 msgid "Management" msgstr "Správa" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "Host" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "{name} — {role}" #: src/window.py:790 msgid "Excellent" msgstr "Vynikající" #: src/window.py:790 msgid "Good" msgstr "Dobrý" #: src/window.py:791 msgid "Fair" msgstr "Slušný" #: src/window.py:791 msgid "Poor" msgstr "Špatný" #: src/window.py:792 msgid "Very poor" msgstr "Velmi špatný" #: src/window.py:1014 msgid "flood routed" msgstr "směrováno floodem" #: src/window.py:1014 msgid "zero-hop" msgstr "zero-hop" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "Advert odeslán ({})" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "Kanál „{}“ přidán" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "Nejsou dostupné žádné volné sloty kanálů" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "Nelze se připojit ke sběrnici relace: {}" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "Na tomto systému nebyla zjištěna žádná kamera." #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "Nepodařilo se přistoupit ke kameře: {}" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "Přístup ke kameře byl odepřen." #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "Nepodařilo se otevřít proud kamery." #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "Nebyl přijat deskriptor souboru kamery." #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "Nepodařilo se otevřít kameru: {}" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "Naskenovat QR kód" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "Namiřte kameru na QR kód…" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "Chyba kanálu kamery: {}" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" "QR kód zatím nebyl rozpoznán.\n" "Ujistěte se, že je kód dobře osvětlený, ostrý a zabírá většinu snímku." #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" "Nepodařilo se rozpoznat QR kód.\n" "Zkuste kontakt naimportovat ze schránky." #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "QR kód nalezen!" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "Chyba kamery: {}" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "Chyba kamery" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "OK" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "Odebrat" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "Bluetooth je vypnutý" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "Zapněte Bluetooth v nastavení systému pro připojení" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "Žádná spárovaná zařízení" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "Pomocí tlačítka níže spárujte nového companiona" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "Nebyla zjištěna žádná USB zařízení" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "Připojte companiona MeshCore přes USB" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "Odebrat companiona" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "Odebrat {} ({})? Pro opětovné připojení bude potřeba znovu spárovat." #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "Smazat uložená data" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "Připojení USB selhalo" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "Přístup k USB odepřen" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" "Nelze přistoupit k {}.\n" "\n" "Pro přístup k USB sériovým zařízením je potřeba pravidlo udev. Klikněte na " "„Nainstalovat pravidlo“ pro jeho instalaci (vyžaduje heslo správce)." #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "Nainstalovat pravidlo" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" "Nelze přistoupit k {}.\n" "\n" "Pro přístup k USB sériovým zařízením je potřeba pravidlo udev. Pusťte " "následující příkaz v terminálu:\n" "\n" "{}" #: src/views/connection_view.py:270 msgid "Close" msgstr "Zavřít" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "Kopírovat příkaz" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "Příkaz zkopírován do schránky" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "Instalace selhala" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" "Nepodařilo se nainstalovat pravidlo udev:\n" "{}" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "Žádní uložení companioni" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "Pomocí tlačítka níže přidejte companiona" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "Odebrat TCP companiona {}?" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "Zadejte IP adresu a port vašeho zařízení MeshCore." #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "Název hostitele / IP adresa" #: src/views/connection_view.py:436 msgid "Port" msgstr "Port" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "Spárovat nového companiona" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "Hledání blízkých zařízení MeshCore" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "Spárovat" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "Zadejte PIN pro párování" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "Zadejte PIN zobrazený na vašem zařízení MeshCore." #: src/views/connection_view.py:603 msgid "PIN" msgstr "PIN" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "Přidat ručně" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "Importovat ze schránky" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "Vše" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "Oblíbené" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "Uživatelé" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "A–Z" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "Nedávno slyšené" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "Nejnovější zprávy" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "Nejdříve oblíbené" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "{visible}/{total} kontaktů" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "{total} kontaktů" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "{total} kontakt" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" "Zatím nebyly zaznamenány žádné nové kontakty. Nechte aplikaci běžet a " "kontakty se objeví, jakmile budou přijaty jejich adverty." #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "Naposledy viděn" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "Vzdálenost" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "{visible}/{total} objeveno" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "{total} objeveno" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "Zadejte údaje kontaktu." #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "Typ kontaktu" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "Room server" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "Veřejný klíč (64 hex znaků)" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "Tento kontakt je již ve vašem seznamu." #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "Import selhal" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "Ve schránce nebyl nalezen odkaz meshcore://." #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "Neplatný tajný klíč kanálu v QR kódu." #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "Tajný klíč kanálu musí mít 16 bajtů, obdrženo {}." #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "Neplatný veřejný klíč v QR kódu." #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "Veřejný klíč musí mít 32 bajtů, obdrženo {}." #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" "Nepodařilo se zpracovat data QR kódu:\n" "{}" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "QR kód naskenován" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "Veřejný klíč: {}…{}" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "Přihlásit se k {}" #: src/views/contacts_view.py:678 msgid "Password" msgstr "Heslo" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "Uložit heslo" #: src/views/contacts_view.py:702 msgid "Login" msgstr "Přihlásit" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "Přihlášení úspěšné" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "Přihlášení selhalo — nesprávné heslo" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "Přihlášení vypršelo" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "Opakování přes flood…" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "Přihlašování…" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "Hledání…" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "Cesta nalezena" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "Bez odpovědi" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "Načítání…" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "Žádná uložená cesta" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "Nepodporováno" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "Ping není podporován s 3bajtovými hashi cest" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "Ping {}: {}" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "Ping {}: vypršel časový limit" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "Vyžadování…" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "Bez odpovědi (vypršel časový limit)" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "Žádná data telemetrie" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "Telemetrie přijata" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "Telemetrie — {}" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "{} — Správa" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "Cesta k {}" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "Moje zařízení" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "Neznámý repeater" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "Informace o kontaktu" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "Oblíbený" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "Připnout na začátek seznamu kontaktů" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "Typ" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "Veřejný klíč zkopírován" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "Nikdy" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "Odchozí cesta" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "Hex skoky oddělené čárkou (např. 9a,74 nebo 9a3b,744c pro 2bajtové)" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "Resetovat cestu" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "Vymazat zavedenou cestu a přepnout na flood" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "Vynutit flood" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "Vždy vysílat všesměrově, nikdy nepoužívat zavedenou cestu" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "Zobrazit cestu na mapě" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "Zobrazit trasu zprávy na mapě" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "Povolit požadavky na telemetrii" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "Povolit tomuto kontaktu dotazovat baterii, napětí, teplotu" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "Zahrnout polohu" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "Zahrnout souřadnice GPS v odpovědích telemetrie" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "Zahrnout senzory prostředí" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "Zahrnout data o vlhkosti, tlaku, kvalitě vzduchu" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "Dotázat se na data telemetrie tohoto kontaktu" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "Správa roomu" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "Přihlásit se a přistoupit k CLI, stavu a nastavení" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "Správa repeateru" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "Přihlásit se a získat přístup ke stavu, CLI, sousedům a nastavení" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "Ping" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "Změřit dobu odezvy k tomuto uzlu" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "Objevit cesty" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "Najít trasy k tomuto uzlu pomocí floodu" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "Odebrat kontakt" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "Odebrat kontakt?" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "Odebrat {} a všechny zprávy?" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "Slyšená opakování" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "Odeslat znovu" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "Smazat" #: src/views/channels_view.py:122 msgid "Reply" msgstr "Odpovědět" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "Kopírovat text" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "Zobrazit cesty zprávy" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "Sdílet polohu" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "Sdílet polohu z mapy" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "Otevřít v mapové aplikaci" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "Sdílíte kontakt:" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "{name} s vámi sdílí kontakt:" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "Již přidán" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "Sdílíte polohu:" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "{name} sdílí polohu:" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "Nové zprávy" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "Sdílená poloha" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "Sdílená" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "Sdílet" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "Poloha není k dispozici" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "Váš companion nemá k dispozici polohu GPS." #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "Cesty zprávy" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" "Informace o cestě nejsou dostupné. Data o cestě se zaznamenávají pouze pro " "zprávy přijaté během připojení." #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "Přímá zpráva" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "Žádné repeatery nebyly použity" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "Žádné podrobnosti o repeateru" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "Data o cestě se zaznamenávají pouze přes LOG_DATA během připojení" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "Cesta zprávy" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "Zatím nebyla slyšena žádná opakování." #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "Veřejný" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "Hashtag" #: src/views/channels_view.py:1671 msgid "Private" msgstr "Soukromý" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "{visible}/{total} kanálů" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "{total} kanálů" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "{total} kanál" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "{name} ({region})" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "Informace o kanálu" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "Soukromý klíč" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "Soukromý klíč zkopírován do schránky" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "Oznámení" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "Úroveň oznámení" #: src/views/channels_view.py:1935 msgid "Default" msgstr "Výchozí" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "Region" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "Omezovat zprávy šíření floodem na konkrétní region" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "Nejsou definovány žádné regiony" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" "Chcete-li kanálům přiřadit regiony, musíte je nejdříve přidat v Nastaveních." #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "Odebrat kanál" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "Odebrat kanál?" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "Odebrat „{}“ a všechny jeho zprávy?" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "Žádné volné sloty" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "Všech 8 slotů kanálů je obsazeno." #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" "Bude vygenerován náhodný PSK. Sdílejte ho s ostatními, aby se mohli připojit." #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "Název kanálu" #: src/views/channels_view.py:2070 msgid "Create" msgstr "Vytvořit" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "Zadejte název kanálu a soukromý klíč, který vám byl sdílen." #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "Soukromý klíč (32 hex znaků)" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "Připojit se" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "Již připojeno" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "Již jste na veřejném kanálu." #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" "Zadejte název hashtagu. Kdokoliv se stejným hashtagem může komunikovat." #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "Hashtag (např. #obecný)" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "Veřejný klíč zařízení není dostupný" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "Knihovna QR kódů není dostupná" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "Váš kontaktní QR kód" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "Naskenujte tento QR kód pro přidání kontaktu" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "Obnovit tovární nastavení?" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" "Tímto trvale vymažete VŠECHNA data na companionu, včetně kontaktů, zpráv, " "kanálů, nastavení a identity zařízení (klíčů). Tuto akci nelze vrátit zpět." #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "Restartovat zařízení?" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "Zařízení se odpojí a restartuje." #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "Restartovat" #: src/views/device_view.py:323 msgid "Not set" msgstr "Nenastaveno" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "Poloha (ručně nastavená)" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "Poloha (GPS)" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "Poloha (GPS zapnutý, bez signálu)" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "Poloha (GPS vypnutý)" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "Vyžaduje se…" #: src/views/device_view.py:405 msgid "Device Location" msgstr "Poloha zařízení" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "{days}d" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "{hours}h" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "{mins}m" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "{} zpráv" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "{count} chyby" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "Připojeno" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "Trasování cesty není podporováno s 3bajtovými hashi cest" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "např. aa,bb,cc" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "např. aabb,ccdd" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "např. aabbcc,ddeeff" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "Přidat z kontaktů" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "V kontaktech nejsou žádné repeatery ani roomy" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "Žádné repeatery se známou polohou" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "Cesta" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "Spustit trasování" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "Zadejte hashe cesty a stiskněte Spustit trasování" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "Trasování dokončeno: {} skoků, {:.1f}s" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "Trasování vypršelo" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "Neplatný hex v cestě" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "Trasování…" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "Hledání… zbývá {}s" #: src/views/device_view.py:977 msgid "Listening..." msgstr "Naslouchání…" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "Čekání na odpověď blízkých uzlů" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "Přidat do kontaktů" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "Přidáno {}" #: src/views/device_view.py:1066 msgid "Node" msgstr "Uzel" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "Hotovo. Nalezeno {} uzlů." #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "Hotovo. Nalezen {} uzel." #: src/views/device_view.py:1222 msgid "Size" msgstr "Velikost" #: src/views/device_view.py:1222 msgid "bytes" msgstr "bajtů" #: src/views/device_view.py:1226 msgid "hops" msgstr "hopů" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "Hashe cesty" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "bajt na hop" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "Dostupná aktualizace: {version}" #: src/views/settings_view.py:131 msgid "Custom" msgstr "Vlastní" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "Odmítnout vše" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "Povolit podle kontaktů" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "Povolit vše" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "Nastavení uloženo na zařízení" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "Neznámá chyba" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "Chyba nastavení: {}" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "Vysílací výkon (dBm, max {})" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "Poloha vyplněna, klikněte na Použít pro uložení" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "Poloha nedostupná" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "Zvolit polohu" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "Nastavit polohu" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "Nejprve se připojte k zařízení" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "V kontaktech nejsou žádné repeatery" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "Objevit regiony" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "Dotazování {} repeaterů…" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "Dotazování {} repeateru…" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "Čekání…" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "Dotazování repeaterů na regiony" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "Přidat vybrané" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "Nalezeny {} regiony" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "Nalezen {} region" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "Žádné regiony nenalezeny" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "od {}" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "(již přidáno)" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "Přidány {} regiony" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "Přidán {} region" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "Exportovat zálohu" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "Uložit do souboru" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "Importovat zálohu" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "Obnovit ze souboru" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "Nepřipojeno" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "Pro export zálohy se nejprve připojte k zařízení." #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "Soubory JSON" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "Záloha úspěšně exportována" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "Export selhal: {}" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "Pro import zálohy se nejprve připojte k zařízení." #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "Nepodařilo se přečíst soubor zálohy: {}" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "Zdroj: {}" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "Nastavení zařízení: název, rádio, poloha" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "Kontakty: {} ({} nových)" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "Kanály: {} ({} nových)" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "Zprávy: {}" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "Zprávy kanálu: {}" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "Uložená hesla: {}" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "Pouze kontakty a kanály" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "Vše" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "{} kontaktů" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "{} kanálů" #: src/views/settings_view.py:1208 msgid "messages" msgstr "zpráv" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "Importováno: {}" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "Nic nového k importu" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "{d}d" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "{h}h" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "{m}m" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "{secs}s" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "před {}s" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "před {}m" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "před {}h" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "před {}d" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "Synchronizace času odeslána na repeateru" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "Resetovat hodiny a restartovat?" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" "Hodiny repeateru jsou napřed oproti aktuálnímu času.\n" "Firmware neumožňuje nastavit čas dozadu.\n" "\n" "Tímto se hodiny resetují a repeater se restartuje." #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "Restartování…" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "Reset hodin a restart odeslán na repeater" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "Požadavek vypršel" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "{} z {} sousedů" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "Mapa sousedů" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "Správce" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "{} záznam" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "{} záznamů" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "Odebrat z ACL?" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "Odebrat {} ze seznamu řízení přístupu?" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "Přidat do řízení přístupu" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "Vyberte kontakt a roli." #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "Role" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "Globální (wildcard)" #: src/views/repeater_view.py:942 msgid "Home" msgstr "Domácí" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "Flood povolen" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "Flood odmítán" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "Odmítat flood" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "Povolit flood" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "Nastavit jako domácí" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "Nejdříve odstranit podřízené regiony" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "Neuložené změny" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "Máte neuložené změny v regionech." #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "Zahodit" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "Přidat region" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "Zadejte název a volitelně vyberte rodičovský region." #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "Název regionu" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "Rodič" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "Heslo správce změněno — je potřeba se znovu přihlásit" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "Nastavení odesláno na {}" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "Restartovat repeater?" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "Restartovat {}? Bude krátce nedostupný." #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "Čas nesouhlasí" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "Odesílá se" #: src/views/map_view.py:99 msgid "User" msgstr "Uživatel" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "{} z {} kontaktů na mapě" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "{} z {} lokalizovaných na mapě" #: src/views/map_view.py:198 msgid "Rooms" msgstr "Roomy" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "Objevené uzly" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "a {} dalších" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "Pozice je ilustrativní - skutečné umístění neznámé" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "SNR: {:.1f} dB" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "Vyberte trasovací cestu" #: src/views/map_view.py:1159 msgid "Undo" msgstr "Vrátit" #: src/views/map_view.py:1169 msgid "Clear" msgstr "Vymazat" #: src/views/map_view.py:1177 msgid "Done" msgstr "Hotovo" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "Trasovací cestu vytvoříte klepáním na repeatery" #, python-brace-format #~ msgid "Syncing channels: {index}/{total}" #~ msgstr "Synchronizace kanálů: {index}/{total}" #, python-brace-format #~ msgid "Syncing contacts: {current}/{total}" #~ msgstr "Synchronizace kontaktů: {current}/{total}" #~ msgid "Search contacts..." #~ msgstr "Hledat kontakty..." #~ msgid "Auto-add Chat nodes" #~ msgstr "Automaticky přidávat chatovací uzly" #~ msgid "Auto-add Repeaters" #~ msgstr "Automaticky přidávat repeatery" #~ msgid "Auto-add Room Servers" #~ msgstr "Automaticky přidávat room servery" #~ msgid "Auto-add Sensors" #~ msgstr "Automaticky přidávat senzory" #~ msgid "Hop limit for auto-adding" #~ msgstr "Limit skoků pro automatické přidávání" #~ msgid "Paired Companions" #~ msgstr "Spárovaní companioni" #~ msgid "No Device Connected" #~ msgstr "Nepřipojeno žádné zařízení" #~ msgid "Select a companion device from the sidebar to connect." #~ msgstr "Vyberte companiona z postranního panelu pro připojení." #, python-brace-format #~ msgid "retrying {current}/{total}..." #~ msgstr "opakování {current}/{total}…" #~ msgid "Settings sent to companion" #~ msgstr "Nastavení odesláno na companiona" #~ msgid "Backup & Restore" #~ msgstr "Záloha a obnova" meshy/po/da.po000066400000000000000000001746001521052255700135610ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/de.po000066400000000000000000001755541521052255700135760ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: 2026-05-15 12:33+0000\n" "Last-Translator: bjawebos \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.17.1\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "MeshCore – Mesh-Netzwerk-Client" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "Mesh;LoRa;Funk;Chat;" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy ist ein Client für MeshCore-LoRa-Netzwerke. Verbinde dich per " "Bluetooth mit deinem Gerät, um verschlüsselte Nachrichten zu senden, " "Kontakte zu verwalten, Kanäle einzurichten und dein Netzwerk zu überwachen." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Funktionen:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Bluetooth LE- und USB-Serial-Verbindung" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Ende-zu-Ende-verschlüsselte Nachrichten" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/es.po000066400000000000000000002420011521052255700135730ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: 2026-06-04 19:07+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 2026.5\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "Cliente de red de malla MeshCore" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "red en malla; LoRa (radio de largo alcance); radio; chat" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy es una aplicación para los dispositivos de red en malla MeshCore LoRa. " "Conéctate a tu dispositivo complementario por Bluetooth para enviar y " "recibir mensajes cifrados, gestionar contactos, configurar canales y " "supervisar tu red en malla." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Características:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Conectividad Bluetooth LE y puerto serie USB" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Mensajería cifrada de extremo a extremo" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "Canales públicos, privados y de hashtags" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "Gestión de contactos con seguimiento de la ubicación" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "Vista de mapa interactiva con trazado de rutas" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "Gestión de dispositivos y repetidores" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "Escanea los códigos QR para intercambiar los contactos rápidamente" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "Jiri Eischmann" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "Vista de contactos con mensajería" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "Lista de canales con varios tipos de canales" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "Vista del mapa con las ubicaciones del contacto" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "Información y ajustes del dispositivo" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "General" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "Salir" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "Atajos del teclado" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "Exploración" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "Dispositivo" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "Contactos" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "Canales" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "Mapa" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "Ajustes" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "Mensajería" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "Buscar contactos" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "Entrada de mensajes destacados" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "Contacto/Canal anterior" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "Contacto/canal siguiente" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "Anteriores sin leer" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "Siguiente sin leer" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "Desconectar" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "Acerca de Meshy" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "Conectar" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "Menú" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "Conectando…" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "Mostrar navegación" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "No conectado/-a" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "Estado" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "Desconectado/-a" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "Información del dispositivo" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "Nombre del nodo" #: data/ui/device-view.ui:53 msgid "Board" msgstr "Teclado" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "Firmware" #: data/ui/device-view.ui:63 msgid "Update" msgstr "Actualización" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "Open firmware flasher" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "Clave pública" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "Telemetría" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "Solicitar datos de telemetría" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "Ubicación" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "Sincronizando…" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "Ver en el mapa" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "Batería y almacenamiento" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "Batería" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "Almacenamiento" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "Ajustar frecuencias" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "Frecuencia" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "Ancho de banda" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "Factor de ensanchamiento" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "Tasa de codificación" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "Potencia de transmisión" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "Tamaño de hash de ruta predeterminado" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "Estadísticas" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "Tiempo de actividad" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "Cola de mensajes" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "Ruido de fondo" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "RSSI más reciente" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "SNR más reciente" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "Airtime" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "Paquetes" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "Flood (inundación)" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "Privado" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "Actualizar estadísticas" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "Aplicación" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "Configurar el comportamiento y la apariencia de la aplicación." #: data/ui/settings-view.ui:20 msgid "Style" msgstr "Estilo" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "Elige la apariencia de la aplicación" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "Sistema" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "Claro" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "Oscuro" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "Paleta de colores" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "MeshCore oscuro" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "Notificaciones del canal" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "Nivel de notificación predeterminado para todos los canales" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "Todos los mensajes" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "Solo menciones" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "Ninguno" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "Ejecutarse en segundo plano" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "Sigo recibiendo mensajes después de cerrar la ventana." #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "Inicio automático" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "Se iniciará automáticamente al iniciar sesión" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "Radio" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "Parámetros de radio LoRa y preajustes regionales" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "Preajuste regional" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "Configuración preestablecida" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" "Frecuencia , ancho de banda , factor de dispersión , tasa de codificación" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "Frecuencia (MHz)" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "Ancho de banda (kHz)" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "Modo de repetición" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "Funciona como un repetidor portátil en una frecuencia fuera de la red." #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "Potencia de transmisión (dBm)" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "Anuncio" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "Configura cómo se anuncia tu nodo en la malla." #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "Nombre del dispositivo" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "Incluir la ubicación en el anuncio" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "Transmite tu posición a otros nodos." #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "Enrutamiento y mensajería" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "Configuración de enrutamiento de rutas y entrega de mensajes" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "1 byte (máximo 64 saltos)" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "2 bytes (máximo 32 saltos)" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "3 bytes (máximo 21 saltos)" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "Confirmaciones por mensaje privado" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "Número de ACK enviados al recibir un mensaje privado" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "Establece la posición de tu compañero mediante GPS o manualmente." #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "GPS compañero" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" "Activa el GPS en el dispositivo, tu posición se actualizará automáticamente." #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "Establecer ubicación manualmente" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "Elija otro método para establecer la ubicación, se fijará." #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "Latitud" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "Longitud" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "Usar mi ubicación" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "Establecer desde la ubicación de este ordenador" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "Seleccionar en el mapa" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "Seleccione una ubicación en el mapa." #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "Añadir contactos automáticamente" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "Añadir automáticamente nodos descubiertos a través de anuncios." #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "Añadir ajustes automáticamente" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "Tipos de dispositivos, política de sobrescritura, límite de saltos" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "Nodos de chat" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "Repetidores" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "Sala de servidores" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "Sensores" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "Sobrescribir los archivos más antiguos cuando se llenen" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "Límite de saltos" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "0–63, dejar vacío para no tener límite" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "Privacidad de la telemetría" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "Controla lo que otros pueden consultar desde tu dispositivo" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "Batería y sensores" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "Batería, voltaje, temperatura, corriente" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "Coordenadas GPS" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "Ambiente" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "Humedad, presión, calidad del aire" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "Regiones" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" "Limitar el alcance de inundación de los mensajes del canal a regiones " "específicas" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "Añadir región (por ejemplo, Praga)" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "Ámbito predeterminado" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" "Todos los paquetes de inundación se limitarán a esta región si está " "configurada" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "Bluetooth" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" "Después de cambiar el PIN, reinicia el dispositivo, luego desvincula y " "vuelve a vincular. Configurar en 0 restablece el PIN al valor predeterminado." #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "PIN de emparejamiento" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "Conéctalo a un dispositivo complementario para empezar a usar Meshy." #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "Empareja a tu nuevo compañero" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "USB serie" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "WiFi / TCP" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "Añadir un complemento TCP" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "Buscar…" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "Sin contactos" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "Añadir contacto" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "Seleccione un contacto" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "Elige un contacto de la lista para empezar a chatear." #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "↑ Nuevos mensajes" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "Escribe un mensaje..." #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "Crear un canal privado" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "Únete a un canal privado" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "Únete al canal público" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "Únete a un canal de hashtags" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "No hay canales" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "Añadir canal" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "Seleccione un canal" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "Elige un canal de la lista." #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "Mensaje para el canal..." #: data/ui/repeater-view.ui:37 msgid "System" msgstr "Sistema" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "Reloj al iniciar sesión" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "Sincronizar ahora" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "Restablecer y reiniciar" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "Eventos de error" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "Tiempo de emisión en TX" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "Tiempo de emisión en RX" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "Utilización del canal" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "Ciclo de trabajo" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "Enviado" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "Recibido" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "Errores en la recepción" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "Duplicados" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "Actualizar" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "Solicitar telemetría" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "CLI" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "Comando cli ..." #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "Vecinos" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "Cargar más" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "Mostrar en el mapa" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "Control de acceso" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "Añadir" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "Cargando regiones ..." #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "Reintentar" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "Ámbito regional predeterminado" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" "Los paquetes de inundación se limitarán a la región especificada . Solo los " "repetidores que permitan el acceso a dicha región reenviarán los paquetes " "limitados . Deje este campo en blanco para que no se apliquen paquetes " "limitados." #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "Aplicar" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "Básico" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "Traer" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "Guardar" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "Nombre" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "Repetición de paquetes" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "Contraseñas" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "Contraseña de administrador" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "Contraseña de invitado" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "Anuncio" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "Intervalo local (min)" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "Intervalo de inundación (horas)" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "Zona de peligro" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "Reiniciar el repetidor" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "Seleccione un repetidor" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "Finalizar la sesión" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "Enviar anuncio" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "Sin saltos" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "Transmitido localmente" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "Desviación de la inundación" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "Transmitido a través de la malla" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "Al portapapeles" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "Copiar mis datos de contacto" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "Mostrar código QR" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "Para que otros puedan escanearte y agregarte" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "Rastrear ruta" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "Traza una ruta y muestra la calidad de la señal en cada salto." #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "Descubre nodos cercanos" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "Escanea la red en busca de nodos cercanos." #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "Recepción de datos" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "Visualización de los paquetes de radio recibidos" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "Reiniciar dispositivo" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "Borrar registro" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "Sin paquetes" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "Los paquetes de radio recibidos aparecerán aquí." #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "Copia de seguridad" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "Guarda la configuración y los mensajes en un archivo." #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "Restaurar" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "Restaurar de una copia de seguridad" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "Restaurar valores de fábrica" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "Borrar todos los datos del dispositivo" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "Descubre tus contactos" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "Buscar" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "Filtrar por tipo" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "Ordenar por" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "Compartir contacto" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "Elegir el tema" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "Chat" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "Repetidor" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "Room" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "Sensor" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "Desconocido" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" "Nombre: {name}\n" "Tipo: {type}" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" "Tipo: {type}\n" "Clave: {key}…" #: src/application.py:101 msgid "Import Contact" msgstr "Importar contacto" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "Contacto {}" #: src/application.py:105 msgid "imported" msgstr "importado" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "Enlace meshcore:// no válido" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "Secreto de canal no válido en el enlace" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "Longitud de secreto de canal no válida" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" "Nombre: {name}\n" "Tipo: Privado" #: src/application.py:130 msgid "Unnamed" msgstr "Sin nombre" #: src/application.py:146 msgid "Invalid public key in link" msgstr "Clave pública no válida en el enlace" #: src/application.py:149 msgid "Invalid public key length" msgstr "Longitud de clave pública no válida" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "{} ya aparece en tu lista" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "Contacto" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "Añadir {} contacto" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" "Nombre: {name}\n" "Clave pública: {key}…" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "Contacto {} añadido" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "Enlace «meshcore://» no reconocido" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "Cancelar" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "Recibir mensajes en segundo plano" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "Cliente GTK para dispositivos de red mallada MeshCore LoRa" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "Iconos simbólicos de Adwaita" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "Conectando…" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "Buscar dispositivos" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "Buscando dispositivos MeshCore..." #: src/connection_controller.py:140 msgid "Scan" msgstr "Escanear" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "Escaneando…" #: src/connection_controller.py:263 msgid "Stop" msgstr "Detener" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "Sincronizando…" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" "El dispositivo no ha respondido. Es posible que no sea compatible con este " "tipo de conexión." #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "Volviendo a conectar ({current}/{total})}…" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "Sincronizando canales" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "Has recibido {} mensajes mientras estabas desconectado." #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "Error del dispositivo: {}" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "Sincronizando contactos" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "{}s (tarde)" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "{}ms (tarde)" #: src/frame_handler.py:352 msgid "late" msgstr "tarde" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "El reloj del dispositivo se retrasaba {} minutos — sincronizado" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "El reloj del dispositivo se adelanta {} minutos al del sistema" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "Canal {}" #: src/frame_handler.py:886 msgid "Someone" msgstr "Alguien" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "{sender}: {text}" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "A mí" #: src/message_controller.py:171 msgid "flood" msgstr "flood (inundación)" #: src/message_controller.py:173 msgid "direct" msgstr "privado" #: src/message_controller.py:175 msgid "path" msgstr "ruta" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "– {route}" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "({attempt}/{total}) – {route}" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "Esperando {:.1f} s (la radio está ocupada)" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "Filtrar" #: src/window.py:473 msgid "Sort" msgstr "Clasificar" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "Comportamiento" #: src/window.py:536 msgid "Channel" msgstr "Canal" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "Copia de seguridad y restauración" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "{name} ({path})" #: src/window.py:687 msgid "Management" msgstr "Dirección" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "Invitado" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "{name} — {role}" #: src/window.py:790 msgid "Excellent" msgstr "Excelente" #: src/window.py:790 msgid "Good" msgstr "Bien" #: src/window.py:791 msgid "Fair" msgstr "Bueno" #: src/window.py:791 msgid "Poor" msgstr "Pobre" #: src/window.py:792 msgid "Very poor" msgstr "Muy pobre" #: src/window.py:1014 msgid "flood routed" msgstr "enrutamiento" #: src/window.py:1014 msgid "zero-hop" msgstr "sin salto" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "Anuncio enviado ( {} )" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "Canal «{}» añadido" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "No hay canales libres disponibles" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "No se puede conectar al bus de sesión: {}" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "No se ha detectado ninguna cámara en este sistema." #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "No se ha podido acceder a la cámara: {}" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "Se ha denegado el acceso a la cámara." #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "No se ha podido abrir la transmisión de la cámara." #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "No se ha recibido ningún descriptor de archivo de cámara." #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "No se ha podido abrir la cámara: {}" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "Escanear el código QR" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "Apunta la cámara hacia un código QR..." #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "Error en el proceso de la cámara: {}" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" "Aún no se ha detectado ningún código QR.\n" "Asegúrate de que el código esté bien iluminado, nítido y ocupe la mayor " "parte del encuadre." #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" "No se ha podido detectar el código QR.\n" "Prueba a importar el contacto desde el portapapeles." #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "¡Encontrado un código QR!" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "Error de la cámara: {}" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "Error de la cámara" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "De acuerdo" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "Quitar" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "Bluetooth desactivado" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "Activa el Bluetooth en los ajustes del sistema para conectarte" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "No hay dispositivos emparejados" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "Utiliza el botón inferior para emparejar un nuevo dispositivo" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "Sin dispositivos USB" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "Conecte un dispositivo MeshCore complementario mediante USB." #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "Eliminar compañero" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" "¿Eliminar {} ({})? Tendrás que volver a emparejarlo para volver a conectarte." #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "Borrar los datos almacenados" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "Error en la conexión usb" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "Acceso al usb denegado" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" "No se puede acceder a {}.\n" "\n" "Se necesita una regla de udev para permitir el acceso a los dispositivos " "serie USB. Haz clic en «Instalar regla» para instalarla (se requiere la " "contraseña de administrador)." #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "Instalar regla" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" "No se puede acceder a {}.\n" "\n" "Se necesita una regla de udev para permitir el acceso a los dispositivos " "serie USB. Ejecuta el siguiente comando en un terminal:\n" "\n" "{}" #: src/views/connection_view.py:270 msgid "Close" msgstr "Cerrar" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "Comando copy" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "Comando copiado al portapapeles" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "Error en la instalación" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" "No se ha podido instalar la regla de udev:\n" "{}" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "No hay compañeros guardados" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "Utiliza el botón inferior para añadir uno" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "¿Eliminar el complemento TCP {}?" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "Introduce la dirección IP y el puerto de tu dispositivo MeshCore." #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "Nombre de host / Dirección IP" #: src/views/connection_view.py:436 msgid "Port" msgstr "Puerto" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "Emparejar nuevo compañero" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "Buscar dispositivos MeshCore cercanos" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "Par" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "Introduce el pin de emparejamiento" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "Introduce el pin que aparece en tu dispositivo MeshCore." #: src/views/connection_view.py:603 msgid "PIN" msgstr "PIN" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "Añadir manualmente" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "Importar desde el portapapeles" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "Todo" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "Favoritos" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "Usuarios" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "A–Z" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "Escuchado recientemente" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "Últimos mensajes" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "Lo más destacado primero" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "{visible}/{total} contactos" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "{total} contactos" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "{total} contacto" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" "Aún no se han recibido nuevos contactos . Deja la aplicación abierta y los " "contactos aparecerán a medida que se reciban sus anuncios ." #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "Última conexión" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "Distancia" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "{visible}/{total} encontrados" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "{total} encontrados" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "Introduce los datos de contacto." #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "Tipo de contacto" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "Sala de servidores" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "Clave pública (64 caracteres hexadecimales)" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "Este contacto ya figura en tu lista." #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "Error al importar" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "Sin enlace meshcore:// en el portapapeles." #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "Clave secreta de canal no válida en el código QR." #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "El canal privado debe tener 16 bytes; se ha obtenido {}." #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "Clave pública no válida en el código QR." #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "La clave pública debe tener 32 bytes; se ha obtenido {}." #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" "No se han podido analizar los datos del código QR:\n" "{}" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "Código QR escaneado" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "Clave pública: {}...{}" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "Iniciar sesión en {}" #: src/views/contacts_view.py:678 msgid "Password" msgstr "Contraseña" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "Guardar contraseña" #: src/views/contacts_view.py:702 msgid "Login" msgstr "Acceso" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "Inicio de sesión correcto" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "Error al iniciar sesión: contraseña incorrecta" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "Se ha agotado el tiempo de espera de la sesión" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "Volviendo a intentarlo mediante inundación..." #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "Iniciando sesión..." #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "Buscando…" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "Ruta encontrada" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "Sin respuesta" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "Cargando…" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "No se ha almacenado ninguna ruta en caché." #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "No compatible" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "La función ping no es compatible con los hash de ruta de 3 bytes" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "Ping {}: {}" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "Ping {}: tiempo de espera agotado" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "Solicitando..." #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "Sin respuesta (se ha agotado el tiempo de espera)" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "Sin telemetría" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "Telemetría recibida" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "Telemetría — {}" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "{} — Dirección" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "Ruta a {}" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "Mi dispositivo" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "Repetidor desconocido" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "Información del contacto" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "Favorito" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "Fijar en la parte superior de la lista de contactos" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "Tipo" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "Clave pública copiada" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "Nunca" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "Ruta de salida" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" "Saltos hexadecimales separados por comas (por ejemplo, 9a,74 o 9a3b,744c " "para 2 bytes)" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "Restablecer ruta" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "Despeje el camino establecido y cambie a inundación." #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "Inundación forzada" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "Transmita siempre, nunca utilice una ruta establecida." #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "Mostrar ruta en el mapa" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "Visualiza la ruta del mensaje en el mapa" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "Permitir solicitudes de telemetría" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" "Permitir que este contacto consulte la batería, el voltaje y la temperatura." #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "Incluir ubicación" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "Incluir las coordenadas GPS en la telemetría" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "Incluir sensores ambientales" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "Incluir datos sobre la humedad, la presión y la calidad del aire" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "Consultar la telemetría de este contacto" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "Gestionar la sala" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "Inicia sesión y accede a la CLI, al estado y a la configuración" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "Gestión de repetidores" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "Estado de inicio de sesión y acceso, CLI, vecinos y configuración" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "Ping" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "Mide el tiempo de respuesta de este nodo." #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "Descubre las rutas" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "Buscar rutas hacia este nodo mediante el algoritmo de inundación" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "Eliminar contacto" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "¿Eliminar contacto?" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "¿Eliminar {} y todos los mensajes?" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "Repeticiones escuchadas" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "Reenviar" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "Borrar" #: src/views/channels_view.py:122 msgid "Reply" msgstr "Responder" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "Copiar texto" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "Ver rutas de mensajes" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "Compartir ubicación" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "Compartir la ubicación desde el mapa" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "Abrir en la aplicación Mapas" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "Has compartido un contacto:" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "{name} te ha enviado un contacto:" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "Ya añadido" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "Has compartido tu ubicación:" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "{name} ha compartido una ubicación:" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "Mensajes nuevos" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "Ubicación compartida" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "Compartida" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "Compartir" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "Ubicación no disponible" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "Tu acompañante no tiene señal de GPS." #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "Rutas de los mensajes" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" "No hay información de la ruta disponible. Los datos de ruta solo se " "registran para los mensajes recibidos mientras se está conectado." #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "Mensaje privado" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "No se utilizaron repetidores" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "No hay datos sobre el repetidor" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" "Los datos de la ruta solo se registran a través de LOG_DATA mientras se está " "conectado" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "Ruta del mensaje" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "Aún no se han escuchado repeticiones." #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "Público" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "Hashtag" #: src/views/channels_view.py:1671 msgid "Private" msgstr "Privado" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "{visible}/{total} canales" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "{total} canales" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "{total} canal" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "{name} ({region})" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "Información del canal" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "Clave privada" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "Clave privada copiada en el portapapeles" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "Notificaciones" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "Nivel de notificación" #: src/views/channels_view.py:1935 msgid "Default" msgstr "Por defecto:" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "Región" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "Limitar los mensajes de inundación a una región concreta" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "Sinregiones definidas" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "Añade regiones en «Configuración» para asignarlas a los canales" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "Eliminar el canal" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "¿Eliminar canal?" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "¿Quitar\" {}\" y todos sus mensajes?" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "No hay plazas disponibles" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "Los 8 canales están ocupados." #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" "Se generará una clave PSK aleatoria. Compártela con otros para que puedan " "unirse." #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "Nombre del canal" #: src/views/channels_view.py:2070 msgid "Create" msgstr "Crear" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "Introduce el nombre del canal y la clave privada que te han facilitado." #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "Clave privada (32 caracteres hexadecimales)" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "¡Únete!" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "Ya me uní" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "¡Ya estás en el canal público!." #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" "Escribe un nombre para el hashtag. Cualquiera que utilice ese mismo hashtag " "podrá comunicarse." #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "Hashtag (p. ej. #general )" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "La clave pública del dispositivo no está disponible" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "La biblioteca de códigos QR no está disponible" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "Tu código QR de contacto" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "Escanea este código QR para añadir este contacto" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "¿Restaurar de fábrica?" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" "Esto borrará de forma permanente TODOS los datos del dispositivo asociado, " "incluidos los contactos, los mensajes, los canales, la configuración y la " "identidad del dispositivo (claves). Esta acción no se puede deshacer." #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "¿Reiniciar el dispositivo?" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "El dispositivo se desconectará y se reiniciará." #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "Reiniciar" #: src/views/device_view.py:323 msgid "Not set" msgstr "Sin configurar" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "Ubicación (configurada manualmente)" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "Ubicación (GPS)" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "Ubicación (GPS activado, sin señal)" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "Ubicación (GPS desactivado)" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "Solicitando…" #: src/views/device_view.py:405 msgid "Device Location" msgstr "Ubicación del dispositivo" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "{days}día(s)" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "{hours}hora(s)" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "{mins}minuto(s)" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "{} mensajes" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "{count} error(es)" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "Conectado" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" "La función «Trace Path» no es compatible con los hash de rutas de 3 bytes" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "p. ej. aa,bb,cc" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "p. ej., aabb,ccdd" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "p. ej., aabbcc,ddeeff" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "Añadir desde contactos" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "No hay repetidores ni salas en los contactos" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "No hay repetidores cuya ubicación se conozca" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "Ruta" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "Ejecutar rastreo" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "Introduce los hash de ruta y pulsa «Ejecutar rastreo»" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "Rastreo completado: {} saltos, {:.1f} s" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "El seguimiento ha expirado" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "Valor hexadecimal no válido en la ruta" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "Cargando..." #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "Escaneando... Quedan {}segundo(s)" #: src/views/device_view.py:977 msgid "Listening..." msgstr "Escuchando..." #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "A la espera de que respondan los nodos cercanos" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "Añadir a los contactos" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "Añadido {}" #: src/views/device_view.py:1066 msgid "Node" msgstr "Nodo" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "Hecho. Se han encontrado {} nodos." #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "Hecho. Se ha encontrado el nodo {}." #: src/views/device_view.py:1222 msgid "Size" msgstr "Tamaño" #: src/views/device_view.py:1222 msgid "bytes" msgstr "bytes" #: src/views/device_view.py:1226 msgid "hops" msgstr "saltos" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "Rutas hash" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "byte por salto" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "Hay una actualización disponible: {version}" #: src/views/settings_view.py:131 msgid "Custom" msgstr "Personalizado" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "Rechazar todo" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "Permitir por contacto" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "Permitir todo" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "Configuración aplicada en el dispositivo" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "Error desconocido" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "Error de configuración: {}" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "Potencia de transmisión (dBm, máx. {})" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "El campo ya está rellenado; haz clic en «Aplicar» para guardar" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "Ubicación no disponible" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "Seleccionar ubicación" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "Establecer ubicación" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "Conéctate primero a un dispositivo" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "Sin repetidores en los contactos" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "Mostrar las regiones" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "Consultando {} repetidores..." #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "Consultando {} repetidor..." #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "Esperando..." #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "Consultando repetidores por regiones" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "Añadir seleccionados" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "Se encontraron {} regiones" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "Se encontró {} región" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "Sin regiones" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "de {}" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "(ya añadido)" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "Regiones {} añadidas" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "Región {} añadida" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "Exportar copia de seguridad" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "Guardar en un archivo" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "Importar copia de seguridad" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "Restaurar desde un archivo" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "Sin conexión" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "Primero conéctese a un dispositivo para exportar una copia de seguridad" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "Archivos json" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "¡Copia de seguridad exportada correctamente!" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "Error al exportar: {}" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "Primero conéctese a un dispositivo para importar una copia de seguridad" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "No se ha podido leer el archivo de copia de seguridad: {}" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "Fuente: {}" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "Configuración del dispositivo: nombre, radio, ubicación" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "Contactos: {} ({} nuevo)" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "Canales: {} ({} nuevo(s))" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "Mensajes: {}" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "Mensajes del canal: {}" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "Contraseña(s) guardada(s): {}" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "Solo contactos y canales" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "Todo" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "{} contactos" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "{} canales" #: src/views/settings_view.py:1208 msgid "messages" msgstr "mensajes" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "Importado: {}" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "No hay nada nuevo que importar" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "{d}día(s)" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "{h}hora(s)" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "{m}minuto(s)" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "{secs}segundo(s)" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "hace {}segundo(s)" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "hace {}minuto(s)" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "hace {}hora(s)" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "hace {}día(s)" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "Sincronización horaria enviada al repetidor" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "¿Restablecer el reloj y reiniciar?" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" "El reloj del repetidor está adelantado con respecto a la hora actual.\n" "El firmware no permite retrasar la hora.\n" "\n" "Esto restablecerá el reloj y reiniciará el repetidor." #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "Reiniciando..." #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "Reinicio del reloj y reinicio enviados al repetidor" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "La solicitud ha expirado" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "{} de {} vecinos" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "Mapa del vecindario" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "Administración" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "{} entrada" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "{} entradas" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "¿Eliminar de la lista ACL?" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "¿Eliminar {} de la lista de control de acceso?" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "Añadir al control de acceso" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "Selecciona un contacto y un cargo." #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "Cargo o función" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "Global (comodín)" #: src/views/repeater_view.py:942 msgid "Home" msgstr "Inicio" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "Inundación permitida" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "Inundación denegada" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "Rechazar Flood" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "Permitir inundación" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "Establecer como página de inicio" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "Elimina primero las regiones secundarias" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "Cambios sin guardar" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "Tienes cambios en la región sin guardar." #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "Descartar" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "Añadir una región" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "Introduce un nombre y, si lo deseas, selecciona una región principal." #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "Nombre de la región" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "Principal" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" "La contraseña de administrador ha cambiado; es necesario iniciar sesión de " "nuevo." #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "Configuración enviada a {}" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "¿Reiniciar el repetidor?" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "¿Reiniciar {}? Estará fuera de servicio durante unos instantes." #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "La hora no es correcta" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "Enviando" #: src/views/map_view.py:99 msgid "User" msgstr "Usuario" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "{} de {} contactos en el mapa" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "{} de {} en el mapa" #: src/views/map_view.py:198 msgid "Rooms" msgstr "Salas" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "Nodos detectados" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "y {} más" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "La ubicación es meramente ilustrativa; se desconoce la ubicación real" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "SNR: {:.1f} dB" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "Seleccione Traceroute" #: src/views/map_view.py:1159 msgid "Undo" msgstr "Deshacer" #: src/views/map_view.py:1169 msgid "Clear" msgstr "Borrar" #: src/views/map_view.py:1177 msgid "Done" msgstr "Hecho" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "Pulsa en los repetidores para trazar la ruta" meshy/po/et.po000066400000000000000000001746001521052255700136050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: et\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/fi.po000066400000000000000000001746001521052255700135730ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/fr.po000066400000000000000000002241551521052255700136050ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: 2026-05-20 19:07+0000\n" "Last-Translator: lebeno \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 2026.5\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "MeshCore client de réseau maillé" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "mesh;lora;radio;chat;" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy est un client pour les appareils MeshCore LoRa en réseau maillé. " "Connectez-vous à votre appareil compagnon via Bluetooth pour envoyer et " "recevoir des messages chiffrés, gérer les contacts, configurer les canaux et " "surveiller votre réseau maillé." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Caractéristiques:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Connectivité Bluetooth LE et USB série" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Messagerie chiffrée de bout en bout" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "Canaux publics, privés et hashtag" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "Gestion des contacts avec suivi de localisation" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "Vue cartographique interactive avec traçage d'itinéraire" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "Gestion des appareils et des répéteurs" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "Scan de code QR pour échange rapide de contacts" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "Jiri Eischmann" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "Vue des contacts avec messagerie" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "Liste des canaux avec différents types de canaux" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "Vue cartographique affichant les positions des contacts" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "Informations et paramètres appareil" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "Général" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "Quitter" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "Raccourcis clavier" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "Navigation" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "Appareil" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "Contacts" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "Canaux" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "Carte" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "Paramètres" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "Messagerie" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "Rechercher des Contacts" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "Concentrer Saisie du Message" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "Contact/Canal Précédent" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "Contact/Canal Suivant" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "Non Lu Précédent" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "Non Lu Suivant" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "Déconnecter" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "À propos de Meshy" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "Connecter" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "Menu" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "De Liaison…" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "Afficher la Navigation" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "Non Connecté" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "Statut" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "Déconnecté" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "Informations sur l'Appareil" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "Nom du Nœud" #: data/ui/device-view.ui:53 msgid "Board" msgstr "Carte" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "Firmware" #: data/ui/device-view.ui:63 msgid "Update" msgstr "Mise à Jour" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "Ouvrir le flasheur du firmware" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "Clé Publique" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "Télémétrie" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "Demande de télémétrie" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "Position" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "Synchronisation…" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "Afficher sur la carte" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "Batterie & Stockage" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "Batterie" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "Stockage" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "Configuration Radio" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "Fréquence" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "Débit" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 #, fuzzy msgid "Spreading Factor" msgstr "Facteur d'Étalement" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "Puissance TX" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "Taille Hash Chemin Défaut" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "Statistiques" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "Temps de Disponibilité" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "Liste de messages" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "Plancher de Bruit" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "Dernier RSSI" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "Dernier SNR" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "Temps d'Antenne" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "Paquets" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "Flood" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "Direct" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "Rafraîchir les statistiques" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "Application" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "Configurer le comportement et l'apparence de l'application" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "Thème" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "Choisir l'apparence de l'application" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "Thème du système" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "Clair" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "Sombre" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "Palette du thème" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "Meshcore Dark" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "Notifications de Canal" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "Niveau de notification par défaut pour tous les canaux" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "Tous les messages" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "Mentions uniquement" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "Aucun" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "Actif en arrière-plan" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "Continuer de recevoir des messages après la fermeture de la fenêtre" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "Démarrage automatique" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "Démarrage automatique à la connexion" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "Radio" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "Paramètres LoRa et pré-réglages régionaux" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "Pré-réglages régionaux" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "Configuration de Préréglage" #: data/ui/settings-view.ui:90 #, fuzzy msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "Fréquence, Débit, facteur d'étalement, taux de codage" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "Fréquence (MHz)" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "Débit (kHz)" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "Mode répéteur" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "Utiliser comme répéteur portatif sur une fréquence dédiée" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "Puissance TX (dBm)" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "Annonce" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "Configurer comment votre nœud s'annonce sur le maillage" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "Nom de l'appareil" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "Inclure Position dans Annonce" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "Diffusez votre position aux autres nœuds" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "1-byte (64 rebonds maximum)" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "2-byte (32 rebonds maximum)" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "3-byte (21 rebonds maximum)" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "Indiquer la position de l'appareil par GPS ou manuellement" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "GPS de l'appareil" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" "Activer le GPS de l'appareil, la position sera mise à jour automatiquement" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "Indiquer la position manuellement" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "Choisir une autre méthode pour définir la position, elle sera fixe" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "Latitude" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "Longitude" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "Utiliser ma position" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "Utiliser la position de l'ordinateur" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "Indiquer sur une carte" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "Choisir une position sur la carte" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "Ajout Automatique de Contacts" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "Ajouter automatiquement les nœuds découverts via les annonces" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "Paramètres Ajout Automatique" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "Types d'Appareil, politique de remplacement, limite de saut" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "Nœuds de Chat" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "Répéteurs" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "Serveurs de Salon" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "Capteurs" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "Écraser le plus ancien lorsque pleine" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "Limite de sauts" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "0-63, vide pour ne pas imposer de limite" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "Confidentialité de la Télémétrie" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "Contrôlez ce que les autres peuvent interroger depuis votre appareil" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "Batterie & Capteurs" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "Batterie, tension, température, courant" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "Coordonnées GPS" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "Environnement" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "Humidité, pression, qualité de l'air" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "Régions" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" "Limiter la portée Flood des messages de canal à des régions spécifiques" #: data/ui/settings-view.ui:373 #, fuzzy msgid "Add Region (e.g. Prague)" msgstr "Ajouter Région (ex. Paris)" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "Portée par Défaut" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" "Tous les paquets Flood auront pour portée cette région si elle est définie." #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "Bluetooth" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" "Après avoir changé le code PIN, redémarrez l'appareil, puis désappairez et " "réappairez. La valeur 0 réinitialise le code PIN à la valeur par défaut." #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "Code PIN d'Appariement" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" "Connectez-vous à un appareil compagnon pour commencer à utiliser Meshy." #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "Associer à un Nouveau Compagnon" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "Série USB" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "WiFi / TCP" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "Ajouter un Compagnon TCP" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "Aucun contact" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "Ajouter un Contact" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "Sélectionner un Contact" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "Choisissez un contact dans la liste pour commencer à discuter" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "↑ Nouveaux Messages" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "Tapez un message..." #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "Créer un Canal Privé" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "Rejoindre un Canal Privé" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "Rejoindre le Canal Public" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "Rejoindre un Canal Hashtag" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "Aucun Canal" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "Ajouter un Canal" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "Sélectionner un Canal" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "Choisissez un canal dans la liste" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "Canal de message..." #: data/ui/repeater-view.ui:37 msgid "System" msgstr "Système" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "Horloge à la Connexion" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "Synchroniser Maintenant" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "Réinitialiser et Redémarrer" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "Événements d'Erreur" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "Temps d'Antenne TX" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "Temps d'Antenne RX" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "Utilisation du Canal" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "Cycle d'utilisation" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "Envoyé" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "Reçu" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "Erreurs de Réception" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "Doublons" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "Actualiser" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "CLI" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "Commande CLI..." #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "Voisins" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "Charger plus" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "Afficher sur la Carte" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "Contrôle d'Accès" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "Ajouter" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "Chargement des Régions..." #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "Réessayer" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "Région par défaut" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" "Les paquets Flood auront pour portée la région fournie. Seuls les répéteurs " "autorisant la région transmettront les paquets avec cette portée. Laisser " "vide pour une portée non définie." #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "Appliquer" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "Basique" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "Récupérer" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "Sauvegarder" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "Nom" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "Répétition de Paquet" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "Mots de passe" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "Mot de passe administration" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "Mot de passe invité" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "Annonce" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "Intervalle local (min)" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "Intervalle Flood(hrs)" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "Zone de danger" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "Redémarrer le répéteur" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "Choisir un répéteur" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "Déconnexion" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "Envoyer Annonce" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "Zéro Saut" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "Diffuser Localement" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "Routé en Flood" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "Diffusé via le maillage" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "Copier dans le Presse-Papiers" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "Copier mes données de contact" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "Afficher le Code QR" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "Pour que d'autres puissent scanner et vous ajouter" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "Tracer Chemin" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "Tracer une route et afficher la qualité du signal par saut" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "Découvrir les Nœuds à Proximité" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "Analysez le réseau pour les nœuds à proximité" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "Journal Rx" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "Afficher les paquets radio reçus" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "Redémarrer l'Appareil" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "Effacer le Journal" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "Aucun Paquet" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "Les paquets radio reçus s'afficheront ici" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "Sauvegarde" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "Sauvegarder la configuration et les messages dans un fichier." #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "Restauration" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "Restauration depuis un fichier de sauvegarde" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "Paramètres d'usine" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "Efface toutes les données de l'appareil" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "Découvrir Les Contacts" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "Recherche" #: data/ui/discover-dialog.ui:27 #, fuzzy msgid "Filter by type" msgstr "Filtre" #: data/ui/discover-dialog.ui:36 #, fuzzy msgid "Sort by" msgstr "Tri" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "Choisir un thème" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "Tchat" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "Répéteur" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "Salon" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "Capteur" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "Inconnu" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" "Nom : {name}\n" "Type : {type}" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" "Type : {type}\n" "Clé : {key}" #: src/application.py:101 msgid "Import Contact" msgstr "Importer un Contact" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "Contact {}" #: src/application.py:105 msgid "imported" msgstr "importé" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "Lien meshcore:// non valide" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "Secret du canal invalide dans le lien" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "Secret du canal de longueur invalide" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" "Nom : {name}\n" "Type : Privé" #: src/application.py:130 msgid "Unnamed" msgstr "Sans nom" #: src/application.py:146 msgid "Invalid public key in link" msgstr "Clé publique du lien non valide" #: src/application.py:149 msgid "Invalid public key length" msgstr "Longueur de la clé publique non valide" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "{} est déjà dans votre liste" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "Contact" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "Ajouter {} Contact" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" "Nom: {name}\n" "Clé Publique: {key}…" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "Contact {} ajouté" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "Lien meshcore:// non reconnu" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "Annuler" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "Recevoir des messages en arrière-plan" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "Un client GTK pour les appareils de réseau maillé MeshCore LoRa" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "Icônes Symboliques Adwaita" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "Rechercher des Appareils" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "Recherche de périphériques MeshCore..." #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" "L'appareil n'a pas répondu. Il se peut qu'il ne prenne pas en charge ce type " "de connexion." #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "Reconnexion ({current}/{total})..." #: src/connection_controller.py:483 #, fuzzy msgid "Syncing channels" msgstr "Aucun Canal" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "Vous avez reçu {} message(s) alors que vous étiez déconnecté." #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "Erreur de l'appareil: {}" #: src/frame_handler.py:202 src/frame_handler.py:217 #, fuzzy msgid "Syncing contacts" msgstr "Aucun contact" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "{}s (en retard)" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "{}ms (en retard)" #: src/frame_handler.py:352 msgid "late" msgstr "en retard" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "L'horloge de l'appareil était en retard de {}min — synchronisée" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "L'horloge de l'appareil est en avance de {}min sur celle du système" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "Canal {}" #: src/frame_handler.py:886 msgid "Someone" msgstr "Une personne" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "{sender} : {text}" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "Moi" #: src/message_controller.py:171 msgid "flood" msgstr "Flood" #: src/message_controller.py:173 msgid "direct" msgstr "direct" #: src/message_controller.py:175 msgid "path" msgstr "chemin" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "– {route}" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "({attempt}/{total}) – {route}" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "en attente {:.1f}s (radio occupée)" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "Filtre" #: src/window.py:473 msgid "Sort" msgstr "Tri" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "Actions" #: src/window.py:536 msgid "Channel" msgstr "Canal" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "{name} ({path})" #: src/window.py:687 msgid "Management" msgstr "Gestion" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "Annonce envoyée ({})" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "Canal \"{}\" ajouté" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "Aucun emplacement de canal vide disponible" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "Impossible de se connecter à un bus session : {}" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "Aucune caméra détectée sur ce système." #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "Impossible de se connecter à la caméra : {}" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "Permission d'accès à la caméra refusée" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "Impossible d'ouvrir un flux caméra." #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "Impossible d'ouvrir la caméra : {}" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "Scanner le code QR" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" "Impossible de détecter le code QR.\n" "Essayez d'importer le contact via le presse-papiers à la place." #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "Activer Bluetooth dans les paramètres système pour se connecter" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "Aucuns compagnons sauvegardés" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "{visible}/{total} contacts" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "{total} contacts" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "{total} contact" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" "Aucun nouveau contact n'a encore été détecté. Laissez l'application " "fonctionner et les contacts apparaîtront au fur et à mesure que leurs " "annonces seront reçues." #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, fuzzy, python-brace-format msgid "{visible}/{total} discovered" msgstr "{visible}/{total} contacts" #: src/views/contacts_view.py:438 #, fuzzy, python-brace-format msgid "{total} discovered" msgstr "{total} contact" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "Entrez les détails du contact." #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "Type de Contact" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "Ce contact est déjà dans votre liste." #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "Secret du canal doit être de 16 octets, reçu {}." #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "Se connecter à {}" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "Sauvegarder le mot de passe" #: src/views/contacts_view.py:702 msgid "Login" msgstr "Se Connecter" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "Connexion réussie" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "Échec de la connexion — mot de passe incorrect" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "La connexion a expiré" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "Connexion en cours..." #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "Chemin trouvé" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "Pas de chemin en cache" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "Ping n'est pas supporté avec les hachages de chemin de 3 octets" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "Chemin vers {}" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "Informations de Contact" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "Épingler en haut de la liste de contacts" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "Chemin Sortant" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "Réinitialiser Chemin" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "Effacer le chemin établi et passer au flood" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "Toujours diffuser, ne jamais utiliser un chemin établi" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "Afficher Chemin sur la Carte" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" "Autoriser ce contact à interroger la batterie, la tension et la température" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "Inclure Position" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "Interroger les données télémétriques de ce contact" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "Se connecter et accéder à CLI, statut et paramètres" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "Se connecter et accéder à statut, CLI, voisins et paramètres" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "Mesurer le temps aller-retour vers ce nœud" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "Découvrir Chemins" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "Trouver des routes vers ce nœud via Flood" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "Supprimer le Contact" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "Supprimer le Contact?" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "Afficher Chemins des Messages" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 #, fuzzy msgid "Share Location from Map" msgstr "Indiquer la position manuellement" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "Chemins des Messages" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" "Aucune information de chemin disponible. Les données de chemin sont " "uniquement capturées pour les messages reçus lors de la connexion." #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" "Les données de chemin sont uniquement capturées via LOG_DATA lors de la " "connexion" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "Chemin du Message" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "{visible}/{total} canaux" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "{total} canaux" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "{total} canal" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "Informations du Canal" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "Supprimer le Canal" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "Supprimer le Canal?" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "Tous les 8 emplacements de canal sont utilisés." #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "Nom du Canal" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "Entrez le nom du canal et la clé privée qui vous a été partagée." #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "Vous êtes déjà sur le canal public." #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "Votre Code QR de Contact" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "Scannez ce code QR pour ajouter ce contact." #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" "Cela effacera définitivement TOUTES les données de l'appareil compagnon, y " "compris les contacts, les messages, les canaux, les paramètres et l'identité " "de l'appareil (clés). Cette action est irréversible." #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "Position (définie manuellement)" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "Position (GPS)" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "Position (GPS activé, pas de fix)" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "Position (GPS désactivé)" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "Position Appareil" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" "Traçage de Chemin n'est pas supporté avec les hachages de chemin de 3 octets" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "Ajouter depuis Contacts" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "Aucun répéteur avec position connue" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "Chemin" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "Entrez les hachages de chemin et appuyez sur Exécuter Traçage" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "Hex invalide dans le chemin" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "En attente de la réponse des nœuds à proximité" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "Ajouter aux Contacts" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "Nœud" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "Terminé. {} nœuds trouvés." #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "Terminé. {} nœud trouvé." #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "Hachages de Chemin" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "Autoriser par Contact" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "Paramètres appliqués sur l'appareil" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "Erreur Paramètres: {}" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "Puissance TX (dBm, max {})" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "Position remplie, cliquer sur Appliquer pour sauvegarder" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "Position indisponible" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "Choisir Position" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "Définir Position" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "Aucun répéteur dans les contacts" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "Sauvegarder dans un fichier" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "Paramètres Appareil: nom, radio, position" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "Contacts: {} ({} nouveaux)" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "Canaux: {} ({} nouveaux)" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "Messages de canal : {}" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "Mots de passe sauvegardés: {}" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "Contacts & Canaux Seulement" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "{} contacts" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "{} canaux" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" "L'horloge du répéteur est en avance sur l'heure actuelle.\n" "Le firmware n'autorise pas de régler l'heure en arrière.\n" "\n" "Cela réinitialisera l'horloge et redémarrera le répéteur." #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "Sélectionnez un contact et un rôle." #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "Modifications non sauvegardées" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "Vous avez des modifications de région non sauvegardées." #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "Mot de passe administrateur modifié — reconnexion requise" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "Paramètres envoyés à {}" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "{} de {} contacts sur la carte" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 #, fuzzy msgid "Discovered Nodes" msgstr "Découvrir les Nœuds à Proximité" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "SNR: {:.1f} dB" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" #, python-brace-format #~ msgid "Syncing channels: {index}/{total}" #~ msgstr "Synchronisation des canaux : {index}/{total}" #, python-brace-format #~ msgid "Syncing contacts: {current}/{total}" #~ msgstr "Synchronisation des contacts : {current}/{total}" meshy/po/hu.po000066400000000000000000001746001521052255700136110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/it.po000066400000000000000000001746001521052255700136110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/lt.po000066400000000000000000001747761521052255700136320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10 == 1 && (n % 100 < 11 || n % 100 > " "19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? " "1 : 2);\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/lv.po000066400000000000000000001747331521052255700136250ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: lv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100 <= " "19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2);\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/meshy.pot000066400000000000000000001745541521052255700145160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/meson.build000066400000000000000000000002431521052255700147660ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later i18n.gettext('meshy', preset: 'glib') meshy/po/nl.po000066400000000000000000002347021521052255700136060ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: 2026-05-17 00:53+0000\n" "Last-Translator: lebeno \n" "Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.17.1\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "MeshCore mesh netwerk cliënt" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "mesh;lora;radio;chat;" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy is een cliënt voor MeshCore LoRa mesh netwerk apparaten. Verbind jouw " "companion apparaat over Bluetooth om geëncrypteerde berichten te zenden en " "ontvangen, beheer contacten, configureer kanalen, en monitor jouw mesh " "netwerk." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Kenmerken:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Bluetooth LE en USB seriële verbinding" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Eind-tot-eind versleutelde berichtgeving" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "Openbare, privé- en hashtagkanalen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "Contactbeheer met locatievolgen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "Interactieve kaartweergave met routeopvolging" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "Apparaat- en repeaterbeheer" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "QR-code scannen voor snel contact uitwisselen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "Jiri Eischmann" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "Contactenoverzicht met berichtgeving" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "Kanalenlijst met verschillende kanaalsoorten" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "Kaartweergave met contactlocaties" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "Apparaatinformatie en instellingen" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "Algemeen" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "Afsluiten" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "Sneltoetsen" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "Navigatie" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "Apparaat" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "Contacten" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "Kanalen" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "Kaart" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "Instellingen" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "Berichten" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "Contacten Zoeken" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "Focus berichtinvoer" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "Vorig Contact/Kanaal" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "Volgend Contact/Kanaal" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "Vorig Ongelezen" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "Volgend Ongelezen" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "Verbinding verbreken" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "Over Meshy" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "Verbinden" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "Menu" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "Verbinding maken…" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "Navigatie tonen" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "Niet verbonden" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "Status" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "Niet verbonden" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "Apparaatinformatie" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "Knooppuntnaam" #: data/ui/device-view.ui:53 msgid "Board" msgstr "Bord" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "Firmware" #: data/ui/device-view.ui:63 msgid "Update" msgstr "Update" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "Open firmware flasher" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "Openbare sleutel" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "Telemetrie" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "Telemetrie aanvragen" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "Locatie" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "Synchroniseren…" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "Op kaart tonen" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "Batterij & Opslag" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "Batterij" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "Opslag" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "Radioconfiguratie" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "Frequentie" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "Bandbreedte" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "Spreidingsfactor" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "Coderingssnelheid" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "TX Vermogen" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "Standaard pad-hashgrootte" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "Statistieken" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "Uptime" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "Berichtenwachtrij" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "Ruisvloer" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "Laatste RSSI" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "Laatste SNR" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "Zendduur" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "Pakketten" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "Flood" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "Direct" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "Statistieken vernieuwen" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "Toepassing" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "Configureer het gedrag en uiterlijk van de toepassing" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "Stijl" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "Kies het uiterlijk van de toepassing" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "Volg Systeem" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "Licht" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "Donker" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "Palletthema" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "MeshCore Donker" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "Kanaalmeldingen" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "Standaard meldingsniveau voor alle kanalen" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "Alle berichten" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "Alleen Vermeldingen" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "Geen" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "Op de achtergrond uitvoeren" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "Blijf berichten ontvangen nadat het venster is gesloten" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "Automatisch starten" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "Automatisch starten bij aanmelden" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "Radio" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "LoRa-radioparameters en regionale presets" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "Regionale Preset" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "Presetconfiguratie" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "Frequentie, bandbreedte, spreidingsfactor, coderingssnelheid" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "Frequentie (MHz)" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "Bandbreedte (kHz)" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "Repeat mode" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "Als een draagbare repeater werken op een off-grid frequentie" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "TX Vermogen (dBm)" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "Aankondiging" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "Configureer hoe jouw knooppunt zichzelf aankondigt op het mesh" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "Apparaatnaam" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "Locatie opnemen in Aankondiging" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "Zend je positie uit naar andere knooppunten" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "1-byte (max 64 hops)" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "2-byte (max 32 hops)" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "3-byte (max 21 hops)" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "Stel de positie van je companion in via GPS of handmatig" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "Companion GPS" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" "Schakel GPS in op de companion, je positie wordt automatisch bijgewerkt" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "Locatie handmatig instellen" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "Kies een andere methode om de locatie in te stellen; deze blijft vast" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "Breedtegraad" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "Lengtegraad" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "Gebruik Mijn Locatie" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "Instellen vanaf de locatie van deze computer" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "Kiezen op Kaart" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "Kies een locatie op de kaart" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "Contacten Automatisch Toevoegen" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "Voeg automatisch knooppunten toe die via aankondigingen worden ontdekt" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "Instellingen Automatisch Toevoegen" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "Aapparaattype, overschrijfbeleid, hoplimiet" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "Chat Knooppunten" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "Repeaters" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "Kamerservers" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "Sensoren" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "Oudste overschrijven wanneer vol" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "Hoplimiet" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "0–63, laat leeg voor geen limiet" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "Telemetrieprivacy" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "Bepaal wat anderen van jouw apparaat mogen opvragen" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "Batterij & Sensoren" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "Batterij, spanning, temperatuur, stroom" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "GPS-coördinaten" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "Omgeving" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "Vochtigheid, druk, luchtkwaliteit" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "Regio's" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "Beperk het bereik van kanaalberichten tot specifieke regio's" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "Regio toevoegen (bijv. nl)" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "Standaardbereik" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "Alle flood-pakketten worden aan deze regio gebonden als ingesteld" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "Bluetooth" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" "Na het wijzigen van de pincode: herstart het apparaat, vervolgens ontkoppel " "en koppel opnieuw. Instellen op 0 zet de pincode terug naar standaard." #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "Koppelingspincode" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "Maak verbinding met een companion apparaat om Meshy te gaan gebruiken." #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "Koppel met een Nieuwe Companion" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "USB Serieel" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "WiFi / TCP" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "TCP Companion Toevoegen" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "Zoeken..." #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "Geen contacten" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "Contact Toevoegen" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "Selecteer een Contact" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "Kies een contact uit de lijst om te beginnen met chatten" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "↑ Nieuwe Berichten" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "Typ een bericht..." #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "Privékanaal Maken" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "Deelnemen aan een Privékanaal" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "Deelnemen aan het Openbaar Kanaal" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "Deelnemen aan een Hashtagkanaal" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "Geen Kanalen" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "Kanaal Toevoegen" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "Selecteer een Kanaal" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "Kies een kanaal uit de lijst" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "Berichtkanaal..." #: data/ui/repeater-view.ui:37 msgid "System" msgstr "Systeem" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "Klok bij Aanmelding" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "Nu Synchroniseren" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "Resetten en Opnieuw Opstarten" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "Foutgebeurtenissen" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "TX Zendduur" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "RX Zendduur" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "Kanaalgebruik" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "Arbeidscyclus" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "Verzonden" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "Ontvangen" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "Ontvangstfouten" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "Duplicaten" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "Vernieuwen" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "Telemetrie Aanvragen" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "CLI" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "CLI-opdracht..." #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "Buren" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "Meer Laden" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "Weergeven op Kaart" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "Toegangscontrole" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "Toevoegen" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "Regio's worden geladen..." #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "Opnieuw proberen" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "Standaard Regiobereik" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" "Flood-pakketten worden aan de opgegeven regio gebonden. Alleen repeaters die " "de regio toestaan, zullen gebonden pakketten doorsturen. Laat leeg voor niet-" "gebonden." #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "Toepassen" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "Basis" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "Ophalen" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "Opslaan" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "Naam" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "Pakketherhaling" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "Wachtwoord" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "Beheerderswachtwoord" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "Gastwachtwoord" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "Aankondiging" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "Lokale interval (min)" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "Flood Interval (uren)" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "Gevarenzone" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "Herstart Repeater" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "Selecteer een Repeater" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "Afmelden" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "Aankondiging Verzenden" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "Zero Hop" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "Lokaal uitzenden" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "Flood Gerouteerd" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "Uitzenden door het mesh" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "Naar Klembord" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "Kopieer eigen contactgegevens" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "QR-code Weergeven" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "Zodat anderen je kunnen scannen en toevoegen" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "Pad Traceren" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "Traceer een route en toon signaalkwaliteit per hop" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "Ontdek Nabijgelegen Knooppunten" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "Scan het netwerk op nabijgelegen knooppunten" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "Rx Log" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "Bekijk ontvangen radiopakketten" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "Apparaat Opnieuw Opstarten" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "Log Wissen" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "Geen Pakketten" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "Ontvangen radiopakketten verschijnen hier" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "Back-up" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "Sla configuratie en berichten op in een bestand" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "Terugzetten" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "Terugzetten vanuit een back-upbestand" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "Fabrieksinstellingen Terugzetten" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "Wis alle gegevens op de companion" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "Ontdek contacten" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "Zoeken" #: data/ui/discover-dialog.ui:27 #, fuzzy msgid "Filter by type" msgstr "Filter" #: data/ui/discover-dialog.ui:36 #, fuzzy msgid "Sort by" msgstr "Sorteren" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "Kies Thema" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "Chat" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "Repeater" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "Kamer" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "Sensor" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "Onbekend" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" "Naam: {name}\n" "Type: {type}" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" "Type: {type}\n" "Sleutel: {key}…" #: src/application.py:101 msgid "Import Contact" msgstr "Contact Importeren" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "Contact {}" #: src/application.py:105 msgid "imported" msgstr "geïmporteerd" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "Ongeldige meshcore:/- link" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "Ongeldig kanaalgeheim in link" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "Ongeldige kanaalgeheimlengte" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" "Naam: {name}\n" "Type: Privé" #: src/application.py:130 msgid "Unnamed" msgstr "Naamloos" #: src/application.py:146 msgid "Invalid public key in link" msgstr "Ongeldige openbare sleutel in link" #: src/application.py:149 msgid "Invalid public key length" msgstr "Ongeldige lengte van de openbare sleutel" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "{} is al in jouw lijst" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "Contact" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "Contact {} Toevoegen" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" "Naam: {name}\n" "Openbare Sleutel: {key}…" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "Contact {} toegevoegd" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "Niet-herkende meshcore://-link" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "Annuleren" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "Berichten ontvangen op de achtergrond" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "Een GTK-client voor MeshCore LoRa mesh-netwerkapparaten" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "Adwaita Symbolische Iconen" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "Verbinden..." #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "Scannen Naar Apparaten" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "Scannen naar MeshCore apparaten..." #: src/connection_controller.py:140 msgid "Scan" msgstr "Scan" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "Scannen..." #: src/connection_controller.py:263 msgid "Stop" msgstr "Stop" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "Synchroniseren..." #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" "Apparaat reageerde niet. Het is mogelijk dat dit verbindingstype niet wordt " "ondersteund." #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "Opnieuw verbinding maken ({current}/{total})..." #: src/connection_controller.py:483 #, fuzzy msgid "Syncing channels" msgstr "Geen Kanalen" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "Je hebt {} bericht(en) ontvangen terwijl de verbinding was verbroken." #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "Apparaatfout: {}" #: src/frame_handler.py:202 src/frame_handler.py:217 #, fuzzy msgid "Syncing contacts" msgstr "Geen contacten" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "{}s (later)" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "{}ms (later)" #: src/frame_handler.py:352 msgid "late" msgstr "later" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "Apparaatklok liep {}min achter — gesynchroniseerd" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "Apparaatklok loopt {}min voor op het systeem" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "Kanaal {}" #: src/frame_handler.py:886 msgid "Someone" msgstr "Iemand" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "{sender}: {text}" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "Ik" #: src/message_controller.py:171 msgid "flood" msgstr "flood" #: src/message_controller.py:173 msgid "direct" msgstr "direct" #: src/message_controller.py:175 msgid "path" msgstr "pad" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "– {route}" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "({attempt}/{total}) – {route}" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "wachten {:.1f}s (radio bezet)" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "Filter" #: src/window.py:473 msgid "Sort" msgstr "Sorteren" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "Acties" #: src/window.py:536 msgid "Channel" msgstr "Kanaal" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "Back-up & Terugzetten" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "{name} ({path})" #: src/window.py:687 msgid "Management" msgstr "Beheer" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "Gast" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "{name} — {role}" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "flood gerouteerd" #: src/window.py:1014 msgid "zero-hop" msgstr "zero-hop" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "Aankondiging verzonden ({})" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "Kanaal \"{}\" toegevoegd" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "Geen lege kanaalslots beschikbaar" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "Kan geen verbinding maken met sessiebus: {}" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "Geen camera gedetecteerd op dit systeem." #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "Kon geen toegang krijgen tot de camera: {}" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "Toegang tot de camera is geweigerd." #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "Kan camerastream niet openen." #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "Geen camerabestandsbeschrijving ontvangen." #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "Kon camera niet openen: {}" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "QR-Code Scannen" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "Statuslabel" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "Fout in camerapijplijn: {}" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" "Nog geen QR-code gedetecteerd. \n" "Zorg ervoor dat de code goed belicht en scherp is en het grootste deel van " "het frame vult." #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" "Kon QR-code niet detecteren. \n" "Probeer in plaats daarvan het contact via het klembord te importeren." #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "QR-code gevonden!" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "Camerafout: {}" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "Camerafout" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "OK" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "Verwijder" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "Bluetooth is uitgeschakeld" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "Schakel Bluetooth in de systeeminstellingen in om verbinding te maken" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "Geen gekoppelde apparaten" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "Gebruik de onderstaande knop om een nieuwe companion te koppelen" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "Geen USB-apparaten gedetecteerd" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "Verbind een MeshCore companion via USB" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "Verwijder Companion" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" "{} Verwijderen ({})? Je zal opnieuw moeten koppelen om verbinding te maken." #: src/views/connection_view.py:181 src/views/connection_view.py:413 #, fuzzy msgid "Delete stored data" msgstr "Geen telemetriegegevens" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "USB-Verbinding Mislukt" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "USB-Toestemming Geweigerd" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" "Geen toegang tot {}. \n" "\n" "Er is een udev-regel nodig om toegang te verlenen tot seriële USB-apparaten. " "Klik op \"Installeer Regel\" om deze te installeren (beheerderswachtwoord " "vereist)." #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "Installeer Regel" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" "Geen toegang tot {}. \n" "\n" "Er is een udev-regel nodig om toegang te verlenen tot seriële USB-apparaten. " "Voer het volgende commando uit in een terminal." #: src/views/connection_view.py:270 msgid "Close" msgstr "Sluiten" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "Kopieer Opdracht" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "Opdracht gekopieerd naar klembord" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "Installatie Mislukt" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" "Kon udev-regel niet installeren: \n" "{}" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "Geen opgeslagen companions" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "Gebruik de onderstaande knop om er een toe te voegen" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "TCP-companion {} verwijderen?" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "Voer het IP-adres en de poort van uw MeshCore-apparaat in." #: src/views/connection_view.py:434 #, fuzzy msgid "Hostname / IP Address" msgstr "IP-adres" #: src/views/connection_view.py:436 msgid "Port" msgstr "Poort" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "Nieuwe Companion Koppelen" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "Zoeken naar MeshCore-apparaten in de buurt" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "Koppelen" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "Koppelingspincode Invoeren" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "Voer de pincode in die wordt weergegeven op uw MeshCore-apparaat." #: src/views/connection_view.py:603 msgid "PIN" msgstr "PIN" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "Handmatig Toevoegen" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "Importeren vanaf Klembord" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "Alle" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "Favorieten" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "Gebruikers" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "A–Z" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "Onlangs Gehoord" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "Laatste Berichten" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "Favorieten Eerst" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "{visible}/{total} contacten" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "{total} contacten" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "{total} contact" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" "Er zijn nog geen nieuwe contacten vernomen. Laat de app actief en de " "contacten verschijnen zodra hun aankondigingen worden ontvangen." #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "Laatst Gezien" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, fuzzy, python-brace-format msgid "{visible}/{total} discovered" msgstr "{visible}/{total} contacten" #: src/views/contacts_view.py:438 #, fuzzy, python-brace-format msgid "{total} discovered" msgstr "{total} contact" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "Voer de contactgegevens in." #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "Contacttype" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "Kamerserver" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "Openbare sleutel (64 hexadecimale tekens)" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "Dit contact staat al in uw lijst." #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "Importeren Mislukt" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "Geen meshcore://-link gevonden op het klembord." #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "Ongeldig kanaalgeheim in QR-code." #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "Kanaalgeheim moet 16 bytes zijn, kreeg {}." #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "Ongeldige openbare sleutel in QR-code." #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "Openbare sleutel moet 32 bytes zijn, kreeg {}." #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" "Kon QR-codegegevens niet parseren: \n" "{}" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "QR-Code Gescand" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "Openbare sleutel: {}...{}" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "Inloggen op {}" #: src/views/contacts_view.py:678 msgid "Password" msgstr "Wachtwoord" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "Wachtwoord opslaan" #: src/views/contacts_view.py:702 msgid "Login" msgstr "Login" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "Inloggen succesvol" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "Inloggen mislukt - verkeerd wachtwoord" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "Er is een time-out voor inloggen opgetreden" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "Opnieuw proberen via flood..." #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "Inloggen..." #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "Zoeken..." #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "Gevonden pad" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "Geen reactie" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "Laden..." #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "Er is geen pad in de cache opgeslagen" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "Niet ondersteund" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "Ping wordt niet ondersteund met pad-hashes van 3 bytes" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "Ping {}: {}" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "Ping {}: timed-out" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "Aanvragen..." #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "Geen reactie (time-out)" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "Geen telemetriegegevens" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "Telemetrie ontvangen" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "Telemetrie — {}" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "{} — Beheer" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "Pad naar {}" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "Mijn Apparaat" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "Onbekende repeater" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "Contactinformatie" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "Favoriet" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "Vastzetten bovenaan de lijst met contacten" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "Type" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "Openbare sleutel gekopieerd" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "Nooit" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "Uitgaand Pad" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" "Door komma's gescheiden hex-hops (bijvoorbeeld 9a,74 of 9a3b,744c voor 2 " "bytes)" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "Pad Opnieuw Instellen" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "Maak het gevestigde pad vrij en schakel over naar flood" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "Forceer Flood" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "Zend altijd uit, gebruik nooit een vastgesteld pad" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "Toon Pad op Kaart" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "Visualiseer de berichtenroute op de kaart" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "Telemetrieverzoeken Toestaan" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "Laat dit contact de batterij, spanning en temperatuur opvragen" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "Inclusief Locatie" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "Neem GPS-coördinaten op in telemetriereacties" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "Inclusief Omgevingssensoren" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "Voeg gegevens over vochtigheid, druk en luchtkwaliteit toe" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "Vraag de telemetriegegevens van dit contact op" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "Kamerbeheer" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "Log in en krijg toegang tot CLI, status en instellingen" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "Repeaterbeheer" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "Log in en krijg toegang tot status, CLI, buren, en instellingen" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "Ping" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "Meet de retourtijd naar dit knooppunt" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "Ontdek Paden" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "Vind routes naar deze node via flood" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "Contact verwijderen" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "Contact Verwijderen?" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "{} en alle berichten verwijderen?" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "Gehoorde Herhalingen" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "Opnieuw Verzenden" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "Verwijderen" #: src/views/channels_view.py:122 msgid "Reply" msgstr "Antwoord" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "Tekst Kopiëren" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "Berichtpaden Bekijken" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 #, fuzzy msgid "Share Location from Map" msgstr "Locatie handmatig instellen" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "Nieuwe Berichten" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "Berichtpaden" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" "Geen padinformatie beschikbaar. Padgegevens worden alleen vastgelegd voor " "berichten die worden ontvangen terwijl er verbinding is." #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "Direct bericht" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "Er zijn geen repeaters gebruikt" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "Geen repeatergegevens" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "Padgegevens worden alleen vastgelegd via LOG_DATA als er verbinding is" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "Berichtpad" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "Nog geen herhalingen gehoord." #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "Openbaar" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "Hashtag" #: src/views/channels_view.py:1671 msgid "Private" msgstr "Privé" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "{visible}/{total} kanalen" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "{total} kanalen" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "{total} kanaal" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "{name} ({region})" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "Kanaalinformatie" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "Privésleutel" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "Privésleutel gekopieerd naar klembord" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "Meldingen" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "Meldingsniveau" #: src/views/channels_view.py:1935 msgid "Default" msgstr "Standaard" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "Regio" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "Beperk flood-berichten tot een specifieke regio" #: src/views/channels_view.py:1989 #, fuzzy msgid "No regions defined" msgstr "Geen regio's gevonden" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "Kanaal verwijderen" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "Kanaal Verwijderen?" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "'{}' en alle bijbehorende berichten verwijderen?" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "Geen Slots Beschikbaar" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "Alle 8 kanaalslots zijn in gebruik." #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" "Er wordt een willekeurige PSK gegenereerd. Deel die met anderen, zodat zij " "lid kunnen worden." #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "Kanaalnaam" #: src/views/channels_view.py:2070 msgid "Create" msgstr "Aanmaken" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "Voer de kanaalnaam en de privésleutel in die met u zijn gedeeld." #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "Privésleutel (32 hexadecimale tekens)" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "Lid Worden" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "Al Lid" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "Je bent al op het openbare kanaal." #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" "Voer een hashtagnaam in. Iedereen die dezelfde hashtag gebruikt, kan " "communiceren." #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "Hashtag (bv. #algemeen)" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "Openbare sleutel van apparaat niet beschikbaar" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "QR-codebibliotheek niet beschikbaar" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "Uw Contact-QR-Code" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "Scan deze QR-code om dit contact toe te voegen" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "Fabrieksreset?" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" "Hierdoor worden ALLE gegevens op het companion apparaat permanent gewist, " "inclusief contacten, berichten, kanalen, instellingen en de " "apparaatidentiteit (sleutels). Deze actie kan niet ongedaan worden gemaakt." #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "Apparaat Opnieuw Opstarten?" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "Het apparaat wordt losgekoppeld en opnieuw opgestart." #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "Opnieuw Opstarten" #: src/views/device_view.py:323 msgid "Not set" msgstr "Niet ingesteld" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "Locatie (handmatig ingesteld)" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "Locatie (GPS)" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "Locatie (GPS aan, geen fix)" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "Locatie (GPS uit)" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "Aanvragen…" #: src/views/device_view.py:405 msgid "Device Location" msgstr "Apparaatlocatie" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "{days}d" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "{hours}h" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "{mins}m" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "{} berichten" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "{count} fouten" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "Verbonden" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "Traceer Pad wordt niet ondersteund met pad-hashes van 3 bytes" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "bv. aa,bb,cc" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "bv. aabb,ccdd" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "bv. aabbcc,ddeeff" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "Toevoegen vanuit Contacten" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "Geen repeaters of kamers in contacten" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "Geen repeaters met bekende locatie" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "Pad" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "Trace Uitvoeren" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "Voer pad-hashes in en druk op Trace Uitvoeren" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "Trace voltooid: {} hops, {:.1f}s" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "Er is een time-out opgetreden bij het traceren" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "Ongeldige hex in pad" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "Traceren..." #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "Bezig met scannen... {}s resterend" #: src/views/device_view.py:977 msgid "Listening..." msgstr "Luisteren..." #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "Wachten tot nabijgelegen nodes reageren" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "Toevoegen aan Contacten" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "{} toegevoegd" #: src/views/device_view.py:1066 msgid "Node" msgstr "Node" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "Klaar. {} nodes gevonden." #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "Klaar. {} node gevonden." #: src/views/device_view.py:1222 msgid "Size" msgstr "Grootte" #: src/views/device_view.py:1222 msgid "bytes" msgstr "bytes" #: src/views/device_view.py:1226 msgid "hops" msgstr "hops" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "Pad-hashes" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "byte per hop" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "Update beschikbaar: {version}" #: src/views/settings_view.py:131 msgid "Custom" msgstr "Aangepast" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "Alles Weigeren" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "Toestaan per Contact" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "Iedereen toestaan" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "Instellingen toegepast op apparaat" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "Onbekende fout" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "Instellingenfout: {}" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "TX-vermogen (dBm, max. {})" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "Locatie ingevuld, klik op Toepassen om op te slaan" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "Locatie niet beschikbaar" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "Kies Locatie" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "Locatie Instellen" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "Maak eerst verbinding met een apparaat" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "Geen repeaters in Contacten" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "Ontdek Regio's" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "{} repeaters opvragen..." #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "{} repeater opvragen..." #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "Wachten..." #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "Regio's van repeaters opvragen" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "Geselecteerde Toevoegen" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "{} regio's gevonden" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "{} regio gevonden" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "Geen regio's gevonden" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "van {}" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "(al toegevoegd)" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "{} regio's toegevoegd" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "{} regio toegevoegd" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "Back-up Exporteren" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "Opslaan in een bestand" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "Back-up Importeren" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "Terugzetten vanuit een bestand" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "Niet Verbonden" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "Maak eerst verbinding met een apparaat om een back-up te exporteren." #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "JSON bestanden" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "Back-up succesvol geëxporteerd" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "Exporteren mislukt: {}" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "Maak eerst verbinding met een apparaat om een back-up te importeren." #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "Kon back-upbestand niet lezen: {}" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "Bron: {}" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "Apparaatinstellingen: naam, radio, locatie" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "Contacten: {} ({} nieuw)" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "Kanalen: {} ({} nieuw)" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "Berichten: {}" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "Kanaalberichten: {}" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "Opgeslagen wachtwoorden: {}" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "Alleen Contacten en Kanalen" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "Alles" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "{} contacten" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "{} kanalen" #: src/views/settings_view.py:1208 msgid "messages" msgstr "berichten" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "Geïmporteerd: {}" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "Er is niets nieuws om te importeren" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "{d}d" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "{h}h" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "{m}m" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "{secs}s" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "{}s geleden" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "{}m geleden" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "{}h geleden" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "{}d geleden" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "Tijdsynchronisatie verzonden naar repeater" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "Klok Resetten en Opnieuw Opstarten?" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" "De repeaterklok loopt voor op de huidige tijd. \n" "Firmware staat niet toe dat de tijd achteruit wordt gezet. \n" "\n" "Hierdoor wordt de klok opnieuw ingesteld en wordt de repeater opnieuw " "opgestart." #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "Opnieuw opstarten..." #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "Klokreset en herstart naar repeater verzonden" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "Verzoek is verlopen" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "{} van {} buren" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "Burenkaart" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "Beheerder" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "{} vermelding" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "{} vermeldingen" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "Uit ACL Verwijderen?" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "{} verwijderen uit de toegangscontrolelijst?" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "Voeg toe aan Toegangscontrole" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "Selecteer een contact en een rol." #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "Rol" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "Globaal (wildcard)" #: src/views/repeater_view.py:942 msgid "Home" msgstr "Thuis" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "Flood toegestaan" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "Flood geweigerd" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "Flood Weigeren" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "Flood Toelaten" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "Instellen als Thuis" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "Verwijder eerst onderliggende regio's" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "Niet-opgeslagen wijzigingen" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "U heeft niet-opgeslagen regiowijzigingen." #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "Weggooien" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "Regio Toevoegen" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "Voer een naam in en kies optioneel een bovenliggende regio." #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "Regionaam" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "Ouder" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "Beheerderswachtwoord gewijzigd — opnieuw inloggen vereist" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "Instellingen verzonden naar {}" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "Repeater opnieuw opstarten?" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "{} opnieuw opstarten? Die zal kort offline zijn." #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "Tijd wijkt af" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "Verzenden" #: src/views/map_view.py:99 msgid "User" msgstr "Gebruiker" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "{} van {} contacten op kaart" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "{} van {} gelegen op de kaart" #: src/views/map_view.py:198 msgid "Rooms" msgstr "Kamers" #: src/views/map_view.py:227 #, fuzzy msgid "Discovered Nodes" msgstr "Ontdek Nabijgelegen Knooppunten" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "and {} meer" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "SNR: {:.1f} dB" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "Kies Traceroute" #: src/views/map_view.py:1159 msgid "Undo" msgstr "Ongedaan Maken" #: src/views/map_view.py:1169 msgid "Clear" msgstr "Wissen" #: src/views/map_view.py:1177 msgid "Done" msgstr "Klaar" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "Tik op repeaters om de traceroute op te bouwen" #, python-brace-format #~ msgid "Syncing channels: {index}/{total}" #~ msgstr "Kanalen synchroniseren: {index}/{total}" #, python-brace-format #~ msgid "Syncing contacts: {current}/{total}" #~ msgstr "Contacten synchroniseren: {current}/{total}" meshy/po/nl@formal.po000066400000000000000000002321321521052255700151020ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-27 22:19+0200\n" "PO-Revision-Date: 2026-05-13 19:41+0000\n" "Last-Translator: lebeno \n" "Language-Team: Dutch (formal) \n" "Language: nl@formal\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.17.1\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "Meshy" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "MeshCore mesh netwerk cliënt" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "mesh;lora;radio;chat;" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" "Meshy is een cliënt voor MeshCore LoRa mesh netwerk apparaten. Verbind jouw " "companion apparaat over Bluetooth om geëncrypteerde berichten te zenden en " "ontvangen, beheer contacten, configureer kanalen, en monitor jouw mesh " "netwerk." #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "Kenmerken:" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "Bluetooth LE en USB seriële verbinding" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "Eind-tot-eind versleutelde berichtgeving" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "Openbare, privé- en hashtagkanalen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "Contactbeheer met locatievolgen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "Interactieve kaartweergave met routeopvolging" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "Apparaat- en repeaterbeheer" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "QR-code scannen voor snel contact uitwisselen" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "Jiri Eischmann" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "Contactenoverzicht met berichtgeving" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "Kanalenlijst met verschillende kanaalsoorten" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "Kaartweergave met contactlocaties" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "Apparaatinformatie en instellingen" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "Afsluiten" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "Sneltoetsen" #: data/gtk/help-overlay.ui:27 #, fuzzy msgid "Navigation" msgstr "Navigatie tonen" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "Apparaat" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 msgid "Contacts" msgstr "Contacten" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 msgid "Channels" msgstr "Kanalen" #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:546 #: src/window.py:549 src/views/map_view.py:885 msgid "Map" msgstr "Kaart" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:563 src/window.py:564 msgid "Settings" msgstr "Instellingen" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 #, fuzzy msgid "Search Contacts" msgstr "Selecteer een Contact" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 #, fuzzy msgid "Next Contact/Channel" msgstr "Alleen Contacten en Kanalen" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "Verbinding verbreken" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "Over Meshy" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:888 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:383 #: src/views/connection_view.py:449 msgid "Connect" msgstr "Verbinden" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "Menu" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "Verbinding maken…" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "Navigatie tonen" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:887 #: src/views/device_view.py:354 msgid "Not connected" msgstr "Niet verbonden" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "Status" #: data/ui/device-view.ui:30 src/views/device_view.py:504 msgid "Disconnected" msgstr "Niet verbonden" #: data/ui/device-view.ui:44 src/window.py:510 src/window.py:511 msgid "Device Information" msgstr "Apparaatinformatie" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "Knooppuntnaam" #: data/ui/device-view.ui:53 msgid "Board" msgstr "Bord" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "Firmware" #: data/ui/device-view.ui:63 msgid "Update" msgstr "Update" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "Open firmware flasher" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1507 msgid "Public Key" msgstr "Openbare sleutel" #: data/ui/device-view.ui:90 src/views/contacts_view.py:1628 msgid "Telemetry" msgstr "Telemetrie" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "Telemetrie aanvragen" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1524 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "Locatie" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "Synchroniseren…" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "Op kaart tonen" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "Batterij & Opslag" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "Batterij" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "Opslag" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "Radioconfiguratie" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "Frequentie" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "Bandbreedte" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "Spreidingsfactor" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "Coderingssnelheid" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "TX Vermogen" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "Standaard pad-hashgrootte" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "Statistieken" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "Uptime" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "Berichtenwachtrij" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "Ruisvloer" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "Laatste RSSI" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "Laatste SNR" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "Zendduur" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "Pakketten" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "Vloed" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "Direct" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "Statistieken vernieuwen" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "Toepassing" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "Configureer het gedrag en uiterlijk van de toepassing" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "Stijl" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "Kies het uiterlijk van de toepassing" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "Volg Systeem" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "Licht" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "Donker" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "Palletthema" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "MeshCore Donker" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "Kanaalmeldingen" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "Standaard meldingsniveau voor alle kanalen" #: data/ui/settings-view.ui:54 src/views/channels_view.py:2033 msgid "All Messages" msgstr "Alle berichten" #: data/ui/settings-view.ui:55 src/views/channels_view.py:2033 msgid "Mentions Only" msgstr "Alleen Vermeldingen" #: data/ui/settings-view.ui:56 src/views/channels_view.py:2033 #: src/views/channels_view.py:2065 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "Geen" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "Op de achtergrond uitvoeren" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "Blijf berichten ontvangen nadat het venster is gesloten" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "Automatisch starten" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "Automatisch starten bij aanmelden" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "Radio" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "LoRa-radioparameters en regionale presets" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "Regionale Preset" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "Presetconfiguratie" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "Frequentie, bandbreedte, spreidingsfactor, coderingssnelheid" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "Frequentie (MHz)" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "Bandbreedte (kHz)" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "Repeat mode" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "Als een draagbare repeater werken op een off-grid frequentie" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "TX Vermogen (dBm)" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "Aankondiging" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "Configureer hoe jouw knooppunt zichzelf aankondigt op het mesh" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "Apparaatnaam" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "Locatie opnemen in Aankondiging" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "Zend je positie uit naar andere knooppunten" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "1-byte (max 64 hops)" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "2-byte (max 32 hops)" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "3-byte (max 21 hops)" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "Stel de positie van je companion in via GPS of handmatig" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "Companion GPS" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" "Schakel GPS in op de companion, je positie wordt automatisch bijgewerkt" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "Locatie handmatig instellen" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "Kies een andere methode om de locatie in te stellen; deze blijft vast" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "Breedtegraad" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "Lengtegraad" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "Gebruik Mijn Locatie" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "Instellen vanaf de locatie van deze computer" #: data/ui/settings-view.ui:258 src/views/device_view.py:580 msgid "Pick on Map" msgstr "Kiezen op Kaart" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "Kies een locatie op de kaart" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "Contacten Automatisch Toevoegen" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "Voeg automatisch knooppunten toe die via aankondigingen worden ontdekt" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "Instellingen Automatisch Toevoegen" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "Aapparaattype, overschrijfbeleid, hoplimiet" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "Chat Knooppunten" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "Repeaters" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "Kamerservers" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "Sensoren" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "Oudste overschrijven wanneer vol" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "Hoplimiet" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "0–63, laat leeg voor geen limiet" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "Telemetrieprivacy" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "Bepaal wat anderen van jouw apparaat mogen opvragen" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "Batterij & Sensoren" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "Batterij, spanning, temperatuur, stroom" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "GPS-coördinaten" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "Omgeving" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "Vochtigheid, druk, luchtkwaliteit" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "Regio's" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "Beperk het bereik van kanaalberichten tot specifieke regio's" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "Regio toevoegen (bijv. nl)" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "Standaardbereik" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "Alle flood-pakketten worden aan deze regio gebonden als ingesteld" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "Bluetooth" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" "Na het wijzigen van de pincode: herstart het apparaat, vervolgens ontkoppel " "en koppel opnieuw. Instellen op 0 zet de pincode terug naar standaard." #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "Koppelingspincode" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "Maak verbinding met een companion apparaat om Meshy te gaan gebruiken." #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "Koppel met een Nieuwe Companion" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "USB Serieel" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "WiFi / TCP" #: data/ui/connection-view.ui:84 src/views/connection_view.py:431 msgid "Add TCP Companion" msgstr "TCP Companion Toevoegen" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "Zoeken..." #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:799 src/views/chat_view.py:676 msgid "No contacts" msgstr "Geen contacten" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:284 #: src/views/channels_view.py:481 src/views/chat_view.py:225 #: src/views/chat_view.py:399 msgid "Add Contact" msgstr "Contact Toevoegen" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "Selecteer een Contact" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "Kies een contact uit de lijst om te beginnen met chatten" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "↑ Nieuwe Berichten" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "Typ een bericht..." #: data/ui/channels-view.ui:10 src/views/channels_view.py:2144 msgid "Create a Private Channel" msgstr "Privékanaal Maken" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2174 msgid "Join a Private Channel" msgstr "Deelnemen aan een Privékanaal" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "Deelnemen aan het Openbaar Kanaal" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2224 msgid "Join a Hashtag Channel" msgstr "Deelnemen aan een Hashtagkanaal" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "Geen kanalen" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "Kanaal Toevoegen" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "Selecteer een Kanaal" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "Kies een kanaal uit de lijst" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "Berichtkanaal..." #: data/ui/repeater-view.ui:37 msgid "System" msgstr "Systeem" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "Klok bij Aanmelding" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "Nu Synchroniseren" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "Resetten en Opnieuw Opstarten" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "Foutgebeurtenissen" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "TX Zendduur" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "RX Zendduur" #: data/ui/repeater-view.ui:137 #, fuzzy msgid "Channel Utilization" msgstr "Kanaalmeldingen" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "Verzonden" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "Ontvangen" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "Ontvangstfouten" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "Duplicaten" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "Vernieuwen" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1657 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "Telemetrie Aanvragen" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "CLI" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "CLI-opdracht..." #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "Buren" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "Meer Laden" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:324 #: src/views/channels_view.py:1251 src/views/chat_view.py:265 msgid "Show on Map" msgstr "Weergeven op Kaart" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "Toegangscontrole" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "Toevoegen" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "Regio's worden geladen..." #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "Opnieuw proberen" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "Standaard Regiobereik" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" "Flood-pakketten worden aan de opgegeven regio gebonden. Alleen repeaters die " "de regio toestaan, zullen gebonden pakketten doorsturen. Laat leeg voor niet-" "gebonden." #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "Toepassen" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "Basis" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "Ophalen" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "Opslaan" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1482 src/views/channels_view.py:1985 #: src/views/channels_view.py:1998 msgid "Name" msgstr "Naam" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "Pakketherhaling" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "Wachtwoord" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "Beheerderswachtwoord" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "Gastwachtwoord" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "Aankondiging" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "Lokale interval (min)" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "Flood Interval (uren)" #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1746 msgid "Danger Zone" msgstr "Gevarenzone" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "Herstart Repeater" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "Selecteer een Repeater" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "Afmelden" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "Aankondiging Verzenden" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "Zero Hop" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "Lokaal uitzenden" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "Flood Gerouteerd" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "Uitzenden door het mesh" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "Naar Klembord" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "Kopieer eigen contactgegevens" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "QR-code Weergeven" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "Zodat anderen je kunnen scannen en toevoegen" #: data/ui/device-actions.ui:76 src/views/device_view.py:518 #: src/views/device_view.py:861 msgid "Trace Path" msgstr "Pad Traceren" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "Traceer een route en toon signaalkwaliteit per hop" #: data/ui/device-actions.ui:91 src/views/device_view.py:942 msgid "Discover Nearby Nodes" msgstr "Ontdek Nabijgelegen Knooppunten" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "Scan het netwerk op nabijgelegen knooppunten" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "Rx Log" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "Bekijk ontvangen radiopakketten" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "Apparaat Opnieuw Opstarten" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "Log Wissen" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "Geen Pakketten" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "Ontvangen radiopakketten verschijnen hier" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "Back-up" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "Sla configuratie en berichten op in een bestand" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "Terugzetten" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "Terugzetten vanuit een back-upbestand" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "Fabrieksinstellingen Terugzetten" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "Wis alle gegevens op de companion" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "Ontdek contacten" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:464 msgid "Search" msgstr "Zoeken" #: data/ui/discover-dialog.ui:27 #, fuzzy msgid "Filter by type" msgstr "Filter" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:175 #: src/views/chat_view.py:137 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "Kies Thema" #: src/application.py:87 src/application.py:154 src/window.py:522 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:999 msgid "Chat" msgstr "Chat" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1000 #: src/views/map_view.py:100 msgid "Repeater" msgstr "Repeater" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "Kamer" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1002 #: src/views/map_view.py:102 msgid "Sensor" msgstr "Sensor" #: src/application.py:95 src/application.py:157 src/views/contacts_view.py:1392 #: src/views/channels_view.py:1345 src/views/device_view.py:886 #: src/views/device_view.py:1130 src/views/device_view.py:1138 #: src/views/settings_view.py:1183 msgid "Unknown" msgstr "Onbekend" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" "Naam: {name}\n" "Type: {type}" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" "Type: {type}\n" "Sleutel: {key}…" #: src/application.py:101 msgid "Import Contact" msgstr "Contact Importeren" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "Contact {}" #: src/application.py:105 msgid "imported" msgstr "geïmporteerd" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "Ongeldige meshcore:/- link" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "Ongeldig kanaalgeheim in link" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "Ongeldige kanaalgeheimlengte" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" "Naam: {name}\n" "Type: Privé" #: src/application.py:130 msgid "Unnamed" msgstr "Naamloos" #: src/application.py:146 msgid "Invalid public key in link" msgstr "Ongeldige openbare sleutel in link" #: src/application.py:149 msgid "Invalid public key length" msgstr "Ongeldige lengte van de openbare sleutel" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "{} is al in jouw lijst" #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "Contact" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "{} Contact toevoegen" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" "Naam: {name}\n" "Openbare Sleutel: {key}…" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "Contact {} toegevoegd" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "Niet-herkende meshcore://-link" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:411 #: src/views/connection_view.py:448 src/views/connection_view.py:613 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1757 src/views/channels_view.py:2101 #: src/views/channels_view.py:2152 src/views/channels_view.py:2184 #: src/views/channels_view.py:2232 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1223 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "Annuleren" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "Berichten ontvangen op de achtergrond" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "Een GTK-client voor MeshCore LoRa mesh-netwerkapparaten" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "Adwaita Symbolische Iconen" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:898 msgid "Connecting..." msgstr "Verbinden..." #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "Scannen Naar Apparaten" #: src/connection_controller.py:137 src/views/connection_view.py:512 msgid "Scanning for MeshCore devices..." msgstr "Scannen naar MeshCore apparaten..." #: src/connection_controller.py:140 msgid "Scan" msgstr "Scan" #: src/connection_controller.py:262 src/views/connection_view.py:529 msgid "Scanning..." msgstr "Scannen..." #: src/connection_controller.py:263 msgid "Stop" msgstr "Stop" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "Synchroniseren..." #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" "Apparaat reageerde niet. Het is mogelijk dat dit verbindingstype niet wordt " "ondersteund." #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "Opnieuw verbinding maken ({current}/{total})..." #: src/connection_controller.py:483 #, python-brace-format msgid "Syncing channels: {index}/{total}" msgstr "Kanalen synchroniseren: {index}/{total}" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "Je hebt {} bericht(en) ontvangen terwijl de verbinding was verbroken." #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "Apparaatfout: {}" #: src/frame_handler.py:201 src/frame_handler.py:216 #, python-brace-format msgid "Syncing contacts: {current}/{total}" msgstr "Contacten synchroniseren: {current}/{total}" #: src/frame_handler.py:343 #, python-brace-format msgid "{}s (late)" msgstr "{}s (later)" #: src/frame_handler.py:343 #, python-brace-format msgid "{}ms (late)" msgstr "{}ms (later)" #: src/frame_handler.py:343 msgid "late" msgstr "later" #: src/frame_handler.py:403 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "Apparaatklok liep {}min achter — gesynchroniseerd" #: src/frame_handler.py:405 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "Apparaatklok loopt {}min voor op het systeem" #: src/frame_handler.py:876 src/window.py:541 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1849 src/views/channels_view.py:1869 #: src/views/channels_view.py:1966 src/views/channels_view.py:1998 #: src/views/channels_view.py:2099 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "Kanaal {}" #: src/frame_handler.py:877 msgid "Someone" msgstr "Iemand" #: src/frame_handler.py:880 #, python-brace-format msgid "{sender}: {text}" msgstr "{sender}: {text}" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "Ik" #: src/message_controller.py:171 msgid "flood" msgstr "flood" #: src/message_controller.py:173 msgid "direct" msgstr "direct" #: src/message_controller.py:175 msgid "path" msgstr "pad" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "– {route}" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "({attempt}/{total}) – {route}" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "wachten {:.1f}s (radio bezet)" #: src/window.py:468 src/window.py:545 msgid "Filter" msgstr "Filter" #: src/window.py:472 msgid "Sort" msgstr "Sorteren" #: src/window.py:509 src/views/contacts_view.py:1671 #: src/views/channels_view.py:2089 src/views/repeater_view.py:964 msgid "Actions" msgstr "Acties" #: src/window.py:533 msgid "Channel" msgstr "Kanaal" #: src/window.py:562 msgid "Backup & Restore" msgstr "Back-up & Terugzetten" #: src/window.py:634 src/window.py:670 src/window.py:673 src/window.py:1393 #: src/window.py:1395 src/window.py:1446 src/window.py:1449 #, python-brace-format msgid "{name} ({path})" msgstr "{name} ({path})" #: src/window.py:684 msgid "Management" msgstr "Beheer" #: src/window.py:684 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "Gast" #: src/window.py:685 #, python-brace-format msgid "{name} — {role}" msgstr "{name} — {role}" #: src/window.py:787 msgid "Excellent" msgstr "" #: src/window.py:787 msgid "Good" msgstr "" #: src/window.py:788 msgid "Fair" msgstr "" #: src/window.py:788 msgid "Poor" msgstr "" #: src/window.py:789 msgid "Very poor" msgstr "" #: src/window.py:1011 msgid "flood routed" msgstr "flood gerouteerd" #: src/window.py:1011 msgid "zero-hop" msgstr "zero-hop" #: src/window.py:1012 #, python-brace-format msgid "Advert sent ({})" msgstr "Aankondiging verzonden ({})" #: src/window.py:1463 #, python-brace-format msgid "Channel \"{}\" added" msgstr "Kanaal \"{}\" toegevoegd" #: src/window.py:1467 msgid "No empty channel slots available" msgstr "Geen lege kanaalslots beschikbaar" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "Kan geen verbinding maken met sessiebus: {}" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "Geen camera gedetecteerd op dit systeem." #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "Kon geen toegang krijgen tot de camera: {}" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "Toegang tot de camera is geweigerd." #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "Kan camerastream niet openen." #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "Geen camerabestandsbeschrijving ontvangen." #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "Kon camera niet openen: {}" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "QR-Code Scannen" #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "Statuslabel" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "Fout in camerapijplijn: {}" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" "Nog geen QR-code gedetecteerd. \n" "Zorg ervoor dat de code goed belicht en scherp is en het grootste deel van " "het frame vult." #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" "Kon QR-code niet detecteren. \n" "Probeer in plaats daarvan het contact via het klembord te importeren." #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "QR-code gevonden!" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "Camerafout: {}" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "Camerafout" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:332 src/views/connection_view.py:339 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:818 #: src/views/channels_view.py:1178 src/views/channels_view.py:1400 #: src/views/channels_view.py:1420 src/views/channels_view.py:2135 #: src/views/channels_view.py:2206 src/views/settings_view.py:1096 #: src/views/settings_view.py:1137 src/views/settings_view.py:1173 #: src/views/chat_view.py:695 msgid "OK" msgstr "OK" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:412 src/views/contacts_view.py:1758 #: src/views/channels_view.py:2102 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "Verwijder" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "Bluetooth is uitgeschakeld" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "Schakel Bluetooth in de systeeminstellingen in om verbinding te maken" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "Geen gekoppelde apparaten" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "Gebruik de onderstaande knop om een nieuwe companion te koppelen" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "Geen USB-apparaten gedetecteerd" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "Verbind een MeshCore companion via USB" #: src/views/connection_view.py:174 src/views/connection_view.py:408 msgid "Remove Companion" msgstr "Verwijder Companion" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" "{} verwijderen ({})? Je zal opnieuw moeten koppelen om verbinding te maken." #: src/views/connection_view.py:181 src/views/connection_view.py:415 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "USB-Verbinding Mislukt" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "USB-Toestemming Geweigerd" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" "Geen toegang tot {}. \n" "\n" "Er is een udev-regel nodig om toegang te verlenen tot seriële USB-apparaten. " "Klik op \"Installeer Regel\" om deze te installeren (beheerderswachtwoord " "vereist)." #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "Installeer Regel" #: src/views/connection_view.py:265 #, fuzzy, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" "Geen toegang tot {}. \n" "\n" "Er is een udev-regel nodig om toegang te verlenen tot seriële USB-apparaten. " "Klik op \"Installeer Regel\" om deze te installeren (beheerderswachtwoord " "vereist)." #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 #, fuzzy msgid "Copy Command" msgstr "CLI-opdracht..." #: src/views/connection_view.py:279 #, fuzzy msgid "Command copied to clipboard" msgstr "Privésleutel gekopieerd naar klembord" #: src/views/connection_view.py:329 src/views/connection_view.py:336 msgid "Installation Failed" msgstr "Installatie Mislukt" #: src/views/connection_view.py:330 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" "Kon udev-regel niet installeren: \n" "{}" #: src/views/connection_view.py:365 msgid "No saved companions" msgstr "Geen opgeslagen companions" #: src/views/connection_view.py:366 msgid "Use the button below to add one" msgstr "Gebruik de onderstaande knop om er een toe te voegen" #: src/views/connection_view.py:409 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "TCP-companion {} verwijderen?" #: src/views/connection_view.py:432 msgid "Enter the IP address and port of your MeshCore device." msgstr "Voer het IP-adres en de poort van uw MeshCore-apparaat in." #: src/views/connection_view.py:436 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:438 msgid "Port" msgstr "Poort" #: src/views/connection_view.py:495 msgid "Pair New Companion" msgstr "Nieuwe Companion Koppelen" #: src/views/connection_view.py:530 msgid "Looking for nearby MeshCore devices" msgstr "Zoeken naar MeshCore-apparaten in de buurt" #: src/views/connection_view.py:557 src/views/connection_view.py:614 msgid "Pair" msgstr "Koppelen" #: src/views/connection_view.py:600 msgid "Enter Pairing PIN" msgstr "Koppelingspincode Invoeren" #: src/views/connection_view.py:601 msgid "Enter the PIN shown on your MeshCore device." msgstr "Voer de pincode in die wordt weergegeven op uw MeshCore-apparaat." #: src/views/connection_view.py:605 msgid "PIN" msgstr "PIN" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "Handmatig Toevoegen" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "Importeren vanaf Klembord" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1768 msgid "All" msgstr "Alle" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "Favorieten" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "Gebruikers" #: src/views/contacts_view.py:139 src/views/channels_view.py:1780 msgid "A–Z" msgstr "A–Z" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "Onlangs Gehoord" #: src/views/contacts_view.py:140 src/views/channels_view.py:1780 msgid "Latest Messages" msgstr "Laatste Berichten" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "Favorieten Eerst" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "{visible}/{total} contacten" #: src/views/contacts_view.py:188 src/views/channels_view.py:798 #: src/views/chat_view.py:675 #, python-brace-format msgid "{total} contacts" msgstr "{total} contacten" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "{total} contact" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" "Er zijn nog geen nieuwe contacten vernomen. Laat de app actief en de " "contacten verschijnen zodra hun aankondigingen worden ontvangen." #: src/views/contacts_view.py:260 src/views/contacts_view.py:1521 msgid "Last Seen" msgstr "Laatst Gezien" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, fuzzy, python-brace-format msgid "{visible}/{total} discovered" msgstr "{visible}/{total} contacten" #: src/views/contacts_view.py:438 #, fuzzy, python-brace-format msgid "{total} discovered" msgstr "{total} contact" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "Voer de contactgegevens in." #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "Contacttype" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1001 msgid "Room Server" msgstr "Kamerserver" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "Openbare sleutel (64 hexadecimale tekens)" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "Dit contact staat al in uw lijst." #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1170 msgid "Import Failed" msgstr "Importeren Mislukt" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "Geen meshcore://-link gevonden op het klembord." #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "Ongeldig kanaalgeheim in QR-code." #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "Kanaalgeheim moet 16 bytes zijn, kreeg {}." #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "Ongeldige openbare sleutel in QR-code." #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "Openbare sleutel moet 32 bytes zijn, kreeg {}." #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" "Kon QR-codegegevens niet parseren: \n" "{}" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "QR-Code Gescand" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "Openbare sleutel: {}...{}" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "Inloggen op {}" #: src/views/contacts_view.py:678 msgid "Password" msgstr "Wachtwoord" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "Wachtwoord opslaan" #: src/views/contacts_view.py:702 msgid "Login" msgstr "Login" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "Inloggen succesvol" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "Inloggen mislukt - verkeerd wachtwoord" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "Er is een time-out voor inloggen opgetreden" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "Opnieuw proberen via flood..." #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "Inloggen..." #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "Zoeken..." #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "Gevonden pad" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "Geen reactie" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "Laden..." #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "Er is geen pad in de cache opgeslagen" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "Niet ondersteund" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "Ping wordt niet ondersteund met pad-hashes van 3 bytes" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "Ping {}: {}" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "Ping {}: timed-out" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "Aanvragen..." #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "Geen reactie (time-out)" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "Geen telemetriegegevens" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "Telemetrie ontvangen" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "Telemetrie — {}" #: src/views/contacts_view.py:1255 #, python-brace-format msgid "{} — Management" msgstr "{} — Beheer" #: src/views/contacts_view.py:1366 #, python-brace-format msgid "Path to {}" msgstr "Pad naar {}" #: src/views/contacts_view.py:1386 src/views/channels_view.py:613 #: src/views/channels_view.py:1338 src/views/device_view.py:880 #: src/views/chat_view.py:530 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1124 msgid "My Device" msgstr "Mijn Apparaat" #: src/views/contacts_view.py:1417 src/views/channels_view.py:1235 #: src/views/channels_view.py:1346 src/views/device_view.py:887 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "Onbekende repeater" #: src/views/contacts_view.py:1480 msgid "Contact Info" msgstr "Contactinformatie" #: src/views/contacts_view.py:1488 msgid "Favorite" msgstr "Favoriet" #: src/views/contacts_view.py:1489 msgid "Pin to top of contacts list" msgstr "Vastzetten bovenaan de lijst met contacten" #: src/views/contacts_view.py:1506 src/views/channels_view.py:2002 msgid "Type" msgstr "Type" #: src/views/contacts_view.py:1516 src/views/device_view.py:150 msgid "Public key copied" msgstr "Openbare sleutel gekopieerd" #: src/views/contacts_view.py:1520 msgid "Never" msgstr "Nooit" #: src/views/contacts_view.py:1540 src/views/contacts_view.py:1545 msgid "Out Path" msgstr "Uitgaand Pad" #: src/views/contacts_view.py:1541 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" "Door komma's gescheiden hex-hops (bijvoorbeeld 9a,74 of 9a3b,744c voor 2 " "bytes)" #: src/views/contacts_view.py:1552 msgid "Reset Path" msgstr "Pad Opnieuw Instellen" #: src/views/contacts_view.py:1553 msgid "Clear the established path and switch to flood" msgstr "Maak het gevestigde pad vrij en schakel over naar flood" #: src/views/contacts_view.py:1568 msgid "Force Flood" msgstr "Forceer Flood" #: src/views/contacts_view.py:1569 msgid "Always broadcast, never use an established path" msgstr "Zend altijd uit, gebruik nooit een vastgesteld pad" #: src/views/contacts_view.py:1611 msgid "Show Path on Map" msgstr "Toon Pad op Kaart" #: src/views/contacts_view.py:1612 msgid "Visualize the message route on the map" msgstr "Visualiseer de berichtenroute op de kaart" #: src/views/contacts_view.py:1646 msgid "Allow Telemetry Requests" msgstr "Telemetrieverzoeken Toestaan" #: src/views/contacts_view.py:1646 msgid "Allow this contact to query battery, voltage, temperature" msgstr "Laat dit contact de batterij, spanning en temperatuur opvragen" #: src/views/contacts_view.py:1649 msgid "Include Location" msgstr "Inclusief Locatie" #: src/views/contacts_view.py:1649 msgid "Include GPS coordinates in telemetry responses" msgstr "Neem GPS-coördinaten op in telemetriereacties" #: src/views/contacts_view.py:1652 msgid "Include Environment Sensors" msgstr "Inclusief Omgevingssensoren" #: src/views/contacts_view.py:1652 msgid "Include humidity, pressure, air quality data" msgstr "Voeg gegevens over vochtigheid, druk en luchtkwaliteit toe" #: src/views/contacts_view.py:1658 msgid "Query this contact's telemetry data" msgstr "Vraag de telemetriegegevens van dit contact op" #: src/views/contacts_view.py:1676 msgid "Room Management" msgstr "Kamerbeheer" #: src/views/contacts_view.py:1677 msgid "Login and access CLI, status, and settings" msgstr "Log in en krijg toegang tot CLI, status en instellingen" #: src/views/contacts_view.py:1693 msgid "Repeater Management" msgstr "Repeaterbeheer" #: src/views/contacts_view.py:1694 msgid "Login and access status, CLI, neighbors, and settings" msgstr "Log in en krijg toegang tot status, CLI, buren, en instellingen" #: src/views/contacts_view.py:1710 msgid "Ping" msgstr "Ping" #: src/views/contacts_view.py:1711 msgid "Measure round-trip time to this node" msgstr "Meet de retourtijd naar dit knooppunt" #: src/views/contacts_view.py:1725 msgid "Discover Paths" msgstr "Ontdek Paden" #: src/views/contacts_view.py:1726 msgid "Find routes to this node via flood" msgstr "Vind routes naar dit knooppunt via flood" #: src/views/contacts_view.py:1748 msgid "Remove Contact" msgstr "Contact verwijderen" #: src/views/contacts_view.py:1754 msgid "Remove Contact?" msgstr "Contact Verwijderen?" #: src/views/contacts_view.py:1755 #, python-brace-format msgid "Remove {} and all messages?" msgstr "{} en alle berichten verwijderen?" #: src/views/channels_view.py:116 src/views/channels_view.py:1397 #: src/views/channels_view.py:1404 msgid "Heard Repeats" msgstr "Gehoorde Herhalingen" #: src/views/channels_view.py:117 src/views/chat_view.py:92 msgid "Send Again" msgstr "Opnieuw Verzenden" #: src/views/channels_view.py:118 src/views/channels_view.py:124 #: src/views/chat_view.py:93 msgid "Delete" msgstr "Verwijderen" #: src/views/channels_view.py:121 msgid "Reply" msgstr "Antwoord" #: src/views/channels_view.py:122 msgid "Copy Text" msgstr "Tekst Kopiëren" #: src/views/channels_view.py:123 msgid "View Message Paths" msgstr "Berichtpaden Bekijken" #: src/views/channels_view.py:176 src/views/chat_view.py:138 msgid "Share Location" msgstr "" #: src/views/channels_view.py:325 src/views/chat_view.py:266 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:457 src/views/chat_view.py:375 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:461 src/views/chat_view.py:379 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:481 src/views/channels_view.py:591 #: src/views/chat_view.py:399 src/views/chat_view.py:508 msgid "Already added" msgstr "" #: src/views/channels_view.py:496 src/views/chat_view.py:414 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:500 src/views/chat_view.py:418 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:558 src/views/chat_view.py:475 msgid "New Messages" msgstr "Nieuwe Berichten" #: src/views/channels_view.py:598 src/views/chat_view.py:515 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:602 src/views/chat_view.py:519 msgid "Shared" msgstr "" #: src/views/channels_view.py:783 src/views/chat_view.py:660 msgid "Share" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:692 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:816 src/views/chat_view.py:693 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1174 src/views/channels_view.py:1183 msgid "Message Paths" msgstr "Berichtpaden" #: src/views/channels_view.py:1175 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" "Geen padinformatie beschikbaar. Padgegevens worden alleen vastgelegd voor " "berichten die worden ontvangen terwijl er verbinding is." #: src/views/channels_view.py:1258 msgid "Direct message" msgstr "Direct bericht" #: src/views/channels_view.py:1259 msgid "No repeaters were used" msgstr "Er zijn geen repeaters gebruikt" #: src/views/channels_view.py:1267 msgid "No repeater details" msgstr "Geen repeatergegevens" #: src/views/channels_view.py:1268 msgid "Path data is only captured via LOG_DATA while connected" msgstr "Padgegevens worden alleen vastgelegd via LOG_DATA als er verbinding is" #: src/views/channels_view.py:1317 msgid "Message Path" msgstr "Berichtpad" #: src/views/channels_view.py:1398 msgid "No repeats heard yet." msgstr "Nog geen herhalingen gehoord." #: src/views/channels_view.py:1768 src/views/channels_view.py:2216 msgid "Public" msgstr "Openbaar" #: src/views/channels_view.py:1769 msgid "Hashtag" msgstr "Hashtag" #: src/views/channels_view.py:1769 msgid "Private" msgstr "Privé" #: src/views/channels_view.py:1814 #, python-brace-format msgid "{visible}/{total} channels" msgstr "{visible}/{total} kanalen" #: src/views/channels_view.py:1817 #, python-brace-format msgid "{total} channels" msgstr "{total} kanalen" #: src/views/channels_view.py:1818 #, python-brace-format msgid "{total} channel" msgstr "{total} kanaal" #: src/views/channels_view.py:1851 #, python-brace-format msgid "{name} ({region})" msgstr "{name} ({region})" #: src/views/channels_view.py:1982 msgid "Channel Info" msgstr "Kanaalinformatie" #: src/views/channels_view.py:2009 msgid "Private Key" msgstr "Privésleutel" #: src/views/channels_view.py:2020 msgid "Private key copied to clipboard" msgstr "Privésleutel gekopieerd naar klembord" #: src/views/channels_view.py:2029 msgid "Notifications" msgstr "Meldingen" #: src/views/channels_view.py:2031 msgid "Notification Level" msgstr "Meldingsniveau" #: src/views/channels_view.py:2033 msgid "Default" msgstr "Standaard" #: src/views/channels_view.py:2059 src/views/channels_view.py:2063 msgid "Region" msgstr "Regio" #: src/views/channels_view.py:2060 msgid "Limit flood messages to a specific region" msgstr "Beperk flood-berichten tot een specifieke regio" #: src/views/channels_view.py:2091 msgid "Remove Channel" msgstr "Kanaal verwijderen" #: src/views/channels_view.py:2097 msgid "Remove Channel?" msgstr "Kanaal verwijderen?" #: src/views/channels_view.py:2098 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "'{}' en alle bijbehorende berichten verwijderen?" #: src/views/channels_view.py:2132 msgid "No Slots Available" msgstr "Geen Slots Beschikbaar" #: src/views/channels_view.py:2133 msgid "All 8 channel slots are in use." msgstr "Alle 8 kanaalslots zijn in gebruik." #: src/views/channels_view.py:2144 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" "Er wordt een willekeurige PSK gegenereerd. Deel die met anderen, zodat zij " "lid kunnen worden." #: src/views/channels_view.py:2146 src/views/channels_view.py:2176 msgid "Channel Name" msgstr "Kanaalnaam" #: src/views/channels_view.py:2153 msgid "Create" msgstr "Aanmaken" #: src/views/channels_view.py:2174 msgid "Enter the channel name and private key shared with you." msgstr "Voer de kanaalnaam en de privésleutel in die met u zijn gedeeld." #: src/views/channels_view.py:2177 msgid "Private Key (32 hex characters)" msgstr "Privésleutel (32 hexadecimale tekens)" #: src/views/channels_view.py:2185 src/views/channels_view.py:2233 msgid "Join" msgstr "Lid Worden" #: src/views/channels_view.py:2205 msgid "Already Joined" msgstr "Al Lid" #: src/views/channels_view.py:2205 msgid "You are already on the public channel." msgstr "Je bent al op het openbare kanaal." #: src/views/channels_view.py:2224 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" "Voer een hashtagnaam in. Iedereen die dezelfde hashtag gebruikt, kan " "communiceren." #: src/views/channels_view.py:2226 msgid "Hashtag (e.g. #general)" msgstr "Hashtag (bv. #algemeen)" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "Openbare sleutel van apparaat niet beschikbaar" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "QR-codebibliotheek niet beschikbaar" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "Uw Contact-QR-Code" #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "Scan deze QR-code om dit contact toe te voegen" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "Fabrieksreset?" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" "Hierdoor worden ALLE gegevens op het companion apparaat permanent gewist, " "inclusief contacten, berichten, kanalen, instellingen en de " "apparaatidentiteit (sleutels). Deze actie kan niet ongedaan worden gemaakt." #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "Apparaat Opnieuw Opstarten?" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "Het apparaat wordt losgekoppeld en opnieuw opgestart." #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "Opnieuw Opstarten" #: src/views/device_view.py:323 msgid "Not set" msgstr "Niet ingesteld" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "Locatie (handmatig ingesteld)" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "Locatie (GPS)" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "Locatie (GPS aan, geen fix)" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "Locatie (GPS uit)" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "Aanvragen…" #: src/views/device_view.py:405 msgid "Device Location" msgstr "Apparaatlocatie" #: src/views/device_view.py:463 #, python-brace-format msgid "{days}d" msgstr "{days}d" #: src/views/device_view.py:465 #, python-brace-format msgid "{hours}h" msgstr "{hours}h" #: src/views/device_view.py:466 #, python-brace-format msgid "{mins}m" msgstr "{mins}m" #: src/views/device_view.py:468 #, python-brace-format msgid "{} messages" msgstr "{} berichten" #: src/views/device_view.py:486 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:499 src/views/device_view.py:501 msgid "Connected" msgstr "Verbonden" #: src/views/device_view.py:514 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "Traceer Pad wordt niet ondersteund met pad-hashes van 3 bytes" #: src/views/device_view.py:533 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:533 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:533 msgid "e.g. aabbcc,ddeeff" msgstr "" #: src/views/device_view.py:541 msgid "Add from Contacts" msgstr "Toevoegen vanuit Contacten" #: src/views/device_view.py:573 msgid "No repeaters or rooms in contacts" msgstr "Geen repeaters of kamers in contacten" #: src/views/device_view.py:586 #, fuzzy msgid "No repeaters with known location" msgstr "Geen repeaters in Contacten" #: src/views/device_view.py:589 src/views/device_view.py:1225 msgid "Path" msgstr "Pad invoer" #: src/views/device_view.py:649 msgid "Run Trace" msgstr "Trace Uitvoeren" #: src/views/device_view.py:662 msgid "Enter path hashes and press Run Trace" msgstr "Voer pad-hashes in en druk op Trace Uitvoeren" #: src/views/device_view.py:729 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "Trace voltooid: {} hops, {:.1f}s" #: src/views/device_view.py:776 msgid "Trace timed out" msgstr "Er is een time-out opgetreden bij het traceren" #: src/views/device_view.py:792 msgid "Invalid hex in path" msgstr "Ongeldige hex in pad" #: src/views/device_view.py:799 msgid "Tracing..." msgstr "Traceren..." #: src/views/device_view.py:959 src/views/device_view.py:1149 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "Bezig met scannen... {}s resterend" #: src/views/device_view.py:976 msgid "Listening..." msgstr "Luisteren..." #: src/views/device_view.py:977 msgid "Waiting for nearby nodes to respond" msgstr "Wachten tot nabijgelegen knooppunten reageren" #: src/views/device_view.py:1030 msgid "Add to Contacts" msgstr "Toevoegen aan Contacten" #: src/views/device_view.py:1037 #, python-brace-format msgid "Added {}" msgstr "{} toegevoegd" #: src/views/device_view.py:1065 msgid "Node" msgstr "Knooppunt" #: src/views/device_view.py:1158 #, python-brace-format msgid "Done. {} nodes found." msgstr "Klaar. {} knoppunten gevonden." #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} node found." msgstr "Klaar. {} knoppunt gevonden." #: src/views/device_view.py:1221 msgid "Size" msgstr "Grootte" #: src/views/device_view.py:1221 msgid "bytes" msgstr "bytes" #: src/views/device_view.py:1225 msgid "hops" msgstr "hops" #: src/views/device_view.py:1227 msgid "Path Hashes" msgstr "Pad-hashes" #: src/views/device_view.py:1227 msgid "byte per hop" msgstr "byte per hop" #: src/views/device_view.py:1306 #, python-brace-format msgid "Update available: {version}" msgstr "Update beschikbaar: {version}" #: src/views/settings_view.py:131 msgid "Custom" msgstr "Aangepast" #: src/views/settings_view.py:139 msgid "Deny All" msgstr "Alles Weigeren" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "Toestaan per Contact" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "Iedereen toestaan" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "Instellingen toegepast op apparaat" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "Onbekende fout" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "Instellingenfout: {}" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "TX-vermogen (dBm, max. {})" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "Locatie ingevuld, klik op Toepassen om op te slaan" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "Locatie niet beschikbaar" #: src/views/settings_view.py:749 msgid "Pick Location" msgstr "Kies Locatie" #: src/views/settings_view.py:758 msgid "Set Location" msgstr "Locatie Instellen" #: src/views/settings_view.py:897 msgid "Connect to a device first" msgstr "Maak eerst verbinding met een apparaat" #: src/views/settings_view.py:903 msgid "No repeaters in contacts" msgstr "Geen repeaters in Contacten" #: src/views/settings_view.py:907 msgid "Discover Regions" msgstr "Ontdek Regio's" #: src/views/settings_view.py:924 #, python-brace-format msgid "Querying {} repeaters..." msgstr "{} repeaters opvragen..." #: src/views/settings_view.py:925 #, python-brace-format msgid "Querying {} repeater..." msgstr "{} repeater opvragen..." #: src/views/settings_view.py:937 msgid "Waiting..." msgstr "Wachten..." #: src/views/settings_view.py:938 msgid "Querying repeaters for regions" msgstr "Regio's van repeaters opvragen" #: src/views/settings_view.py:950 msgid "Add Selected" msgstr "Geselecteerde Toevoegen" #: src/views/settings_view.py:969 #, python-brace-format msgid "Found {} regions" msgstr "{} regio's gevonden" #: src/views/settings_view.py:970 #, python-brace-format msgid "Found {} region" msgstr "{} regio gevonden" #: src/views/settings_view.py:973 msgid "No regions found" msgstr "Geen regio's gevonden" #: src/views/settings_view.py:989 #, python-brace-format msgid "from {}" msgstr "van {}" #: src/views/settings_view.py:991 msgid "(already added)" msgstr "(al toegevoegd)" #: src/views/settings_view.py:1027 #, python-brace-format msgid "Added {} regions" msgstr "{} regio's toegevoegd" #: src/views/settings_view.py:1028 #, python-brace-format msgid "Added {} region" msgstr "{} regio toegevoegd" #: src/views/settings_view.py:1059 msgid "Export Backup" msgstr "Back-up Exporteren" #: src/views/settings_view.py:1060 msgid "Save to a file" msgstr "Opslaan in een bestand" #: src/views/settings_view.py:1072 src/views/settings_view.py:1219 msgid "Import Backup" msgstr "Back-up Importeren" #: src/views/settings_view.py:1073 msgid "Restore from a file" msgstr "Terugzetten vanuit een bestand" #: src/views/settings_view.py:1093 src/views/settings_view.py:1134 msgid "Not Connected" msgstr "Niet Verbonden" #: src/views/settings_view.py:1094 msgid "Connect to a device first to export a backup." msgstr "Maak eerst verbinding met een apparaat om een back-up te exporteren." #: src/views/settings_view.py:1109 src/views/settings_view.py:1144 msgid "JSON files" msgstr "JSON bestanden" #: src/views/settings_view.py:1122 msgid "Backup exported successfully" msgstr "Back-up succesvol geëxporteerd" #: src/views/settings_view.py:1126 #, python-brace-format msgid "Export failed: {}" msgstr "Exporteren mislukt: {}" #: src/views/settings_view.py:1135 msgid "Connect to a device first to import a backup." msgstr "Maak eerst verbinding met een apparaat om een back-up te importeren." #: src/views/settings_view.py:1171 #, python-brace-format msgid "Could not read backup file: {}" msgstr "Kon back-upbestand niet lezen: {}" #: src/views/settings_view.py:1204 #, python-brace-format msgid "Source: {}" msgstr "Bron: {}" #: src/views/settings_view.py:1206 msgid "Device settings: name, radio, location" msgstr "Apparaatinstellingen: naam, radio, locatie" #: src/views/settings_view.py:1208 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "Contacten: {} ({} nieuw)" #: src/views/settings_view.py:1210 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "Kanalen: {} ({} nieuw)" #: src/views/settings_view.py:1212 #, python-brace-format msgid "Messages: {}" msgstr "Berichten: {}" #: src/views/settings_view.py:1214 #, python-brace-format msgid "Channel messages: {}" msgstr "Kanaalberichten: {}" #: src/views/settings_view.py:1216 #, python-brace-format msgid "Saved passwords: {}" msgstr "Opgeslagen wachtwoorden: {}" #: src/views/settings_view.py:1224 msgid "Contacts & Channels Only" msgstr "Alleen Contacten en Kanalen" #: src/views/settings_view.py:1225 msgid "Everything" msgstr "Alles" #: src/views/settings_view.py:1236 #, python-brace-format msgid "{} contacts" msgstr "{} contacten" #: src/views/settings_view.py:1238 #, python-brace-format msgid "{} channels" msgstr "{} kanalen" #: src/views/settings_view.py:1240 msgid "messages" msgstr "berichten" #: src/views/settings_view.py:1243 #, python-brace-format msgid "Imported: {}" msgstr "Geïmporteerd: {}" #: src/views/settings_view.py:1245 msgid "Nothing new to import" msgstr "Er is niets nieuws om te importeren" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "{d}d" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "{h}h" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "{m}m" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "{secs}s" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "{}s geleden" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "{}m geleden" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "{}h geleden" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "{}d geleden" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "Tijdsynchronisatie verzonden naar repeater" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "Klok Resetten en Opnieuw Opstarten?" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" "De repeaterklok loopt voor op de huidige tijd. \n" "Firmware staat niet toe dat de tijd achteruit wordt gezet. \n" "\n" "Hierdoor wordt de klok opnieuw ingesteld en wordt de repeater opnieuw " "opgestart." #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "Opnieuw opstarten..." #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "Klokreset en herstart naar repeater verzonden" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "Verzoek is verlopen" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "{} van {} buren" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "Burenkaart" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "Beheerder" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "{} vermelding" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "{} vermeldingen" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "Uit ACL Verwijderen?" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "{} verwijderen uit de toegangscontrolelijst?" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "Voeg toe aan Toegangscontrole" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "Selecteer een contact en een rol." #: src/views/repeater_view.py:757 msgid "Role" msgstr "Rol" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "Globaal (wildcard)" #: src/views/repeater_view.py:942 msgid "Home" msgstr "Thuis" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "Flood toegestaan" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "Flood geweigerd" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "Flood Weigeren" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "Flood Toelaten" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "Instellen als Thuis" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "Verwijder eerst onderliggende regio's" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "Niet-opgeslagen wijzigingen" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "U heeft niet-opgeslagen regiowijzigingen." #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "Weggooien" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "Regio Toevoegen" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "Voer een naam in en kies optioneel een bovenliggende regio." #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "Regionaam" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "Ouder" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "Beheerderswachtwoord gewijzigd — opnieuw inloggen vereist" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "Instellingen verzonden naar {}" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "Repeater opnieuw opstarten?" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "{} opnieuw opstarten? Die zal kort offline zijn." #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "Tijd wijkt af" #: src/views/chat_view.py:549 src/views/chat_view.py:550 msgid "Sending" msgstr "Verzenden" #: src/views/map_view.py:99 msgid "User" msgstr "Gebruiker" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "{} van {} contacten op kaart" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "{} van {} gelegen op de kaart" #: src/views/map_view.py:198 msgid "Rooms" msgstr "Kamers" #: src/views/map_view.py:227 #, fuzzy msgid "Discovered Nodes" msgstr "Ontdek Nabijgelegen Knooppunten" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "en {} meer" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "SNR: {:.1f} dB" #: src/views/map_view.py:970 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1157 msgid "Undo" msgstr "" #: src/views/map_view.py:1167 #, fuzzy msgid "Clear" msgstr "Log Wissen" #: src/views/map_view.py:1175 msgid "Done" msgstr "" #: src/views/map_view.py:1192 msgid "Tap repeaters to build the trace route" msgstr "" #~ msgid "IP Address" #~ msgstr "IP-adres" meshy/po/pl.po000066400000000000000000001746751521052255700136240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/pt.po000066400000000000000000001745771521052255700136350ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/sk.po000066400000000000000000001746351521052255700136220ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: sk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/sv.po000066400000000000000000001746001521052255700136250ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the meshy package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: meshy\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-06-01 13:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" #: data/page.codeberg.sesivany.Meshy.desktop.in:4 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:8 data/ui/window.ui:111 msgid "Meshy" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:5 #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:9 msgid "MeshCore mesh network client" msgstr "" #: data/page.codeberg.sesivany.Meshy.desktop.in:11 msgid "mesh;lora;radio;chat;" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:11 msgid "" "Meshy is a client for MeshCore LoRa mesh networking devices. Connect to your " "companion device over Bluetooth to send and receive encrypted messages, " "manage contacts, configure channels, and monitor your mesh network." msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:17 msgid "Features:" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:19 msgid "Bluetooth LE and USB serial connectivity" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:20 msgid "End-to-end encrypted messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:21 msgid "Public, private, and hashtag channels" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:22 msgid "Contact management with location tracking" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:23 msgid "Interactive map view with route tracing" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:24 msgid "Device and repeater management" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:25 msgid "QR code scanning for quick contact exchange" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:30 msgid "Jiri Eischmann" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:42 msgid "Contacts view with messaging" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:46 msgid "Channel list with different channel types" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:50 msgid "Map view showing contact locations" msgstr "" #: data/page.codeberg.sesivany.Meshy.metainfo.xml.in:54 msgid "Device information and settings" msgstr "" #: data/gtk/help-overlay.ui:10 msgid "General" msgstr "" #: data/gtk/help-overlay.ui:13 data/ui/window.ui:23 data/ui/window.ui:40 msgid "Quit" msgstr "" #: data/gtk/help-overlay.ui:19 data/ui/window.ui:15 data/ui/window.ui:32 msgid "Keyboard Shortcuts" msgstr "" #: data/gtk/help-overlay.ui:27 msgid "Navigation" msgstr "" #: data/gtk/help-overlay.ui:30 data/ui/window.ui:169 msgid "Device" msgstr "" #: data/gtk/help-overlay.ui:36 data/ui/window.ui:182 src/window.py:523 msgid "Contacts" msgstr "" #: data/gtk/help-overlay.ui:42 data/ui/window.ui:195 src/window.py:535 msgid "Channels" msgstr "" #. Navigation button for collapsed mode #: data/gtk/help-overlay.ui:48 data/ui/window.ui:208 src/window.py:550 #: src/window.py:553 src/views/map_view.py:885 msgid "Map" msgstr "" #: data/gtk/help-overlay.ui:54 data/ui/window.ui:228 #: data/ui/repeater-view.ui:574 src/window.py:567 src/window.py:568 msgid "Settings" msgstr "" #: data/gtk/help-overlay.ui:62 msgid "Messaging" msgstr "" #: data/gtk/help-overlay.ui:65 msgid "Search Contacts" msgstr "" #: data/gtk/help-overlay.ui:71 msgid "Focus Message Input" msgstr "" #: data/gtk/help-overlay.ui:77 msgid "Previous Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:83 msgid "Next Contact/Channel" msgstr "" #: data/gtk/help-overlay.ui:89 msgid "Previous Unread" msgstr "" #: data/gtk/help-overlay.ui:95 msgid "Next Unread" msgstr "" #: data/ui/window.ui:11 msgid "Disconnect" msgstr "" #: data/ui/window.ui:19 data/ui/window.ui:36 msgid "About Meshy" msgstr "" #: data/ui/window.ui:66 data/ui/window.ui:284 src/connection_controller.py:256 #: src/window.py:891 src/views/connection_view.py:112 #: src/views/connection_view.py:160 src/views/connection_view.py:381 #: src/views/connection_view.py:447 msgid "Connect" msgstr "" #: data/ui/window.ui:73 data/ui/window.ui:118 data/ui/window.ui:272 msgid "Menu" msgstr "" #: data/ui/window.ui:129 msgid "Connecting…" msgstr "" #: data/ui/window.ui:261 msgid "Show navigation" msgstr "" #: data/ui/window.ui:282 src/connection_controller.py:255 src/window.py:890 #: src/views/device_view.py:354 msgid "Not connected" msgstr "" #: data/ui/device-view.ui:29 data/ui/repeater-view.ui:19 msgid "Status" msgstr "" #: data/ui/device-view.ui:30 src/views/device_view.py:505 msgid "Disconnected" msgstr "" #: data/ui/device-view.ui:44 src/window.py:512 src/window.py:513 msgid "Device Information" msgstr "" #: data/ui/device-view.ui:47 msgid "Node Name" msgstr "" #: data/ui/device-view.ui:53 msgid "Board" msgstr "" #: data/ui/device-view.ui:59 msgid "Firmware" msgstr "" #: data/ui/device-view.ui:63 msgid "Update" msgstr "" #: data/ui/device-view.ui:66 msgid "Open firmware flasher" msgstr "" #: data/ui/device-view.ui:77 src/views/contacts_view.py:1509 msgid "Public Key" msgstr "" #. Telemetry group #: data/ui/device-view.ui:90 src/views/contacts_view.py:1630 msgid "Telemetry" msgstr "" #: data/ui/device-view.ui:96 msgid "Request telemetry" msgstr "" #: data/ui/device-view.ui:106 data/ui/settings-view.ui:212 #: data/ui/settings-view.ui:353 data/ui/repeater-view.ui:672 #: src/views/contacts_view.py:1526 src/views/device_view.py:332 #: src/views/settings_view.py:121 msgid "Location" msgstr "" #: data/ui/device-view.ui:107 msgid "Syncing…" msgstr "" #: data/ui/device-view.ui:112 msgid "Show on map" msgstr "" #: data/ui/device-view.ui:127 msgid "Battery & Storage" msgstr "" #: data/ui/device-view.ui:130 data/ui/repeater-view.ui:76 #: src/views/contacts_view.py:1191 src/views/repeater_view.py:448 msgid "Battery" msgstr "" #: data/ui/device-view.ui:149 msgid "Storage" msgstr "" #: data/ui/device-view.ui:172 msgid "Radio Configuration" msgstr "" #: data/ui/device-view.ui:175 msgid "Frequency" msgstr "" #: data/ui/device-view.ui:181 msgid "Bandwidth" msgstr "" #: data/ui/device-view.ui:187 data/ui/settings-view.ui:103 #: data/ui/repeater-view.ui:758 msgid "Spreading Factor" msgstr "" #: data/ui/device-view.ui:193 data/ui/settings-view.ui:116 #: data/ui/repeater-view.ui:770 msgid "Coding Rate" msgstr "" #: data/ui/device-view.ui:199 msgid "TX Power" msgstr "" #: data/ui/device-view.ui:205 data/ui/settings-view.ui:178 msgid "Default Path Hash Size" msgstr "" #: data/ui/device-view.ui:215 msgid "Statistics" msgstr "" #: data/ui/device-view.ui:218 data/ui/repeater-view.ui:82 msgid "Uptime" msgstr "" #: data/ui/device-view.ui:224 data/ui/repeater-view.ui:88 msgid "Message Queue" msgstr "" #: data/ui/device-view.ui:230 data/ui/repeater-view.ui:119 msgid "Noise Floor" msgstr "" #: data/ui/device-view.ui:236 data/ui/repeater-view.ui:107 msgid "Last RSSI" msgstr "" #: data/ui/device-view.ui:242 data/ui/repeater-view.ui:113 msgid "Last SNR" msgstr "" #: data/ui/device-view.ui:248 msgid "Airtime" msgstr "" #: data/ui/device-view.ui:254 data/ui/repeater-view.ui:153 msgid "Packets" msgstr "" #: data/ui/device-view.ui:260 data/ui/repeater-view.ui:175 #: src/views/contacts_view.py:1050 msgid "Flood" msgstr "" #: data/ui/device-view.ui:266 data/ui/repeater-view.ui:181 #: src/views/contacts_view.py:1000 src/views/contacts_view.py:1052 msgid "Direct" msgstr "" #: data/ui/device-view.ui:272 msgid "Refresh Statistics" msgstr "" #: data/ui/settings-view.ui:16 src/views/settings_view.py:118 msgid "Application" msgstr "" #: data/ui/settings-view.ui:17 msgid "Configure the behavior and appearance of the application" msgstr "" #: data/ui/settings-view.ui:20 msgid "Style" msgstr "" #: data/ui/settings-view.ui:21 msgid "Choose the appearance of the application" msgstr "" #: data/ui/settings-view.ui:25 msgid "Follow System" msgstr "" #: data/ui/settings-view.ui:26 msgid "Light" msgstr "" #: data/ui/settings-view.ui:27 msgid "Dark" msgstr "" #: data/ui/settings-view.ui:28 data/ui/settings-view.ui:36 msgid "Palette Theme" msgstr "" #: data/ui/settings-view.ui:37 msgid "MeshCore Dark" msgstr "" #: data/ui/settings-view.ui:49 msgid "Channel Notifications" msgstr "" #: data/ui/settings-view.ui:50 msgid "Default notification level for all channels" msgstr "" #: data/ui/settings-view.ui:54 src/views/channels_view.py:1935 msgid "All Messages" msgstr "" #: data/ui/settings-view.ui:55 src/views/channels_view.py:1935 msgid "Mentions Only" msgstr "" #: data/ui/settings-view.ui:56 src/views/channels_view.py:1935 #: src/views/channels_view.py:1967 src/views/settings_view.py:146 #: src/views/repeater_view.py:1116 msgid "None" msgstr "" #: data/ui/settings-view.ui:64 msgid "Run in Background" msgstr "" #: data/ui/settings-view.ui:65 msgid "Keep receiving messages after closing the window" msgstr "" #: data/ui/settings-view.ui:70 msgid "Autostart" msgstr "" #: data/ui/settings-view.ui:71 msgid "Automatically start when you log in" msgstr "" #: data/ui/settings-view.ui:80 data/ui/repeater-view.ui:104 #: data/ui/repeater-view.ui:717 src/views/settings_view.py:122 msgid "Radio" msgstr "" #: data/ui/settings-view.ui:81 msgid "LoRa radio parameters and regional presets" msgstr "" #: data/ui/settings-view.ui:84 msgid "Regional Preset" msgstr "" #: data/ui/settings-view.ui:89 msgid "Preset Configuration" msgstr "" #: data/ui/settings-view.ui:90 msgid "Frequency, bandwidth, spreading factor, coding rate" msgstr "" #: data/ui/settings-view.ui:93 data/ui/repeater-view.ui:748 msgid "Frequency (MHz)" msgstr "" #: data/ui/settings-view.ui:98 data/ui/repeater-view.ui:753 msgid "Bandwidth (kHz)" msgstr "" #: data/ui/settings-view.ui:131 msgid "Repeat Mode" msgstr "" #: data/ui/settings-view.ui:132 msgid "Act as a portable repeater on an off-grid frequency" msgstr "" #: data/ui/settings-view.ui:138 data/ui/repeater-view.ui:782 msgid "TX Power (dBm)" msgstr "" #: data/ui/settings-view.ui:155 src/views/settings_view.py:119 msgid "Advert" msgstr "" #: data/ui/settings-view.ui:156 msgid "Configure how your node advertises itself on the mesh" msgstr "" #: data/ui/settings-view.ui:159 msgid "Device Name" msgstr "" #: data/ui/settings-view.ui:164 msgid "Include Location in Advert" msgstr "" #: data/ui/settings-view.ui:165 msgid "Broadcast your position to other nodes" msgstr "" #: data/ui/settings-view.ui:174 src/views/settings_view.py:120 msgid "Routing and Messaging" msgstr "" #: data/ui/settings-view.ui:175 msgid "Path routing and message delivery settings" msgstr "" #: data/ui/settings-view.ui:183 src/views/device_view.py:290 msgid "1-byte (max 64 hops)" msgstr "" #: data/ui/settings-view.ui:184 src/views/device_view.py:290 msgid "2-byte (max 32 hops)" msgstr "" #: data/ui/settings-view.ui:185 src/views/device_view.py:290 msgid "3-byte (max 21 hops)" msgstr "" #: data/ui/settings-view.ui:193 msgid "Direct Message Confirmations" msgstr "" #: data/ui/settings-view.ui:194 msgid "Number of ACKs sent when receiving a direct message" msgstr "" #: data/ui/settings-view.ui:213 msgid "Set your companion's position via GPS or manually" msgstr "" #: data/ui/settings-view.ui:216 msgid "Companion GPS" msgstr "" #: data/ui/settings-view.ui:217 msgid "Enable GPS on the companion, your position will update automatically" msgstr "" #: data/ui/settings-view.ui:223 msgid "Set Location Manually" msgstr "" #: data/ui/settings-view.ui:224 msgid "Choose another method to set the location, it will be fixed" msgstr "" #: data/ui/settings-view.ui:227 data/ui/repeater-view.ui:703 msgid "Latitude" msgstr "" #: data/ui/settings-view.ui:232 data/ui/repeater-view.ui:708 msgid "Longitude" msgstr "" #: data/ui/settings-view.ui:237 msgid "Use My Location" msgstr "" #: data/ui/settings-view.ui:238 msgid "Set from this computer's location" msgstr "" #. Pick on Map button #: data/ui/settings-view.ui:258 src/views/device_view.py:581 msgid "Pick on Map" msgstr "" #: data/ui/settings-view.ui:259 msgid "Choose a location on the map" msgstr "" #: data/ui/settings-view.ui:285 data/ui/settings-view.ui:289 #: src/views/settings_view.py:123 msgid "Auto-Add Contacts" msgstr "" #: data/ui/settings-view.ui:286 msgid "Automatically add nodes discovered via adverts" msgstr "" #: data/ui/settings-view.ui:294 msgid "Auto-Add Settings" msgstr "" #: data/ui/settings-view.ui:295 msgid "Device types, overwrite policy, hop limit" msgstr "" #: data/ui/settings-view.ui:298 msgid "Chat Nodes" msgstr "" #: data/ui/settings-view.ui:303 src/views/contacts_view.py:126 #: src/views/contacts_view.py:246 src/views/map_view.py:197 msgid "Repeaters" msgstr "" #: data/ui/settings-view.ui:308 src/views/contacts_view.py:127 #: src/views/contacts_view.py:246 msgid "Room Servers" msgstr "" #: data/ui/settings-view.ui:313 src/views/contacts_view.py:127 #: src/views/contacts_view.py:247 src/views/map_view.py:199 msgid "Sensors" msgstr "" #: data/ui/settings-view.ui:318 msgid "Overwrite oldest when full" msgstr "" #: data/ui/settings-view.ui:323 msgid "Hop limit" msgstr "" #: data/ui/settings-view.ui:324 msgid "0–63, leave empty for no limit" msgstr "" #: data/ui/settings-view.ui:343 src/views/settings_view.py:124 msgid "Telemetry Privacy" msgstr "" #: data/ui/settings-view.ui:344 msgid "Control what others can query from your device" msgstr "" #: data/ui/settings-view.ui:347 msgid "Battery & Sensors" msgstr "" #: data/ui/settings-view.ui:348 msgid "Battery, voltage, temperature, current" msgstr "" #: data/ui/settings-view.ui:354 msgid "GPS coordinates" msgstr "" #: data/ui/settings-view.ui:359 msgid "Environment" msgstr "" #: data/ui/settings-view.ui:360 msgid "Humidity, pressure, air quality" msgstr "" #: data/ui/settings-view.ui:369 data/ui/repeater-view.ui:458 #: src/views/settings_view.py:125 msgid "Regions" msgstr "" #: data/ui/settings-view.ui:370 msgid "Limit flood scope of channel messages to specific regions" msgstr "" #: data/ui/settings-view.ui:373 msgid "Add Region (e.g. Prague)" msgstr "" #: data/ui/settings-view.ui:379 msgid "Default Scope" msgstr "" #: data/ui/settings-view.ui:380 msgid "All flood packets will be scoped to this region if set" msgstr "" #: data/ui/settings-view.ui:390 data/ui/connection-view.ui:35 #: src/views/settings_view.py:126 msgid "Bluetooth" msgstr "" #: data/ui/settings-view.ui:391 msgid "" "After changing the PIN, restart the device, then unpair and re-pair. Setting " "0 resets the PIN to default." msgstr "" #: data/ui/settings-view.ui:394 msgid "Pairing PIN" msgstr "" #: data/ui/connection-view.ui:23 msgid "Connect to a companion device to start using Meshy." msgstr "" #: data/ui/connection-view.ui:48 msgid "Pair with a New Companion" msgstr "" #: data/ui/connection-view.ui:56 msgid "USB Serial" msgstr "" #: data/ui/connection-view.ui:71 msgid "WiFi / TCP" msgstr "" #: data/ui/connection-view.ui:84 src/views/connection_view.py:429 msgid "Add TCP Companion" msgstr "" #: data/ui/contacts-view.ui:14 data/ui/channels-view.ui:33 #: data/ui/discover-dialog.ui:51 data/ui/share-contact-dialog.ui:32 msgid "Search..." msgstr "" #: data/ui/contacts-view.ui:40 data/ui/discover-dialog.ui:74 #: data/ui/share-contact-dialog.ui:53 src/views/contacts_view.py:431 #: src/views/channels_view.py:798 src/views/chat_view.py:673 msgid "No contacts" msgstr "" #: data/ui/contacts-view.ui:53 src/views/contacts_view.py:442 #: src/views/contacts_view.py:623 src/views/channels_view.py:291 #: src/views/channels_view.py:475 src/views/chat_view.py:232 #: src/views/chat_view.py:393 msgid "Add Contact" msgstr "" #: data/ui/chat-view.ui:22 msgid "Select a Contact" msgstr "" #: data/ui/chat-view.ui:23 msgid "Choose a contact from the list to start chatting" msgstr "" #: data/ui/chat-view.ui:85 data/ui/channel-chat-widget.ui:85 msgid "↑ New Messages" msgstr "" #: data/ui/chat-view.ui:134 msgid "Type a message..." msgstr "" #: data/ui/channels-view.ui:10 src/views/channels_view.py:2061 msgid "Create a Private Channel" msgstr "" #: data/ui/channels-view.ui:14 src/views/channels_view.py:2091 msgid "Join a Private Channel" msgstr "" #: data/ui/channels-view.ui:18 msgid "Join the Public Channel" msgstr "" #: data/ui/channels-view.ui:22 src/views/channels_view.py:2141 msgid "Join a Hashtag Channel" msgstr "" #: data/ui/channels-view.ui:58 msgid "No channels" msgstr "" #: data/ui/channels-view.ui:70 src/application.py:129 msgid "Add Channel" msgstr "" #: data/ui/channel-chat-widget.ui:22 msgid "Select a Channel" msgstr "" #: data/ui/channel-chat-widget.ui:23 msgid "Choose a channel from the list" msgstr "" #: data/ui/channel-chat-widget.ui:134 msgid "Message channel..." msgstr "" #: data/ui/repeater-view.ui:37 msgid "System" msgstr "" #: data/ui/repeater-view.ui:40 msgid "Clock at Login" msgstr "" #: data/ui/repeater-view.ui:54 msgid "Sync Now" msgstr "" #: data/ui/repeater-view.ui:64 src/views/repeater_view.py:303 msgid "Reset & Reboot" msgstr "" #: data/ui/repeater-view.ui:94 msgid "Error Events" msgstr "" #: data/ui/repeater-view.ui:125 msgid "TX Airtime" msgstr "" #: data/ui/repeater-view.ui:131 msgid "RX Airtime" msgstr "" #: data/ui/repeater-view.ui:137 msgid "Channel Utilization" msgstr "" #: data/ui/repeater-view.ui:143 msgid "Duty Cycle" msgstr "" #: data/ui/repeater-view.ui:156 msgid "Sent" msgstr "" #: data/ui/repeater-view.ui:162 msgid "Received" msgstr "" #: data/ui/repeater-view.ui:168 msgid "Receive Errors" msgstr "" #: data/ui/repeater-view.ui:187 msgid "Duplicates" msgstr "" #: data/ui/repeater-view.ui:201 data/ui/repeater-view.ui:344 #: data/ui/repeater-view.ui:429 msgid "Refresh" msgstr "" #: data/ui/repeater-view.ui:209 src/views/contacts_view.py:1660 #: src/views/repeater_view.py:380 msgid "Request Telemetry" msgstr "" #: data/ui/repeater-view.ui:229 msgid "CLI" msgstr "" #: data/ui/repeater-view.ui:272 msgid "CLI command..." msgstr "" #: data/ui/repeater-view.ui:297 msgid "Neighbors" msgstr "" #: data/ui/repeater-view.ui:352 msgid "Load More" msgstr "" #: data/ui/repeater-view.ui:361 src/views/channels_view.py:1263 msgid "Show on Map" msgstr "" #: data/ui/repeater-view.ui:382 msgid "Access Control" msgstr "" #: data/ui/repeater-view.ui:437 data/ui/repeater-view.ui:542 #: src/application.py:206 src/views/contacts_view.py:344 #: src/views/contacts_view.py:466 src/views/repeater_view.py:750 #: src/views/repeater_view.py:1104 msgid "Add" msgstr "" #: data/ui/repeater-view.ui:487 src/views/repeater_view.py:840 msgid "Loading regions..." msgstr "" #: data/ui/repeater-view.ui:495 msgid "Retry" msgstr "" #: data/ui/repeater-view.ui:520 msgid "Default Region Scope" msgstr "" #: data/ui/repeater-view.ui:521 msgid "" "Flood packets will be scoped to the provided region. Only repeaters allowing " "the region will forward scoped packets. Leave blank for unscoped." msgstr "" #: data/ui/repeater-view.ui:550 src/views/settings_view.py:265 #: src/views/repeater_view.py:1073 msgid "Apply" msgstr "" #: data/ui/repeater-view.ui:592 msgid "Basic" msgstr "" #: data/ui/repeater-view.ui:603 data/ui/repeater-view.ui:683 #: data/ui/repeater-view.ui:728 data/ui/repeater-view.ui:809 msgid "Fetch" msgstr "" #: data/ui/repeater-view.ui:612 data/ui/repeater-view.ui:692 #: data/ui/repeater-view.ui:737 data/ui/repeater-view.ui:818 msgid "Save" msgstr "" #: data/ui/repeater-view.ui:623 src/views/contacts_view.py:259 #: src/views/contacts_view.py:454 src/views/contacts_view.py:613 #: src/views/contacts_view.py:1484 src/views/channels_view.py:1887 #: src/views/channels_view.py:1900 msgid "Name" msgstr "" #: data/ui/repeater-view.ui:628 msgid "Packet Repeat" msgstr "" #: data/ui/repeater-view.ui:637 msgid "Passwords" msgstr "" #: data/ui/repeater-view.ui:640 msgid "Admin Password" msgstr "" #: data/ui/repeater-view.ui:654 msgid "Guest Password" msgstr "" #: data/ui/repeater-view.ui:798 msgid "Advertisement" msgstr "" #: data/ui/repeater-view.ui:829 msgid "Local Interval (min)" msgstr "" #: data/ui/repeater-view.ui:841 msgid "Flood Interval (hrs)" msgstr "" #. Danger Zone #: data/ui/repeater-view.ui:857 src/views/contacts_view.py:1749 msgid "Danger Zone" msgstr "" #: data/ui/repeater-view.ui:860 msgid "Reboot Repeater" msgstr "" #: data/ui/repeater-view.ui:884 msgid "Select a repeater" msgstr "" #: data/ui/repeater-view.ui:913 msgid "Log Out" msgstr "" #: data/ui/device-actions.ui:12 msgid "Send Advert" msgstr "" #: data/ui/device-actions.ui:20 msgid "Zero Hop" msgstr "" #: data/ui/device-actions.ui:21 msgid "Broadcast locally" msgstr "" #: data/ui/device-actions.ui:33 msgid "Flood Routed" msgstr "" #: data/ui/device-actions.ui:34 msgid "Broadcast through the mesh" msgstr "" #: data/ui/device-actions.ui:46 msgid "To Clipboard" msgstr "" #: data/ui/device-actions.ui:47 msgid "Copy self contact data" msgstr "" #: data/ui/device-actions.ui:59 msgid "Show QR Code" msgstr "" #: data/ui/device-actions.ui:60 msgid "For others to scan and add you" msgstr "" #: data/ui/device-actions.ui:76 src/views/device_view.py:519 #: src/views/device_view.py:862 msgid "Trace Path" msgstr "" #: data/ui/device-actions.ui:77 msgid "Trace a route and show signal quality per hop" msgstr "" #: data/ui/device-actions.ui:91 src/views/device_view.py:943 msgid "Discover Nearby Nodes" msgstr "" #: data/ui/device-actions.ui:92 msgid "Scan the network for nearby nodes" msgstr "" #: data/ui/device-actions.ui:106 data/ui/rx-log-dialog.ui:8 msgid "Rx Log" msgstr "" #: data/ui/device-actions.ui:107 msgid "View received radio packets" msgstr "" #: data/ui/device-actions.ui:121 msgid "Reboot Device" msgstr "" #: data/ui/rx-log-dialog.ui:18 msgid "Clear log" msgstr "" #: data/ui/rx-log-dialog.ui:50 msgid "No Packets" msgstr "" #: data/ui/rx-log-dialog.ui:51 msgid "Received radio packets will appear here" msgstr "" #: data/ui/settings-sidebar.ui:15 msgid "Backup" msgstr "" #: data/ui/settings-sidebar.ui:16 msgid "Save configuration and messages to a file" msgstr "" #: data/ui/settings-sidebar.ui:28 msgid "Restore" msgstr "" #: data/ui/settings-sidebar.ui:29 msgid "Restore from a backup file" msgstr "" #: data/ui/settings-sidebar.ui:53 src/views/device_view.py:227 msgid "Factory Reset" msgstr "" #: data/ui/settings-sidebar.ui:54 msgid "Erase all data on the companion" msgstr "" #: data/ui/discover-dialog.ui:8 src/views/contacts_view.py:75 #: src/views/contacts_view.py:216 msgid "Discover Contacts" msgstr "" #: data/ui/discover-dialog.ui:18 data/ui/share-contact-dialog.ui:18 #: src/window.py:465 msgid "Search" msgstr "" #: data/ui/discover-dialog.ui:27 msgid "Filter by type" msgstr "" #: data/ui/discover-dialog.ui:36 msgid "Sort by" msgstr "" #: data/ui/share-contact-dialog.ui:8 src/views/channels_view.py:176 #: src/views/chat_view.py:138 msgid "Share Contact" msgstr "" #: data/ui/theme-chooser-dialog.ui:9 msgid "Choose Theme" msgstr "" #: src/application.py:87 src/application.py:154 src/window.py:524 #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1000 msgid "Chat" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1001 #: src/views/map_view.py:100 msgid "Repeater" msgstr "" #: src/application.py:87 src/views/map_view.py:101 msgid "Room" msgstr "" #: src/application.py:87 src/views/contacts_view.py:449 #: src/views/contacts_view.py:608 src/views/device_view.py:1003 #: src/views/map_view.py:102 msgid "Sensor" msgstr "" #: src/application.py:95 src/application.py:157 src/views/__init__.py:276 #: src/views/contacts_view.py:1394 src/views/channels_view.py:1357 #: src/views/device_view.py:887 src/views/device_view.py:1131 #: src/views/device_view.py:1139 src/views/settings_view.py:1151 msgid "Unknown" msgstr "" #: src/application.py:97 #, python-brace-format msgid "" "Name: {name}\n" "Type: {type}" msgstr "" #: src/application.py:99 #, python-brace-format msgid "" "Type: {type}\n" "Key: {key}…" msgstr "" #: src/application.py:101 msgid "Import Contact" msgstr "" #: src/application.py:105 #, python-brace-format msgid "Contact {}" msgstr "" #: src/application.py:105 msgid "imported" msgstr "" #: src/application.py:109 msgid "Invalid meshcore:// link" msgstr "" #: src/application.py:123 msgid "Invalid channel secret in link" msgstr "" #: src/application.py:126 msgid "Invalid channel secret length" msgstr "" #: src/application.py:130 #, python-brace-format msgid "" "Name: {name}\n" "Type: Private" msgstr "" #: src/application.py:130 msgid "Unnamed" msgstr "" #: src/application.py:146 msgid "Invalid public key in link" msgstr "" #: src/application.py:149 msgid "Invalid public key length" msgstr "" #: src/application.py:152 #, python-brace-format msgid "{} is already in your list" msgstr "" #. Contact list #: src/application.py:152 src/views/repeater_view.py:766 msgid "Contact" msgstr "" #: src/application.py:156 #, python-brace-format msgid "Add {} Contact" msgstr "" #: src/application.py:157 #, python-brace-format msgid "" "Name: {name}\n" "Public Key: {key}…" msgstr "" #: src/application.py:160 #, python-brace-format msgid "Contact {} added" msgstr "" #: src/application.py:165 msgid "Unrecognized meshcore:// link" msgstr "" #: src/application.py:205 src/connection_controller.py:139 #: src/connection_controller.py:442 src/views/connection_view.py:177 #: src/views/connection_view.py:243 src/views/connection_view.py:409 #: src/views/connection_view.py:446 src/views/connection_view.py:611 #: src/views/contacts_view.py:465 src/views/contacts_view.py:622 #: src/views/contacts_view.py:1760 src/views/channels_view.py:2018 #: src/views/channels_view.py:2069 src/views/channels_view.py:2101 #: src/views/channels_view.py:2149 src/views/device_view.py:226 #: src/views/device_view.py:242 src/views/settings_view.py:1191 #: src/views/repeater_view.py:302 src/views/repeater_view.py:727 #: src/views/repeater_view.py:749 src/views/repeater_view.py:1103 #: src/views/repeater_view.py:1373 msgid "Cancel" msgstr "" #: src/application.py:333 src/application.py:354 src/application.py:379 msgid "Receiving messages in the background" msgstr "" #: src/application.py:434 msgid "A GTK client for MeshCore LoRa mesh network devices" msgstr "" #: src/application.py:482 msgid "Adwaita Symbolic Icons" msgstr "" #: src/connection_controller.py:130 src/connection_controller.py:265 #: src/window.py:901 msgid "Connecting..." msgstr "" #: src/connection_controller.py:136 msgid "Scan for Devices" msgstr "" #: src/connection_controller.py:137 src/views/connection_view.py:510 msgid "Scanning for MeshCore devices..." msgstr "" #: src/connection_controller.py:140 msgid "Scan" msgstr "" #: src/connection_controller.py:262 src/views/connection_view.py:527 msgid "Scanning..." msgstr "" #: src/connection_controller.py:263 msgid "Stop" msgstr "" #: src/connection_controller.py:277 msgid "Syncing..." msgstr "" #: src/connection_controller.py:324 msgid "Device did not respond. It may not support this connection type." msgstr "" #: src/connection_controller.py:441 #, python-brace-format msgid "Reconnecting ({current}/{total})..." msgstr "" #: src/connection_controller.py:483 msgid "Syncing channels" msgstr "" #: src/connection_controller.py:562 #, python-brace-format msgid "You received {} message(s) while disconnected." msgstr "" #: src/frame_handler.py:94 #, python-brace-format msgid "Device error: {}" msgstr "" #: src/frame_handler.py:202 src/frame_handler.py:217 msgid "Syncing contacts" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}s (late)" msgstr "" #: src/frame_handler.py:352 #, python-brace-format msgid "{}ms (late)" msgstr "" #: src/frame_handler.py:352 msgid "late" msgstr "" #: src/frame_handler.py:412 #, python-brace-format msgid "Device clock was {}min behind — synced" msgstr "" #: src/frame_handler.py:414 #, python-brace-format msgid "Device clock is {}min ahead of system" msgstr "" #. Navigate to content panel (matters in collapsed/phone mode) #: src/frame_handler.py:885 src/window.py:545 src/views/contacts_view.py:1175 #: src/views/channels_view.py:1751 src/views/channels_view.py:1771 #: src/views/channels_view.py:1868 src/views/channels_view.py:1900 #: src/views/channels_view.py:2016 src/views/repeater_view.py:432 #, python-brace-format msgid "Channel {}" msgstr "" #: src/frame_handler.py:886 msgid "Someone" msgstr "" #: src/frame_handler.py:889 #, python-brace-format msgid "{sender}: {text}" msgstr "" #: src/message_controller.py:126 src/views/repeater_view.py:682 #: src/views/repeater_view.py:716 msgid "Me" msgstr "" #: src/message_controller.py:171 msgid "flood" msgstr "" #: src/message_controller.py:173 msgid "direct" msgstr "" #: src/message_controller.py:175 msgid "path" msgstr "" #: src/message_controller.py:178 src/message_controller.py:182 #, python-brace-format msgid "– {route}" msgstr "" #: src/message_controller.py:183 #, python-brace-format msgid "({attempt}/{total}) – {route}" msgstr "" #: src/message_controller.py:221 #, python-brace-format msgid "waiting {:.1f}s (radio busy)" msgstr "" #: src/window.py:469 src/window.py:549 msgid "Filter" msgstr "" #: src/window.py:473 msgid "Sort" msgstr "" #. Sidebar = Device Info button + Actions, content = device info #. Actions group (only for repeaters/rooms) #. Actions group #: src/window.py:511 src/views/contacts_view.py:1674 #: src/views/channels_view.py:2006 src/views/repeater_view.py:964 msgid "Actions" msgstr "" #: src/window.py:536 msgid "Channel" msgstr "" #. Sidebar = Settings button + hint, content = settings with Apply in headerbar #: src/window.py:566 msgid "Backup & Restore" msgstr "" #: src/window.py:638 src/window.py:673 src/window.py:676 src/window.py:1396 #: src/window.py:1398 src/window.py:1449 src/window.py:1452 #, python-brace-format msgid "{name} ({path})" msgstr "" #: src/window.py:687 msgid "Management" msgstr "" #: src/window.py:687 src/views/repeater_view.py:687 #: src/views/repeater_view.py:758 msgid "Guest" msgstr "" #: src/window.py:688 #, python-brace-format msgid "{name} — {role}" msgstr "" #: src/window.py:790 msgid "Excellent" msgstr "" #: src/window.py:790 msgid "Good" msgstr "" #: src/window.py:791 msgid "Fair" msgstr "" #: src/window.py:791 msgid "Poor" msgstr "" #: src/window.py:792 msgid "Very poor" msgstr "" #: src/window.py:1014 msgid "flood routed" msgstr "" #: src/window.py:1014 msgid "zero-hop" msgstr "" #: src/window.py:1015 #, python-brace-format msgid "Advert sent ({})" msgstr "" #: src/window.py:1466 #, python-brace-format msgid "Channel \"{}\" added" msgstr "" #: src/window.py:1470 msgid "No empty channel slots available" msgstr "" #: src/qr_scanner.py:41 #, python-brace-format msgid "Cannot connect to session bus: {}" msgstr "" #: src/qr_scanner.py:59 msgid "No camera detected on this system." msgstr "" #: src/qr_scanner.py:110 #, python-brace-format msgid "Could not access camera: {}" msgstr "" #: src/qr_scanner.py:119 msgid "Camera access was denied." msgstr "" #: src/qr_scanner.py:153 msgid "Could not open camera stream." msgstr "" #: src/qr_scanner.py:159 msgid "No camera file descriptor received." msgstr "" #: src/qr_scanner.py:168 src/qr_scanner.py:171 #, python-brace-format msgid "Could not open camera: {}" msgstr "" #: src/qr_scanner.py:176 src/views/contacts_view.py:80 msgid "Scan QR Code" msgstr "" #. Status label #: src/qr_scanner.py:198 msgid "Point the camera at a QR code..." msgstr "" #: src/qr_scanner.py:221 #, python-brace-format msgid "Camera pipeline error: {}" msgstr "" #: src/qr_scanner.py:296 msgid "" "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." msgstr "" #: src/qr_scanner.py:305 msgid "" "Could not detect QR code.\n" "Try importing the contact via clipboard instead." msgstr "" #: src/qr_scanner.py:311 msgid "QR code found!" msgstr "" #: src/qr_scanner.py:322 #, python-brace-format msgid "Camera error: {}" msgstr "" #: src/qr_scanner.py:341 msgid "Camera Error" msgstr "" #: src/qr_scanner.py:342 src/views/connection_view.py:210 #: src/views/connection_view.py:330 src/views/connection_view.py:337 #: src/views/contacts_view.py:221 src/views/contacts_view.py:503 #: src/views/contacts_view.py:595 src/views/channels_view.py:817 #: src/views/channels_view.py:1190 src/views/channels_view.py:1412 #: src/views/channels_view.py:1432 src/views/channels_view.py:2052 #: src/views/channels_view.py:2123 src/views/settings_view.py:1064 #: src/views/settings_view.py:1105 src/views/settings_view.py:1141 #: src/views/chat_view.py:692 msgid "OK" msgstr "" #: src/views/connection_view.py:41 src/views/connection_view.py:178 #: src/views/connection_view.py:410 src/views/contacts_view.py:1761 #: src/views/channels_view.py:2019 src/views/repeater_view.py:728 #: src/views/repeater_view.py:982 msgid "Remove" msgstr "" #: src/views/connection_view.py:81 msgid "Bluetooth is disabled" msgstr "" #: src/views/connection_view.py:82 msgid "Enable Bluetooth in system settings to connect" msgstr "" #: src/views/connection_view.py:94 msgid "No paired devices" msgstr "" #: src/views/connection_view.py:95 msgid "Use the button below to pair a new companion" msgstr "" #: src/views/connection_view.py:142 msgid "No USB devices detected" msgstr "" #: src/views/connection_view.py:143 msgid "Connect a MeshCore companion via USB" msgstr "" #: src/views/connection_view.py:174 src/views/connection_view.py:406 msgid "Remove Companion" msgstr "" #: src/views/connection_view.py:175 #, python-brace-format msgid "Remove {} ({})? You will need to pair again to reconnect." msgstr "" #: src/views/connection_view.py:181 src/views/connection_view.py:413 msgid "Delete stored data" msgstr "" #: src/views/connection_view.py:207 msgid "USB Connection Failed" msgstr "" #: src/views/connection_view.py:237 src/views/connection_view.py:264 msgid "USB Permission Denied" msgstr "" #: src/views/connection_view.py:238 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Click \"Install " "Rule\" to install it (requires administrator password)." msgstr "" #: src/views/connection_view.py:244 msgid "Install Rule" msgstr "" #: src/views/connection_view.py:265 #, python-brace-format msgid "" "Cannot access {}.\n" "\n" "A udev rule is needed to grant access to USB serial devices. Run the " "following command in a terminal:\n" "\n" "{}" msgstr "" #: src/views/connection_view.py:270 msgid "Close" msgstr "" #: src/views/connection_view.py:271 msgid "Copy Command" msgstr "" #: src/views/connection_view.py:279 msgid "Command copied to clipboard" msgstr "" #: src/views/connection_view.py:327 src/views/connection_view.py:334 msgid "Installation Failed" msgstr "" #: src/views/connection_view.py:328 #, python-brace-format msgid "" "Could not install udev rule:\n" "{}" msgstr "" #: src/views/connection_view.py:363 msgid "No saved companions" msgstr "" #: src/views/connection_view.py:364 msgid "Use the button below to add one" msgstr "" #: src/views/connection_view.py:407 #, python-brace-format msgid "Remove TCP companion {}?" msgstr "" #: src/views/connection_view.py:430 msgid "Enter the IP address and port of your MeshCore device." msgstr "" #: src/views/connection_view.py:434 msgid "Hostname / IP Address" msgstr "" #: src/views/connection_view.py:436 msgid "Port" msgstr "" #: src/views/connection_view.py:493 msgid "Pair New Companion" msgstr "" #: src/views/connection_view.py:528 msgid "Looking for nearby MeshCore devices" msgstr "" #: src/views/connection_view.py:555 src/views/connection_view.py:612 msgid "Pair" msgstr "" #: src/views/connection_view.py:598 msgid "Enter Pairing PIN" msgstr "" #: src/views/connection_view.py:599 msgid "Enter the PIN shown on your MeshCore device." msgstr "" #: src/views/connection_view.py:603 msgid "PIN" msgstr "" #: src/views/contacts_view.py:76 msgid "Add Manually" msgstr "" #: src/views/contacts_view.py:77 msgid "Import from Clipboard" msgstr "" #: src/views/contacts_view.py:125 src/views/contacts_view.py:245 #: src/views/channels_view.py:1670 msgid "All" msgstr "" #: src/views/contacts_view.py:125 msgid "Favourites" msgstr "" #: src/views/contacts_view.py:126 src/views/contacts_view.py:245 #: src/views/map_view.py:196 msgid "Users" msgstr "" #: src/views/contacts_view.py:139 src/views/channels_view.py:1682 msgid "A–Z" msgstr "" #: src/views/contacts_view.py:139 msgid "Heard Recently" msgstr "" #: src/views/contacts_view.py:140 src/views/channels_view.py:1682 msgid "Latest Messages" msgstr "" #: src/views/contacts_view.py:148 msgid "Favorites First" msgstr "" #: src/views/contacts_view.py:185 #, python-brace-format msgid "{visible}/{total} contacts" msgstr "" #: src/views/contacts_view.py:188 src/views/channels_view.py:797 #: src/views/chat_view.py:672 #, python-brace-format msgid "{total} contacts" msgstr "" #: src/views/contacts_view.py:189 #, python-brace-format msgid "{total} contact" msgstr "" #: src/views/contacts_view.py:217 msgid "" "No new contacts have been heard yet. Leave the app running and contacts will " "appear as their adverts are received." msgstr "" #: src/views/contacts_view.py:260 src/views/contacts_view.py:1523 msgid "Last Seen" msgstr "" #: src/views/contacts_view.py:265 msgid "Distance" msgstr "" #: src/views/contacts_view.py:434 #, python-brace-format msgid "{visible}/{total} discovered" msgstr "" #: src/views/contacts_view.py:438 #, python-brace-format msgid "{total} discovered" msgstr "" #: src/views/contacts_view.py:443 msgid "Enter the contact details." msgstr "" #: src/views/contacts_view.py:447 src/views/contacts_view.py:606 msgid "Contact Type" msgstr "" #: src/views/contacts_view.py:449 src/views/contacts_view.py:608 #: src/views/device_view.py:1002 msgid "Room Server" msgstr "" #: src/views/contacts_view.py:455 msgid "Public Key (64 hex characters)" msgstr "" #: src/views/contacts_view.py:478 src/views/contacts_view.py:559 #: src/views/contacts_view.py:584 msgid "This contact is already in your list." msgstr "" #: src/views/contacts_view.py:500 src/views/contacts_view.py:594 #: src/views/settings_view.py:1138 msgid "Import Failed" msgstr "" #: src/views/contacts_view.py:501 msgid "No meshcore:// link found in clipboard." msgstr "" #: src/views/contacts_view.py:532 msgid "Invalid channel secret in QR code." msgstr "" #: src/views/contacts_view.py:535 #, python-brace-format msgid "Channel secret must be 16 bytes, got {}." msgstr "" #: src/views/contacts_view.py:551 msgid "Invalid public key in QR code." msgstr "" #: src/views/contacts_view.py:555 #, python-brace-format msgid "Public key must be 32 bytes, got {}." msgstr "" #: src/views/contacts_view.py:591 #, python-brace-format msgid "" "Could not parse QR code data:\n" "{}" msgstr "" #: src/views/contacts_view.py:601 msgid "QR Code Scanned" msgstr "" #: src/views/contacts_view.py:602 #, python-brace-format msgid "Public key: {}...{}" msgstr "" #: src/views/contacts_view.py:665 #, python-brace-format msgid "Login to {}" msgstr "" #: src/views/contacts_view.py:678 msgid "Password" msgstr "" #: src/views/contacts_view.py:679 msgid "Save password" msgstr "" #: src/views/contacts_view.py:702 msgid "Login" msgstr "" #: src/views/contacts_view.py:718 msgid "Login successful" msgstr "" #: src/views/contacts_view.py:737 msgid "Login failed — wrong password" msgstr "" #: src/views/contacts_view.py:744 msgid "Login timed out" msgstr "" #: src/views/contacts_view.py:751 msgid "Retrying via flood..." msgstr "" #: src/views/contacts_view.py:758 msgid "Logging in..." msgstr "" #: src/views/contacts_view.py:969 msgid "Searching..." msgstr "" #: src/views/contacts_view.py:1020 msgid "Path found" msgstr "" #: src/views/contacts_view.py:1026 msgid "No response" msgstr "" #: src/views/contacts_view.py:1035 msgid "Loading..." msgstr "" #: src/views/contacts_view.py:1045 msgid "No path cached" msgstr "" #: src/views/contacts_view.py:1058 msgid "Not supported" msgstr "" #: src/views/contacts_view.py:1070 msgid "Ping is not supported with 3-byte path hashes" msgstr "" #: src/views/contacts_view.py:1092 #, python-brace-format msgid "Ping {}: {}" msgstr "" #: src/views/contacts_view.py:1103 #, python-brace-format msgid "Ping {}: timed out" msgstr "" #: src/views/contacts_view.py:1116 src/views/repeater_view.py:509 #: src/views/repeater_view.py:661 msgid "Requesting..." msgstr "" #: src/views/contacts_view.py:1121 src/views/device_view.py:364 msgid "No response (timed out)" msgstr "" #: src/views/contacts_view.py:1130 src/views/contacts_view.py:1134 #: src/views/device_view.py:374 src/views/device_view.py:378 msgid "No telemetry data" msgstr "" #: src/views/contacts_view.py:1137 src/views/device_view.py:393 msgid "Telemetry received" msgstr "" #: src/views/contacts_view.py:1155 src/views/repeater_view.py:411 #, python-brace-format msgid "Telemetry — {}" msgstr "" #: src/views/contacts_view.py:1257 #, python-brace-format msgid "{} — Management" msgstr "" #: src/views/contacts_view.py:1368 #, python-brace-format msgid "Path to {}" msgstr "" #: src/views/contacts_view.py:1388 src/views/channels_view.py:637 #: src/views/channels_view.py:1350 src/views/device_view.py:881 #: src/views/chat_view.py:554 src/views/map_view.py:477 #: src/views/map_view.py:771 src/views/map_view.py:1126 msgid "My Device" msgstr "" #: src/views/contacts_view.py:1419 src/views/channels_view.py:1247 #: src/views/channels_view.py:1358 src/views/device_view.py:888 #: src/views/map_view.py:802 msgid "Unknown repeater" msgstr "" #. Info group #: src/views/contacts_view.py:1482 msgid "Contact Info" msgstr "" #: src/views/contacts_view.py:1490 msgid "Favorite" msgstr "" #: src/views/contacts_view.py:1491 msgid "Pin to top of contacts list" msgstr "" #: src/views/contacts_view.py:1508 src/views/channels_view.py:1904 msgid "Type" msgstr "" #: src/views/contacts_view.py:1518 src/views/device_view.py:150 msgid "Public key copied" msgstr "" #: src/views/contacts_view.py:1522 msgid "Never" msgstr "" #. Out Path entry #: src/views/contacts_view.py:1542 src/views/contacts_view.py:1547 msgid "Out Path" msgstr "" #: src/views/contacts_view.py:1543 msgid "Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)" msgstr "" #: src/views/contacts_view.py:1554 msgid "Reset Path" msgstr "" #: src/views/contacts_view.py:1555 msgid "Clear the established path and switch to flood" msgstr "" #: src/views/contacts_view.py:1570 msgid "Force Flood" msgstr "" #: src/views/contacts_view.py:1571 msgid "Always broadcast, never use an established path" msgstr "" #: src/views/contacts_view.py:1613 msgid "Show Path on Map" msgstr "" #: src/views/contacts_view.py:1614 msgid "Visualize the message route on the map" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow Telemetry Requests" msgstr "" #: src/views/contacts_view.py:1649 msgid "Allow this contact to query battery, voltage, temperature" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include Location" msgstr "" #: src/views/contacts_view.py:1652 msgid "Include GPS coordinates in telemetry responses" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include Environment Sensors" msgstr "" #: src/views/contacts_view.py:1655 msgid "Include humidity, pressure, air quality data" msgstr "" #: src/views/contacts_view.py:1661 msgid "Query this contact's telemetry data" msgstr "" #: src/views/contacts_view.py:1679 msgid "Room Management" msgstr "" #: src/views/contacts_view.py:1680 msgid "Login and access CLI, status, and settings" msgstr "" #: src/views/contacts_view.py:1696 msgid "Repeater Management" msgstr "" #: src/views/contacts_view.py:1697 msgid "Login and access status, CLI, neighbors, and settings" msgstr "" #: src/views/contacts_view.py:1713 msgid "Ping" msgstr "" #: src/views/contacts_view.py:1714 msgid "Measure round-trip time to this node" msgstr "" #: src/views/contacts_view.py:1728 msgid "Discover Paths" msgstr "" #: src/views/contacts_view.py:1729 msgid "Find routes to this node via flood" msgstr "" #: src/views/contacts_view.py:1751 msgid "Remove Contact" msgstr "" #: src/views/contacts_view.py:1757 msgid "Remove Contact?" msgstr "" #: src/views/contacts_view.py:1758 #, python-brace-format msgid "Remove {} and all messages?" msgstr "" #: src/views/channels_view.py:117 src/views/channels_view.py:1409 #: src/views/channels_view.py:1416 msgid "Heard Repeats" msgstr "" #: src/views/channels_view.py:118 src/views/chat_view.py:93 msgid "Send Again" msgstr "" #: src/views/channels_view.py:119 src/views/channels_view.py:125 #: src/views/chat_view.py:94 msgid "Delete" msgstr "" #: src/views/channels_view.py:122 msgid "Reply" msgstr "" #: src/views/channels_view.py:123 msgid "Copy Text" msgstr "" #: src/views/channels_view.py:124 msgid "View Message Paths" msgstr "" #: src/views/channels_view.py:177 src/views/channels_view.py:833 #: src/views/chat_view.py:139 src/views/chat_view.py:707 msgid "Share Location" msgstr "" #: src/views/channels_view.py:178 src/views/chat_view.py:140 msgid "Share Location from Map" msgstr "" #: src/views/channels_view.py:322 src/views/chat_view.py:263 msgid "Open in Maps App" msgstr "" #: src/views/channels_view.py:451 src/views/chat_view.py:369 msgid "You shared a contact:" msgstr "" #: src/views/channels_view.py:455 src/views/chat_view.py:373 #, python-brace-format msgid "{name} shared a contact with you:" msgstr "" #: src/views/channels_view.py:475 src/views/channels_view.py:587 #: src/views/chat_view.py:393 src/views/chat_view.py:504 msgid "Already added" msgstr "" #: src/views/channels_view.py:490 src/views/chat_view.py:408 msgid "You shared a location:" msgstr "" #: src/views/channels_view.py:494 src/views/chat_view.py:412 #, python-brace-format msgid "{name} shared a location:" msgstr "" #: src/views/channels_view.py:553 src/views/chat_view.py:470 msgid "New Messages" msgstr "" #: src/views/channels_view.py:622 src/views/chat_view.py:539 msgid "Shared Location" msgstr "" #: src/views/channels_view.py:626 src/views/chat_view.py:543 msgid "Shared" msgstr "" #: src/views/channels_view.py:782 src/views/channels_view.py:833 #: src/views/chat_view.py:657 src/views/chat_view.py:707 msgid "Share" msgstr "" #: src/views/channels_view.py:814 src/views/chat_view.py:689 msgid "Location Not Available" msgstr "" #: src/views/channels_view.py:815 src/views/chat_view.py:690 msgid "Your companion does not have a GPS location." msgstr "" #: src/views/channels_view.py:1186 src/views/channels_view.py:1195 msgid "Message Paths" msgstr "" #: src/views/channels_view.py:1187 msgid "" "No path information available. Path data is only captured for messages " "received while connected." msgstr "" #: src/views/channels_view.py:1270 msgid "Direct message" msgstr "" #: src/views/channels_view.py:1271 msgid "No repeaters were used" msgstr "" #: src/views/channels_view.py:1279 msgid "No repeater details" msgstr "" #: src/views/channels_view.py:1280 msgid "Path data is only captured via LOG_DATA while connected" msgstr "" #: src/views/channels_view.py:1329 msgid "Message Path" msgstr "" #: src/views/channels_view.py:1410 msgid "No repeats heard yet." msgstr "" #: src/views/channels_view.py:1670 src/views/channels_view.py:2133 msgid "Public" msgstr "" #: src/views/channels_view.py:1671 msgid "Hashtag" msgstr "" #: src/views/channels_view.py:1671 msgid "Private" msgstr "" #: src/views/channels_view.py:1716 #, python-brace-format msgid "{visible}/{total} channels" msgstr "" #: src/views/channels_view.py:1719 #, python-brace-format msgid "{total} channels" msgstr "" #: src/views/channels_view.py:1720 #, python-brace-format msgid "{total} channel" msgstr "" #: src/views/channels_view.py:1753 #, python-brace-format msgid "{name} ({region})" msgstr "" #. Info group #: src/views/channels_view.py:1884 msgid "Channel Info" msgstr "" #: src/views/channels_view.py:1911 msgid "Private Key" msgstr "" #: src/views/channels_view.py:1922 msgid "Private key copied to clipboard" msgstr "" #. Notifications group #: src/views/channels_view.py:1931 msgid "Notifications" msgstr "" #: src/views/channels_view.py:1933 msgid "Notification Level" msgstr "" #: src/views/channels_view.py:1935 msgid "Default" msgstr "" #: src/views/channels_view.py:1960 src/views/channels_view.py:1965 msgid "Region" msgstr "" #: src/views/channels_view.py:1961 msgid "Limit flood messages to a specific region" msgstr "" #: src/views/channels_view.py:1989 msgid "No regions defined" msgstr "" #: src/views/channels_view.py:1990 msgid "Add regions in Settings to assign them to channels" msgstr "" #: src/views/channels_view.py:2008 msgid "Remove Channel" msgstr "" #: src/views/channels_view.py:2014 msgid "Remove Channel?" msgstr "" #: src/views/channels_view.py:2015 #, python-brace-format msgid "Remove \"{}\" and all its messages?" msgstr "" #: src/views/channels_view.py:2049 msgid "No Slots Available" msgstr "" #: src/views/channels_view.py:2050 msgid "All 8 channel slots are in use." msgstr "" #: src/views/channels_view.py:2061 msgid "A random PSK will be generated. Share it with others so they can join." msgstr "" #: src/views/channels_view.py:2063 src/views/channels_view.py:2093 msgid "Channel Name" msgstr "" #: src/views/channels_view.py:2070 msgid "Create" msgstr "" #: src/views/channels_view.py:2091 msgid "Enter the channel name and private key shared with you." msgstr "" #: src/views/channels_view.py:2094 msgid "Private Key (32 hex characters)" msgstr "" #: src/views/channels_view.py:2102 src/views/channels_view.py:2150 msgid "Join" msgstr "" #: src/views/channels_view.py:2122 msgid "Already Joined" msgstr "" #: src/views/channels_view.py:2122 msgid "You are already on the public channel." msgstr "" #: src/views/channels_view.py:2141 msgid "Enter a hashtag name. Anyone using the same hashtag can communicate." msgstr "" #: src/views/channels_view.py:2143 msgid "Hashtag (e.g. #general)" msgstr "" #: src/views/device_view.py:160 msgid "Device public key not available" msgstr "" #: src/views/device_view.py:170 msgid "QR code library not available" msgstr "" #: src/views/device_view.py:174 msgid "Your Contact QR Code" msgstr "" #. Instructions #: src/views/device_view.py:210 msgid "Scan this QR code to add this contact" msgstr "" #: src/views/device_view.py:220 msgid "Factory Reset?" msgstr "" #: src/views/device_view.py:221 msgid "" "This will permanently erase ALL data on the companion device, including " "contacts, messages, channels, settings, and the device identity (keys). This " "action cannot be undone." msgstr "" #: src/views/device_view.py:239 msgid "Reboot Device?" msgstr "" #: src/views/device_view.py:240 msgid "The device will disconnect and restart." msgstr "" #: src/views/device_view.py:243 src/views/repeater_view.py:1374 msgid "Reboot" msgstr "" #: src/views/device_view.py:323 msgid "Not set" msgstr "" #: src/views/device_view.py:332 src/views/device_view.py:339 msgid "Location (manually set)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS)" msgstr "" #: src/views/device_view.py:335 msgid "Location (GPS on, no fix)" msgstr "" #: src/views/device_view.py:339 msgid "Location (GPS off)" msgstr "" #: src/views/device_view.py:358 src/views/repeater_view.py:374 msgid "Requesting…" msgstr "" #: src/views/device_view.py:405 msgid "Device Location" msgstr "" #: src/views/device_view.py:464 #, python-brace-format msgid "{days}d" msgstr "" #: src/views/device_view.py:466 #, python-brace-format msgid "{hours}h" msgstr "" #: src/views/device_view.py:467 #, python-brace-format msgid "{mins}m" msgstr "" #: src/views/device_view.py:469 #, python-brace-format msgid "{} messages" msgstr "" #: src/views/device_view.py:487 #, python-brace-format msgid "{count} errors" msgstr "" #: src/views/device_view.py:500 src/views/device_view.py:502 msgid "Connected" msgstr "" #: src/views/device_view.py:515 msgid "Trace Path is not supported with 3-byte path hashes" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aa,bb,cc" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabb,ccdd" msgstr "" #: src/views/device_view.py:534 msgid "e.g. aabbcc,ddeeff" msgstr "" #. Add from Contacts button (above path entry) #: src/views/device_view.py:542 msgid "Add from Contacts" msgstr "" #: src/views/device_view.py:574 msgid "No repeaters or rooms in contacts" msgstr "" #: src/views/device_view.py:587 msgid "No repeaters with known location" msgstr "" #. Path input #: src/views/device_view.py:590 src/views/device_view.py:1226 msgid "Path" msgstr "" #. Run button #: src/views/device_view.py:650 msgid "Run Trace" msgstr "" #: src/views/device_view.py:663 msgid "Enter path hashes and press Run Trace" msgstr "" #: src/views/device_view.py:730 #, python-brace-format msgid "Trace complete: {} hops, {:.1f}s" msgstr "" #: src/views/device_view.py:777 msgid "Trace timed out" msgstr "" #: src/views/device_view.py:793 msgid "Invalid hex in path" msgstr "" #: src/views/device_view.py:800 msgid "Tracing..." msgstr "" #: src/views/device_view.py:960 src/views/device_view.py:1150 #, python-brace-format msgid "Scanning... {}s remaining" msgstr "" #: src/views/device_view.py:977 msgid "Listening..." msgstr "" #: src/views/device_view.py:978 msgid "Waiting for nearby nodes to respond" msgstr "" #: src/views/device_view.py:1031 msgid "Add to Contacts" msgstr "" #: src/views/device_view.py:1038 #, python-brace-format msgid "Added {}" msgstr "" #: src/views/device_view.py:1066 msgid "Node" msgstr "" #: src/views/device_view.py:1159 #, python-brace-format msgid "Done. {} nodes found." msgstr "" #: src/views/device_view.py:1160 #, python-brace-format msgid "Done. {} node found." msgstr "" #: src/views/device_view.py:1222 msgid "Size" msgstr "" #: src/views/device_view.py:1222 msgid "bytes" msgstr "" #: src/views/device_view.py:1226 msgid "hops" msgstr "" #: src/views/device_view.py:1228 msgid "Path Hashes" msgstr "" #: src/views/device_view.py:1228 msgid "byte per hop" msgstr "" #: src/views/device_view.py:1307 #, python-brace-format msgid "Update available: {version}" msgstr "" #: src/views/settings_view.py:131 msgid "Custom" msgstr "" #. Telemetry combo models — shared label set #: src/views/settings_view.py:139 msgid "Deny All" msgstr "" #: src/views/settings_view.py:139 msgid "Allow per Contact" msgstr "" #: src/views/settings_view.py:139 msgid "Allow All" msgstr "" #: src/views/settings_view.py:601 msgid "Settings applied on device" msgstr "" #: src/views/settings_view.py:613 msgid "Unknown error" msgstr "" #: src/views/settings_view.py:614 #, python-brace-format msgid "Settings error: {}" msgstr "" #: src/views/settings_view.py:632 #, python-brace-format msgid "TX Power (dBm, max {})" msgstr "" #: src/views/settings_view.py:735 msgid "Location filled, click Apply to save" msgstr "" #: src/views/settings_view.py:737 msgid "Location unavailable" msgstr "" #: src/views/settings_view.py:763 msgid "Pick Location" msgstr "" #: src/views/settings_view.py:763 msgid "Set Location" msgstr "" #: src/views/settings_view.py:865 msgid "Connect to a device first" msgstr "" #: src/views/settings_view.py:871 msgid "No repeaters in contacts" msgstr "" #: src/views/settings_view.py:875 msgid "Discover Regions" msgstr "" #: src/views/settings_view.py:892 #, python-brace-format msgid "Querying {} repeaters..." msgstr "" #: src/views/settings_view.py:893 #, python-brace-format msgid "Querying {} repeater..." msgstr "" #: src/views/settings_view.py:905 msgid "Waiting..." msgstr "" #: src/views/settings_view.py:906 msgid "Querying repeaters for regions" msgstr "" #. Add selected button #: src/views/settings_view.py:918 msgid "Add Selected" msgstr "" #: src/views/settings_view.py:937 #, python-brace-format msgid "Found {} regions" msgstr "" #: src/views/settings_view.py:938 #, python-brace-format msgid "Found {} region" msgstr "" #: src/views/settings_view.py:941 msgid "No regions found" msgstr "" #: src/views/settings_view.py:957 #, python-brace-format msgid "from {}" msgstr "" #: src/views/settings_view.py:959 msgid "(already added)" msgstr "" #: src/views/settings_view.py:995 #, python-brace-format msgid "Added {} regions" msgstr "" #: src/views/settings_view.py:996 #, python-brace-format msgid "Added {} region" msgstr "" #: src/views/settings_view.py:1027 msgid "Export Backup" msgstr "" #: src/views/settings_view.py:1028 msgid "Save to a file" msgstr "" #: src/views/settings_view.py:1040 src/views/settings_view.py:1187 msgid "Import Backup" msgstr "" #: src/views/settings_view.py:1041 msgid "Restore from a file" msgstr "" #: src/views/settings_view.py:1061 src/views/settings_view.py:1102 msgid "Not Connected" msgstr "" #: src/views/settings_view.py:1062 msgid "Connect to a device first to export a backup." msgstr "" #: src/views/settings_view.py:1077 src/views/settings_view.py:1112 msgid "JSON files" msgstr "" #: src/views/settings_view.py:1090 msgid "Backup exported successfully" msgstr "" #: src/views/settings_view.py:1094 #, python-brace-format msgid "Export failed: {}" msgstr "" #: src/views/settings_view.py:1103 msgid "Connect to a device first to import a backup." msgstr "" #: src/views/settings_view.py:1139 #, python-brace-format msgid "Could not read backup file: {}" msgstr "" #: src/views/settings_view.py:1172 #, python-brace-format msgid "Source: {}" msgstr "" #: src/views/settings_view.py:1174 msgid "Device settings: name, radio, location" msgstr "" #: src/views/settings_view.py:1176 #, python-brace-format msgid "Contacts: {} ({} new)" msgstr "" #: src/views/settings_view.py:1178 #, python-brace-format msgid "Channels: {} ({} new)" msgstr "" #: src/views/settings_view.py:1180 #, python-brace-format msgid "Messages: {}" msgstr "" #: src/views/settings_view.py:1182 #, python-brace-format msgid "Channel messages: {}" msgstr "" #: src/views/settings_view.py:1184 #, python-brace-format msgid "Saved passwords: {}" msgstr "" #: src/views/settings_view.py:1192 msgid "Contacts & Channels Only" msgstr "" #: src/views/settings_view.py:1193 msgid "Everything" msgstr "" #: src/views/settings_view.py:1204 #, python-brace-format msgid "{} contacts" msgstr "" #: src/views/settings_view.py:1206 #, python-brace-format msgid "{} channels" msgstr "" #: src/views/settings_view.py:1208 msgid "messages" msgstr "" #: src/views/settings_view.py:1211 #, python-brace-format msgid "Imported: {}" msgstr "" #: src/views/settings_view.py:1213 msgid "Nothing new to import" msgstr "" #: src/views/repeater_view.py:24 #, python-brace-format msgid "{d}d" msgstr "" #: src/views/repeater_view.py:26 #, python-brace-format msgid "{h}h" msgstr "" #: src/views/repeater_view.py:28 #, python-brace-format msgid "{m}m" msgstr "" #: src/views/repeater_view.py:29 #, python-brace-format msgid "{secs}s" msgstr "" #: src/views/repeater_view.py:35 #, python-brace-format msgid "{}s ago" msgstr "" #: src/views/repeater_view.py:37 #, python-brace-format msgid "{}m ago" msgstr "" #: src/views/repeater_view.py:39 #, python-brace-format msgid "{}h ago" msgstr "" #: src/views/repeater_view.py:40 #, python-brace-format msgid "{}d ago" msgstr "" #: src/views/repeater_view.py:290 msgid "Time sync sent to repeater" msgstr "" #: src/views/repeater_view.py:297 msgid "Reset Clock & Reboot?" msgstr "" #: src/views/repeater_view.py:298 msgid "" "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n" "\n" "This will reset the clock and reboot the repeater." msgstr "" #: src/views/repeater_view.py:312 msgid "Rebooting..." msgstr "" #: src/views/repeater_view.py:313 msgid "Clock reset & reboot sent to repeater" msgstr "" #: src/views/repeater_view.py:325 src/views/repeater_view.py:518 #: src/views/repeater_view.py:667 src/views/repeater_view.py:883 msgid "Request timed out" msgstr "" #: src/views/repeater_view.py:542 #, python-brace-format msgid "{} of {} neighbors" msgstr "" #: src/views/repeater_view.py:560 msgid "Neighbor Map" msgstr "" #: src/views/repeater_view.py:687 src/views/repeater_view.py:758 msgid "Admin" msgstr "" #: src/views/repeater_view.py:705 #, python-brace-format msgid "{} entry" msgstr "" #: src/views/repeater_view.py:706 #, python-brace-format msgid "{} entries" msgstr "" #: src/views/repeater_view.py:724 msgid "Remove from ACL?" msgstr "" #: src/views/repeater_view.py:725 #, python-brace-format msgid "Remove {} from the access control list?" msgstr "" #: src/views/repeater_view.py:746 msgid "Add to Access Control" msgstr "" #: src/views/repeater_view.py:747 msgid "Select a contact and role." msgstr "" #. Role selector #: src/views/repeater_view.py:757 msgid "Role" msgstr "" #: src/views/repeater_view.py:939 msgid "Global (wildcard)" msgstr "" #: src/views/repeater_view.py:942 msgid "Home" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood allowed" msgstr "" #: src/views/repeater_view.py:943 msgid "Flood denied" msgstr "" #: src/views/repeater_view.py:977 msgid "Deny Flood" msgstr "" #: src/views/repeater_view.py:977 msgid "Allow Flood" msgstr "" #: src/views/repeater_view.py:981 msgid "Set as Home" msgstr "" #: src/views/repeater_view.py:1032 msgid "Remove child regions first" msgstr "" #: src/views/repeater_view.py:1069 msgid "Unsaved Changes" msgstr "" #: src/views/repeater_view.py:1070 msgid "You have unsaved region changes." msgstr "" #: src/views/repeater_view.py:1072 msgid "Discard" msgstr "" #: src/views/repeater_view.py:1100 msgid "Add Region" msgstr "" #: src/views/repeater_view.py:1101 msgid "Enter a name and optionally choose a parent region." msgstr "" #: src/views/repeater_view.py:1110 msgid "Region Name" msgstr "" #: src/views/repeater_view.py:1118 msgid "Parent" msgstr "" #: src/views/repeater_view.py:1301 msgid "Admin password changed — re-login required" msgstr "" #: src/views/repeater_view.py:1366 #, python-brace-format msgid "Settings sent to {}" msgstr "" #: src/views/repeater_view.py:1370 msgid "Reboot Repeater?" msgstr "" #: src/views/repeater_view.py:1371 #, python-brace-format msgid "Reboot {}? It will be offline briefly." msgstr "" #. positive = behind, negative = ahead #. more than 5 minutes off #: src/views/repeater_view.py:1490 msgid "Time is off" msgstr "" #: src/views/chat_view.py:573 src/views/chat_view.py:574 msgid "Sending" msgstr "" #: src/views/map_view.py:99 msgid "User" msgstr "" #: src/views/map_view.py:178 #, python-brace-format msgid "{} of {} contacts on map" msgstr "" #: src/views/map_view.py:181 #, python-brace-format msgid "{} of {} located on map" msgstr "" #: src/views/map_view.py:198 msgid "Rooms" msgstr "" #: src/views/map_view.py:227 msgid "Discovered Nodes" msgstr "" #: src/views/map_view.py:437 #, python-brace-format msgid "and {} more" msgstr "" #: src/views/map_view.py:614 msgid "Position is illustrative — real location unknown" msgstr "" #: src/views/map_view.py:623 #, python-brace-format msgid "SNR: {:.1f} dB" msgstr "" #: src/views/map_view.py:972 msgid "Pick Trace Route" msgstr "" #: src/views/map_view.py:1159 msgid "Undo" msgstr "" #: src/views/map_view.py:1169 msgid "Clear" msgstr "" #: src/views/map_view.py:1177 msgid "Done" msgstr "" #: src/views/map_view.py:1194 msgid "Tap repeaters to build the trace route" msgstr "" meshy/po/update-translations.sh000077500000000000000000000037761521052255700172020ustar00rootroot00000000000000#!/bin/bash # Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later # Regenerate .pot template and update .po files. # Also checks for source files with translatable strings missing from POTFILES. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BUILD_DIR="$PROJECT_DIR/_build" POTFILES="$SCRIPT_DIR/POTFILES" # Ensure build directory exists if [ ! -f "$BUILD_DIR/build.ninja" ]; then echo "Setting up build directory..." meson setup "$BUILD_DIR" "$PROJECT_DIR" fi # Check for source files missing from POTFILES echo "Checking POTFILES completeness..." missing=() # Python files with _() calls while IFS= read -r f; do rel="${f#"$PROJECT_DIR/"}" if ! grep -qxF "$rel" "$POTFILES"; then missing+=("$rel") fi done < <(grep -rlnP '(?/dev/null | sort) # UI files with translatable="yes" while IFS= read -r f; do rel="${f#"$PROJECT_DIR/"}" if ! grep -qxF "$rel" "$POTFILES"; then missing+=("$rel") fi done < <(grep -rln 'translatable="yes"' "$PROJECT_DIR/data/" --include="*.ui" 2>/dev/null | sort) if [ ${#missing[@]} -gt 0 ]; then echo "WARNING: Files with translatable strings not listed in POTFILES:" for f in "${missing[@]}"; do echo " $f" done echo "" echo "Add them to po/POTFILES if they contain user-facing strings." echo "" fi # Regenerate .pot and update .po files echo "Regenerating .pot template..." meson compile -C "$BUILD_DIR" meshy-pot 2>/dev/null echo "Updating .po files..." meson compile -C "$BUILD_DIR" meshy-update-po 2>/dev/null # Print statistics echo "" echo "=== Translation statistics ===" pot_count=$(grep -c "^msgid " "$SCRIPT_DIR/meshy.pot") echo "Total strings in .pot: $pot_count" echo "" for po in "$SCRIPT_DIR"/*.po; do [ -f "$po" ] || continue lang=$(basename "$po" .po) stats=$(msgfmt --statistics "$po" 2>&1) echo "$lang: $stats" done meshy/pyproject.toml000066400000000000000000000031771521052255700151330ustar00rootroot00000000000000[project] name = "meshy" version = "26.06" description = "A GTK4/libadwaita client for MeshCore" readme = "README.md" requires-python = ">=3.13" dependencies = [] [dependency-groups] dev = [ "pre-commit>=4.6.0", "pycryptodome>=3.23.0", "pyserial>=3.5", "pyzbar>=0.1.9", "ruff>=0.15.15", "segno>=1.6.6", ] [tool.ruff] line-length = 100 target-version = "py313" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "UP", # pyupgrade "B", # flake8-bugbear "SIM", # flake8-simplify "PIE", # flake8-pie ] ignore = [ "E501", # line too long (handled by formatter) "E402", # module level import not at top (required for gi.require_version) "E701", # multiple statements on one line (colon) - compact code style "E702", # multiple statements on one line (semicolon) - index increment pattern "E731", # lambda assignments - valid for GTK signal handlers "F821", # undefined name '_' - gettext is injected by GTK builtins "F841", # unused variable - often intentional (unpacking, callbacks) "B007", # unused loop control variable - itertools pattern "SIM102", # nested if - more readable in complex conditionals "SIM103", # needless bool - explicit is better than implicit "SIM105", # suppressible exception - contextlib not always clearer "SIM108", # if-else-block - ternary not always more readable "SIM118", # in dict.keys() - explicit for readability ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # unused imports in __init__ files meshy/src/000077500000000000000000000000001521052255700127765ustar00rootroot00000000000000meshy/src/__init__.py000066400000000000000000000004441521052255700151110ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import os as _os pkgdatadir = _os.path.join(_os.path.dirname(_os.path.dirname(__file__)), "share", "meshy") QR_SCANNER_ENABLED = True SHORTCUTS_DIALOG_ENABLED = True meshy/src/application.py000066400000000000000000000442061521052255700156610ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import logging import gi gi.require_version("Gtk", "4.0") gi.require_version("Gdk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gio, GLib, Gtk from meshy.background_portal import BackgroundPortal log = logging.getLogger(__name__) class MeshyApplication(Adw.Application): def __init__(self, version="26.06", **kwargs): super().__init__( application_id="page.codeberg.sesivany.Meshy", flags=Gio.ApplicationFlags.HANDLES_OPEN, **kwargs, ) self.version = version self._window = None self._pending_uri = None # URI received before window is ready self._hidden_start = False self._background_held = False self._bg_portal = BackgroundPortal() self.add_main_option( "debug", ord("d"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Enable debug logging", None, ) self.add_main_option( "hidden", 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Start without showing the window (used for autostart)", None, ) def do_handle_local_options(self, options): if options.contains("debug"): logging.basicConfig(level=logging.DEBUG, format="%(name)s %(levelname)s %(message)s") else: logging.basicConfig(level=logging.WARNING) self._hidden_start = options.contains("hidden") return -1 # continue normal startup def do_activate(self): if not self._window: from meshy.window import MeshyWindow self._window = MeshyWindow(application=self) settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") if settings.get_boolean("run-in-background"): self._enable_background_mode() if self._hidden_start: self._hidden_start = False return self._window.present() # Process any URI that arrived before the window was created if self._pending_uri: uri = self._pending_uri self._pending_uri = None GLib.idle_add(lambda: self._handle_meshcore_uri(uri) or False) def do_open(self, files, n_files, hint): """Handle meshcore:// URIs opened from the system.""" self.do_activate() for f in files: uri = f.get_uri() if uri and uri.startswith("meshcore://"): if self._window and self._window.is_connected: self._handle_meshcore_uri(uri) else: self._pending_uri = uri def _handle_meshcore_uri(self, uri): """Process a meshcore:// URI -- import contact or channel.""" if not self._window: return import re from urllib.parse import parse_qs, urlparse type_labels = {1: _("Chat"), 2: _("Repeater"), 3: _("Room"), 4: _("Sensor")} # Raw hex format: meshcore:// — no slashes, no query params hex_data = uri[len("meshcore://") :].strip("/") if re.fullmatch(r"[0-9a-fA-F]+", hex_data): try: contact_frame = bytes.fromhex(hex_data) name, contact_type = self._parse_advert_name(contact_frame) type_label = type_labels.get(contact_type, _("Unknown")) if name: body = _("Name: {name}\nType: {type}").format(name=name, type=type_label) else: body = _("Type: {type}\nKey: {key}…").format( type=type_label, key=hex_data[4:36] ) self._show_import_dialog( _("Import Contact"), body, lambda: ( self._window.import_contact(contact_frame), self._window.show_toast(_("Contact {}").format(name or _("imported"))), ), ) except ValueError: self._window.show_toast(_("Invalid meshcore:// link")) return parsed = urlparse(uri) params = parse_qs(parsed.query) # meshcore://channel/add?name=...&secret=... full_path = f"{parsed.netloc}{parsed.path}".rstrip("/") if full_path == "channel/add" and "secret" in params: ch_name = params.get("name", [""])[0] secret_hex = params["secret"][0].strip() try: secret = bytes.fromhex(secret_hex) except ValueError: self._window.show_toast(_("Invalid channel secret in link")) return if len(secret) != 16: self._window.show_toast(_("Invalid channel secret length")) return self._show_import_dialog( _("Add Channel"), _("Name: {name}\nType: Private").format(name=ch_name or _("Unnamed")), lambda: self._window.add_channel_from_qr(ch_name, secret), ) return # meshcore://contact/add?name=...&public_key=...&type=... if "public_key" in params: pub_key_hex = params["public_key"][0].strip() name = params.get("name", [""])[0] try: contact_type = int(params.get("type", ["1"])[0]) except ValueError: contact_type = 1 try: pub_key = bytes.fromhex(pub_key_hex) except ValueError: self._window.show_toast(_("Invalid public key in link")) return if len(pub_key) != 32: self._window.show_toast(_("Invalid public key length")) return if self._window.is_contact_known(pub_key_hex): self._window.show_toast( _("{} is already in your list").format(name or _("Contact")) ) return type_label = type_labels.get(contact_type, _("Chat")) self._show_import_dialog( _("Add {} Contact").format(type_label), _("Name: {name}\nPublic Key: {key}…").format( name=name or _("Unknown"), key=pub_key_hex[:16] ), lambda: ( self._window.add_contact(pub_key, contact_type, name or "Unknown"), self._window.show_toast(_("Contact {} added").format(name or pub_key_hex[:16])), ), ) return self._window.show_toast(_("Unrecognized meshcore:// link")) @staticmethod def _parse_advert_name(frame: bytes) -> tuple: """Extract name and type from a raw advert packet frame. Returns (name, adv_type) or ('', 0) if parsing fails. Frame layout: header(1) + path_len(1) + path(var) + pubkey(32) + timestamp(4) + signature(64) + appdata(flags + ...) """ try: i = 0 frame[i] i += 1 path_len_byte = frame[i] i += 1 hop_count = path_len_byte & 0x3F hash_size = ((path_len_byte >> 6) & 0x03) + 1 i += hop_count * hash_size # skip path i += 32 # pub key i += 4 # timestamp i += 64 # signature if i >= len(frame): return ("", 0) flags = frame[i] i += 1 adv_type = flags & 0x0F if flags & 0x10: # has latlon i += 8 if flags & 0x20: # has feat1 i += 2 if flags & 0x40: # has feat2 i += 2 if flags & 0x80: # has name name = frame[i:].decode("utf-8", errors="replace").rstrip("\x00") return (name, adv_type) return ("", adv_type) except (IndexError, ValueError): return ("", 0) def _show_import_dialog(self, heading, body, on_confirm): """Show a confirmation dialog before importing.""" dialog = Adw.AlertDialog(heading=heading, body=body) dialog.add_response("cancel", _("Cancel")) dialog.add_response("add", _("Add")) dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED) dialog.connect("response", lambda d, r: on_confirm() if r == "add" else None) dialog.present(self._window) def do_startup(self): # Register GResource before chaining up so GTK can auto-discover # gtk/help-overlay.ui during Gtk.Application.do_startup(). self._load_gresource() Adw.Application.do_startup(self) self._setup_icon_theme() self._load_css() self._setup_actions() self._apply_saved_theme() def _load_gresource(self): """Load the bundled GResource file.""" import os import meshy as _meshy_pkg resource_path = os.path.join(_meshy_pkg.pkgdatadir, "meshy.gresource") if os.path.exists(resource_path): resource = Gio.Resource.load(resource_path) Gio.resources_register(resource) def _setup_icon_theme(self): """Add custom icon resource path (requires display, call after startup).""" icon_theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()) icon_theme.add_resource_path("/page/codeberg/sesivany/Meshy/icons") def _load_css(self): css = b""" .outgoing-message { background-color: alpha(@accent_bg_color, 0.15); border-color: alpha(@accent_bg_color, 0.3); } .navigation-sidebar row { margin-left: 4px; margin-right: 4px; padding-left: 4px; padding-right: 4px; } .unread-badge { background-color: @accent_bg_color; color: @accent_fg_color; border-radius: 12px; min-width: 12px; font-size: 0.8em; font-weight: bold; padding: 2px 5px 1px; } .emoji-avatar { background-color: alpha(@accent_bg_color, 0.15); border-radius: 50%; min-width: 32px; min-height: 32px; font-size: 18px; } .unread-divider { color: @accent_bg_color; font-weight: bold; font-size: 0.8em; } .date-divider-line { min-height: 1px; margin-top: 0; margin-bottom: 0; } .unread-divider-line { min-height: 1px; margin-top: 0; margin-bottom: 0; background-color: @accent_bg_color; } .map-marker-icon { color: white; } .map-marker-badge { background-color: rgba(60, 60, 60, 0.85); color: white; font-size: 8px; font-weight: bold; min-width: 14px; min-height: 14px; border-radius: 7px; padding: 0; } .nav-rail { background-color: @sidebar_bg_color; border-right: 1px solid alpha(@borders, 0.5); padding-top: 4px; padding-left: 4px; padding-right: 3px; } listview.background > row { min-height: 0; padding: 0; border-radius: 0; background: none; outline: none; } .pill { border-radius: 16px; padding-left: 16px; padding-right: 16px; } .theme-tile { border-radius: 12px; min-width: 100px; min-height: 50px; box-shadow: 0 0 0 1px alpha(@borders, 0.5); } button.flat:checked .theme-tile { box-shadow: 0 0 0 2px @accent_bg_color; } """ provider = Gtk.CssProvider() provider.load_from_data(css) Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) def _request_background_portal(self): settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") autostart = settings.get_boolean("autostart") self._bg_portal.request_background( _("Receiving messages in the background"), autostart, ["meshy", "--hidden"] if autostart else [], self._on_background_response, ) def _enable_background_mode(self): if not self._background_held: self.hold() self._background_held = True if self._window: self._window.set_background_mode(True) self._request_background_portal() def _disable_background_mode(self): if self._background_held: self.release() self._background_held = False if self._window: self._window.set_background_mode(False) self._bg_portal.request_background( _("Receiving messages in the background"), False, [], lambda bg, auto: None, ) self._bg_portal.clear_status() def _on_background_response(self, bg_granted, autostart_granted): log.info("Background portal response: bg=%s autostart=%s", bg_granted, autostart_granted) if not bg_granted and self._background_held: self.release() self._background_held = False if self._window: self._window.set_background_mode(False) def update_background_mode(self, enabled): if enabled: self._enable_background_mode() else: self._disable_background_mode() def update_autostart(self, enabled): if not enabled: self._bg_portal.request_background( _("Receiving messages in the background"), False, [], self._on_background_response, ) elif self._background_held: self._request_background_portal() def _do_full_quit(self, *_args): if self._window: self._window.shutdown() if self._background_held: self.release() self._background_held = False self._bg_portal.clear_status() self.quit() def _setup_actions(self): quit_action = Gio.SimpleAction.new("quit", None) quit_action.connect("activate", self._do_full_quit) self.add_action(quit_action) self.set_accels_for_action("win.quit", ["q"]) about_action = Gio.SimpleAction.new("about", None) about_action.connect("activate", self._on_about) self.add_action(about_action) import meshy as _meshy_pkg if _meshy_pkg.SHORTCUTS_DIALOG_ENABLED: self.set_accels_for_action("win.show-help-overlay", ["question"]) self.set_accels_for_action("win.navigate-device", ["1"]) self.set_accels_for_action("win.navigate-contacts", ["2"]) self.set_accels_for_action("win.navigate-channels", ["3"]) self.set_accels_for_action("win.navigate-map", ["4"]) self.set_accels_for_action("win.navigate-settings", ["5"]) self.set_accels_for_action("win.search", ["f"]) self.set_accels_for_action("win.new-message", ["n"]) self.set_accels_for_action("win.navigate-prev", ["Up"]) self.set_accels_for_action("win.navigate-next", ["Down"]) self.set_accels_for_action("win.navigate-prev-unread", ["Up"]) self.set_accels_for_action("win.navigate-next-unread", ["Down"]) def _apply_saved_theme(self): from meshy.theme_manager import ThemeManager ThemeManager.get_default().apply_from_settings() def _on_about(self, action, param): about = Adw.AboutDialog( application_name="Meshy", application_icon="page.codeberg.sesivany.Meshy", version=self.version, developer_name="Jiri Eischmann", developers=[ "Jiri Eischmann", "stereo", "vanous", "Moritz Bitsch", "Petr Menšík", "Manuel Traut", ], translator_credits=_( "gallegonovato — Spanish\n" "lebeno — French, Dutch\n" "bjawebos — German\n" "Manuel Traut — German\n" "lejun — French\n" "LievenBlancke — Dutch" ), license_type=Gtk.License.GPL_3_0, comments=_("A GTK client for MeshCore LoRa mesh network devices"), website="https://meshy-app.org/", issue_url="https://codeberg.org/sesivany/meshy/issues", copyright="\u00a9 2026 Jiri Eischmann", ) about.add_legal_section( "libshumate", "\u00a9 libshumate contributors", Gtk.License.LGPL_2_1, None, ) about.add_legal_section( "zbar", "\u00a9 zbar contributors", Gtk.License.LGPL_2_1, None, ) about.add_legal_section( "protobuf-c", "\u00a9 protobuf-c contributors", Gtk.License.BSD, None, ) about.add_legal_section( "PyCryptodome", "\u00a9 PyCryptodome contributors", Gtk.License.BSD, None, ) about.add_legal_section( "pyzbar", "\u00a9 pyzbar contributors", Gtk.License.MIT_X11, None, ) about.add_legal_section( "pySerial", "\u00a9 pySerial contributors", Gtk.License.BSD, None, ) about.add_legal_section( "Segno", "\u00a9 Segno contributors", Gtk.License.BSD, None, ) about.add_legal_section( _("Adwaita Symbolic Icons"), "\u00a9 GNOME Project", Gtk.License.LGPL_3_0, None, ) about.present(self._window) meshy/src/background_portal.py000066400000000000000000000110161521052255700170470ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """XDG Background Portal integration for background mode and autostart.""" import logging import os from gi.repository import Gio, GLib log = logging.getLogger(__name__) PORTAL_BUS_NAME = "org.freedesktop.portal.Desktop" PORTAL_OBJECT_PATH = "/org/freedesktop/portal/desktop" PORTAL_BACKGROUND_IFACE = "org.freedesktop.portal.Background" PORTAL_REQUEST_IFACE = "org.freedesktop.portal.Request" class BackgroundPortal: def __init__(self): self._bus = None self._signal_id = None def _ensure_bus(self): if self._bus is None: try: self._bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) except GLib.Error as e: log.warning("Cannot connect to session bus: %s", e.message) return False return True def request_background(self, reason, autostart, commandline, callback): """Request permission to run in background and optionally autostart. callback(bg_granted, autostart_granted) is called on the main thread. """ if not self._ensure_bus(): GLib.idle_add(lambda: callback(False, False) or False) return token = f"meshy_bg_{os.getpid()}" sender = self._bus.get_unique_name().replace(".", "_").lstrip(":") request_path = f"/org/freedesktop/portal/desktop/request/{sender}/{token}" self._cleanup_signal() self._signal_id = self._bus.signal_subscribe( PORTAL_BUS_NAME, PORTAL_REQUEST_IFACE, "Response", request_path, None, Gio.DBusSignalFlags.NO_MATCH_RULE, lambda *args: self._on_response(callback, *args), ) options = { "handle_token": GLib.Variant("s", token), "reason": GLib.Variant("s", reason), "autostart": GLib.Variant("b", autostart), } if commandline: options["commandline"] = GLib.Variant("as", commandline) params = GLib.Variant("(sa{sv})", ("", options)) self._bus.call( PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, PORTAL_BACKGROUND_IFACE, "RequestBackground", params, None, Gio.DBusCallFlags.NONE, 30000, None, self._on_call_done, ) def _on_call_done(self, bus, result): try: bus.call_finish(result) except GLib.Error as e: log.warning( "Could not request background permission via portal: %s", e.message, ) self._cleanup_signal() def _on_response(self, callback, bus, sender, path, iface, signal, params): self._cleanup_signal() response = params.get_child_value(0).get_uint32() if response != 0: log.info("Background permission denied (response=%d)", response) GLib.idle_add(lambda: callback(False, False) or False) return results = params.get_child_value(1) bg_val = results.lookup_value("background", GLib.VariantType.new("b")) autostart_val = results.lookup_value("autostart", GLib.VariantType.new("b")) bg_granted = bg_val.get_boolean() if bg_val else False autostart_granted = autostart_val.get_boolean() if autostart_val else False log.info( "Background portal: bg=%s autostart=%s", bg_granted, autostart_granted, ) GLib.idle_add(lambda: callback(bg_granted, autostart_granted) or False) def set_status(self, message): if not self._ensure_bus(): return try: options = {} if message: options["message"] = GLib.Variant("s", message) self._bus.call_sync( PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, PORTAL_BACKGROUND_IFACE, "SetStatus", GLib.Variant("(a{sv})", (options,)), None, Gio.DBusCallFlags.NONE, 5000, None, ) except GLib.Error as e: log.warning("Could not set background status via portal: %s", e.message) def clear_status(self): self.set_status("") def _cleanup_signal(self): if self._signal_id and self._bus: self._bus.signal_unsubscribe(self._signal_id) self._signal_id = None meshy/src/backup.py000066400000000000000000000307671521052255700146320ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Backup and restore for MeshCore device configuration and messages.""" import uuid from datetime import datetime from gi.repository import GLib from meshy.models import ( ChannelMessage, Message, MessageStatus, NotificationLevel, ) from meshy.protocol import ( build_add_update_contact, build_device_query, build_set_auto_add_config, build_set_channel, ) class BackupManager: """Stateless helper for building and applying backup dicts.""" def export_backup(self, win) -> dict: si = win.self_info di = win.device_info backup = { "name": di.name or si.get("name", ""), "public_key": di.public_key or si.get("public_key", ""), "radio_settings": { "frequency": int(di.radio_freq * 1000) if di.radio_freq else si.get("radio_freq", 0) * 1000, "bandwidth": int(di.radio_bw * 1000) if di.radio_bw else si.get("radio_bw", 0) * 1000, "spreading_factor": di.radio_sf or si.get("radio_sf", 0), "coding_rate": di.radio_cr or si.get("radio_cr", 0), "tx_power": di.tx_power or si.get("tx_power", 0), }, "position_settings": { "latitude": str(si.get("adv_lat", 0.0)), "longitude": str(si.get("adv_lon", 0.0)), }, "other_settings": { "manual_add_contacts": 1 if si.get("manual_add_contacts") else 0, "advert_location_policy": si.get("adv_loc_policy", 0), }, "auto_add_settings": win.get_auto_add_config(), } backup["channels"] = [] for ch in win.channels: if not ch.is_empty: backup["channels"].append( { "name": ch.name, "secret": ch.psk_hex, } ) backup["contacts"] = [] for c in win.contacts: entry = { "type": c.type, "name": c.name, "public_key": c.public_key_hex, "flags": c.flags, "latitude": str(c.latitude) if c.latitude else "0.0", "longitude": str(c.longitude) if c.longitude else "0.0", "last_advert": int(c.last_seen.timestamp()) if c.last_seen else 0, "last_modified": int(c.device_lastmod.timestamp()) if c.device_lastmod else 0, "out_path": ",".join(c.path_hops) if c.path_hops else None, } backup["contacts"].append(entry) if win.storage: backup["messages"] = {} for c in win.contacts: msgs = win.storage.get_messages(c.public_key_hex, limit=10000) if msgs: backup["messages"][c.public_key_hex] = [ { "text": m.text, "timestamp": m.timestamp.timestamp(), "is_outgoing": m.is_outgoing, "sender_key_hex": m.sender_key_hex, "room_sender_name": m.room_sender_name, } for m in msgs ] backup["channel_messages"] = {} for ch in win.channels: if ch.is_empty: continue msgs = win.storage.get_channel_messages(ch.index, limit=10000) if msgs: backup["channel_messages"][str(ch.index)] = [ { "sender_name": m.sender_name, "text": m.text, "timestamp": m.timestamp.timestamp(), "is_outgoing": m.is_outgoing, } for m in msgs ] passwords = {} for c in win.contacts: pw = win.storage.get_room_password(c.public_key_hex) if pw: passwords[c.public_key_hex] = pw if passwords: backup["room_passwords"] = passwords notif = {} for ch in win.channels: if not ch.is_empty and ch.notification_level != NotificationLevel.DEFAULT: notif[str(ch.index)] = ch.notification_level.value if notif: backup["notification_levels"] = notif return backup def import_backup(self, win, backup: dict, restore_messages: bool = True): imported_contacts = 0 imported_channels = 0 settings_cmds = [] name = backup.get("name", "") if name: settings_cmds.append(lambda: win.set_advert_name(name)) radio = backup.get("radio_settings", {}) if radio: freq = radio.get("frequency", 0) bw = radio.get("bandwidth", 0) sf = radio.get("spreading_factor", 0) cr = radio.get("coding_rate", 0) tx = radio.get("tx_power", 0) if freq and bw and sf and cr: settings_cmds.append(lambda: win.set_radio_params(freq, bw, sf, cr)) if tx: settings_cmds.append(lambda: win.set_tx_power(tx)) pos = backup.get("position_settings", {}) if pos: try: lat = float(pos.get("latitude", 0)) lon = float(pos.get("longitude", 0)) if abs(lat) > 1e-6 or abs(lon) > 1e-6: settings_cmds.append(lambda: win.set_advert_latlon(lat, lon)) except (ValueError, TypeError): pass other = backup.get("other_settings", {}) if other: loc_policy = other.get("advert_location_policy", None) if loc_policy is not None: settings_cmds.append(lambda: win.set_advert_loc_policy(loc_policy)) auto = backup.get("auto_add_settings", {}) if auto: frame = build_set_auto_add_config( chat=auto.get("auto_add_chat", False), repeater=auto.get("auto_add_repeater", False), room=auto.get("auto_add_room_server", False), sensor=auto.get("auto_add_sensor", False), overwrite_oldest=auto.get("overwrite_oldest", False), max_hops=auto.get("max_hops", 0), ) settings_cmds.append(lambda: win.send_frame(frame)) for i, cmd in enumerate(settings_cmds): GLib.timeout_add(i * 300, lambda c=cmd: c() or False) settings_delay = len(settings_cmds) * 300 # Optimistically update device info and settings UI di = win._device_info if name: di.name = name if radio: if freq and bw and sf and cr: di.radio_freq = freq di.radio_bw = bw di.radio_sf = sf di.radio_cr = cr if tx: di.tx_power = tx if pos: try: lat_v = float(pos.get("latitude", 0)) lon_v = float(pos.get("longitude", 0)) if abs(lat_v) > 1e-6 or abs(lon_v) > 1e-6: di.latitude = lat_v di.longitude = lon_v except (ValueError, TypeError): pass win._settings_view.update_device_info(di) device_cmds = [] channels = backup.get("channels", []) used = {c.index for c in win.channels if not c.is_empty} for ch_data in channels: ch_name = ch_data.get("name", "") secret_hex = ch_data.get("secret", "") if ch_name and secret_hex and len(secret_hex) == 32: try: psk = bytes.fromhex(secret_hex) already = any(c.psk_hex == secret_hex for c in win.channels if not c.is_empty) if already: continue slot = None for idx in range(win.device_info.max_channels or 8): if idx not in used: slot = idx break if slot is not None: device_cmds.append( lambda s=slot, n=ch_name, p=psk: win.send_frame( build_set_channel(s, n, p) ) ) imported_channels += 1 used.add(slot) except ValueError: pass contacts = backup.get("contacts", []) contacts_by_key = {c.public_key_hex: c for c in win.contacts} for c_data in contacts: pub_key_hex = c_data.get("public_key", "") if not pub_key_hex or len(pub_key_hex) != 64: continue if pub_key_hex in contacts_by_key: continue try: pub_key = bytes.fromhex(pub_key_hex) c_name = c_data.get("name", "Unknown") contact_type = c_data.get("type", 1) flags = c_data.get("flags", 0) lat = float(c_data.get("latitude", 0)) lon = float(c_data.get("longitude", 0)) last_mod = c_data.get("last_modified") device_cmds.append( lambda pk=pub_key, ct=contact_type, fl=flags, n=c_name, la=lat, lo=lon, lm=last_mod: win.send_frame( build_add_update_contact( pub_key=pk, contact_type=ct, flags=fl, name=n, lat=la if abs(la) > 1e-6 else None, lon=lo if abs(lo) > 1e-6 else None, last_modified=lm, ) ) ) imported_contacts += 1 except (ValueError, TypeError): pass for i, cmd in enumerate(device_cmds): GLib.timeout_add(settings_delay + i * 300, lambda c=cmd: c() or False) if restore_messages and win.storage: with win.storage.batch(): for pub_key_hex, msgs in backup.get("messages", {}).items(): for m in msgs: ts = m.get("timestamp", 0) text = m.get("text", "") if win.storage.has_duplicate_message(pub_key_hex, ts, text): continue message = Message( sender_key=bytes.fromhex(m.get("sender_key_hex", pub_key_hex)), text=text, timestamp=datetime.fromtimestamp(ts), is_outgoing=m.get("is_outgoing", False), status=MessageStatus.DELIVERED, message_id=str(uuid.uuid4()), room_sender_name=m.get("room_sender_name", ""), ) win.storage.save_message(pub_key_hex, message) for ch_idx_str, msgs in backup.get("channel_messages", {}).items(): ch_idx = int(ch_idx_str) for m in msgs: msg = ChannelMessage( channel_index=ch_idx, sender_name=m.get("sender_name", ""), text=m.get("text", ""), timestamp=datetime.fromtimestamp(m.get("timestamp", 0)), is_outgoing=m.get("is_outgoing", False), ) win.storage.save_channel_message(msg) for pub_key_hex, pw in backup.get("room_passwords", {}).items(): win.storage.save_room_password(pub_key_hex, pw) total_delay = settings_delay + len(device_cmds) * 300 + 1000 GLib.timeout_add(total_delay, lambda: win._frame_handler.send_get_contacts() or False) GLib.timeout_add(total_delay + 500, lambda: win._fetch_channels_sequential(0) or False) GLib.timeout_add(total_delay + 1000, lambda: win.send_frame(build_device_query()) or False) return imported_contacts, imported_channels meshy/src/ble.py000066400000000000000000001363231521052255700141220ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """BlueZ D-Bus BLE transport for MeshCore companion devices. Uses the BlueZ D-Bus API to discover, connect, and communicate with MeshCore devices via the Nordic UART Service (NUS). """ import logging import time from collections import deque from collections.abc import Callable import gi gi.require_version("Gtk", "4.0") from gi.repository import Gio, GLib from meshy.transport_base import ConnectionState log = logging.getLogger(__name__) # Nordic UART Service UUIDs NUS_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e" NUS_TX_CHAR_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e" # Notify (device -> app) NUS_RX_CHAR_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e" # Write (app -> device) BLUEZ_BUS_NAME = "org.bluez" BLUEZ_ADAPTER_IFACE = "org.bluez.Adapter1" BLUEZ_DEVICE_IFACE = "org.bluez.Device1" BLUEZ_AGENT_IFACE = "org.bluez.Agent1" BLUEZ_AGENT_MANAGER_IFACE = "org.bluez.AgentManager1" BLUEZ_GATT_SERVICE_IFACE = "org.bluez.GattService1" BLUEZ_GATT_CHAR_IFACE = "org.bluez.GattCharacteristic1" DBUS_OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" DBUS_PROPERTIES_IFACE = "org.freedesktop.DBus.Properties" AGENT_PATH = "/io/github/meshy/agent" _MAX_WRITE_RETRIES = 3 _WRITE_RETRY_DELAY_MS = 200 _CONNECT_TIMEOUT_MS = 8000 _MAX_CONNECT_ATTEMPTS = 3 _CONNECT_RETRY_DELAY_MS = 700 _CONNECT_RETRY_LONG_DELAY_MS = 1200 # Agent1 D-Bus interface XML for registration AGENT_INTROSPECTION_XML = """ """ class BleDevice: """Represents a discovered BLE device.""" def __init__(self, path: str, address: str, name: str, rssi: int = -100): self.path = path self.address = address self.name = name self.rssi = rssi def __repr__(self): return f"BleDevice({self.name}, {self.address})" class BleTransport: """BLE transport using BlueZ D-Bus API.""" def __init__(self): self._bus: Gio.DBusConnection | None = None self._state = ConnectionState.DISCONNECTED self._adapter_path: str | None = None self._device_path: str | None = None self._tx_char_path: str | None = None self._rx_char_path: str | None = None self._device_address: str | None = None self._notify_signal_id: int | None = None self._disconnect_signal_id: int | None = None self._iface_added_signal_id: int | None = None self._discovered_devices: dict[str, BleDevice] = {} self._services_check_count: int = 0 self._recent_rx: deque = deque(maxlen=32) # (timestamp, data_hash) for dedup self._write_queue: deque = deque() # pending write data self._write_in_flight: bool = False self._write_current: bytes | None = None # data currently being written self._write_retries: int = 0 # Callbacks self.on_state_changed: Callable[[ConnectionState], None] | None = None self.on_data_received: Callable[[bytes], None] | None = None self.on_device_discovered: Callable[[BleDevice], None] | None = None self.on_passkey_requested: Callable[[str, Callable], None] | None = ( None # (device_path, reply_callback) ) self._agent_registered = False self._agent_reg_id: int | None = None self._passkey_invocation = None # pending D-Bus invocation for passkey reply self._connect_to_system_bus() def _connect_to_system_bus(self): try: self._bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) log.info("Connected to system D-Bus") except GLib.Error as e: log.error(f"Failed to connect to system bus: {e}") def _is_bluez_available(self) -> bool: """Quick check whether the org.bluez D-Bus name is owned. This queries the D-Bus daemon itself (always fast) rather than calling into BlueZ, so it never blocks the main loop even when no Bluetooth hardware is present. """ if not self._bus: return False try: result = self._bus.call_sync( "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameHasOwner", GLib.Variant("(s)", (BLUEZ_BUS_NAME,)), GLib.VariantType.new("(b)"), Gio.DBusCallFlags.NONE, 1000, None, ) return result.get_child_value(0).get_boolean() except GLib.Error: return False @property def state(self) -> ConnectionState: return self._state @property def is_connected(self) -> bool: return self._state == ConnectionState.CONNECTED @property def connected_address(self) -> str | None: return self._device_address @property def discovered_devices(self) -> list[BleDevice]: return list(self._discovered_devices.values()) def _set_state(self, state: ConnectionState): if self._state != state: old = self._state self._state = state log.info(f"BLE state: {old.name} -> {state.name}") if self.on_state_changed: self.on_state_changed(state) def _call_bluez( self, object_path: str, interface: str, method: str, parameters: GLib.Variant | None = None, reply_type: GLib.VariantType | None = None, ) -> GLib.Variant | None: """Synchronous D-Bus method call to BlueZ.""" try: result = self._bus.call_sync( BLUEZ_BUS_NAME, object_path, interface, method, parameters, reply_type, Gio.DBusCallFlags.NONE, 10000, None, ) return result except GLib.Error as e: log.error(f"D-Bus call {interface}.{method} on {object_path} failed: {e}") return None def _call_bluez_async( self, object_path: str, interface: str, method: str, parameters: GLib.Variant | None = None, callback: Callable | None = None, timeout_ms: int = 30000, ): """Async D-Bus method call to BlueZ.""" def _on_done(bus, result): try: res = bus.call_finish(result) if callback: callback(res, None) except GLib.Error as e: log.error(f"Async D-Bus call {interface}.{method} on {object_path} failed: {e}") if callback: callback(None, e) self._bus.call( BLUEZ_BUS_NAME, object_path, interface, method, parameters, None, Gio.DBusCallFlags.NONE, timeout_ms, None, _on_done, ) def _get_property(self, object_path: str, interface: str, prop: str) -> GLib.Variant | None: result = self._call_bluez( object_path, DBUS_PROPERTIES_IFACE, "Get", GLib.Variant("(ss)", (interface, prop)), GLib.VariantType.new("(v)"), ) if result: return result.get_child_value(0).get_variant() return None def _find_adapter(self) -> str | None: """Find the first Bluetooth adapter.""" if not self._is_bluez_available(): return None result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if not result: return None objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_ADAPTER_IFACE: return path return None def start_scan(self): """Start BLE device discovery.""" if self._state != ConnectionState.DISCONNECTED: return self._adapter_path = self._find_adapter() if not self._adapter_path: log.error("No Bluetooth adapter found") return self._discovered_devices.clear() self._set_state(ConnectionState.SCANNING) # Subscribe to InterfacesAdded for new devices self._iface_added_signal_id = self._bus.signal_subscribe( BLUEZ_BUS_NAME, DBUS_OBJECT_MANAGER_IFACE, "InterfacesAdded", None, None, Gio.DBusSignalFlags.NONE, self._on_interfaces_added, ) # Set discovery filter - use NUS service UUID (what MeshCore devices actually advertise) self._call_bluez( self._adapter_path, BLUEZ_ADAPTER_IFACE, "SetDiscoveryFilter", GLib.Variant( "(a{sv})", ( { "Transport": GLib.Variant("s", "le"), }, ), ), ) # Check already known/paired devices first self._check_existing_devices() # Start discovery self._call_bluez(self._adapter_path, BLUEZ_ADAPTER_IFACE, "StartDiscovery") log.info("BLE scan started") def _check_existing_devices(self): """Check for already-known devices that have the NUS service.""" result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if not result: return objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_DEVICE_IFACE: props = iface_entry.get_child_value(1) self._process_device(path, props) def _on_interfaces_added( self, connection, sender, object_path, interface_name, signal_name, parameters ): """Handle new D-Bus interface (new device discovered).""" path = parameters.get_child_value(0).get_string() interfaces = parameters.get_child_value(1) for i in range(interfaces.n_children()): entry = interfaces.get_child_value(i) iface_name = entry.get_child_value(0).get_string() if iface_name == BLUEZ_DEVICE_IFACE: props = entry.get_child_value(1) self._process_device(path, props) def _process_device(self, path: str, props: GLib.Variant): """Process a discovered device's properties. Accept devices that have the NUS service UUID in their advertised UUIDs. """ address = name = "" rssi = -100 uuids = [] for k in range(props.n_children()): prop = props.get_child_value(k) key = prop.get_child_value(0).get_string() val = prop.get_child_value(1).get_variant() if key == "Address": address = val.get_string() elif key == "Name": name = val.get_string() elif key == "Alias": if not name: name = val.get_string() elif key == "RSSI": rssi = val.get_int16() elif key == "UUIDs": uuids = [val.get_child_value(i).get_string() for i in range(val.n_children())] if not address: return # Accept devices that have NUS service UUID has_nus = NUS_SERVICE_UUID.lower() in [u.lower() for u in uuids] if not has_nus: return log.info(f"Found NUS device: {name} ({address}) at {path}") device = BleDevice(path=path, address=address, name=name or address, rssi=rssi) self._discovered_devices[path] = device if self.on_device_discovered: self.on_device_discovered(device) def stop_scan(self): """Stop BLE device discovery.""" if self._state != ConnectionState.SCANNING: return if self._adapter_path: self._call_bluez(self._adapter_path, BLUEZ_ADAPTER_IFACE, "StopDiscovery") if self._iface_added_signal_id: self._bus.signal_unsubscribe(self._iface_added_signal_id) self._iface_added_signal_id = None self._set_state(ConnectionState.DISCONNECTED) log.info("BLE scan stopped") def connect_device(self, device_path: str): """Connect to a BLE device by its D-Bus object path.""" if self._state == ConnectionState.SCANNING: self.stop_scan() self._set_state(ConnectionState.CONNECTING) self._device_path = device_path self._services_check_count = 0 self._reconnect_attempted = False # Read the device address addr_var = self._get_property(device_path, BLUEZ_DEVICE_IFACE, "Address") self._device_address = addr_var.get_string() if addr_var else None # Register agent - needed for devices requiring auth on every connection self._register_agent() # Ensure device is trusted (required for GATT service resolution) self._call_bluez( device_path, DBUS_PROPERTIES_IFACE, "Set", GLib.Variant("(ssv)", (BLUEZ_DEVICE_IFACE, "Trusted", GLib.Variant("b", True))), ) # Check if device is paired, if not try pairing first paired_var = self._get_property(device_path, BLUEZ_DEVICE_IFACE, "Paired") if paired_var and not paired_var.get_boolean(): log.info(f"Device {device_path} not paired, initiating pairing") self.pair_device(device_path) return # Check if already connected connected_var = self._get_property(device_path, BLUEZ_DEVICE_IFACE, "Connected") if connected_var and connected_var.get_boolean(): log.info(f"Device {device_path} is already connected") # Check if services are already resolved resolved_var = self._get_property(device_path, BLUEZ_DEVICE_IFACE, "ServicesResolved") if resolved_var and resolved_var.get_boolean(): log.info("Services already resolved, finding characteristics") self._find_nus_characteristics() return else: log.info("Waiting for services to resolve...") GLib.timeout_add(500, self._check_services_resolved) return # Not connected, initiate connection with retry log.info(f"Initiating connection to {device_path}") self._attempt_connect(device_path, attempt=1) def _attempt_connect(self, device_path: str, attempt: int): """Try to connect with automatic retry on failure.""" if self._state != ConnectionState.CONNECTING: return log.info(f"Connect attempt {attempt}/{_MAX_CONNECT_ATTEMPTS} for {device_path}") def _on_connected(result, error): if self._state != ConnectionState.CONNECTING: return if error: error_msg = str(error) if "Already Connected" in error_msg: log.info("Device reports already connected") GLib.timeout_add(500, self._check_services_resolved) return log.warning(f"Connect attempt {attempt} failed: {error}") if attempt < _MAX_CONNECT_ATTEMPTS: self._call_bluez(device_path, BLUEZ_DEVICE_IFACE, "Disconnect") delay = _CONNECT_RETRY_LONG_DELAY_MS if attempt > 1 else _CONNECT_RETRY_DELAY_MS log.info(f"Retrying connect after BlueZ disconnect " f"(delay {delay}ms)") def _retry(): self._attempt_connect(device_path, attempt + 1) return False GLib.timeout_add(delay, _retry) else: log.error( f"All {_MAX_CONNECT_ATTEMPTS} connect attempts " f"failed for {device_path}" ) self._set_state(ConnectionState.DISCONNECTED) return log.info(f"Connect succeeded on attempt {attempt} for {device_path}") GLib.timeout_add(500, self._check_services_resolved) self._call_bluez_async( device_path, BLUEZ_DEVICE_IFACE, "Connect", callback=_on_connected, timeout_ms=_CONNECT_TIMEOUT_MS, ) def connect_by_address(self, address: str): """Connect to a device by its Bluetooth address.""" # Find the device path from known devices for path, device in self._discovered_devices.items(): if device.address.upper() == address.upper(): self.connect_device(path) return # Try to find it in BlueZ known/paired devices result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if result: objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_DEVICE_IFACE: addr_var = self._get_property(path, BLUEZ_DEVICE_IFACE, "Address") if addr_var and addr_var.get_string().upper() == address.upper(): log.info(f"Found device at {path} for address {address}") self.connect_device(path) return # Device not in BlueZ (maybe removed) - scan to find it log.info(f"Device {address} not in BlueZ, scanning to rediscover...") target_address = address.upper() orig_callback = self.on_device_discovered def _on_found(device): if device.address.upper() == target_address: log.info(f"Rediscovered {device.name} at {device.path}") self.on_device_discovered = orig_callback if self.state == ConnectionState.SCANNING: self.stop_scan() # Need to pair since bond was removed self._set_state(ConnectionState.CONNECTING) self.pair_device(device.path) self.on_device_discovered = _on_found self.start_scan() # Give up after 10s def _scan_timeout(): if self.on_device_discovered == _on_found: self.on_device_discovered = orig_callback if self.state == ConnectionState.SCANNING: self.stop_scan() log.error(f"Device {address} not found after scanning") self._set_state(ConnectionState.DISCONNECTED) return False GLib.timeout_add(10000, _scan_timeout) def _check_services_resolved(self) -> bool: """Check if GATT services are resolved after connection.""" if not self._device_path or self._state != ConnectionState.CONNECTING: return False self._services_check_count += 1 if not self._device_path or self._state != ConnectionState.CONNECTING: return False # Check if still connected connected_var = self._get_property(self._device_path, BLUEZ_DEVICE_IFACE, "Connected") is_connected = connected_var and connected_var.get_boolean() log.debug( f"Checking ServicesResolved (attempt {self._services_check_count}, " f"connected={is_connected})" ) if not is_connected and not self._reconnect_attempted: # Device not connected yet - retry Connect with a longer delay self._reconnect_attempted = True device_path = self._device_path log.info("Device not connected, retrying Connect in 3s...") def _do_retry(): if self._state != ConnectionState.CONNECTING: return False self._services_check_count = 0 log.info("Retrying Connect...") def _on_connect(result, error): if error: log.error(f"Retry connect failed: {error}") self._set_state(ConnectionState.DISCONNECTED) return log.info("Retry connect succeeded, waiting for services...") GLib.timeout_add(500, self._check_services_resolved) self._call_bluez_async( device_path, BLUEZ_DEVICE_IFACE, "Connect", callback=_on_connect, ) return False GLib.timeout_add(3000, _do_retry) return False # Stop polling, retry starts new one if not is_connected and self._reconnect_attempted: # Already tried reconnect, still not connected log.error("Device not connected after reconnect") self._set_state(ConnectionState.DISCONNECTED) return False # Connected but services not resolved resolved_var = self._get_property(self._device_path, BLUEZ_DEVICE_IFACE, "ServicesResolved") if resolved_var and resolved_var.get_boolean(): log.info("Services resolved, finding NUS characteristics") self._find_nus_characteristics() return False # After 20 attempts (10s) with connection but no services, try disconnect+reconnect if self._services_check_count == 20 and not self._reconnect_attempted: self._reconnect_attempted = True device_path = self._device_path log.info("Services not resolving despite connection, trying disconnect+reconnect") self._call_bluez(device_path, BLUEZ_DEVICE_IFACE, "Disconnect") def _do_reconnect(): if self._state != ConnectionState.CONNECTING: return False log.info("Reconnecting after disconnect...") self._services_check_count = 0 def _on_reconnect(result, error): if error: log.error(f"Reconnect failed: {error}") self._set_state(ConnectionState.DISCONNECTED) return log.info("Reconnect succeeded, waiting for services...") GLib.timeout_add(500, self._check_services_resolved) self._call_bluez_async( device_path, BLUEZ_DEVICE_IFACE, "Connect", callback=_on_reconnect, ) return False GLib.timeout_add(3000, _do_reconnect) return False # Stop polling, reconnect starts new one # Give up after 40 attempts (20 seconds) if self._services_check_count > 40: log.error("Timed out waiting for services to resolve") self._set_state(ConnectionState.DISCONNECTED) return False return True def _find_nus_characteristics(self): """Find NUS TX/RX characteristics after services are resolved.""" result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if not result: log.error("Failed to enumerate GATT services") self._set_state(ConnectionState.DISCONNECTED) return self._tx_char_path = None self._rx_char_path = None objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() if not path.startswith(self._device_path): continue interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_GATT_CHAR_IFACE: # Read UUID from the properties dict in the managed objects char_props = iface_entry.get_child_value(1) uuid_var = char_props.lookup_value("UUID", GLib.VariantType.new("s")) if uuid_var: uuid = uuid_var.get_string().lower() log.debug(f" Characteristic {path}: UUID={uuid}") if uuid == NUS_TX_CHAR_UUID: self._tx_char_path = path log.info(f"Found NUS TX (notify) at {path}") elif uuid == NUS_RX_CHAR_UUID: self._rx_char_path = path log.info(f"Found NUS RX (write) at {path}") if self._tx_char_path and self._rx_char_path: log.info("Found both NUS characteristics, starting notifications") self._start_notifications() else: log.error( f"NUS characteristics not found (TX={self._tx_char_path}, RX={self._rx_char_path})" ) # Fallback: try reading UUID via Get property call self._find_nus_characteristics_fallback() def _find_nus_characteristics_fallback(self): """Fallback: enumerate characteristics by calling Get on each one.""" log.info("Trying fallback characteristic discovery...") result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if not result: self._set_state(ConnectionState.DISCONNECTED) return objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() if not path.startswith(self._device_path): continue interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_GATT_CHAR_IFACE: uuid_var = self._get_property(path, BLUEZ_GATT_CHAR_IFACE, "UUID") if uuid_var: uuid = uuid_var.get_string().lower() log.debug(f" Fallback: Char {path} UUID={uuid}") if uuid == NUS_TX_CHAR_UUID: self._tx_char_path = path elif uuid == NUS_RX_CHAR_UUID: self._rx_char_path = path if self._tx_char_path and self._rx_char_path: log.info("Fallback found NUS characteristics") self._start_notifications() else: log.error(f"Fallback also failed (TX={self._tx_char_path}, RX={self._rx_char_path})") self._set_state(ConnectionState.DISCONNECTED) def _start_notifications(self): """Start notifications on the TX characteristic to receive data.""" # Subscribe to PropertiesChanged for the TX characteristic self._notify_signal_id = self._bus.signal_subscribe( BLUEZ_BUS_NAME, DBUS_PROPERTIES_IFACE, "PropertiesChanged", self._tx_char_path, None, Gio.DBusSignalFlags.NONE, self._on_tx_notify, ) # Listen for disconnect self._disconnect_signal_id = self._bus.signal_subscribe( BLUEZ_BUS_NAME, DBUS_PROPERTIES_IFACE, "PropertiesChanged", self._device_path, None, Gio.DBusSignalFlags.NONE, self._on_device_properties_changed, ) # Start notifications on the TX characteristic def _on_notify_started(result, error): if error: error_msg = str(error) if "Already notifying" in error_msg or "In Progress" in error_msg: log.info("Notifications already active") self._set_state(ConnectionState.CONNECTED) else: log.error(f"Failed to start notifications: {error}") self._set_state(ConnectionState.DISCONNECTED) return log.info("BLE notifications started successfully") self._set_state(ConnectionState.CONNECTED) self._call_bluez_async( self._tx_char_path, BLUEZ_GATT_CHAR_IFACE, "StartNotify", callback=_on_notify_started ) def _on_tx_notify( self, connection, sender, object_path, interface_name, signal_name, parameters ): """Handle data received via TX characteristic notification.""" iface = parameters.get_child_value(0).get_string() if iface != BLUEZ_GATT_CHAR_IFACE: return changed = parameters.get_child_value(1) value_var = changed.lookup_value("Value", GLib.VariantType.new("ay")) if value_var: data = bytes(value_var.unpack()) if data: # Deduplicate: BlueZ sometimes delivers the same notification twice now = time.monotonic() data_hash = hash(data) for ts, dh in self._recent_rx: if dh == data_hash and now - ts < 0.5: return # duplicate within 500ms, skip self._recent_rx.append((now, data_hash)) log.debug(f"BLE RX [{len(data)} bytes]: {data[:20].hex()}...") if self.on_data_received: self.on_data_received(data) def _on_device_properties_changed( self, connection, sender, object_path, interface_name, signal_name, parameters ): """Handle device property changes (e.g., disconnect).""" # Only handle events for the currently connected device if object_path != self._device_path: return iface = parameters.get_child_value(0).get_string() if iface != BLUEZ_DEVICE_IFACE: return changed = parameters.get_child_value(1) connected_var = changed.lookup_value("Connected", GLib.VariantType.new("b")) if connected_var and not connected_var.get_boolean(): log.info(f"Device {object_path} disconnected") self._cleanup() def send_data(self, data: bytes): """Queue data for sending via the RX characteristic (WriteValue). Writes are serialized: only one WriteValue is in-flight at a time. Subsequent calls are queued and drained as each write completes. """ if not self._rx_char_path or self._state != ConnectionState.CONNECTED: log.warning(f"Cannot send: state={self._state.name}, rx_path={self._rx_char_path}") return if self._write_in_flight: self._write_queue.append(data) return self._do_write(data) def _do_write(self, data: bytes): """Send data immediately via BlueZ WriteValue.""" self._write_in_flight = True self._write_current = data log.debug(f"BLE TX [{len(data)} bytes]: {data[:20].hex()}...") value = GLib.Variant( "(aya{sv})", ( list(data), {}, ), ) self._call_bluez_async( self._rx_char_path, BLUEZ_GATT_CHAR_IFACE, "WriteValue", parameters=value, callback=self._on_write_done, ) def _is_device_connected(self) -> bool: if not self._device_path: return False connected_var = self._get_property(self._device_path, BLUEZ_DEVICE_IFACE, "Connected") return connected_var.get_boolean() if connected_var else False def _on_write_done(self, result, error): failed_data = self._write_current self._write_current = None self._write_in_flight = False if error: error_msg = str(error) log.error(f"Write failed: {error_msg}") if "In Progress" in error_msg and failed_data is not None: log.info("Retrying write after In Progress error") self._write_queue.appendleft(failed_data) GLib.timeout_add(50, self._drain_write_queue) return if self._is_device_connected(): if self._write_retries < _MAX_WRITE_RETRIES and failed_data is not None: self._write_retries += 1 log.warning( f"Write error but device connected, retry " f"{self._write_retries}/{_MAX_WRITE_RETRIES}" ) self._write_queue.appendleft(failed_data) GLib.timeout_add(_WRITE_RETRY_DELAY_MS, self._drain_write_queue) return log.warning(f"Write failed after {_MAX_WRITE_RETRIES} retries, dropping frame") self._write_retries = 0 GLib.idle_add(self._drain_write_queue) return log.warning("Write failure and device disconnected, cleaning up") self._cleanup() return self._write_retries = 0 GLib.idle_add(self._drain_write_queue) def _drain_write_queue(self): """Send the next queued write if idle.""" if self._write_in_flight or not self._write_queue: return False if not self._rx_char_path or self._state != ConnectionState.CONNECTED: self._write_queue.clear() return False self._do_write(self._write_queue.popleft()) return False def _register_agent(self): """Register our Agent1 with BlueZ for handling pairing.""" if self._agent_registered: return introspection = Gio.DBusNodeInfo.new_for_xml(AGENT_INTROSPECTION_XML) self._agent_reg_id = self._bus.register_object( AGENT_PATH, introspection.interfaces[0], self._on_agent_method_call, None, None, ) result = self._call_bluez( "/org/bluez", BLUEZ_AGENT_MANAGER_IFACE, "RegisterAgent", GLib.Variant("(os)", (AGENT_PATH, "KeyboardDisplay")), ) if result is None: # Might already be registered from a previous session, # re-register the D-Bus object but skip the BlueZ call log.warning("Agent registration call failed (may already be registered)") # Set as default agent so BlueZ always uses ours self._call_bluez( "/org/bluez", BLUEZ_AGENT_MANAGER_IFACE, "RequestDefaultAgent", GLib.Variant("(o)", (AGENT_PATH,)), ) self._agent_registered = True log.info("BlueZ agent registered as default") def _unregister_agent(self): """Unregister our Agent1.""" if not self._agent_registered: return self._call_bluez( "/org/bluez", BLUEZ_AGENT_MANAGER_IFACE, "UnregisterAgent", GLib.Variant("(o)", (AGENT_PATH,)), ) if self._agent_reg_id: self._bus.unregister_object(self._agent_reg_id) self._agent_reg_id = None self._agent_registered = False log.info("BlueZ agent unregistered") def _set_device_trusted(self, device_path: str): """Mark a device as trusted in BlueZ.""" log.info(f"Setting {device_path} as trusted") self._call_bluez( device_path, DBUS_PROPERTIES_IFACE, "Set", GLib.Variant("(ssv)", (BLUEZ_DEVICE_IFACE, "Trusted", GLib.Variant("b", True))), ) def _on_agent_method_call( self, connection, sender, object_path, interface_name, method_name, parameters, invocation ): """Handle Agent1 D-Bus method calls from BlueZ.""" log.info(f"Agent1 call: {method_name} params={parameters}") if method_name == "Release": invocation.return_value(None) elif method_name == "RequestPasskey": device_path = parameters.get_child_value(0).get_string() log.info(f"Passkey requested for {device_path}") self._set_device_trusted(device_path) self._passkey_invocation = invocation if self.on_passkey_requested: self.on_passkey_requested(device_path, self._reply_passkey) else: invocation.return_value(GLib.Variant("(u)", (0,))) elif method_name == "RequestPinCode": device_path = parameters.get_child_value(0).get_string() log.info(f"PIN code requested for {device_path}") self._set_device_trusted(device_path) self._passkey_invocation = invocation if self.on_passkey_requested: self.on_passkey_requested(device_path, self._reply_pincode) else: invocation.return_value(GLib.Variant("(s)", ("000000",))) elif method_name == "RequestConfirmation": device_path = parameters.get_child_value(0).get_string() passkey = parameters.get_child_value(1).get_uint32() log.info(f"Confirmation requested for {device_path}: {passkey:06d}") self._set_device_trusted(device_path) invocation.return_value(None) elif method_name == "DisplayPasskey": device_path = parameters.get_child_value(0).get_string() passkey = parameters.get_child_value(1).get_uint32() log.info(f"Display passkey for {device_path}: {passkey:06d}") invocation.return_value(None) elif method_name in ("RequestAuthorization", "AuthorizeService"): device_path = parameters.get_child_value(0).get_string() self._set_device_trusted(device_path) invocation.return_value(None) elif method_name == "Cancel": log.info("Pairing cancelled by BlueZ") self._passkey_invocation = None invocation.return_value(None) else: invocation.return_dbus_error("org.bluez.Error.Rejected", "Not implemented") def _reply_passkey(self, passkey: int): """Reply to a pending RequestPasskey with the user-provided passkey.""" if self._passkey_invocation: self._passkey_invocation.return_value(GLib.Variant("(u)", (passkey,))) self._passkey_invocation = None def _reply_pincode(self, passkey: int): """Reply to a pending RequestPinCode with the user-provided PIN.""" if self._passkey_invocation: self._passkey_invocation.return_value(GLib.Variant("(s)", (f"{passkey:06d}",))) self._passkey_invocation = None def pair_device(self, device_path: str): """Initiate pairing with a device. Registers agent first, then calls Pair() which handles connect+auth+bond.""" self._register_agent() self._set_state(ConnectionState.CONNECTING) self._device_path = device_path # Read address before pairing addr_var = self._get_property(device_path, BLUEZ_DEVICE_IFACE, "Address") self._device_address = addr_var.get_string() if addr_var else None def _on_pair_done(result, error): if error: error_msg = str(error) if "Already Exists" in error_msg: log.info("Device already paired, connecting...") self.connect_device(device_path) else: log.error(f"Pairing failed: {error}") self._set_state(ConnectionState.DISCONNECTED) return log.info(f"Pairing succeeded for {device_path}") self.connect_device(device_path) log.info(f"Calling Pair() on {device_path}") self._call_bluez_async( device_path, BLUEZ_DEVICE_IFACE, "Pair", callback=_on_pair_done, ) def is_bluetooth_enabled(self) -> bool: """Check if the Bluetooth adapter is powered on.""" adapter_path = self._adapter_path or self._find_adapter() if not adapter_path: return False powered = self._get_property(adapter_path, BLUEZ_ADAPTER_IFACE, "Powered") return powered.get_boolean() if powered else False def has_bluetooth_adapter(self) -> bool: """Check if a Bluetooth adapter exists.""" return self._find_adapter() is not None def get_paired_devices(self) -> list[BleDevice]: """Get list of already-paired devices that have NUS service.""" if not self._is_bluez_available(): return [] result = self._call_bluez( "/", DBUS_OBJECT_MANAGER_IFACE, "GetManagedObjects", None, GLib.VariantType.new("(a{oa{sa{sv}}})"), ) if not result: return [] devices = [] objects = result.get_child_value(0) for i in range(objects.n_children()): entry = objects.get_child_value(i) path = entry.get_child_value(0).get_string() interfaces = entry.get_child_value(1) for j in range(interfaces.n_children()): iface_entry = interfaces.get_child_value(j) iface_name = iface_entry.get_child_value(0).get_string() if iface_name == BLUEZ_DEVICE_IFACE: props = iface_entry.get_child_value(1) address = name = "" paired = False uuids = [] for k in range(props.n_children()): prop = props.get_child_value(k) key = prop.get_child_value(0).get_string() val = prop.get_child_value(1).get_variant() if key == "Address": address = val.get_string() elif key == "Name" or key == "Alias" and not name: name = val.get_string() elif key == "Paired": paired = val.get_boolean() elif key == "UUIDs": uuids = [ val.get_child_value(m).get_string() for m in range(val.n_children()) ] has_nus = NUS_SERVICE_UUID.lower() in [u.lower() for u in uuids] if paired and has_nus and address: devices.append(BleDevice(path=path, address=address, name=name or address)) return devices def remove_device(self, device_path: str) -> bool: """Remove (unpair) a Bluetooth device via Adapter1.RemoveDevice.""" adapter = self._adapter_path or self._find_adapter() if not adapter: log.error("No Bluetooth adapter found for device removal") return False result = self._call_bluez( adapter, BLUEZ_ADAPTER_IFACE, "RemoveDevice", GLib.Variant("(o)", (device_path,)) ) return result is not None def disconnect(self): """Disconnect from the current device.""" if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING): return self._set_state(ConnectionState.DISCONNECTING) # Unsubscribe signals FIRST to prevent race conditions # when connecting to a different device if self._notify_signal_id: self._bus.signal_unsubscribe(self._notify_signal_id) self._notify_signal_id = None if self._disconnect_signal_id: self._bus.signal_unsubscribe(self._disconnect_signal_id) self._disconnect_signal_id = None if self._tx_char_path: self._call_bluez(self._tx_char_path, BLUEZ_GATT_CHAR_IFACE, "StopNotify") device_path = self._device_path self._tx_char_path = None self._rx_char_path = None self._device_path = None self._device_address = None self._write_queue.clear() self._write_in_flight = False self._write_current = None if device_path: self._call_bluez_async( device_path, BLUEZ_DEVICE_IFACE, "Disconnect", callback=lambda r, e: self._set_state(ConnectionState.DISCONNECTED), ) else: self._set_state(ConnectionState.DISCONNECTED) def _cleanup(self): """Clean up connection state.""" if self._notify_signal_id: self._bus.signal_unsubscribe(self._notify_signal_id) self._notify_signal_id = None if self._disconnect_signal_id: self._bus.signal_unsubscribe(self._disconnect_signal_id) self._disconnect_signal_id = None self._tx_char_path = None self._rx_char_path = None self._device_path = None self._device_address = None self._write_queue.clear() self._write_in_flight = False self._write_current = None self._write_retries = 0 self._set_state(ConnectionState.DISCONNECTED) meshy/src/connection_controller.py000066400000000000000000000571411521052255700177620ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Connection, sync, and reconnect logic extracted from MeshyWindow.""" import logging import time from gi.repository import Adw, Gio, GLib, Gtk from meshy.ble import BleDevice from meshy.models import DeviceInfo from meshy.protocol import ( build_app_start, build_device_query, build_get_auto_add_config, build_get_batt_and_storage, build_get_channel, build_get_custom_vars, build_get_device_time, build_sync_next_message, ) from meshy.storage import Storage from meshy.transport_base import ConnectionState log = logging.getLogger(__name__) _HANDSHAKE_TIMEOUT_MS = 5000 _SYNC_TIMEOUT_MS = 90000 _MAX_RECONNECT_ATTEMPTS = 3 _RECONNECT_DELAYS = [2000, 4000, 8000] _CONTACTS_SYNC_TIMEOUT_MS = 10000 _CONTACTS_SYNC_MAX_RETRIES = 3 _CHANNEL_SYNC_TIMEOUT_MS = 5000 _CHANNEL_SYNC_MAX_RETRIES = 3 _MSG_SYNC_TIMEOUT_MS = 5000 _MSG_SYNC_MAX_RETRIES = 3 class ConnectionController: """Owns connection lifecycle, sync state, and reconnect logic.""" def __init__(self, win): self._win = win self.syncing: bool = False self.msg_syncing: bool = False self.sync_msg_count: int = 0 self.sync_contact_total: int = 0 self.sync_contact_current: int = 0 self.synced_contact_keys: set | None = None self.contacts_iter_busy: bool = False self.contacts_refetch_pending: bool = False self.incremental_sync: bool = False self.gps_poll_pending: bool = False self.contacts_sync_timeout_id: int | None = None self.contacts_sync_retries: int = 0 self.channel_sync_index: int = -1 self.channel_sync_retries: int = 0 self.channel_sync_timeout_id: int | None = None self.msg_sync_timeout_id: int | None = None self.msg_sync_retries: int = 0 self.handshake_timeout_id: int | None = None self.handshake_received: bool = False self.connect_timeout_id: int | None = None self.sync_timeout_id: int | None = None self.reconnect_attempts: int = 0 self.reconnect_timer_id: int | None = None self.last_connected_address: str | None = None self.manual_disconnect: bool = False self.batt_timer_id: int | None = None self.watchdog_timer_id: int | None = None self.last_device_rx: float = 0.0 self.pending_device_address: str | None = None self.pending_transport_type: str | None = None def setup_transport_callbacks(self, transport): transport.on_state_changed = self.on_ble_state_changed transport.on_data_received = self._win._on_ble_data_received transport.on_device_discovered = self.on_ble_device_discovered def auto_connect(self): self._win._debug_log(f"auto_connect: state={self._win._transport.state}") if self._win._transport.state != ConnectionState.DISCONNECTED: return False settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") last_addr = settings.get_string("last-device-address") last_transport = settings.get_string("last-transport") self._win._debug_log(f"auto_connect: last_addr={last_addr}, transport={last_transport}") if last_addr: if last_transport == "tcp": from meshy.tcp import TcpTransport self._win._tcp = TcpTransport() self._win._transport = self._win._tcp self.setup_transport_callbacks(self._win._tcp) elif last_transport == "usb": from meshy.usb_serial import UsbSerialTransport self._win._usb = UsbSerialTransport() self._win._transport = self._win._usb self.setup_transport_callbacks(self._win._usb) else: self._win._transport = self._win._ble self._win._show_connecting_screen(last_addr) self._win._transport.connect_by_address(last_addr) else: self._win._show_connection_screen() return False def connect_to_device(self, device, transport=None): if transport: self._win._transport = transport self.setup_transport_callbacks(transport) else: self._win._transport = self._win._ble self.setup_transport_callbacks(self._win._ble) from meshy.tcp import TcpDevice from meshy.usb_serial import UsbDevice self.pending_device_address = device.address if isinstance(device, TcpDevice): self.pending_transport_type = "tcp" elif isinstance(device, UsbDevice): self.pending_transport_type = "usb" else: self.pending_transport_type = "ble" self._win._connection_banner.set_title(_("Connecting...")) self._win._connection_banner.set_revealed(True) self._win._transport.connect_device(device.path) def show_scan_dialog(self): dialog = Adw.AlertDialog( heading=_("Scan for Devices"), body=_("Scanning for MeshCore devices..."), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("scan", _("Scan")) dialog.set_response_appearance("scan", Adw.ResponseAppearance.SUGGESTED) self._scan_listbox = Gtk.ListBox() self._scan_listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) self._scan_listbox.add_css_class("boxed-list") scroll = Gtk.ScrolledWindow(min_content_height=200) list_wrapper = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) list_wrapper.append(self._scan_listbox) scroll.set_child(list_wrapper) clamp = Adw.Clamp(maximum_size=400, child=scroll) dialog.set_extra_child(clamp) self._scan_dialog = dialog self._win._ble.start_scan() def _on_response(d, response): if response == "cancel": self._win._ble.stop_scan() self._scan_dialog = None dialog.connect("response", _on_response) self._scan_listbox.connect("row-activated", self._on_scan_device_selected) dialog.present(self._win) def _on_scan_device_selected(self, listbox, row): device = row._ble_device if self._scan_dialog: self._scan_dialog.close() self._scan_dialog = None self._win._ble.stop_scan() self.pending_device_address = device.address self.pending_transport_type = "ble" self._win._ble.connect_device(device.path) def on_ble_device_discovered(self, device: BleDevice): def _add_to_list(): if not hasattr(self, "_scan_dialog") or not self._scan_dialog: return False row = Adw.ActionRow( title=device.name, subtitle=device.address, ) row.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-symbolic")) row.set_activatable(True) row._ble_device = device self._scan_listbox.append(row) return False GLib.idle_add(_add_to_list) def on_ble_state_changed(self, state: ConnectionState): def _update_ui(): if state == ConnectionState.DISCONNECTED: self._cancel_connect_timeout() self._cancel_handshake_timeout() self._cancel_sync_timeout() self._cancel_contacts_sync_timeout() self._cancel_channel_sync_timeout() self._cancel_msg_sync_timeout() self._win.stop_login() self.channel_sync_index = -1 if self._win._device_view_inst is not None: self._win._device_view_inst.update_connection_status(False) if self.batt_timer_id: GLib.source_remove(self.batt_timer_id) self.batt_timer_id = None if self.watchdog_timer_id: GLib.source_remove(self.watchdog_timer_id) self.watchdog_timer_id = None if self._win._storage: self._win._storage.fail_orphaned_messages() self._win._storage.close() self._win._storage = None self._win._contacts.clear() self._win._contacts_by_key.clear() self._win._discovered.clear() self._win._channels.clear() self._win._device_info = DeviceInfo() self._win._direct_repeaters.clear() if self._win._signal_icon_timer_id: GLib.source_remove(self._win._signal_icon_timer_id) self._win._signal_icon_timer_id = None self._win._self_info.clear() self._win._msg_ctrl.cleanup_on_disconnect() self._win._logged_in_rooms.clear() if self._win._frame_handler._contacts_refresh_id: GLib.source_remove(self._win._frame_handler._contacts_refresh_id) self._win._frame_handler._contacts_refresh_id = None if self._win._frame_handler._contacts_refetch_id: GLib.source_remove(self._win._frame_handler._contacts_refetch_id) self._win._frame_handler._contacts_refetch_id = None self.contacts_iter_busy = False self.contacts_refetch_pending = False if self._win._contacts_view_inst is not None: self._win._contacts_view_inst.update_contacts([]) if self._win._channels_view_inst is not None: self._win._channels_view_inst.update_channels([]) self.syncing = False self.msg_syncing = False self.sync_msg_count = 0 self._win._companion_box.set_visible(False) if self._should_auto_reconnect(): self._schedule_reconnect() else: self.reconnect_attempts = 0 settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") settings.set_string("last-device-address", "") settings.set_string("last-device-name", "") self._win._connection_banner.set_title(_("Not connected")) self._win._connection_banner.set_button_label(_("Connect")) self._win._connection_banner.set_revealed(True) self._win._show_connection_screen() if not self._win.get_visible(): self._win.present() elif state == ConnectionState.SCANNING: self._win._connection_banner.set_title(_("Scanning...")) self._win._connection_banner.set_button_label(_("Stop")) elif state == ConnectionState.CONNECTING: self._win._connection_banner.set_title(_("Connecting...")) self._win._connection_banner.set_button_label("") self._start_connect_timeout() elif state == ConnectionState.CONNECTED: self._cancel_connect_timeout() addr = self.pending_device_address if addr: settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") settings.set_string("last-device-address", addr) settings.set_string("last-transport", self.pending_transport_type or "ble") self.pending_device_address = None self._win._connection_banner.set_title(_("Syncing...")) self._win._connection_banner.set_button_label("") self._win._connection_banner.set_revealed(True) transport_type = self.pending_transport_type or Gio.Settings.new( "page.codeberg.sesivany.Meshy" ).get_string("last-transport") self._win._device_view.update_connection_status(True, transport_type) self._win._settings_view.update_transport_type(transport_type) self._win._hide_connection_screen() self.on_connected() return False GLib.idle_add(_update_ui) def _start_connect_timeout(self): self._cancel_connect_timeout() def _on_timeout(): self.connect_timeout_id = None if self._win._transport and self._win._transport.state == ConnectionState.CONNECTING: log.info("Connection timed out after 25 seconds") self._win._transport.disconnect() return False self.connect_timeout_id = GLib.timeout_add(25000, _on_timeout) def _cancel_connect_timeout(self): if self.connect_timeout_id is not None: GLib.source_remove(self.connect_timeout_id) self.connect_timeout_id = None def _start_handshake_timeout(self): self._cancel_handshake_timeout() self.handshake_timeout_id = GLib.timeout_add( _HANDSHAKE_TIMEOUT_MS, self._on_handshake_timeout ) def _cancel_handshake_timeout(self): if self.handshake_timeout_id is not None: GLib.source_remove(self.handshake_timeout_id) self.handshake_timeout_id = None def _on_handshake_timeout(self): self.handshake_timeout_id = None if self.handshake_received: return False log.warning("Device did not respond to handshake, disconnecting") self.manual_disconnect = True self._win.show_toast(_("Device did not respond. It may not support this connection type.")) self._win._transport.disconnect() return False def on_handshake_received(self): if self.handshake_received: return self.handshake_received = True self._cancel_handshake_timeout() self.reconnect_attempts = 0 def on_connected(self): self.syncing = True self.handshake_received = False self.last_connected_address = self._win._transport.connected_address self._start_sync_timeout() self._start_handshake_timeout() device_addr = self._win._transport.connected_address if self._win._storage: self._win._storage.close() self._win._storage = Storage(device_address=device_addr) self._win._contacts = self._win._storage.get_contacts() self._win._contacts_by_key = {c.public_key_hex: c for c in self._win._contacts} self._win._discovered = self._win._storage.get_discovered_contacts() self._win._channels = self._win._storage.get_channels() self._win._contacts_view.update_contacts(self._win._contacts) self._win._channels_view.update_channels(self._win._channels) self._win._frame_handler.rebuild_key_store() self._win._frame_handler.restore_unread_counts() self._win._settings_view.load_regions() self._win._settings_view.restore_default_scope() self._win.send_frame(build_device_query()) GLib.timeout_add(50, lambda: self._win.send_frame(build_app_start()) or False) GLib.timeout_add(100, lambda: self._win.send_frame(build_get_device_time()) or False) GLib.timeout_add(150, lambda: self._win.send_frame(build_get_batt_and_storage()) or False) self.contacts_sync_retries = 0 GLib.timeout_add(200, self._send_contacts_with_timeout) GLib.timeout_add(250, self._win._apply_default_scope) self.batt_timer_id = GLib.timeout_add(120000, self.poll_battery) self.last_device_rx = time.monotonic() self.watchdog_timer_id = GLib.timeout_add(15000, self.connection_watchdog) def _start_sync_timeout(self): self._cancel_sync_timeout() self.sync_timeout_id = GLib.timeout_add(_SYNC_TIMEOUT_MS, self._on_sync_timeout) def _cancel_sync_timeout(self): if self.sync_timeout_id is not None: GLib.source_remove(self.sync_timeout_id) self.sync_timeout_id = None def _on_sync_timeout(self): self.sync_timeout_id = None if not self.syncing: return False log.warning("Sync timed out, disconnecting") if not self.handshake_received: self.manual_disconnect = True self._cancel_contacts_sync_timeout() self._cancel_channel_sync_timeout() self._win._transport.disconnect() return False def _send_contacts_with_timeout(self): self._win._frame_handler.send_get_contacts() self._start_contacts_sync_timeout() return False def _start_contacts_sync_timeout(self): self._cancel_contacts_sync_timeout() self.contacts_sync_timeout_id = GLib.timeout_add( _CONTACTS_SYNC_TIMEOUT_MS, self._on_contacts_sync_timeout ) def _cancel_contacts_sync_timeout(self): if self.contacts_sync_timeout_id is not None: GLib.source_remove(self.contacts_sync_timeout_id) self.contacts_sync_timeout_id = None def _on_contacts_sync_timeout(self): self.contacts_sync_timeout_id = None if not self.syncing: return False self.contacts_sync_retries += 1 if self.contacts_sync_retries < _CONTACTS_SYNC_MAX_RETRIES: log.warning( f"Contacts sync timed out, retry " f"{self.contacts_sync_retries}/{_CONTACTS_SYNC_MAX_RETRIES}" ) self.contacts_iter_busy = False self._send_contacts_with_timeout() else: log.warning("Contacts sync failed after retries, skipping to channels") self.contacts_iter_busy = False self.synced_contact_keys = None self.fetch_channels_sequential(0) return False def _should_auto_reconnect(self) -> bool: if self.manual_disconnect: self.manual_disconnect = False return False if self.reconnect_attempts >= _MAX_RECONNECT_ATTEMPTS: return False return self.last_connected_address def _schedule_reconnect(self): delay = _RECONNECT_DELAYS[min(self.reconnect_attempts, len(_RECONNECT_DELAYS) - 1)] attempt = self.reconnect_attempts + 1 log.info(f"Auto-reconnect attempt {attempt}/{_MAX_RECONNECT_ATTEMPTS} " f"in {delay}ms") self._win._connection_banner.set_title( _("Reconnecting ({current}/{total})...").format( current=attempt, total=_MAX_RECONNECT_ATTEMPTS ) ) self._win._connection_banner.set_button_label(_("Cancel")) self._win._connection_banner.set_revealed(True) self.reconnect_timer_id = GLib.timeout_add(delay, self._do_reconnect) def _do_reconnect(self): self.reconnect_timer_id = None self.reconnect_attempts += 1 addr = self.last_connected_address if addr and self._win._transport.state == ConnectionState.DISCONNECTED: log.info(f"Reconnecting to {addr}") self._win._transport.connect_by_address(addr) return False def cancel_reconnect(self): if self.reconnect_timer_id is not None: GLib.source_remove(self.reconnect_timer_id) self.reconnect_timer_id = None def poll_battery(self) -> bool: if self._win._transport.is_connected: self._win.send_frame(build_get_batt_and_storage()) if self._win._custom_vars.get("gps") == "1": self.gps_poll_pending = True GLib.timeout_add(50, lambda: self._win.send_frame(build_device_query()) or False) return self._win._transport.is_connected def connection_watchdog(self) -> bool: if not self._win._transport.is_connected: self.watchdog_timer_id = None return False silence = time.monotonic() - self.last_device_rx if silence > 180: log.warning(f"Connection watchdog: no data for {silence:.0f}s, disconnecting") self._win._transport.disconnect() return False return True def fetch_channels_sequential(self, index): self._cancel_channel_sync_timeout() max_ch = self._win._device_info.max_channels or 8 if self.syncing: self._win._connection_banner.set_title(_("Syncing channels") + f"\n{index}/{max_ch}") if index >= max_ch: self.channel_sync_index = -1 if self.syncing: self.syncing = False self._cancel_sync_timeout() self._win._connection_banner.set_revealed(False) self.msg_syncing = True self.sync_msg_count = 0 self._win.send_frame(build_get_auto_add_config()) self._win.send_frame(build_get_custom_vars()) self.msg_sync_retries = 0 self.send_sync_next_message() return self.channel_sync_index = index self.channel_sync_retries = 0 self._win.send_frame(build_get_channel(index)) self._start_channel_sync_timeout(index) def _start_channel_sync_timeout(self, index): self._cancel_channel_sync_timeout() self.channel_sync_timeout_id = GLib.timeout_add( _CHANNEL_SYNC_TIMEOUT_MS, self._on_channel_sync_timeout, index ) def _cancel_channel_sync_timeout(self): if self.channel_sync_timeout_id is not None: GLib.source_remove(self.channel_sync_timeout_id) self.channel_sync_timeout_id = None def _on_channel_sync_timeout(self, index): self.channel_sync_timeout_id = None if self.channel_sync_index != index: return False self.channel_sync_retries += 1 if self.channel_sync_retries < _CHANNEL_SYNC_MAX_RETRIES: log.warning( f"Channel {index} fetch timed out, retry " f"{self.channel_sync_retries}/{_CHANNEL_SYNC_MAX_RETRIES}" ) self._win.send_frame(build_get_channel(index)) self._start_channel_sync_timeout(index) else: log.warning( f"Channel {index} fetch failed after " f"{_CHANNEL_SYNC_MAX_RETRIES} retries, skipping" ) self.fetch_channels_sequential(index + 1) return False def send_sync_next_message(self): self._cancel_msg_sync_timeout() self._win.send_frame(build_sync_next_message()) self.msg_sync_timeout_id = GLib.timeout_add(_MSG_SYNC_TIMEOUT_MS, self._on_msg_sync_timeout) def _cancel_msg_sync_timeout(self): if self.msg_sync_timeout_id is not None: GLib.source_remove(self.msg_sync_timeout_id) self.msg_sync_timeout_id = None def _on_msg_sync_timeout(self): self.msg_sync_timeout_id = None if not self.msg_syncing: return False self.msg_sync_retries += 1 if self.msg_sync_retries < _MSG_SYNC_MAX_RETRIES: log.warning( f"Message sync timed out, retry " f"{self.msg_sync_retries}/{_MSG_SYNC_MAX_RETRIES}" ) self.send_sync_next_message() else: log.warning("Message sync failed after retries, finishing sync") self.finish_msg_sync() return False def finish_msg_sync(self): self._cancel_msg_sync_timeout() if not self.msg_syncing: return self.msg_syncing = False if self.sync_msg_count > 0: self._win._send_notification( "Meshy", _("You received {} message(s) while disconnected.").format(self.sync_msg_count), "sync-summary", ) self.sync_msg_count = 0 def send_get_contacts(self, incremental: bool = False): self._win._frame_handler.send_get_contacts(incremental=incremental) meshy/src/frame_handler.py000066400000000000000000001553221521052255700161470ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Protocol frame handling extracted from MeshyWindow.""" import logging import struct import time from datetime import datetime from gi.repository import Gio, GLib from meshy.models import ( Channel, ChannelMessage, Contact, ContactType, Message, MessageStatus, NotificationLevel, ) from meshy.protocol import ( PUSH_CODE_ADVERT, PUSH_CODE_LOG_DATA, PUSH_CODE_LOGIN_FAILED, PUSH_CODE_LOGIN_SUCCESS, PUSH_CODE_MSG_WAITING, PUSH_CODE_NEW_ADVERT, PUSH_CODE_PATH_DISCOVERY_RESPONSE, PUSH_CODE_PATH_UPDATED, PUSH_CODE_SEND_CONFIRMED, PUSH_CODE_STATUS_RESPONSE, PUSH_CODE_TELEMETRY_RESPONSE, PUSH_CODE_TRACE_DATA, RESP_CODE_ADVERT_PATH, RESP_CODE_AUTO_ADD_CONFIG, RESP_CODE_BATT_AND_STORAGE, RESP_CODE_BINARY_RESPONSE, RESP_CODE_CHANNEL_INFO, RESP_CODE_CHANNEL_MSG_RECV, RESP_CODE_CHANNEL_MSG_RECV_V3, RESP_CODE_CONTACT, RESP_CODE_CONTACT_MSG_RECV, RESP_CODE_CONTACT_MSG_RECV_V3, RESP_CODE_CONTACTS_START, RESP_CODE_CONTROL_DATA, RESP_CODE_CURR_TIME, RESP_CODE_CUSTOM_VARS, RESP_CODE_DEVICE_INFO, RESP_CODE_END_OF_CONTACTS, RESP_CODE_ERR, RESP_CODE_EXPORT_CONTACT, RESP_CODE_NO_MORE_MESSAGES, RESP_CODE_OK, RESP_CODE_SELF_INFO, RESP_CODE_SENT, RESP_CODE_STATS, STATS_TYPE_RADIO, build_get_contacts, build_set_device_time, identify_response, parse_advert_path_response, parse_auto_add_config, parse_batt_and_storage, parse_channel_info, parse_channel_message, parse_contact_frame, parse_contact_message, parse_curr_time, parse_custom_vars, parse_device_info, parse_discover_response, parse_error_response, parse_neighbors_response, parse_push_send_confirmed, parse_self_info, parse_sent_response, parse_stats_response, parse_status_response, parse_trace_response, ) log = logging.getLogger(__name__) class FrameHandler: """Handles incoming protocol frames from the companion device.""" def __init__(self, win): self._win = win self._contacts_refresh_id = None self._contacts_refetch_id = None self._pending_channel_snr = {} self._ok_callbacks = [] def handle_frame(self, data: bytes): """Process a received protocol frame on the main thread.""" code = identify_response(data) if code is None: return False # Log all responses when login callback is active if self._win._login_callback: log.info( f"RX during login wait: code=0x{code:02x} ({code}) len={len(data)} hex={data.hex()[:60]}..." ) log.debug(f"RX code={code} len={len(data)} hex={data.hex()}") if code == RESP_CODE_OK: if self._ok_callbacks: on_ok, _on_error, timeout_id = self._ok_callbacks.pop(0) GLib.source_remove(timeout_id) on_ok() return False self._win._ignore_next_err = False return False if code == RESP_CODE_ERR: err = parse_error_response(data) if self._ok_callbacks: _on_ok, on_error, timeout_id = self._ok_callbacks.pop(0) GLib.source_remove(timeout_id) on_error(err) return False if self._win._ignore_next_err: self._win._ignore_next_err = False if err: log.debug( f'Suppressed device error: {err["message"]} (code=0x{err["code"]:02x})' ) return False if err: log.warning(f'Device error: {err["message"]} (code=0x{err["code"]:02x})') if not self._win._region_callbacks: self._win.show_toast(_("Device error: {}").format(err["message"])) if self._win._msg_ctrl.sent_msg_queue: front = self._win._msg_ctrl.sent_msg_queue[0] if front is None or (isinstance(front, dict) and "type" in front): self._win._msg_ctrl.sent_msg_queue.pop(0) return False if code == RESP_CODE_SELF_INFO: info = parse_self_info(data) if self._win._conn_ctrl.gps_poll_pending: self._win._conn_ctrl.gps_poll_pending = False if info: lat = info.get("adv_lat", 0.0) lon = info.get("adv_lon", 0.0) if abs(lat) > 1e-6 or abs(lon) > 1e-6: self._win._device_info.latitude = lat self._win._device_info.longitude = lon self._win._device_view.update_location(lat, lon) if self._win._map_view_inst is not None: self._win._map_view.update_contacts(self._win._contacts) return False self._win._conn_ctrl.on_handshake_received() if info: self._win._self_info = info self._win._device_info.public_key = info.get("public_key", "") self._win._device_info.name = info.get("name", "") self._win._device_info.tx_power = info.get("tx_power", 0) self._win._device_info.max_tx_power = info.get("max_tx_power", 0) self._win._device_info.radio_freq = info.get("radio_freq", 0) self._win._device_info.radio_bw = info.get("radio_bw", 0) self._win._device_info.radio_sf = info.get("radio_sf", 0) raw_cr = info.get("radio_cr", 0) if 1 <= raw_cr <= 4: self._win._device_info.cr_old_encoding = True self._win._device_info.radio_cr = raw_cr + 4 else: self._win._device_info.cr_old_encoding = False self._win._device_info.radio_cr = raw_cr lat = info.get("adv_lat", 0.0) lon = info.get("adv_lon", 0.0) if abs(lat) > 1e-6 or abs(lon) > 1e-6: self._win._device_info.latitude = lat self._win._device_info.longitude = lon self._win._device_view.update_device_info(self._win._device_info) self._win._device_view.update_location( info.get("adv_lat", 0.0), info.get("adv_lon", 0.0) ) if self._win._map_view_inst is not None: self._win._map_view.update_contacts(self._win._contacts) self._win._settings_view.update_device_info(self._win._device_info) self._win._settings_view.update_advert_location( info.get("adv_lat", 0.0), info.get("adv_lon", 0.0), info.get("adv_loc_policy", 0), ) self._win._settings_view.update_telemetry_modes( info.get("telemetry_mode_base", 0), info.get("telemetry_mode_loc", 0), info.get("telemetry_mode_env", 0), ) self._win._device_info.multi_acks = info.get("multi_acks", 0) self._win._settings_view.update_multi_acks(info.get("multi_acks", 0)) self._win._update_companion_label() self._win._save_tcp_companion_if_needed() elif code == RESP_CODE_CUSTOM_VARS: self._win._custom_vars = parse_custom_vars(data) self._win._settings_view.update_gps_settings(self._win._custom_vars) self._win._device_view.update_gps_enabled(self._win._custom_vars.get("gps") == "1") elif code == RESP_CODE_DEVICE_INFO: info = parse_device_info(data) if info: for key in ( "model", "ver", "fw_build", "fw_ver", "max_contacts", "max_channels", "path_hash_mode", "repeat", "ble_pin", ): if key in info: setattr(self._win._device_info, key, info[key]) self._win._device_view.update_device_info(self._win._device_info) self._win._settings_view.update_device_info(self._win._device_info) elif code == RESP_CODE_BATT_AND_STORAGE: info = parse_batt_and_storage(data) if info: self._win._device_info.battery_mv = info.get("level", 0) self._win._device_info.storage_used_kb = info.get("used_kb", 0) self._win._device_info.storage_total_kb = info.get("total_kb", 0) self._win._device_view.update_device_info(self._win._device_info) self._win._update_companion_label() elif code == RESP_CODE_STATS: stats = parse_stats_response(data) if stats: sub = stats["sub_type"] self._win._device_stats[sub] = stats self._win._device_view.update_stats(self._win._device_stats) if sub == STATS_TYPE_RADIO: total = stats.get("tx_air_secs", 0) + stats.get("rx_air_secs", 0) if total > self._win._msg_ctrl.prev_total_air_secs: self._win._msg_ctrl.last_airtime_bump = time.monotonic() self._win._msg_ctrl.prev_total_air_secs = total elif code == RESP_CODE_CONTACTS_START: self._win._conn_ctrl.contacts_iter_busy = True self._win._storage._batch_depth += 1 self._win._conn_ctrl._start_contacts_sync_timeout() self._win._conn_ctrl.sync_contact_total = ( int.from_bytes(data[1:3], "little") if len(data) >= 3 else 0 ) self._win._conn_ctrl.sync_contact_current = 0 self._win._conn_ctrl.synced_contact_keys = set() if self._win._conn_ctrl.syncing: self._win._connection_banner.set_title( _("Syncing contacts") + f"\n{0}/{self._win._conn_ctrl.sync_contact_total}" ) elif code == RESP_CODE_CONTACT: self._win._conn_ctrl._start_contacts_sync_timeout() contact = parse_contact_frame(data) if contact: self.add_or_update_contact(contact) c = self.dict_to_contact(contact) if c: self._win._conn_ctrl.synced_contact_keys.add(c.public_key_hex) else: log.warning(f"Failed to parse contact frame (raw {len(data)} bytes), skipping") self._win._conn_ctrl.sync_contact_current += 1 if self._win._conn_ctrl.syncing: self._win._connection_banner.set_title( _("Syncing contacts") + f"\n{self._win._conn_ctrl.sync_contact_current}/{self._win._conn_ctrl.sync_contact_total}" ) elif code == RESP_CODE_END_OF_CONTACTS: self._win._conn_ctrl.contacts_iter_busy = False self._win._conn_ctrl._cancel_contacts_sync_timeout() if self._win._storage._batch_depth > 0: self._win._storage._batch_depth -= 1 if self._win._storage._batch_depth == 0: self._win._storage._db.commit() # Remove contacts no longer on the device (full sync only) if ( self._win._conn_ctrl.synced_contact_keys and not self._win._conn_ctrl.incremental_sync ): synced = self._win._conn_ctrl.synced_contact_keys stale_keys = set() for c in self._win._contacts: if c.public_key_hex not in synced: stale_keys.add(c.public_key_hex) self._win._contacts_by_key.pop(c.public_key_hex, None) self._win._storage.deactivate_contact(c.public_key_hex) self._win._contacts = [ c for c in self._win._contacts if c.public_key_hex not in stale_keys ] self._win._conn_ctrl.synced_contact_keys = None self._win._conn_ctrl.incremental_sync = False self._win._contacts_view.update_contacts(self._win._contacts) if self._win._map_view_inst is not None: self._win._map_view.update_contacts(self._win._contacts) self._win._refresh_chat_header() # Trigger deferred refetch if one was requested during iteration if self._win._conn_ctrl.contacts_refetch_pending: self._win._conn_ctrl.contacts_refetch_pending = False self.schedule_contacts_refetch() # Now safe to fetch channels sequentially self._win._fetch_channels_sequential(0) elif code in (RESP_CODE_CONTACT_MSG_RECV, RESP_CODE_CONTACT_MSG_RECV_V3): self._win._msg_ctrl.last_rx_time = time.monotonic() msg_data = parse_contact_message(data) if msg_data: self.handle_incoming_message(msg_data) self._win._conn_ctrl.msg_sync_retries = 0 self._win._send_sync_next_message() elif code in (RESP_CODE_CHANNEL_MSG_RECV, RESP_CODE_CHANNEL_MSG_RECV_V3): self._win._msg_ctrl.last_rx_time = time.monotonic() msg_data = parse_channel_message(data) if msg_data: self.handle_incoming_channel_message(msg_data) self._win._conn_ctrl.msg_sync_retries = 0 self._win._send_sync_next_message() elif code == RESP_CODE_NO_MORE_MESSAGES: self._win._finish_msg_sync() elif code == RESP_CODE_SENT: result = parse_sent_response(data) if result and self._win._msg_ctrl.sent_msg_queue: entry = self._win._msg_ctrl.sent_msg_queue.pop(0) if isinstance(entry, dict) and "type" in entry: if "expected_ack" in result: tag_hex = result["expected_ack"].hex() self._win._pending_binary_requests[tag_hex] = entry req_type = entry["type"] if req_type == "neighbors": self._win._neighbors_callback = None elif req_type == "acl": self._win._acl_callback = None elif req_type == "telemetry": self._win._telemetry_callback = None elif entry is None: # Non-message send (channel, CLI, advert, trace, etc.) if self._win._trace_callback: timeout_ms = result.get("suggested_timeout", 15000) if self._win._trace_timeout_callback: self._win._trace_timeout_callback(timeout_ms) elif isinstance(entry, str) and entry.startswith("region:"): # Region discovery request — map ack tag to callback callback_key = entry[7:] # strip 'region:' prefix if "expected_ack" in result and callback_key in self._win._region_callbacks: ack_hex = result["expected_ack"].hex() self._win._region_callbacks[ack_hex] = self._win._region_callbacks.pop( callback_key ) elif "expected_ack" in result: msg_id = entry ack_key = result["expected_ack"].hex() if msg_id not in self._win._msg_ctrl.retry_context: # Already delivered by an earlier attempt's ACK — don't # overwrite DELIVERED status with SENT from this late # RESP_CODE_SENT response. pass else: if ack_key != "00000000": self._win._msg_ctrl.pending_acks[ack_key] = msg_id is_flood = result.get("type", 0) == 1 detail = self._win._msg_ctrl.send_detail(msg_id, is_flood) self._win._storage.update_message_status(msg_id, MessageStatus.SENT) self._win._chat_view.update_message_status( msg_id, MessageStatus.SENT, detail ) est_timeout = result.get("suggested_timeout", 30000) timeout_id = GLib.timeout_add( int(est_timeout * 1.5), self._win._msg_ctrl._on_message_timeout, msg_id, ack_key, ) self._win._msg_ctrl.pending_timeouts[msg_id] = timeout_id elif result and self._win._trace_callback: # Trace sent confirmation with empty queue (fallback) timeout_ms = result.get("suggested_timeout", 15000) if self._win._trace_timeout_callback: self._win._trace_timeout_callback(timeout_ms) elif code == PUSH_CODE_SEND_CONFIRMED: result = parse_push_send_confirmed(data) if result and "code" in result: ack_key = result["code"] if ack_key in self._win._msg_ctrl.pending_acks: msg_id = self._win._msg_ctrl.pending_acks.pop(ack_key) # Remove remaining ack_keys for this message for k in [ k for k, v in self._win._msg_ctrl.pending_acks.items() if v == msg_id ]: self._win._msg_ctrl.pending_acks.pop(k, None) if msg_id in self._win._msg_ctrl.pending_timeouts: GLib.source_remove(self._win._msg_ctrl.pending_timeouts.pop(msg_id)) self._win._msg_ctrl.retry_context.pop(msg_id, None) trip_ms = result.get("trip_time_ms", 0) detail = ( f"{trip_ms / 1000:.1f}s" if trip_ms and trip_ms >= 1000 else f"{trip_ms}ms" if trip_ms else "" ) self._win._storage.update_message_status(msg_id, MessageStatus.DELIVERED) self._win._chat_view.update_message_status( msg_id, MessageStatus.DELIVERED, detail ) # Re-fetch contacts soon to pick up the newly discovered path self.schedule_contacts_refetch() elif ack_key in self._win._msg_ctrl.grace_acks: # Late ACK arrived during grace period — upgrade from failed to delivered msg_id = self._win._msg_ctrl.grace_acks.pop(ack_key) # Remove remaining ack_keys for this message for k in [k for k, v in self._win._msg_ctrl.grace_acks.items() if v == msg_id]: self._win._msg_ctrl.grace_acks.pop(k, None) if msg_id in self._win._msg_ctrl.grace_timeouts: GLib.source_remove(self._win._msg_ctrl.grace_timeouts.pop(msg_id)) trip_ms = result.get("trip_time_ms", 0) detail = ( _("{}s (late)").format(f"{trip_ms / 1000:.1f}") if trip_ms and trip_ms >= 1000 else _("{}ms (late)").format(trip_ms) if trip_ms else _("late") ) log.info(f"Late ACK received for message {msg_id[:8]}") self._win._storage.update_message_status(msg_id, MessageStatus.DELIVERED) self._win._chat_view.update_message_status( msg_id, MessageStatus.DELIVERED, detail ) self.schedule_contacts_refetch() elif code == PUSH_CODE_MSG_WAITING: self._win._conn_ctrl.msg_sync_retries = 0 self._win._send_sync_next_message() elif code in (PUSH_CODE_ADVERT, PUSH_CODE_NEW_ADVERT): self._win._msg_ctrl.last_rx_time = time.monotonic() contact_dict = parse_contact_frame(data) if contact_dict: contact = self.dict_to_contact(contact_dict) # Notify live advert listener if active if self._win._advert_listener: self._win._advert_listener(contact) # If already a known contact, update it if self._win.is_contact_known(contact.public_key_hex): self.add_or_update_contact(contact_dict) self._win._contacts_view.update_contacts(self._win._contacts) # Always add/update in discovered list (dual-write) self.add_or_update_discovered(contact) elif code == PUSH_CODE_PATH_UPDATED: # Re-fetch contacts to get updated paths (debounced) self.schedule_contacts_refetch() elif code == RESP_CODE_CHANNEL_INFO: ch_info = parse_channel_info(data) if ch_info: channel = Channel( index=ch_info["channel_idx"], name=ch_info.get("channel_name", ""), psk=ch_info.get("channel_secret", bytes(16)), ) self.add_or_update_channel(channel) next_idx = ch_info["channel_idx"] + 1 else: log.warning(f"Failed to parse channel info (raw {len(data)} bytes), skipping") next_idx = ( self._win._conn_ctrl.channel_sync_index + 1 if self._win._conn_ctrl.channel_sync_index >= 0 else 0 ) self._win._fetch_channels_sequential(next_idx) elif code == RESP_CODE_CURR_TIME: device_time = parse_curr_time(data) if device_time is not None: now = int(datetime.now().timestamp()) drift = now - device_time # Auto-sync time on every connection — the official app does this too. # A correct RTC is critical for repeater login (anti-replay protection). # Firmware rejects setting time backwards, so ERR on device-ahead is # expected and harmless — suppress it via _ignore_next_err flag. log.info( f"Device clock: {datetime.fromtimestamp(device_time)} " f"(drift {drift}s), sending time sync" ) self._win._ignore_next_err = True self._win.send_frame(build_set_device_time()) if abs(drift) > 300: mins = abs(drift) // 60 if drift > 0: self._win.show_toast( _("Device clock was {}min behind — synced").format(mins) ) else: self._win.show_toast( _("Device clock is {}min ahead of system").format(mins) ) elif code == RESP_CODE_AUTO_ADD_CONFIG: config = parse_auto_add_config(data) if config: self._win._settings_view.update_auto_add_config(config) elif code == RESP_CODE_EXPORT_CONTACT: if getattr(self._win, "_pending_qr_callback", None): callback = self._win._pending_qr_callback self._win._pending_qr_callback = None callback(data) elif self._win._pending_clipboard_export: self._win._pending_clipboard_export = False clipboard = self._win.get_clipboard() clipboard.set("meshcore://" + data[1:].hex()) # Persist clipboard content so it survives focus loss (Wayland) clipboard.store_async(None, None) elif code == RESP_CODE_BINARY_RESPONSE: # 0x8C format: [code(1)][reserved(1)][tag(4)][payload...] handled = False if len(data) >= 6: tag_hex = data[2:6].hex() region_cb = self._win._region_callbacks.pop(tag_hex, None) if region_cb: region_text = data[6:].decode("utf-8", "ignore").strip("\x00") region_cb(region_text) handled = True elif tag_hex in self._win._pending_binary_requests: req = self._win._pending_binary_requests.pop(tag_hex) payload = data[6:] if len(data) > 6 else b"" if req["type"] == "neighbors": result = parse_neighbors_response(data) req["callback"](result) elif req["type"] == "acl": req["callback"](data) elif req["type"] == "telemetry": req["callback"](payload) handled = True if not handled: if self._win._neighbors_callback: result = parse_neighbors_response(data) callback = self._win._neighbors_callback self._win._neighbors_callback = None callback(result) elif self._win._acl_callback: callback = self._win._acl_callback self._win._acl_callback = None callback(data) elif self._win._telemetry_callback: callback = self._win._telemetry_callback self._win._telemetry_callback = None lpp_data = data[6:] if len(data) > 6 else b"" callback(lpp_data) elif code == PUSH_CODE_TELEMETRY_RESPONSE: # 0x8B format: [code(1)][reserved(1)][pubkey_prefix(6)][lpp_data...] if self._win._telemetry_callback: callback = self._win._telemetry_callback self._win._telemetry_callback = None lpp_data = data[8:] if len(data) > 8 else b"" callback(lpp_data) elif code == RESP_CODE_CONTROL_DATA: result = parse_discover_response(data) if result and self._win._discover_callback: self._win._discover_callback(result) elif code == RESP_CODE_ADVERT_PATH: result = parse_advert_path_response(data) if result and self._win._advert_path_callback: callback = self._win._advert_path_callback self._win._advert_path_callback = None callback(result) elif code == PUSH_CODE_PATH_DISCOVERY_RESPONSE: if self._win._path_discovery_callback: self._win._path_discovery_callback(data) elif code == PUSH_CODE_LOG_DATA: self.handle_log_data(data) elif code == PUSH_CODE_TRACE_DATA: result = parse_trace_response(data) if result and self._win._trace_callback: self._win._trace_callback(result) elif code == PUSH_CODE_LOGIN_SUCCESS: log.info(f"Received LOGIN_SUCCESS response, len={len(data)}, hex={data.hex()}") # Response format: [code][perms][prefix×6][timestamp×4][...] — 14 bytes if len(data) < 8: log.warning(f"LOGIN_SUCCESS too short: {len(data)} bytes, expected >= 8") return False perms = data[1] is_admin = (perms & 1) == 1 log.info(f"Login permissions: 0x{perms:02x}, is_admin={is_admin}") response_prefix = data[2:8] # Extract repeater clock timestamp if present (bytes 8-11, LE uint32) login_timestamp = None if len(data) >= 12: login_timestamp = struct.unpack_from("= 8") return False response_prefix = data[2:8] log.info( f'Response prefix: {response_prefix.hex()}, expected: {self._win._login_target_prefix.hex() if self._win._login_target_prefix else "none"}' ) if self._win._login_callback: if ( self._win._login_target_prefix and response_prefix == self._win._login_target_prefix ): log.info("Prefix matches! Login failed (wrong password).") cb = self._win._login_callback self._win.stop_login() cb(False, None, False) else: log.warning( f'Prefix mismatch: got {response_prefix.hex()}, expected {self._win._login_target_prefix.hex() if self._win._login_target_prefix else "none"}' ) else: log.warning("LOGIN_FAILED received but no callback registered") return False # Don't repeat idle callback def expect_ok(self, on_ok, on_error, timeout_ms=5000): """Register a callback for the next OK/ERROR response.""" timeout_id = GLib.timeout_add(timeout_ms, self._on_expect_timeout) self._ok_callbacks.append((on_ok, on_error, timeout_id)) def _on_expect_timeout(self): if self._ok_callbacks: _on_ok, on_error, _tid = self._ok_callbacks.pop(0) on_error({"message": "timeout", "code": 0}) return False def dict_to_contact(self, c: dict) -> Contact | None: """Convert meshcore contact dict to Contact model.""" try: path_length = c.get("out_path_len", -1) return Contact( public_key=bytes.fromhex(c["public_key"]), name=c.get("adv_name", "Unknown"), type=c.get("type", 1), flags=c.get("flags", 0), path_length=path_length, path=bytes.fromhex(c.get("out_path", "")), path_hash_size=c.get("path_hash_size", 1), latitude=c.get("adv_lat"), longitude=c.get("adv_lon"), last_seen=datetime.fromtimestamp(c["last_advert"]) if c.get("last_advert") else None, device_lastmod=datetime.fromtimestamp(c["lastmod"]) if c.get("lastmod") else None, is_active=True, ) except (ValueError, KeyError) as e: log.warning(f"Invalid contact data: {e}") return None def add_or_update_contact(self, contact_dict: dict): """Add or update a contact from a meshcore contact dict.""" contact = self.dict_to_contact(contact_dict) if contact is None: return key = contact.public_key_hex if key in self._win._contacts_by_key: # Update in-place in the list for i, c in enumerate(self._win._contacts): if c.public_key_hex == key: self._win._contacts[i] = contact break else: self._win._contacts.append(contact) self._win._contacts_by_key[key] = contact self._win._storage.save_contact(contact) self.schedule_contacts_refresh() def schedule_contacts_refresh(self): """Debounce contact list UI updates — refresh once after a burst of changes.""" if self._contacts_refresh_id: GLib.source_remove(self._contacts_refresh_id) self._contacts_refresh_id = GLib.timeout_add(200, self.do_contacts_refresh) def do_contacts_refresh(self): self._contacts_refresh_id = None self._win._contacts_view.update_contacts(self._win._contacts) if self._win._map_view_inst is not None: self._win._map_view.update_contacts(self._win._contacts) self._win._refresh_chat_header() return False def schedule_contacts_refetch(self): """Debounced re-fetch of contacts from device. Coalesces multiple rapid requests (e.g. SEND_CONFIRMED + PATH_UPDATED) into a single GET_CONTACTS call after a short delay. """ if self._contacts_refetch_id: GLib.source_remove(self._contacts_refetch_id) self._contacts_refetch_id = GLib.timeout_add(300, self.do_contacts_refetch) def do_contacts_refetch(self): self._contacts_refetch_id = None self.send_get_contacts(incremental=True) return False def send_get_contacts(self, incremental: bool = False): """Send CMD_GET_CONTACTS, deferring if the firmware iterator is busy.""" if self._win._conn_ctrl.contacts_iter_busy: self._win._conn_ctrl.contacts_refetch_pending = True return False self._win._conn_ctrl.contacts_iter_busy = True since = None if incremental and self._win._storage: since = self._win._storage.get_max_device_lastmod() self._win._conn_ctrl.incremental_sync = incremental and since is not None self._win.send_frame(build_get_contacts(since=since)) return False def add_or_update_discovered(self, contact: Contact): """Add or update a contact in the discovered list (memory + SQLite).""" for i, c in enumerate(self._win._discovered): if c.public_key_hex == contact.public_key_hex: self._win._discovered[i] = contact if self._win._storage: self._win._storage.save_discovered_contact(contact) return self._win._discovered.append(contact) if self._win._storage: self._win._storage.save_discovered_contact(contact) def add_or_update_channel(self, channel: Channel): """Add or update a channel, preserving local settings.""" for i, c in enumerate(self._win._channels): if c.index == channel.index: # Preserve local settings not stored on device channel.unread_count = c.unread_count channel.notification_level = c.notification_level channel.region = c.region self._win._channels[i] = channel self._win._storage.save_channel(channel) self._win._channels_view.update_channels(self._win._channels) self.rebuild_key_store() return # New channel - check if we have stored settings from a previous session stored = self._win._storage.get_channel(channel.index) if stored: channel.notification_level = stored.notification_level channel.region = stored.region self._win._channels.append(channel) self._win._storage.save_channel(channel) self._win._channels_view.update_channels(self._win._channels) self.rebuild_key_store() def is_contact_chat_visible(self, contact: Contact) -> bool: """Check if the chat for this contact is currently visible.""" if not self._win._chat_view_inst: return False return ( self._win._current_view == "contacts" and self._win._chat_view_inst._contact is not None and self._win._chat_view_inst._contact.public_key_hex == contact.public_key_hex and self._win.is_active() ) def is_channel_chat_visible(self, channel_index: int) -> bool: """Check if the chat for this channel is currently visible.""" if not self._win._channels_view_inst: return False chat = self._win._channels_view_inst.get_chat_widget() return ( self._win._current_view == "channels" and chat._current_channel is not None and chat._current_channel.index == channel_index and self._win.is_active() ) def handle_incoming_message(self, msg_data: dict): """Handle an incoming direct message.""" sender_prefix = msg_data.get("pubkey_prefix", "") try: contact = self._win._storage.find_contact_by_prefix(bytes.fromhex(sender_prefix)) except ValueError: log.warning(f"Invalid sender prefix: {sender_prefix}") return if not contact: log.warning(f"Message from unknown sender: {sender_prefix}") return is_cli = msg_data.get("txt_type", 0) == 1 sender_ts = min(msg_data["sender_timestamp"], time.time()) text = msg_data["text"] room_sender_name = "" # Room messages: first 4 bytes of text are the sender's pub key prefix if contact.type == ContactType.ROOM and len(text) >= 4 and not is_cli: sender_bytes = text[:4].encode("latin-1") if isinstance(text, str) else text[:4] text = text[4:] # Try to resolve sender name from contacts for c in self._win._contacts: if c.public_key[:4] == sender_bytes: room_sender_name = c.name break if not room_sender_name: room_sender_name = sender_bytes.hex() # Deduplicate: same sender + text within ±5s of the timestamp if self._win._storage.has_duplicate_message(contact.public_key_hex, sender_ts, text): log.debug(f"Dropping duplicate message from {contact.name}") return message = Message( sender_key=contact.public_key, text=text, timestamp=datetime.fromtimestamp(sender_ts), is_outgoing=False, is_cli=is_cli, status=MessageStatus.DELIVERED, room_sender_name=room_sender_name, ) # Route prefixed CLI responses to their registered callback if ( is_cli and len(text) > 3 and text[2] == "|" and text[:3] in self._win._cli_pending_prefixes ): prefix = text[:3] cb = self._win._cli_pending_prefixes.pop(prefix) cb(text[3:]) return # Route CLI responses to management dialog if open if is_cli and self._win._cli_response_callback: self._win._cli_response_callback(contact, message) return # Repeaters and sensors don't have a chat view – silently drop # any non-CLI messages so no phantom unread badge can appear. if contact.type in (ContactType.REPEATER, ContactType.SENSOR): log.debug(f"Dropping non-CLI message from {contact.type.name} {contact.name}") return self._win._storage.save_message(contact.public_key_hex, message) self._win._chat_view.on_message_received(contact, message) if not self.is_contact_chat_visible(contact): # Initialize last_read_at if not yet set so the divider has a reference if self._win._storage.get_contact_last_read_at(contact.public_key_hex) is None: self._win._storage.set_contact_last_read_at( contact.public_key_hex, message.timestamp.timestamp() - 1, ) self._win._contacts_view.mark_unread(contact.public_key_hex, True) if self._win._conn_ctrl.msg_syncing: self._win._conn_ctrl.sync_msg_count += 1 else: self._win._send_notification( contact.name, message.text, f"contact-{contact.public_key_hex}", ) def handle_incoming_channel_message(self, msg_data: dict): """Handle an incoming channel message.""" my_name = self._win._self_info.get("name", "") sender_name = msg_data.get("sender_name", "") msg_text = msg_data.get("msg_text", msg_data.get("text", "")) raw_sender_ts = msg_data["sender_timestamp"] sender_ts = min(raw_sender_ts, time.time()) channel_idx = msg_data["channel_idx"] # Detect echo/repeat of our own outgoing message if my_name and sender_name == my_name: snr = msg_data.get("SNR") path_len = msg_data.get("path_len", -1) log.info( f"Echo detected: ch={channel_idx} ts={sender_ts} " f'text="{msg_text[:30]}" snr={snr} path_len={path_len}' ) path_hops = [] path_bytes_hex = msg_data.get("path_bytes", "") path_len_val = msg_data.get("path_len", 0) if path_bytes_hex and path_len_val > 0: hash_size = len(path_bytes_hex) // (2 * path_len_val) if path_len_val else 1 chars = hash_size * 2 path_hops = [ path_bytes_hex[i : i + chars].upper() for i in range(0, len(path_bytes_hex), chars) ] self._win._channels_view.on_repeat_received( channel_idx, sender_ts, msg_text, path_hops, ) return channel_msg = ChannelMessage( channel_index=channel_idx, sender_name=sender_name, text=msg_text, timestamp=datetime.fromtimestamp(sender_ts), ) # Cache SNR for LOG_DATA to pick up (use raw timestamp to match # the key used by LOG_DATA before clamping) snr = msg_data.get("SNR") if snr is not None: cache_key = (channel_idx, raw_sender_ts, msg_text) self._pending_channel_snr[cache_key] = snr # Check if this is a duplicate (same message via different repeater) is_dup = self._win._channels_view.on_path_variant_received( channel_msg, ) if is_dup: return # Don't save or notify for duplicate copies self._win._storage.save_channel_message(channel_msg) self._win._channels_view.on_channel_message_received(channel_msg) # Find the channel for notification settings channel = None for ch in self._win._channels: if ch.index == channel_msg.channel_index: channel = ch break if not self.is_channel_chat_visible(channel_msg.channel_index): # Initialize last_read_at if not yet set so the divider has a reference if self._win._storage.get_channel_last_read_at(channel_msg.channel_index) is None: self._win._storage.set_channel_last_read_at( channel_msg.channel_index, channel_msg.timestamp.timestamp() - 1, ) self._win._channels_view.mark_unread(channel_msg.channel_index, True) # Send notification based on channel settings if channel: level = channel.notification_level if level == NotificationLevel.DEFAULT: settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") level = NotificationLevel( settings.get_int("default-channel-notification-level") ) should_notify = False if level == NotificationLevel.ALL: should_notify = True elif level == NotificationLevel.MENTIONS: if my_name and my_name.lower() in channel_msg.text.lower(): should_notify = True if should_notify: if self._win._conn_ctrl.msg_syncing: self._win._conn_ctrl.sync_msg_count += 1 else: ch_name = channel.name or _("Channel {}").format(channel.index) sender = channel_msg.sender_name or _("Someone") self._win._send_notification( ch_name, _("{sender}: {text}").format(sender=sender, text=channel_msg.text), f"channel-{channel_msg.channel_index}", ) def restore_unread_counts(self): """Restore unread message counts from storage on startup.""" if not self._win._storage: return for contact in self._win._contacts: # Repeaters and sensors have no chat view – skip them. if contact.type in (ContactType.REPEATER, ContactType.SENSOR): continue count = self._win._storage.count_unread_messages(contact.public_key_hex) if count > 0: if self._win._storage.get_contact_last_read_at(contact.public_key_hex) is None: oldest = self._win._storage.get_oldest_unread_contact_timestamp( contact.public_key_hex ) ts = (oldest - 1) if oldest is not None else (datetime.now().timestamp() - 1) self._win._storage.set_contact_last_read_at(contact.public_key_hex, ts) self._win._contacts_view._unread_counts[contact.public_key_hex] = count self._win._contacts_view._unread_keys.add(contact.public_key_hex) # Refresh to show badges self._win._contacts_view.update_contacts(self._win._contacts) for channel in self._win._channels: if channel.is_empty: continue count = self._win._storage.count_unread_channel_messages(channel.index) if count > 0: # Initialize last_read_at if not set, so the unread divider # is placed correctly when the user opens the channel. if self._win._storage.get_channel_last_read_at(channel.index) is None: oldest = self._win._storage.get_oldest_unread_channel_timestamp(channel.index) ts = (oldest - 1) if oldest is not None else (datetime.now().timestamp() - 1) self._win._storage.set_channel_last_read_at(channel.index, ts) self._win._channels_view._unread_counts[channel.index] = count self._win._channels_view._unread_indices.add(channel.index) # Refresh to show badges self._win._channels_view.update_channels(self._win._channels) def rebuild_key_store(self): """Rebuild the channel hash->PSK map from current channels.""" from meshy.mesh_crypto import channel_hash self._win._channel_hash_map = {} self._win._channel_psk_map = {} # hash -> psk bytes for ch in self._win._channels: if ch.is_empty: continue h = channel_hash(ch.psk_hex) self._win._channel_hash_map[h] = ch.index self._win._channel_psk_map[h] = ch.psk def handle_log_data(self, data: bytes): """Handle LOG_DATA (0x88) push - raw radio packet with path bytes.""" from meshy.mesh_crypto import ( PAYLOAD_ADVERT, PAYLOAD_GROUP_TEXT, decode_log_packet, decrypt_group_text, ) from meshy.models import RxLogEntry if len(data) < 4: return snr = struct.unpack("b", data[1:2])[0] / 4.0 rssi = struct.unpack("b", data[2:3])[0] mesh_data = data[3:] try: result = decode_log_packet(mesh_data) except Exception as e: log.warning(f"LOG_DATA decode failed: {e}") return if result: entry = RxLogEntry( timestamp=datetime.now(), snr=snr, rssi=rssi, payload_type=result["payload_type"], route_type=result.get("route_type", 0), path=result.get("path", []), path_hash_size=result.get("path_hash_size", 1), pkt_hash=result.get("pkt_hash", ""), size=len(mesh_data), transport_from=result.get("transport_from", ""), transport_to=result.get("transport_to", ""), raw_hex=mesh_data.hex(), ) self._win._rx_log.append(entry) if self._win._rx_log_callback: self._win._rx_log_callback(entry) if entry.path: key = entry.path[-1] now = time.monotonic() existing = self._win._direct_repeaters.get(key) best = max(snr, existing[0]) if existing else snr self._win._direct_repeaters[key] = (best, now, 0) self._win._repeaters_refreshed.add(key) stale = [ k for k, (_, ts, _mc) in self._win._direct_repeaters.items() if now - ts > 600 ] for k in stale: del self._win._direct_repeaters[k] if len(self._win._direct_repeaters) > 5: weakest = min( self._win._direct_repeaters, key=lambda k: self._win._direct_repeaters[k][0] ) del self._win._direct_repeaters[weakest] if not self._win._signal_icon_timer_id: self._win._signal_icon_timer_id = GLib.timeout_add_seconds( 1, self._win._update_signal_icon_tick ) if not result: return if result["payload_type"] == PAYLOAD_ADVERT and "advert" in result: advert = result["advert"] self_key = self._win._self_info.get("public_key", "") if advert["public_key"] != self_key: contact = self.dict_to_contact(advert) if contact: self.add_or_update_discovered(contact) if result["payload_type"] != PAYLOAD_GROUP_TEXT: return if not self._win._channel_hash_map: return ch_hash = result.get("channel_hash") ciphertext = result.get("ciphertext") if not ch_hash or not ciphertext: return psk = self._win._channel_psk_map.get(ch_hash) if not psk: return decrypted = decrypt_group_text(ciphertext, psk) if not decrypted: log.warning( f"LOG_DATA: decryption failed, " f"channel_hash={ch_hash}, " f"available_channels={[ch.index for ch in self._win._channels if not ch.is_empty]}" ) return sender = decrypted.get("sender", "") text = decrypted.get("message", "") raw_timestamp = decrypted.get("timestamp", 0) timestamp = min(raw_timestamp, time.time()) path_hops = result.get("path", []) if not sender or not text: return path_bytes_hex = "".join(h.lower() for h in path_hops) my_name = self._win._self_info.get("name", "") log.info( f'LOG_DATA: sender="{sender}" path={path_hops} ' f'text="{text[:30]}..." ts={timestamp}' ) # Match channel using pre-built hash map channel_idx = self._win._channel_hash_map.get(ch_hash.lower()) if channel_idx is None: log.debug(f"LOG_DATA: no channel match for hash={ch_hash}") return # Pull SNR cached by CHANNEL_MSG_RECV handler (use raw timestamp # to match the key stored by CHANNEL_MSG_RECV before clamping) cache_key = (channel_idx, raw_timestamp, text) cached_snr = self._pending_channel_snr.pop(cache_key, None) path_info = { "snr": cached_snr, "path_len": len(path_hops), "path_bytes": path_bytes_hex, } if my_name and sender == my_name: # Our own message echoed back through repeaters original_key = self._win._channels_view.on_repeat_received( channel_idx, timestamp, text, path_hops, ) store_ts = original_key[1] if original_key else timestamp if self._win._storage: self._win._storage.save_channel_message_path( channel_idx, store_ts, text, path_info, ) else: # Someone else's message - record path variant channel_msg = ChannelMessage( channel_index=channel_idx, sender_name=sender, text=text, timestamp=datetime.fromtimestamp(timestamp), ) if self._win._storage: self._win._storage.save_channel_message_path( channel_idx, timestamp, text, path_info, ) self._win._channels_view.add_path_info( channel_msg, path_info, ) meshy/src/host_integration.py000066400000000000000000000013271521052255700167330ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Helpers for invoking host commands across Flatpak and native runtimes.""" from __future__ import annotations import shutil import subprocess from collections.abc import Sequence from pathlib import Path def is_flatpak() -> bool: return Path("/.flatpak-info").exists() def host_command(args: Sequence[str]) -> list[str]: if is_flatpak() and shutil.which("flatpak-spawn"): return ["flatpak-spawn", "--host", *args] return list(args) def run_host_command(args: Sequence[str], **kwargs) -> subprocess.CompletedProcess: return subprocess.run(host_command(args), **kwargs) meshy/src/main.py000066400000000000000000000005421521052255700142750ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import sys import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from meshy.application import MeshyApplication def main(version): app = MeshyApplication(version=version) return app.run(sys.argv) meshy/src/mesh_crypto.py000066400000000000000000000144571521052255700157170ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Lightweight MeshCore packet decoding and channel crypto. Replaces meshcoredecoder for the subset of features Meshy needs: - Channel hash calculation (SHA-256) - GroupText payload decryption (HMAC-SHA256 + AES-128-ECB) - Minimal packet header/path parsing for LOG_DATA frames """ import hashlib import hmac import struct # Payload type constants (from header bits 2-5) PAYLOAD_ADVERT = 0x04 PAYLOAD_GROUP_TEXT = 0x05 def channel_hash(psk_hex: str) -> str: """Calculate the 1-byte channel hash from a PSK hex string. This is SHA-256(psk_bytes)[0] as a 2-char lowercase hex string. """ h = hashlib.sha256(bytes.fromhex(psk_hex)).digest() return f"{h[0]:02x}" def decrypt_group_text(ciphertext: bytes, psk: bytes) -> dict | None: """Decrypt a GroupText payload using the channel PSK. Args: ciphertext: The encrypted payload (after channel_hash byte). First 2 bytes are HMAC-SHA256 MAC, rest is AES-128-ECB ciphertext. psk: The 16-byte channel pre-shared key. Returns: Dict with 'sender', 'message', 'timestamp', 'flags' or None on failure. """ if len(ciphertext) < 3: return None mac = ciphertext[:2] encrypted = ciphertext[2:] # Verify HMAC-SHA256: key is PSK padded to 32 bytes with zeros key32 = psk + b"\x00" * (32 - len(psk)) if len(psk) < 32 else psk[:32] expected_mac = hmac.new(key32, encrypted, hashlib.sha256).digest() if mac[0] != expected_mac[0] or mac[1] != expected_mac[1]: return None # Decrypt with AES-128-ECB try: from Crypto.Cipher import AES key16 = psk[:16] if len(psk) >= 16 else psk + b"\x00" * (16 - len(psk)) cipher = AES.new(key16, AES.MODE_ECB) plaintext = cipher.decrypt(encrypted) except ImportError: return None # Parse plaintext: timestamp(4 LE) + flags(1) + "sender: message" if len(plaintext) < 6: return None timestamp = struct.unpack(" dict | None: """Parse a raw mesh packet from LOG_DATA, extract GroupText if present. Parses the MeshCore packet header, skips path data, and extracts the payload. Only GroupText payloads are fully decoded. Args: raw: Raw packet bytes (after LOG_DATA framing). Returns: Dict with 'payload_type', 'path' (list of hex hop strings), 'channel_hash', and 'decrypted' (if GroupText), or None. """ if len(raw) < 2: return None header = raw[0] route_type = header & 0x03 payload_type = (header >> 2) & 0x0F offset = 1 # Parse transport codes for TC_FLOOD (0) and TC_DIRECT (3) transport_from = "" transport_to = "" if route_type == 0 or route_type == 3: if len(raw) < offset + 4: return None transport_from = raw[offset : offset + 2].hex() transport_to = raw[offset + 2 : offset + 4].hex() offset += 4 # Parse path length byte if len(raw) < offset + 1: return None path_len_byte = raw[offset] offset += 1 # Decode path: bits 7:6 = hash_size selector, bits 5:0 = hop count hash_size = (path_len_byte >> 6) + 1 hop_count = path_len_byte & 63 path_byte_length = hop_count * hash_size # Legacy fallback: reserved hash_size (4) or path extends past packet if hash_size == 4 or offset + path_byte_length > len(raw): path_byte_length = min(path_len_byte, len(raw) - offset, 64) hop_count = path_byte_length hash_size = 1 # Extract path hops as hex strings path = [] for i in range(hop_count): hop_start = offset + i * hash_size hop_bytes = raw[hop_start : hop_start + hash_size] path.append(hop_bytes.hex()) offset += path_byte_length # Extract payload payload = raw[offset:] pkt_hash = hashlib.sha256(bytes([payload_type]) + payload).hexdigest()[:16] result = { "payload_type": payload_type, "route_type": route_type, "path": path, "path_hash_size": hash_size, "pkt_hash": pkt_hash, "transport_from": transport_from, "transport_to": transport_to, } if payload_type == PAYLOAD_ADVERT: advert = parse_advert_payload(payload) if advert: result["advert"] = advert # Decode GroupText payload: channel_hash(1) + mac(2) + ciphertext elif payload_type == PAYLOAD_GROUP_TEXT and len(payload) >= 3: result["channel_hash"] = f"{payload[0]:02x}" result["ciphertext"] = payload[1:] # mac + encrypted data return result def parse_advert_payload(payload: bytes) -> dict | None: """Parse an ADVERT payload from a LOG_DATA frame. Format: pubkey(32) + timestamp(4) + signature(64) + flags(1) + [lat(4) + lon(4) if flag 0x10] + [name(null-terminated) if flag 0x80] """ if len(payload) < 101: # 32 + 4 + 64 + 1 return None pub_key = payload[:32].hex() timestamp = struct.unpack_from("= offset + 8: lat = struct.unpack_from(" offset: end = payload.index(0, offset) if 0 in payload[offset:] else len(payload) name = payload[offset:end].decode("utf-8", "ignore") return { "public_key": pub_key, "type": node_type, "adv_name": name, "last_advert": timestamp, "adv_lat": lat, "adv_lon": lon, } meshy/src/meshcore_packets.py000066400000000000000000000057231521052255700166760ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later from enum import Enum class AnonReqType(Enum): REGIONS = 0x01 OWNER = 0x02 BASIC = 0x03 # just remote clock class BinaryReqType(Enum): STATUS = 0x01 KEEP_ALIVE = 0x02 TELEMETRY = 0x03 MMA = 0x04 ACL = 0x05 NEIGHBOURS = 0x06 class ControlType(Enum): NODE_DISCOVER_REQ = 0x80 NODE_DISCOVER_RESP = 0x90 class CommandType(Enum): APP_START = 1 SEND_TXT_MSG = 2 SEND_CHANNEL_TXT_MSG = 3 GET_CONTACTS = 4 # with optional 'since' (for efficient sync) GET_DEVICE_TIME = 5 SET_DEVICE_TIME = 6 SEND_SELF_ADVERT = 7 SET_ADVERT_NAME = 8 ADD_UPDATE_CONTACT = 9 SYNC_NEXT_MESSAGE = 10 SET_RADIO_PARAMS = 11 SET_RADIO_TX_POWER = 12 RESET_PATH = 13 SET_ADVERT_LATLON = 14 REMOVE_CONTACT = 15 SHARE_CONTACT = 16 EXPORT_CONTACT = 17 IMPORT_CONTACT = 18 REBOOT = 19 GET_BATT_AND_STORAGE = 20 # was CMD_GET_BATTERY_VOLTAGE SET_TUNING_PARAMS = 21 DEVICE_QUERY = 22 EXPORT_PRIVATE_KEY = 23 IMPORT_PRIVATE_KEY = 24 SEND_RAW_DATA = 25 SEND_LOGIN = 26 SEND_STATUS_REQ = 27 HAS_CONNECTION = 28 LOGOUT = 29 # 'Disconnect' GET_CONTACT_BY_KEY = 30 GET_CHANNEL = 31 SET_CHANNEL = 32 SIGN_START = 33 SIGN_DATA = 34 SIGN_FINISH = 35 SEND_TRACE_PATH = 36 SET_DEVICE_PIN = 37 SET_OTHER_PARAMS = 38 SEND_TELEMETRY_REQ = 39 # can deprecate this GET_CUSTOM_VARS = 40 SET_CUSTOM_VAR = 41 GET_ADVERT_PATH = 42 GET_TUNING_PARAMS = 43 # NOTE: CMD range 44..49 parked, potentially for WiFi operations BINARY_REQ = 50 FACTORY_RESET = 51 PATH_DISCOVERY = 52 SET_FLOOD_SCOPE = 54 SEND_CONTROL_DATA = 55 SEND_ANON_REQ = 57 SET_AUTOADD_CONFIG = 58 GET_AUTOADD_CONFIG = 59 GET_ALLOWED_REPEAT_FREQ = 60 SET_PATH_HASH_MODE = 61 # Packet prefixes for the protocol class PacketType(Enum): OK = 0 ERROR = 1 CONTACT_START = 2 CONTACT = 3 CONTACT_END = 4 SELF_INFO = 5 MSG_SENT = 6 CONTACT_MSG_RECV = 7 CHANNEL_MSG_RECV = 8 CURRENT_TIME = 9 NO_MORE_MSGS = 10 CONTACT_URI = 11 BATTERY = 12 DEVICE_INFO = 13 PRIVATE_KEY = 14 DISABLED = 15 CONTACT_MSG_RECV_V3 = 16 CHANNEL_MSG_RECV_V3 = 17 CHANNEL_INFO = 18 SIGN_START = 19 SIGNATURE = 20 CUSTOM_VARS = 21 ADVERT_PATH = 22 TUNING_PARAMS = 23 STATS = 24 AUTOADD_CONFIG = 25 ALLOWED_REPEAT_FREQ = 26 # Push notifications ADVERTISEMENT = 0x80 PATH_UPDATE = 0x81 ACK = 0x82 MESSAGES_WAITING = 0x83 RAW_DATA = 0x84 LOGIN_SUCCESS = 0x85 LOGIN_FAILED = 0x86 STATUS_RESPONSE = 0x87 LOG_DATA = 0x88 TRACE_DATA = 0x89 PUSH_CODE_NEW_ADVERT = 0x8A TELEMETRY_RESPONSE = 0x8B BINARY_RESPONSE = 0x8C PATH_DISCOVERY_RESPONSE = 0x8D CONTROL_DATA = 0x8E CONTACT_DELETED = 0x8F meshy/src/meshy.in000066400000000000000000000021121521052255700144470ustar00rootroot00000000000000#!@PYTHON@ # Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import os import sys import signal import locale import gettext VERSION = '@VERSION@' pkgdatadir = '@pkgdatadir@' appdir = os.environ.get('APPDIR') if appdir: pkgdatadir = os.path.join(appdir, 'usr', 'share', 'meshy') localedir = os.path.join(appdir, 'usr', 'share', 'locale') else: localedir = '@localedir@' QR_SCANNER_ENABLED = '@QR_SCANNER_ENABLED@' == 'true' SHORTCUTS_DIALOG_ENABLED = '@SHORTCUTS_DIALOG_ENABLED@' == 'true' signal.signal(signal.SIGINT, signal.SIG_DFL) locale.bindtextdomain('meshy', localedir) locale.textdomain('meshy') gettext.install('meshy', localedir) if __name__ == '__main__': import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') import meshy as _meshy_pkg _meshy_pkg.pkgdatadir = pkgdatadir _meshy_pkg.QR_SCANNER_ENABLED = QR_SCANNER_ENABLED _meshy_pkg.SHORTCUTS_DIALOG_ENABLED = SHORTCUTS_DIALOG_ENABLED from meshy.main import main sys.exit(main(VERSION)) meshy/src/meson.build000066400000000000000000000023441521052255700151430ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later meshy_sources = [ '__init__.py', 'main.py', 'application.py', 'window.py', 'protocol.py', 'ble.py', 'models.py', 'storage.py', 'meshcore_packets.py', 'mesh_crypto.py', 'transport_base.py', 'usb_serial.py', 'tcp.py', 'utils.py', 'backup.py', 'frame_handler.py', 'message_controller.py', 'connection_controller.py', 'host_integration.py', 'theme_manager.py', 'theme_chooser_dialog.py', 'background_portal.py', 'smaz.py', ] if get_option('qr_scanner') meshy_sources += ['qr_scanner.py'] endif views_sources = [ 'views/__init__.py', 'views/chat_mixin.py', 'views/chat_view.py', 'views/contacts_view.py', 'views/channels_view.py', 'views/settings_view.py', 'views/device_view.py', 'views/connection_view.py', 'views/repeater_view.py', 'views/map_view.py', ] py_installation.install_sources(meshy_sources, subdir: 'meshy') py_installation.install_sources(views_sources, subdir: 'meshy' / 'views') configure_file( input: 'meshy.in', output: 'meshy', configuration: conf, install: true, install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x', ) meshy/src/message_controller.py000066400000000000000000000275021521052255700172450ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Message sending, retry, and delivery tracking extracted from MeshyWindow.""" import logging import time import uuid from datetime import datetime from gi.repository import GLib from meshy.models import ( ChannelMessage, Contact, Message, MessageStatus, ) from meshy.protocol import ( STATS_TYPE_RADIO, build_reset_path, build_send_channel_text_msg, build_send_text_msg, build_set_flood_scope, ) log = logging.getLogger(__name__) PATH_RETRIES = 4 FLOOD_RETRIES = 3 class MessageController: """Owns message send/retry state and delivery tracking.""" def __init__(self, win): self._win = win self.pending_acks: dict[str, str] = {} self.retry_context: dict[str, dict] = {} self.sent_msg_queue: list[str] = [] self.pending_timeouts: dict[str, int] = {} self.grace_acks: dict[str, str] = {} self.grace_timeouts: dict[str, int] = {} self.last_rx_time: float = 0.0 self.prev_total_air_secs: int = 0 self.last_airtime_bump: float = 0.0 def cleanup_on_disconnect(self): self.pending_acks.clear() self.retry_context.clear() for tid in self.pending_timeouts.values(): GLib.source_remove(tid) self.pending_timeouts.clear() for tid in self.grace_timeouts.values(): GLib.source_remove(tid) self.grace_timeouts.clear() self.grace_acks.clear() self.sent_msg_queue.clear() def send_message(self, contact: Contact, text: str): msg_id = str(uuid.uuid4()) timestamp = int(datetime.now().timestamp()) message = Message( sender_key=contact.public_key, text=text, timestamp=datetime.fromtimestamp(timestamp), is_outgoing=True, status=MessageStatus.PENDING, message_id=msg_id, ) self._win._chat_view.on_message_sent(message) has_path = contact.path_length >= 0 and contact.path_length != 0xFF self.retry_context[msg_id] = { "contact": contact, "text": text, "timestamp": timestamp, "attempt": 0, "started_with_path": has_path, } def _after_render(): self._win._storage.save_message(contact.public_key_hex, message) self._send_message_attempt(msg_id) return False GLib.idle_add(_after_render) def resend_message(self, contact: Contact, message: Message): msg_id = message.message_id timestamp = int(message.timestamp.timestamp()) if msg_id in self.pending_timeouts: GLib.source_remove(self.pending_timeouts.pop(msg_id)) for ack_key, tracked_id in list(self.pending_acks.items()): if tracked_id == msg_id: self.pending_acks.pop(ack_key) for ack_key, tracked_id in list(self.grace_acks.items()): if tracked_id == msg_id: self.grace_acks.pop(ack_key) if msg_id in self.grace_timeouts: GLib.source_remove(self.grace_timeouts.pop(msg_id)) if msg_id in self.sent_msg_queue: self.sent_msg_queue.remove(msg_id) self._win._storage.update_message_status(msg_id, MessageStatus.PENDING) self._win._chat_view.update_message_status(msg_id, MessageStatus.PENDING) has_path = contact.path_length >= 0 and contact.path_length != 0xFF self.retry_context[msg_id] = { "contact": contact, "text": message.text, "timestamp": timestamp, "attempt": 0, "started_with_path": has_path, } self._send_message_attempt(msg_id) def send_channel_message(self, channel_index: int, text: str): channel = next((ch for ch in self._win._channels if ch.index == channel_index), None) region = channel.region if channel else "" timestamp = int(datetime.now().timestamp()) frame = build_send_channel_text_msg(channel_index, text, timestamp=timestamp) if region: self._win.send_frame(build_set_flood_scope(region)) self._win.send_frame(frame) GLib.timeout_add(300, self._win._apply_default_scope) else: self._win.send_frame(frame) name = self._win._self_info.get("name", _("Me")) msg = ChannelMessage( channel_index=channel_index, sender_name=name, text=text, timestamp=datetime.fromtimestamp(timestamp), is_outgoing=True, ) self._win._storage.save_channel_message(msg) self._win._channels_view.on_channel_message_sent(msg, timestamp) def resend_channel_message(self, channel_index: int, text: str) -> int: """Resend a channel message with a fresh timestamp. Returns the new timestamp used for sending. """ channel = next((ch for ch in self._win._channels if ch.index == channel_index), None) region = channel.region if channel else "" new_ts = int(datetime.now().timestamp()) frame = build_send_channel_text_msg(channel_index, text, timestamp=new_ts) if region: self._win.send_frame(build_set_flood_scope(region)) self._win.send_frame(frame) GLib.timeout_add(300, self._win._apply_default_scope) else: self._win.send_frame(frame) return new_ts def delete_message(self, message_id: str): self._win._storage.delete_message(message_id) def delete_channel_message(self, channel_index: int, timestamp: float, text: str): self._win._storage.delete_channel_message(channel_index, timestamp, text) def max_retries_for(self, ctx: dict) -> int: if ctx.get("started_with_path"): return PATH_RETRIES + 1 return FLOOD_RETRIES def send_detail(self, msg_id: str, is_flood: bool) -> str: ctx = self.retry_context.get(msg_id) contact = ctx.get("contact") if ctx else None if is_flood: route = _("flood") elif contact and contact.path_length == 0: route = _("direct") else: route = _("path") if not ctx: return _("– {route}").format(route=route) attempt = ctx["attempt"] + 1 max_retries = self.max_retries_for(ctx) if attempt <= 1: return _("– {route}").format(route=route) return _("({attempt}/{total}) – {route}").format( attempt=attempt, total=max_retries, route=route ) def _airtime_busy_fraction(self) -> float: if self.last_airtime_bump == 0.0: return 0.0 ms = (time.monotonic() - self.last_airtime_bump) * 1000 if ms >= 8000: return 0.0 return 1.0 - ms / 8000 def _radio_quiet_delay_ms(self) -> int: elapsed = time.monotonic() - self.last_rx_time if elapsed > 3.0: return 0 radio = self._win._device_stats.get(STATS_TYPE_RADIO) if not radio: return 1000 noise = radio.get("noise_floor", -118) noise_severity = max(0.0, min(1.0, (noise + 118) / 30.0)) snr = radio.get("last_snr", 12.0) snr_severity = max(0.0, min(1.0, (12.0 - snr) / 14.0)) air_busy = self._airtime_busy_fraction() severity = max(noise_severity, snr_severity) * 0.82 + air_busy * 0.18 return int(500 + severity * 7500) def _send_message_attempt(self, msg_id: str): ctx = self.retry_context.get(msg_id) if not ctx: return delay = self._radio_quiet_delay_ms() if delay > 0: self._win._chat_view.update_message_status( msg_id, MessageStatus.PENDING, _("waiting {:.1f}s (radio busy)").format(delay / 1000), ) GLib.timeout_add(delay, self._do_send_message, msg_id) else: self._do_send_message(msg_id) def _do_send_message(self, msg_id: str) -> bool: ctx = self.retry_context.get(msg_id) if not ctx: return False contact_key = ctx["contact"].public_key_hex contact = self._win._contacts_by_key.get(contact_key, ctx["contact"]) ctx["contact"] = contact if ctx.get("started_with_path") and ctx["attempt"] >= PATH_RETRIES: if contact.path_length >= 0 and contact.path_length != 0xFF: self._win.send_frame(build_reset_path(contact.public_key)) contact.path_length = -1 contact.path = b"" self.sent_msg_queue.append(msg_id) frame = build_send_text_msg( contact.public_key, ctx["text"], attempt=ctx["attempt"], timestamp=ctx["timestamp"], ) self._win.send_frame(frame) GLib.timeout_add(10000, self._on_sent_pending_timeout, msg_id) return False def _on_sent_pending_timeout(self, msg_id: str) -> bool: if msg_id in self.sent_msg_queue: log.warning(f"Message {msg_id[:8]} stuck in sent queue, forcing retry") self.sent_msg_queue.remove(msg_id) self._on_message_timeout(msg_id, "") return False def _on_message_timeout(self, msg_id: str, ack_key: str) -> bool: if msg_id in self.pending_timeouts: self.pending_timeouts.pop(msg_id) ctx = self.retry_context.get(msg_id) max_retries = self.max_retries_for(ctx) if ctx else 0 if ctx and ctx["attempt"] < max_retries - 1: ctx["attempt"] += 1 backoff_ms = 1000 * (1 << (ctx["attempt"] - 1)) detail = "({current}/{total})".format(current=ctx["attempt"] + 1, total=max_retries) self._win._storage.update_message_status(msg_id, MessageStatus.PENDING) self._win._chat_view.update_message_status(msg_id, MessageStatus.PENDING, detail) GLib.timeout_add(backoff_ms, lambda: self._send_message_attempt(msg_id) or False) elif ctx: attempt = ctx["attempt"] + 1 contact = ctx.get("contact") started_with_path = ctx.get("started_with_path") self.retry_context.pop(msg_id, None) self._win._storage.update_message_status(msg_id, MessageStatus.FAILED) self._win._chat_view.update_message_status( msg_id, MessageStatus.FAILED, f"{attempt}/{max_retries}" ) # Move all ack_keys for this message to grace_acks all_ack_keys = [k for k, v in self.pending_acks.items() if v == msg_id] if ack_key and ack_key != "00000000" and ack_key not in all_ack_keys: all_ack_keys.append(ack_key) for k in all_ack_keys: self.pending_acks.pop(k, None) self.grace_acks[k] = msg_id if all_ack_keys: grace_id = GLib.timeout_add(30000, self._on_grace_expired, msg_id) self.grace_timeouts[msg_id] = grace_id if contact: contact_key = contact.public_key_hex current = self._win._contacts_by_key.get(contact_key) if current and current.path_length >= 0 and started_with_path: self._win.send_frame(build_reset_path(current.public_key)) current.path_length = -1 current.path = b"" self._win._contacts_view.update_contacts(self._win._contacts) self._win._refresh_chat_header() GLib.timeout_add(500, lambda: self._win._frame_handler.send_get_contacts() or False) return False def _on_grace_expired(self, msg_id: str) -> bool: for k in [k for k, v in self.grace_acks.items() if v == msg_id]: self.grace_acks.pop(k, None) self.grace_timeouts.pop(msg_id, None) return False meshy/src/models.py000066400000000000000000000223061521052255700146360ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Data models for MeshCore entities.""" from dataclasses import dataclass, field from datetime import datetime from enum import IntEnum, auto def battery_percent_from_mv(mv: int) -> int: """Estimate LiPo battery percentage from voltage. Piecewise linear approximation with breakpoints at 4.2V/100%, 4.0V/90%, 3.6V/10%, 3.5V/5%, 3.0V/0%. """ _TABLE = [(4200, 100), (4000, 90), (3600, 10), (3500, 5), (3000, 0)] if mv >= _TABLE[0][0]: return 100 if mv <= _TABLE[-1][0]: return 0 for i in range(len(_TABLE) - 1): if mv >= _TABLE[i + 1][0]: v_hi, p_hi = _TABLE[i] v_lo, p_lo = _TABLE[i + 1] return round(p_lo + (p_hi - p_lo) * (mv - v_lo) / (v_hi - v_lo)) return 0 def battery_icon_name(percent: int) -> str: if percent >= 90: return "battery-level-100-symbolic" if percent >= 80: return "battery-level-80-symbolic" if percent >= 70: return "battery-level-70-symbolic" if percent >= 60: return "battery-level-60-symbolic" if percent >= 40: return "battery-level-50-symbolic" if percent >= 30: return "battery-level-30-symbolic" if percent >= 20: return "battery-level-20-symbolic" return "battery-level-low-symbolic" SNR_THRESHOLDS = { 7: (4.0, -2.0, -4.0, -6.0), 8: (4.0, -4.0, -6.0, -8.0), 9: (4.0, -6.0, -8.0, -10.0), 10: (4.0, -8.0, -10.0, -13.0), 11: (4.0, -10.0, -12.5, -15.0), 12: (4.0, -12.5, -15.0, -18.0), } SIGNAL_ICONS = ( "strength-bars-1-symbolic", "strength-bars-2-symbolic", "strength-bars-3-symbolic", "strength-bars-4-symbolic", "strength-bars-5-symbolic", ) SIGNAL_LABELS = ( "Excellent", "Good", "Fair", "Poor", "Very poor", ) def snr_to_icon(snr: float, sf: int = 12) -> str: thresholds = SNR_THRESHOLDS.get(sf, SNR_THRESHOLDS[12]) for i, t in enumerate(thresholds): if snr >= t: return SIGNAL_ICONS[i] return SIGNAL_ICONS[4] def snr_to_label(snr: float, sf: int = 12) -> str: thresholds = SNR_THRESHOLDS.get(sf, SNR_THRESHOLDS[12]) for i, t in enumerate(thresholds): if snr >= t: return SIGNAL_LABELS[i] return SIGNAL_LABELS[4] class ContactType(IntEnum): CHAT = 1 REPEATER = 2 ROOM = 3 SENSOR = 4 class MessageStatus(IntEnum): PENDING = auto() SENT = auto() DELIVERED = auto() FAILED = auto() CONTACT_FLAG_FAVORITE = 0x01 CONTACT_FLAG_TELE_BASE = 0x02 CONTACT_FLAG_TELE_LOC = 0x04 CONTACT_FLAG_TELE_ENV = 0x08 @dataclass class Contact: public_key: bytes name: str type: int = ContactType.CHAT flags: int = 0 path_length: int = -1 # -1 = flood path: bytes = b"" path_hash_size: int = 1 # 1, 2, or 3 bytes per hop latitude: float | None = None longitude: float | None = None last_seen: datetime | None = None device_lastmod: datetime | None = None last_message_at: datetime | None = None is_active: bool = True @property def public_key_hex(self) -> str: return self.public_key.hex() @property def type_label(self) -> str: labels = { ContactType.CHAT: "Chat", ContactType.REPEATER: "Repeater", ContactType.ROOM: "Room", ContactType.SENSOR: "Sensor", } return labels.get(self.type, "Unknown") @property def path_label(self) -> str: if self.path_length < 0: return "Flood" if self.path_length == 0: return "Direct" return f"{self.path_length} hops" @property def path_hops(self) -> list[str]: """Split path bytes into individual hop prefixes using path_hash_size.""" if self.path_length <= 0 or not self.path: return [] hs = self.path_hash_size return [ self.path[i : i + hs].hex().upper() for i in range(0, min(len(self.path), self.path_length * hs), hs) ] @property def is_favorite(self) -> bool: return bool(self.flags & CONTACT_FLAG_FAVORITE) @property def has_location(self) -> bool: if self.latitude is None or self.longitude is None: return False lat, lon = self.latitude, self.longitude # Filter out (0,0)..(1,1) range — unset or deliberately hidden return not (0.0 <= lat <= 1.0 and 0.0 <= lon <= 1.0) @property def short_key(self) -> str: h = self.public_key_hex return f"{h[:8]}...{h[-8:]}" @dataclass class Message: sender_key: bytes text: str timestamp: datetime is_outgoing: bool is_cli: bool = False status: MessageStatus = MessageStatus.PENDING message_id: str | None = None retry_count: int = 0 expected_ack_hash: int | None = None sent_at: datetime | None = None delivered_at: datetime | None = None trip_time_ms: int | None = None path_length: int | None = None path_bytes: bytes = b"" room_sender_name: str = "" # resolved sender name for room messages @property def sender_key_hex(self) -> str: return self.sender_key.hex() class NotificationLevel(IntEnum): DEFAULT = -1 ALL = 0 MENTIONS = 1 NONE = 2 @dataclass class Channel: index: int name: str psk: bytes # 16 bytes unread_count: int = 0 notification_level: NotificationLevel = NotificationLevel.DEFAULT region: str = "" PUBLIC_CHANNEL_PSK = "8b3387e9c5cdea6ac9e5edbaa115cd72" @property def psk_hex(self) -> str: return self.psk.hex() @property def is_empty(self) -> bool: return not self.name and all(b == 0 for b in self.psk) @property def is_public_channel(self) -> bool: return self.psk_hex == self.PUBLIC_CHANNEL_PSK @property def is_hashtag_channel(self) -> bool: if self.is_public_channel or not self.name.startswith("#"): return False import hashlib expected = hashlib.sha256(self.name.encode()).digest()[:16] return self.psk == expected @property def channel_type(self) -> str: if self.is_public_channel: return "Public" if self.is_hashtag_channel: return "Hashtag" return "Private" @staticmethod def empty(index: int) -> "Channel": return Channel(index=index, name="", psk=bytes(16)) @staticmethod def from_hex(index: int, name: str, psk_hex: str) -> "Channel": psk = bytes.fromhex(psk_hex) if len(psk) != 16: raise ValueError("PSK must be 16 bytes") return Channel(index=index, name=name, psk=psk) @dataclass class DeviceInfo: # From SELF_INFO (meshcore reader field names) name: str = "" # Node name public_key: str = "" # Hex string tx_power: int = 0 max_tx_power: int = 0 radio_freq: float = 0.0 # MHz radio_bw: float = 0.0 # kHz radio_sf: int = 0 radio_cr: int = 0 cr_old_encoding: bool = False # From DEVICE_INFO model: str = "" # Board/manufacturer name ver: str = "" # Firmware version string fw_build: str = "" # Build date fw_ver: int = 0 # Firmware version code max_contacts: int = 32 max_channels: int = 8 path_hash_mode: int = 0 # 0=1-byte, 1=2-byte, 2=3-byte (fw v10+) multi_acks: int = 0 # 0=off, 1=send extra ACK (fw v10+) repeat: bool = False # client repeat / off-grid mode (fw v9+) ble_pin: int = 0 # BLE pairing PIN (from DEVICE_INFO, fw v3+) # Location from SELF_INFO (adv_lat/adv_lon) latitude: float | None = None longitude: float | None = None # From BATT_AND_STORAGE battery_mv: int = 0 storage_used_kb: int = 0 storage_total_kb: int = 0 @property def battery_percent(self) -> int: return battery_percent_from_mv(self.battery_mv) @property def storage_percent(self) -> int: if self.storage_total_kb == 0: return 0 return int(self.storage_used_kb * 100 / self.storage_total_kb) PAYLOAD_TYPENAMES = [ "REQ", "RESPONSE", "TEXT_MSG", "ACK", "ADVERT", "GRP_TXT", "GRP_DATA", "ANON_REQ", "PATH", "TRACE", "MULTIPART", "CONTROL", ] ROUTE_TYPENAMES = ["TC_FLOOD", "FLOOD", "DIRECT", "TC_DIRECT"] @dataclass class RxLogEntry: timestamp: datetime snr: float rssi: int payload_type: int route_type: int path: list = field(default_factory=list) path_hash_size: int = 1 pkt_hash: str = "" size: int = 0 transport_from: str = "" transport_to: str = "" raw_hex: str = "" @property def payload_type_name(self) -> str: if 0 <= self.payload_type < len(PAYLOAD_TYPENAMES): return PAYLOAD_TYPENAMES[self.payload_type] return f"UNK({self.payload_type})" @property def route_type_name(self) -> str: if 0 <= self.route_type < len(ROUTE_TYPENAMES): return ROUTE_TYPENAMES[self.route_type] return f"UNK({self.route_type})" @dataclass class ChannelMessage: channel_index: int sender_name: str text: str timestamp: datetime is_outgoing: bool = False status: MessageStatus = MessageStatus.DELIVERED meshy/src/protocol.py000066400000000000000000001217151521052255700152200ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """MeshCore protocol - sync wrappers using meshcore library's packets and parsing logic. Uses meshcore.packets for constants and follows meshcore.reader's parsing format for field names and response handling. """ import io import struct from datetime import datetime from meshy.meshcore_packets import CommandType, ControlType, PacketType from meshy.smaz import try_decode_prefixed as _smaz_decode # Re-export constants that the rest of the app uses CMD_APP_START = CommandType.APP_START.value CMD_DEVICE_QUERY = CommandType.DEVICE_QUERY.value CMD_GET_CONTACTS = CommandType.GET_CONTACTS.value CMD_GET_DEVICE_TIME = CommandType.GET_DEVICE_TIME.value CMD_SET_DEVICE_TIME = CommandType.SET_DEVICE_TIME.value CMD_SEND_SELF_ADVERT = CommandType.SEND_SELF_ADVERT.value CMD_SET_ADVERT_NAME = CommandType.SET_ADVERT_NAME.value CMD_ADD_UPDATE_CONTACT = CommandType.ADD_UPDATE_CONTACT.value CMD_SYNC_NEXT_MESSAGE = CommandType.SYNC_NEXT_MESSAGE.value CMD_SET_RADIO_PARAMS = CommandType.SET_RADIO_PARAMS.value CMD_SET_RADIO_TX_POWER = CommandType.SET_RADIO_TX_POWER.value CMD_RESET_PATH = CommandType.RESET_PATH.value CMD_SET_ADVERT_LATLON = CommandType.SET_ADVERT_LATLON.value CMD_REMOVE_CONTACT = CommandType.REMOVE_CONTACT.value CMD_EXPORT_CONTACT = CommandType.EXPORT_CONTACT.value CMD_IMPORT_CONTACT = CommandType.IMPORT_CONTACT.value CMD_REBOOT = 19 CMD_GET_BATT_AND_STORAGE = CommandType.GET_BATT_AND_STORAGE.value CMD_SEND_LOGIN = CommandType.SEND_LOGIN.value CMD_SEND_STATUS_REQ = CommandType.SEND_STATUS_REQ.value CMD_GET_CHANNEL = CommandType.GET_CHANNEL.value CMD_SET_CHANNEL = CommandType.SET_CHANNEL.value CMD_SEND_TRACE_PATH = CommandType.SEND_TRACE_PATH.value CMD_SET_OTHER_PARAMS = CommandType.SET_OTHER_PARAMS.value CMD_SEND_TXT_MSG = CommandType.SEND_TXT_MSG.value CMD_SEND_CHANNEL_TXT_MSG = CommandType.SEND_CHANNEL_TXT_MSG.value CMD_GET_AUTO_ADD_CONFIG = CommandType.GET_AUTOADD_CONFIG.value CMD_SET_AUTO_ADD_CONFIG = CommandType.SET_AUTOADD_CONFIG.value CMD_SEND_BINARY_REQ = CommandType.BINARY_REQ.value CMD_GET_CUSTOM_VAR = CommandType.GET_CUSTOM_VARS.value CMD_SET_CUSTOM_VAR = CommandType.SET_CUSTOM_VAR.value CMD_GET_STATS = 56 CMD_SET_DEVICE_PIN = CommandType.SET_DEVICE_PIN.value CMD_SET_PATH_HASH_MODE = CommandType.SET_PATH_HASH_MODE.value # Response/push codes from meshcore.packets.PacketType RESP_CODE_OK = PacketType.OK.value RESP_CODE_ERR = PacketType.ERROR.value RESP_CODE_CONTACTS_START = PacketType.CONTACT_START.value RESP_CODE_CONTACT = PacketType.CONTACT.value RESP_CODE_END_OF_CONTACTS = PacketType.CONTACT_END.value RESP_CODE_SELF_INFO = PacketType.SELF_INFO.value RESP_CODE_SENT = PacketType.MSG_SENT.value RESP_CODE_CONTACT_MSG_RECV = PacketType.CONTACT_MSG_RECV.value RESP_CODE_CHANNEL_MSG_RECV = PacketType.CHANNEL_MSG_RECV.value RESP_CODE_CURR_TIME = PacketType.CURRENT_TIME.value RESP_CODE_NO_MORE_MESSAGES = PacketType.NO_MORE_MSGS.value RESP_CODE_EXPORT_CONTACT = PacketType.CONTACT_URI.value RESP_CODE_BINARY_RESPONSE = PacketType.BINARY_RESPONSE.value RESP_CODE_BATT_AND_STORAGE = PacketType.BATTERY.value RESP_CODE_DEVICE_INFO = PacketType.DEVICE_INFO.value RESP_CODE_CONTACT_MSG_RECV_V3 = 16 RESP_CODE_CHANNEL_MSG_RECV_V3 = 17 RESP_CODE_CHANNEL_INFO = PacketType.CHANNEL_INFO.value RESP_CODE_AUTO_ADD_CONFIG = PacketType.AUTOADD_CONFIG.value RESP_CODE_STATS = PacketType.STATS.value RESP_CODE_ADVERT_PATH = PacketType.ADVERT_PATH.value PUSH_CODE_ADVERT = PacketType.ADVERTISEMENT.value PUSH_CODE_PATH_UPDATED = PacketType.PATH_UPDATE.value PUSH_CODE_SEND_CONFIRMED = PacketType.ACK.value PUSH_CODE_MSG_WAITING = PacketType.MESSAGES_WAITING.value PUSH_CODE_NEW_ADVERT = PacketType.PUSH_CODE_NEW_ADVERT.value PUSH_CODE_TRACE_DATA = PacketType.TRACE_DATA.value PUSH_CODE_LOG_DATA = PacketType.LOG_DATA.value # 0x88 - raw radio packet PUSH_CODE_TELEMETRY_RESPONSE = PacketType.TELEMETRY_RESPONSE.value PUSH_CODE_PATH_DISCOVERY_RESPONSE = PacketType.PATH_DISCOVERY_RESPONSE.value # 0x8D PUSH_CODE_LOGIN_SUCCESS = PacketType.LOGIN_SUCCESS.value # 0x85 PUSH_CODE_LOGIN_FAILED = PacketType.LOGIN_FAILED.value # 0x86 PUSH_CODE_STATUS_RESPONSE = PacketType.STATUS_RESPONSE.value # 0x87 # Text types TXT_TYPE_PLAIN = 0 TXT_TYPE_CLI_DATA = 1 TXT_TYPE_SIGNED = 2 # Contact types ADV_TYPE_CHAT = 1 ADV_TYPE_REPEATER = 2 ADV_TYPE_ROOM = 3 ADV_TYPE_SENSOR = 4 # Sizes PUB_KEY_SIZE = 32 MAX_PATH_SIZE = 64 MAX_NAME_SIZE = 32 MAX_FRAME_SIZE = 172 MAX_TEXT_PAYLOAD = 160 # firmware payload limit for text messages APP_PROTOCOL_VERSION = 4 # Message overhead: cmd(1) + type(1) + attempt(1) + ts(4) + pubkey(6) + null(1) + safety(2) _DM_OVERHEAD = 16 # Channel overhead: cmd(1) + type(1) + chan_idx(1) + ts(4) + null(1) + safety(2) _CHAN_OVERHEAD = 10 def max_dm_text_bytes() -> int: """Max UTF-8 bytes for a direct message.""" return min(MAX_FRAME_SIZE - _DM_OVERHEAD, MAX_TEXT_PAYLOAD) def max_channel_text_bytes(sender_name: str) -> int: """Max UTF-8 bytes for a channel message (firmware prepends 'name: ').""" prefix_len = len(sender_name.encode("utf-8")) + 2 # ": " appended return min(MAX_TEXT_PAYLOAD - prefix_len, MAX_FRAME_SIZE - _CHAN_OVERHEAD) # Binary request types REQ_TYPE_STATUS = 0x01 REQ_TYPE_TELEMETRY = 0x03 REQ_TYPE_ACL = 0x05 REQ_TYPE_GET_NEIGHBORS = 0x06 # Control types (for zero-hop discovery) CTRL_NODE_DISCOVER_REQ = ControlType.NODE_DISCOVER_REQ.value # 0x80 CTRL_NODE_DISCOVER_RESP = ControlType.NODE_DISCOVER_RESP.value # 0x90 # Response code for control data RESP_CODE_CONTROL_DATA = PacketType.CONTROL_DATA.value # 0x8E CMD_SEND_CONTROL_DATA = CommandType.SEND_CONTROL_DATA.value # 55 # Auto-add flags AUTO_ADD_OVERWRITE_OLDEST = 1 << 0 AUTO_ADD_CHAT = 1 << 1 AUTO_ADD_REPEATER = 1 << 2 AUTO_ADD_ROOM_SERVER = 1 << 3 AUTO_ADD_SENSOR = 1 << 4 # Stats sub-types STATS_TYPE_CORE = 0 STATS_TYPE_RADIO = 1 STATS_TYPE_PACKETS = 2 # Error codes from companion firmware (MyMesh.cpp) ERROR_CODES = { 0x01: "Unsupported command", 0x02: "Not found", 0x03: "Table full", 0x04: "Bad state", 0x05: "File I/O error", 0x06: "Illegal argument", } def _now_ts() -> int: return int(datetime.now().timestamp()) def _decode_cstring(data: bytes) -> str: try: idx = data.index(0) return data[:idx].decode("utf-8", errors="replace") except ValueError: return data.decode("utf-8", errors="replace") # ─── Frame Builders ─────────────────────────────────────────────── # Following meshcore_py commands/* patterns for frame construction def build_app_start(app_name: str = "Meshy", app_version: int = APP_PROTOCOL_VERSION) -> bytes: return bytes([CMD_APP_START, app_version]) + bytes(6) + app_name.encode("utf-8") + b"\x00" def build_device_query(app_version: int = APP_PROTOCOL_VERSION) -> bytes: return bytes([CMD_DEVICE_QUERY, app_version]) def build_get_contacts(since: int | None = None) -> bytes: frame = bytes([CMD_GET_CONTACTS]) if since is not None: frame += struct.pack(" bytes: return bytes([CMD_GET_DEVICE_TIME]) def build_set_device_time(timestamp: int | None = None) -> bytes: if timestamp is None: timestamp = _now_ts() return bytes([CMD_SET_DEVICE_TIME]) + struct.pack(" bytes: return bytes([CMD_GET_BATT_AND_STORAGE]) def build_sync_next_message() -> bytes: return bytes([CMD_SYNC_NEXT_MESSAGE]) def build_send_text_msg( recipient_pub_key: bytes, text: str, attempt: int = 0, timestamp: int | None = None ) -> bytes: if timestamp is None: timestamp = _now_ts() frame = bytes([CMD_SEND_TXT_MSG, TXT_TYPE_PLAIN, min(attempt, 255)]) frame += struct.pack(" bytes: if timestamp is None: timestamp = _now_ts() frame = bytes([CMD_SEND_CHANNEL_TXT_MSG, TXT_TYPE_PLAIN, channel_index]) frame += struct.pack(" bytes: if timestamp is None: timestamp = _now_ts() frame = bytes([CMD_SEND_TXT_MSG, TXT_TYPE_CLI_DATA, min(attempt, 255)]) frame += struct.pack(" bytes: if flood: return bytes([CMD_SEND_SELF_ADVERT, 1]) return bytes([CMD_SEND_SELF_ADVERT]) def build_set_advert_name(name: str) -> bytes: return bytes([CMD_SET_ADVERT_NAME]) + name.encode("utf-8")[: MAX_NAME_SIZE - 1] def build_set_advert_latlon(lat: float, lon: float) -> bytes: frame = bytes([CMD_SET_ADVERT_LATLON]) frame += struct.pack(" bytes: return bytes([CMD_REMOVE_CONTACT]) + pub_key def build_add_update_contact( pub_key: bytes, contact_type: int = ADV_TYPE_CHAT, flags: int = 0, path_len: int = 0, path: bytes = b"", name: str = "", lat: float | None = None, lon: float | None = None, last_modified: int | None = None, ) -> bytes: frame = bytes([CMD_ADD_UPDATE_CONTACT]) frame += pub_key frame += bytes([contact_type, flags, path_len]) path_padded = bytearray(MAX_PATH_SIZE) path_padded[: len(path)] = path[:MAX_PATH_SIZE] frame += bytes(path_padded) name_bytes = name.encode("utf-8")[: MAX_NAME_SIZE - 1] frame += name_bytes + b"\x00" * (MAX_NAME_SIZE - len(name_bytes)) frame += struct.pack(" bytes: return bytes([CMD_RESET_PATH]) + pub_key def build_export_contact(pub_key: bytes) -> bytes: return bytes([CMD_EXPORT_CONTACT]) + pub_key def build_import_contact(contact_frame: bytes) -> bytes: return bytes([CMD_IMPORT_CONTACT]) + contact_frame def build_get_channel(channel_index: int) -> bytes: return bytes([CMD_GET_CHANNEL, channel_index]) def build_set_channel(channel_index: int, name: str, psk: bytes) -> bytes: name_bytes = name.encode("utf-8")[:32] name_bytes = name_bytes.ljust(32, b"\x00") psk_padded = bytearray(16) psk_padded[: len(psk)] = psk[:16] return bytes([CMD_SET_CHANNEL, channel_index]) + name_bytes + bytes(psk_padded) def build_set_radio_params( freq_hz: int, bw_hz: int, sf: int, cr: int, client_repeat: bool | None = None ) -> bytes: # freq_hz and bw_hz are in kHz (matching meshcore_py: freq*1000, bw*1000) frame = bytes([CMD_SET_RADIO_PARAMS]) frame += struct.pack(" bytes: return bytes([CMD_SET_RADIO_TX_POWER]) + struct.pack(" bytes: return bytes([CMD_REBOOT]) + b"reboot" def build_factory_reset() -> bytes: return bytes([CommandType.FACTORY_RESET.value]) + b"reset" def build_set_path_hash_mode(mode: int) -> bytes: """Set path hash mode: 0=1-byte, 1=2-byte, 2=3-byte.""" return bytes([CMD_SET_PATH_HASH_MODE, 0, mode]) def build_set_device_pin(pin: int) -> bytes: return bytes([CMD_SET_DEVICE_PIN]) + struct.pack(" bytes: """Set other params. Format: [0x26][manual_add_contacts][telemetry_mode][advert_loc_policy][multi_acks] """ return bytes( [CMD_SET_OTHER_PARAMS, manual_add_contacts, telemetry_flags, advert_loc_policy, multi_acks] ) def build_get_custom_vars() -> bytes: return bytes([CMD_GET_CUSTOM_VAR]) def build_set_custom_var(key_value: str) -> bytes: """Set a custom variable. Format: 'key:value' (e.g. 'gps:1').""" return bytes([CMD_SET_CUSTOM_VAR]) + key_value.encode("utf-8") + b"\x00" RESP_CODE_CUSTOM_VARS = PacketType.CUSTOM_VARS.value def parse_custom_vars(data: bytes) -> dict: """Parse CUSTOM_VARS response. Format: 'key1:val1,key2:val2,...'""" if len(data) < 2 or data[0] != RESP_CODE_CUSTOM_VARS: return {} raw = data[1:].decode("utf-8", "ignore").rstrip("\x00") if not raw: return {} result = {} for pair in raw.split(","): parts = pair.split(":", 1) if len(parts) == 2: result[parts[0]] = parts[1] return result def build_set_flood_scope(scope: str = "") -> bytes: """Set flood scope. Empty string or '*' to disable, '#name' for hashtag scope.""" import hashlib cmd = CommandType.SET_FLOOD_SCOPE.value if not scope or scope in ("0", "None", "*", ""): return bytes([cmd, 0]) + bytes(16) if not scope.startswith("#"): scope = "#" + scope scope_key = hashlib.sha256(scope.encode("utf-8")).digest()[:16] return bytes([cmd, 0]) + scope_key def build_get_advert_path(pub_key: bytes) -> bytes: """Get the advertisement path cached for a contact. Uses full 32-byte key.""" return bytes([CommandType.GET_ADVERT_PATH.value, 0]) + pub_key[:PUB_KEY_SIZE] def build_path_discovery(pub_key: bytes) -> bytes: """Send a path discovery request for a contact.""" return bytes([CommandType.PATH_DISCOVERY.value, 0]) + pub_key def parse_advert_path_response(data: bytes) -> dict | None: """Parse RESP_CODE_ADVERT_PATH (0x16). Format: [code][timestamp×4 LE][path_len_byte][path_bytes...] """ if len(data) < 6 or data[0] != PacketType.ADVERT_PATH.value: return None timestamp = struct.unpack_from("> 6) & 0x03) + 1 path_byte_len = hop_count * hash_size path_data = data[6 : 6 + path_byte_len] if path_byte_len > 0 else b"" return { "path_length": hop_count, "path_hash_size": hash_size, "path": path_data, "timestamp": timestamp, "hops": [ path_data[i : i + hash_size].hex().upper() for i in range(0, len(path_data), hash_size) ] if path_data else [], } def build_trace_path(tag: int, path_bytes: bytes, flags: int = 0) -> bytes: """Build a SEND_TRACE_PATH request. Format: [cmd][tag×4 LE][auth×4 LE][flag][path_hops...] flags encodes path hash size: 0=1-byte, 1=2-byte, 2=4-byte, 3=8-byte. """ frame = bytes([CMD_SEND_TRACE_PATH]) frame += struct.pack(" dict | None: """Parse PUSH_CODE_TRACE_DATA (0x89) response. Format: [code][reserved][path_byte_len][flags][tag×4][auth×4][path_data...][snr_values...] flags lower 2 bits encode hash size: 0→1B, 1→2B, 2→4B, 3→8B. """ if len(data) < 12 or data[0] != PUSH_CODE_TRACE_DATA: return None path_byte_len = data[2] flags = data[3] path_hash_size = 1 << (flags & 3) hop_count = path_byte_len // path_hash_size if path_hash_size else path_byte_len tag = data[4:8] # path data starts at offset 12 offset = 12 if offset + path_byte_len > len(data): return None path_data = data[offset : offset + path_byte_len] snr_raw = data[offset + path_byte_len :] snr_values = [int.from_bytes([b], signed=True, byteorder="little") / 4.0 for b in snr_raw] # Split path into per-hop hashes path_hops = [ path_data[i : i + path_hash_size] for i in range(0, hop_count * path_hash_size, path_hash_size) ] return { "tag": tag, "path_hops": path_hops, "path_hash_size": path_hash_size, "snr_values": snr_values, } def build_send_login(recipient_pub_key: bytes, password: str) -> bytes: return bytes([CMD_SEND_LOGIN]) + recipient_pub_key + password.encode("utf-8") + b"\x00" def build_send_status_req(recipient_pub_key: bytes) -> bytes: return bytes([CMD_SEND_STATUS_REQ]) + recipient_pub_key def build_get_auto_add_config() -> bytes: return bytes([CMD_GET_AUTO_ADD_CONFIG]) def build_set_auto_add_config( chat: bool = False, repeater: bool = False, room: bool = False, sensor: bool = False, overwrite_oldest: bool = False, max_hops: int = 0, ) -> bytes: flags = 0 if chat: flags |= AUTO_ADD_CHAT if repeater: flags |= AUTO_ADD_REPEATER if room: flags |= AUTO_ADD_ROOM_SERVER if sensor: flags |= AUTO_ADD_SENSOR if overwrite_oldest: flags |= AUTO_ADD_OVERWRITE_OLDEST return bytes([CMD_SET_AUTO_ADD_CONFIG, flags, min(max_hops, 64)]) def build_send_discover_req( type_filter: int = 0xFF, prefix_only: bool = True, tag: int | None = None ) -> bytes: """Send a zero-hop NODE_DISCOVER_REQ control packet. Args: type_filter: bitmask of ADV_TYPE_* bits (0xFF = all types) prefix_only: if True, responders send 8-byte key prefix; if False, full 32-byte key tag: random correlation tag (auto-generated if None) """ import random if tag is None: tag = random.randint(0, 0xFFFFFFFF) flags = CTRL_NODE_DISCOVER_REQ # 0x80 = upper nibble is sub_type 8 if prefix_only: flags |= 0x01 # bit 0 = prefix_only frame = bytes([CMD_SEND_CONTROL_DATA, flags, type_filter]) frame += struct.pack(" dict | None: """Parse a NODE_DISCOVER_RESP from a CONTROL_DATA push (0x8E). Companion protocol format: 0x8E + 2 prefix bytes + path_len(1) + path + flags(1) + snr(1) + tag(4) + pubkey """ if len(data) < 8: return None # data[0] = 0x8E, data[1:3] = prefix bytes, data[3] = path_length offset = 3 # skip code + 2 prefix bytes path_len_byte = data[offset] hop_count = path_len_byte & 0x3F hash_size = ((path_len_byte >> 6) & 0x03) + 1 offset += 1 offset += hop_count * hash_size # skip path hops if offset + 6 > len(data): # need flags + snr + tag(4) return None flags = data[offset] sub_type = (flags >> 4) & 0x0F if sub_type != 0x09: # DISCOVER_RESP sub_type = 9 return None node_type = flags & 0x0F snr = int.from_bytes(data[offset + 1 : offset + 2], "little", signed=True) / 4.0 tag = int.from_bytes(data[offset + 2 : offset + 6], "little", signed=False) pub_key_hex = data[offset + 6 :].hex() return { "node_type": node_type, "snr": snr, "tag": tag, "pub_key_hex": pub_key_hex, } def build_get_neighbors( repeater_pub_key: bytes, max_results: int = 10, offset: int = 0, key_len: int = 4 ) -> bytes: """Build a binary request to get neighbors from a repeater.""" frame = bytes([CMD_SEND_BINARY_REQ]) frame += repeater_pub_key frame += bytes( [ REQ_TYPE_GET_NEIGHBORS, 0x00, # version/reserved max_results, # max number of neighbors ] ) frame += struct.pack(" bytes: """Build a binary request to get ACL from a repeater.""" frame = bytes([CMD_SEND_BINARY_REQ]) frame += repeater_pub_key frame += bytes([REQ_TYPE_ACL, 0x00, 0x00]) return frame def build_get_telemetry(repeater_pub_key: bytes) -> bytes: """Build a binary request to get telemetry from a repeater/sensor.""" frame = bytes([CMD_SEND_BINARY_REQ]) frame += repeater_pub_key frame += bytes([REQ_TYPE_TELEMETRY]) return frame def build_get_telemetry_chat(pub_key: bytes = None) -> bytes: """Build a telemetry request for a chat companion node. Uses CMD_SEND_TELEMETRY_REQ (0x27) instead of CMD_SEND_BINARY_REQ. Response arrives as PUSH_CODE_TELEMETRY_RESPONSE (0x8B). If pub_key is None, requests self-telemetry from the connected device. """ cmd = CommandType.SEND_TELEMETRY_REQ.value frame = bytes([cmd]) if pub_key and len(pub_key) >= 32: frame += bytes(3) # reserved frame += pub_key[:32] else: frame += bytes(3) # reserved (self-telemetry) return frame ANON_REQ_REGIONS = 0x01 def build_anon_req_regions(pub_key: bytes, path_length: int, path: bytes) -> bytes: """Build an anonymous request for regions from a repeater. Format: [CMD_SEND_ANON_REQ][pub_key_32][req_type][path_len][path_reversed] """ cmd = CommandType.SEND_ANON_REQ.value frame = bytes([cmd]) frame += pub_key[:32] frame += bytes([ANON_REQ_REGIONS]) frame += bytes([path_length]) frame += bytes(reversed(path[:path_length])) if path_length > 0 else b"" return frame def parse_status_response(data: bytes) -> dict | None: """Parse PUSH_CODE_STATUS_RESPONSE (0x87) — 52/56-byte binary status. Frame: [code(1)][reserved(1)][sender_prefix(6)][status_data(52+)] """ if len(data) < 60: # 8 header + 52 payload return None # Skip 8 bytes: code(1) + reserved(1) + sender_prefix(6) d = data[8:] return { "battery_mv": struct.unpack("= 46 else 0, "dup_flood": struct.unpack("= 48 else 0, "rx_air_secs": struct.unpack("= 52 else 0, "recv_errors": struct.unpack("= 56 else None, } def parse_neighbors_response(data: bytes, key_len: int = 4) -> dict: """Parse a neighbors binary response (0x8C). Frame: [code(1)][reserved(1)][tag(4)][total_count(2 LE)][results_count(2 LE)][entries...] Each entry: key_prefix(key_len) + last_heard(4 LE) + snr(int8). Returns dict with 'total', 'neighbors' list. """ if len(data) < 10: return {"total": 0, "neighbors": []} # Skip 6-byte header: code(1) + reserved(1) + tag(4) total_count = struct.unpack(" bytes: """Build a stats request. sub_type: 0=Core, 1=Radio, 2=Packets.""" return bytes([CMD_GET_STATS, sub_type]) def parse_stats_response(data: bytes) -> dict | None: """Parse stats response (code 0x18). Byte 1 is sub-type.""" if len(data) < 4: return None sub_type = data[1] result = {"sub_type": sub_type} if sub_type == STATS_TYPE_CORE and len(data) >= 11: result["battery_mv"] = int.from_bytes(data[2:4], "little", signed=False) result["uptime_secs"] = int.from_bytes(data[4:8], "little", signed=False) result["errors"] = int.from_bytes(data[8:10], "little", signed=False) result["queue_len"] = data[10] elif sub_type == STATS_TYPE_RADIO and len(data) >= 14: result["noise_floor"] = int.from_bytes(data[2:4], "little", signed=True) result["last_rssi"] = int.from_bytes(data[4:5], "little", signed=True) result["last_snr"] = int.from_bytes(data[5:6], "little", signed=True) / 4.0 result["tx_air_secs"] = int.from_bytes(data[6:10], "little", signed=False) result["rx_air_secs"] = int.from_bytes(data[10:14], "little", signed=False) elif sub_type == STATS_TYPE_PACKETS and len(data) >= 26: result["recv"] = int.from_bytes(data[2:6], "little", signed=False) result["sent"] = int.from_bytes(data[6:10], "little", signed=False) result["flood_tx"] = int.from_bytes(data[10:14], "little", signed=False) result["direct_tx"] = int.from_bytes(data[14:18], "little", signed=False) result["flood_rx"] = int.from_bytes(data[18:22], "little", signed=False) result["direct_rx"] = int.from_bytes(data[22:26], "little", signed=False) if len(data) >= 30: result["recv_errors"] = int.from_bytes(data[26:30], "little", signed=False) return result def parse_error_response(data: bytes) -> dict | None: """Parse error response (code 0x01). Byte 1 is error code.""" if len(data) < 2: return {"code": 0, "message": "Unknown error"} code = data[1] return { "code": code, "message": ERROR_CODES.get(code, f"Unknown error (0x{code:02x})"), } # ─── Frame Parsers ──────────────────────────────────────────────── # Sync parsing following meshcore.reader.MessageReader.handle_rx field names def identify_response(data: bytes) -> int | None: if not data: return None return data[0] def parse_contact_frame(data: bytes) -> dict | None: """Parse contact frame, matching meshcore reader field names.""" if len(data) < 100: return None code = data[0] if code not in (RESP_CODE_CONTACT, PUSH_CODE_NEW_ADVERT): return None dbuf = io.BytesIO(data[1:]) c = {} c["public_key"] = dbuf.read(32).hex() c["type"] = dbuf.read(1)[0] c["flags"] = dbuf.read(1)[0] plen = int.from_bytes(dbuf.read(1), signed=False, byteorder="little") if plen == 255: c["out_path_len"] = -1 c["path_hash_size"] = 1 else: c["out_path_len"] = plen & 0x3F c["path_hash_size"] = ((plen >> 6) & 0x03) + 1 # bits 6-7: hash_size - 1 raw_path = dbuf.read(64) # Extract meaningful path bytes based on hop count and hash size hop_count = max(c["out_path_len"], 0) hash_size = c["path_hash_size"] path_byte_len = hop_count * hash_size c["out_path"] = raw_path[:path_byte_len].hex() if path_byte_len > 0 else "" c["adv_name"] = dbuf.read(32).decode("utf-8", "ignore").replace("\0", "") c["last_advert"] = int.from_bytes(dbuf.read(4), byteorder="little") c["adv_lat"] = int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6 c["adv_lon"] = int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6 lastmod_bytes = dbuf.read(4) if lastmod_bytes and len(lastmod_bytes) == 4: c["lastmod"] = int.from_bytes(lastmod_bytes, byteorder="little") return c def parse_self_info(data: bytes) -> dict | None: """Parse SELF_INFO, matching meshcore reader field names.""" if len(data) < 36 or data[0] != RESP_CODE_SELF_INFO: return None dbuf = io.BytesIO(data[1:]) si = {} si["adv_type"] = dbuf.read(1)[0] si["tx_power"] = int.from_bytes(dbuf.read(1), signed=True, byteorder="little") si["max_tx_power"] = dbuf.read(1)[0] si["public_key"] = dbuf.read(32).hex() si["adv_lat"] = int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6 si["adv_lon"] = int.from_bytes(dbuf.read(4), byteorder="little", signed=True) / 1e6 si["multi_acks"] = dbuf.read(1)[0] si["adv_loc_policy"] = dbuf.read(1)[0] telemetry_mode = dbuf.read(1)[0] si["telemetry_mode_env"] = (telemetry_mode >> 4) & 0b11 si["telemetry_mode_loc"] = (telemetry_mode >> 2) & 0b11 si["telemetry_mode_base"] = telemetry_mode & 0b11 si["manual_add_contacts"] = dbuf.read(1)[0] > 0 si["radio_freq"] = int.from_bytes(dbuf.read(4), byteorder="little") / 1000 si["radio_bw"] = int.from_bytes(dbuf.read(4), byteorder="little") / 1000 si["radio_sf"] = dbuf.read(1)[0] si["radio_cr"] = dbuf.read(1)[0] si["name"] = dbuf.read().decode("utf-8", "ignore").rstrip("\x00") return si def parse_device_info(data: bytes) -> dict | None: """Parse DEVICE_INFO, matching meshcore reader field names.""" if len(data) < 4 or data[0] != RESP_CODE_DEVICE_INFO: return None dbuf = io.BytesIO(data[1:]) res = {} fw_ver = dbuf.read(1)[0] res["fw_ver"] = fw_ver if fw_ver >= 3: res["max_contacts"] = dbuf.read(1)[0] * 2 res["max_channels"] = dbuf.read(1)[0] res["ble_pin"] = int.from_bytes(dbuf.read(4), byteorder="little") res["fw_build"] = dbuf.read(12).decode("utf-8", "ignore").replace("\0", "") res["model"] = dbuf.read(40).decode("utf-8", "ignore").replace("\0", "") res["ver"] = dbuf.read(20).decode("utf-8", "ignore").replace("\0", "") if fw_ver >= 9: rpt = dbuf.read(1) if len(rpt) > 0: res["repeat"] = rpt[0] != 0 if fw_ver >= 10: phm = dbuf.read(1) if len(phm) > 0: res["path_hash_mode"] = phm[0] return res def parse_batt_and_storage(data: bytes) -> dict | None: """Parse battery/storage, matching meshcore reader field names.""" if len(data) < 3 or data[0] != RESP_CODE_BATT_AND_STORAGE: return None dbuf = io.BytesIO(data[1:]) result = {} result["level"] = int.from_bytes(dbuf.read(2), byteorder="little") if len(data) > 3: result["used_kb"] = int.from_bytes(dbuf.read(4), byteorder="little") result["total_kb"] = int.from_bytes(dbuf.read(4), byteorder="little") return result def parse_contact_message(data: bytes) -> dict | None: """Parse incoming DM, matching meshcore reader field names.""" if len(data) < 10: return None code = data[0] if code not in (RESP_CODE_CONTACT_MSG_RECV, RESP_CODE_CONTACT_MSG_RECV_V3): return None dbuf = io.BytesIO(data[1:]) res = {} res["type"] = "PRIV" if code == RESP_CODE_CONTACT_MSG_RECV_V3: res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4 dbuf.read(2) # reserved res["pubkey_prefix"] = dbuf.read(6).hex() plen = dbuf.read(1)[0] if plen == 255: res["path_len"] = -1 res["path_hash_size"] = 1 else: res["path_len"] = plen & 0x3F res["path_hash_size"] = ((plen >> 6) & 0x03) + 1 txt_type = dbuf.read(1)[0] res["txt_type"] = txt_type res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little") if txt_type == 2: res["signature"] = dbuf.read(4).hex() res["text"] = _smaz_decode(dbuf.read().decode("utf-8", "ignore").rstrip("\x00")) return res def parse_channel_message(data: bytes) -> dict | None: """Parse incoming channel message, matching meshcore reader field names.""" if len(data) < 8: return None code = data[0] if code not in (RESP_CODE_CHANNEL_MSG_RECV, RESP_CODE_CHANNEL_MSG_RECV_V3): return None dbuf = io.BytesIO(data[1:]) res = {} res["type"] = "CHAN" if code == RESP_CODE_CHANNEL_MSG_RECV_V3: res["SNR"] = int.from_bytes(dbuf.read(1), byteorder="little", signed=True) / 4 dbuf.read(2) # reserved res["channel_idx"] = dbuf.read(1)[0] plen = dbuf.read(1)[0] if plen == 255: res["path_len"] = -1 res["path_hash_size"] = 1 else: res["path_len"] = plen & 0x3F res["path_hash_size"] = ((plen >> 6) & 0x03) + 1 res["txt_type"] = dbuf.read(1)[0] res["path_bytes"] = "" # Path bytes come via LOG_DATA (0x88), not here res["sender_timestamp"] = int.from_bytes(dbuf.read(4), byteorder="little", signed=False) text = dbuf.read().strip(b"\0") res["text"] = text.decode("utf-8", "ignore") # Channel messages have format "name: message" if ": " in res["text"]: res["sender_name"], res["msg_text"] = res["text"].split(": ", 1) else: res["sender_name"] = "" res["msg_text"] = res["text"] res["msg_text"] = _smaz_decode(res["msg_text"]) return res def parse_channel_info(data: bytes) -> dict | None: """Parse channel info, matching meshcore reader field names.""" if len(data) < 50 or data[0] != RESP_CODE_CHANNEL_INFO: return None dbuf = io.BytesIO(data[1:]) res = {} res["channel_idx"] = dbuf.read(1)[0] name_bytes = dbuf.read(32) null_pos = name_bytes.find(0) if null_pos >= 0: res["channel_name"] = name_bytes[:null_pos].decode("utf-8", "ignore") else: res["channel_name"] = name_bytes.decode("utf-8", "ignore") res["channel_secret"] = dbuf.read(16) return res def parse_sent_response(data: bytes) -> dict | None: """Parse RESP_CODE_SENT, matching meshcore reader field names.""" if len(data) < 2 or data[0] != RESP_CODE_SENT: return None dbuf = io.BytesIO(data[1:]) res = {} res["type"] = dbuf.read(1)[0] res["expected_ack"] = dbuf.read(4) res["suggested_timeout"] = int.from_bytes(dbuf.read(4), byteorder="little") return res def parse_push_send_confirmed(data: bytes) -> dict | None: """Parse ACK push.""" if len(data) < 5 or data[0] != PUSH_CODE_SEND_CONFIRMED: return None res = {} res["code"] = data[1:5].hex() if len(data) >= 9: res["trip_time_ms"] = struct.unpack_from(" dict | None: """Parse auto-add config response.""" if len(data) < 2 or data[0] != RESP_CODE_AUTO_ADD_CONFIG: return None config = data[1] max_hops = data[2] if len(data) >= 3 else 0 return {"config": config, "max_hops": max_hops} def parse_curr_time(data: bytes) -> int | None: if len(data) < 5 or data[0] != RESP_CODE_CURR_TIME: return None return struct.unpack_from(" list[dict]: """Parse Cayenne LPP encoded telemetry data. Returns list of dicts with 'channel', 'type', 'name', 'value', 'unit' keys. """ results = [] buf = io.BytesIO(data) while buf.tell() < len(data) - 1: channel = buf.read(1) type_byte = buf.read(1) if len(channel) < 1 or len(type_byte) < 1: break ch = channel[0] tp = type_byte[0] if ch == 0 and tp == 0: break name = LPP_TYPE_NAMES.get(tp, f"Type {tp}") unit = LPP_TYPE_UNITS.get(tp, "") try: if tp in (LPP_DIGITAL_INPUT, LPP_DIGITAL_OUTPUT, LPP_PRESENCE, LPP_SWITCH): val = buf.read(1)[0] elif tp in (LPP_ANALOG_INPUT, LPP_ANALOG_OUTPUT): val = struct.unpack(">h", buf.read(2))[0] / 100.0 elif tp == LPP_GENERIC_SENSOR: val = struct.unpack(">I", buf.read(4))[0] elif tp == LPP_LUMINOSITY: val = struct.unpack(">H", buf.read(2))[0] elif tp == LPP_TEMPERATURE: val = struct.unpack(">h", buf.read(2))[0] / 10.0 elif tp == LPP_HUMIDITY: val = buf.read(1)[0] / 2.0 elif tp == LPP_PERCENTAGE: val = buf.read(1)[0] elif tp in (LPP_ACCELEROMETER, LPP_GYROMETER): raw = buf.read(6) scale = 0.001 if tp == LPP_ACCELEROMETER else 0.01 val = { "x": struct.unpack(">h", raw[0:2])[0] * scale, "y": struct.unpack(">h", raw[2:4])[0] * scale, "z": struct.unpack(">h", raw[4:6])[0] * scale, } elif tp == LPP_PRESSURE: val = struct.unpack(">H", buf.read(2))[0] / 10.0 elif tp == LPP_VOLTAGE: val = struct.unpack(">h", buf.read(2))[0] / 100.0 elif tp == LPP_CURRENT: val = struct.unpack(">h", buf.read(2))[0] / 1000.0 elif tp in (LPP_FREQUENCY, LPP_UNIX_TIME): val = struct.unpack(">I", buf.read(4))[0] elif tp == LPP_ALTITUDE: val = struct.unpack(">h", buf.read(2))[0] elif tp in (LPP_CONCENTRATION, LPP_POWER, LPP_DIRECTION): val = struct.unpack(">H", buf.read(2))[0] elif tp in (LPP_DISTANCE, LPP_ENERGY): val = struct.unpack(">I", buf.read(4))[0] / 1000.0 elif tp == LPP_COLOUR: raw = buf.read(3) val = {"r": raw[0], "g": raw[1], "b": raw[2]} elif tp == LPP_GPS: raw = buf.read(9) lat = int.from_bytes(raw[0:3], "big", signed=True) / 10000.0 lon = int.from_bytes(raw[3:6], "big", signed=True) / 10000.0 alt = int.from_bytes(raw[6:9], "big", signed=True) / 100.0 val = {"lat": lat, "lon": lon, "alt": alt} else: # Unknown type — can't determine size, stop parsing break results.append( { "channel": ch, "type": tp, "name": name, "value": val, "unit": unit, } ) except (struct.error, IndexError): break return results meshy/src/qr_scanner.py000066400000000000000000000306221521052255700155060ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """QR code scanner using XDG Camera Portal + PipeWire + GStreamer.""" import logging import os import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Gst", "1.0") from gi.repository import Adw, Gio, GLib, Gst, Gtk log = logging.getLogger(__name__) Gst.init(None) PORTAL_BUS_NAME = "org.freedesktop.portal.Desktop" PORTAL_OBJECT_PATH = "/org/freedesktop/portal/desktop" PORTAL_CAMERA_IFACE = "org.freedesktop.portal.Camera" PORTAL_REQUEST_IFACE = "org.freedesktop.portal.Request" class QRScannerDialog: """Dialog that opens the camera via XDG portal and scans for QR codes.""" def __init__(self, window, on_result): self._window = window self._on_result = on_result self._pipeline = None self._dialog = None self._signal_id = None def start(self): """Request camera access via the XDG portal.""" try: self._bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) except GLib.Error as e: log.error(f"Cannot connect to session bus: {e.message}") self._show_error(_("Cannot connect to session bus: {}").format(e.message)) return # Check if camera portal is available try: result = self._bus.call_sync( PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, "org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", (PORTAL_CAMERA_IFACE, "IsCameraPresent")), GLib.VariantType.new("(v)"), Gio.DBusCallFlags.NONE, 5000, None, ) is_present = result.get_child_value(0).get_variant().get_boolean() if not is_present: self._show_error(_("No camera detected on this system.")) return except GLib.Error as e: log.warning(f"Could not check IsCameraPresent: {e.message}") # Continue anyway, the portal might still work self._access_camera() def _access_camera(self): """Call AccessCamera on the portal.""" token = f"meshy_{os.getpid()}" sender = self._bus.get_unique_name().replace(".", "_").lstrip(":") request_path = f"/org/freedesktop/portal/desktop/request/{sender}/{token}" log.info(f"AccessCamera: subscribing to {request_path}") # Subscribe to the Response signal before making the call self._signal_id = self._bus.signal_subscribe( PORTAL_BUS_NAME, PORTAL_REQUEST_IFACE, "Response", request_path, None, Gio.DBusSignalFlags.NO_MATCH_RULE, self._on_access_response, ) params = GLib.Variant( "(a{sv})", ( { "handle_token": GLib.Variant("s", token), }, ), ) self._bus.call( PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, PORTAL_CAMERA_IFACE, "AccessCamera", params, None, # accept any reply type Gio.DBusCallFlags.NONE, 30000, None, self._on_access_call_done, ) def _on_access_call_done(self, bus, result): try: res = bus.call_finish(result) log.info(f"AccessCamera returned: {res}") except GLib.Error as e: log.error(f"AccessCamera call failed: {e.message}") self._cleanup_signal() self._show_error(_("Could not access camera: {}").format(e.message)) def _on_access_response(self, bus, sender, path, iface, signal, params): log.info(f"AccessCamera response received: {params}") self._cleanup_signal() response = params.get_child_value(0).get_uint32() if response != 0: log.warning(f"Camera access denied, response={response}") GLib.idle_add(lambda: self._show_error(_("Camera access was denied.")) or False) return # Permission granted, open PipeWire remote GLib.idle_add(lambda: self._open_pipewire_remote() or False) def _cleanup_signal(self): if self._signal_id: self._bus.signal_unsubscribe(self._signal_id) self._signal_id = None def _open_pipewire_remote(self): """Get a PipeWire fd from the portal.""" try: msg = Gio.DBusMessage.new_method_call( PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, PORTAL_CAMERA_IFACE, "OpenPipeWireRemote", ) msg.set_body(GLib.Variant("(a{sv})", ({},))) result = self._bus.send_message_with_reply_sync( msg, Gio.DBusSendMessageFlags.NONE, 5000, None, ) # send_message_with_reply_sync returns (reply_msg, out_serial) reply_msg = result[0] if isinstance(result, tuple) else result if reply_msg.get_message_type() == Gio.DBusMessageType.ERROR: error_name = reply_msg.get_error_name() log.error(f"OpenPipeWireRemote error: {error_name}") self._show_error(_("Could not open camera stream.")) return fd_list = reply_msg.get_unix_fd_list() if not fd_list or fd_list.get_length() == 0: log.error("OpenPipeWireRemote: no fd in reply") self._show_error(_("No camera file descriptor received.")) return pw_fd = fd_list.get(0) log.info(f"Got PipeWire fd: {pw_fd}") self._start_scanning(pw_fd) except GLib.Error as e: log.error(f"OpenPipeWireRemote failed: {e.message}") self._show_error(_("Could not open camera: {}").format(e.message)) except Exception as e: log.error(f"OpenPipeWireRemote unexpected error: {e}") self._show_error(_("Could not open camera: {}").format(e)) def _start_scanning(self, pw_fd): """Build GStreamer pipeline and show the camera dialog.""" self._dialog = Adw.Dialog() self._dialog.set_title(_("Scan QR Code")) self._dialog.set_content_width(400) self._dialog.set_content_height(400) self._dialog.connect("closed", self._on_dialog_closed) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=8, margin_end=8, margin_bottom=8, ) # Video preview self._picture = Gtk.Picture() self._picture.set_size_request(360, 270) self._picture.set_vexpand(True) self._picture.set_content_fit(Gtk.ContentFit.CONTAIN) content.append(self._picture) # Status label self._status_label = Gtk.Label(label=_("Point the camera at a QR code...")) self._status_label.add_css_class("dim-label") content.append(self._status_label) toolbar_view.set_content(content) self._dialog.set_child(toolbar_view) # Build GStreamer pipeline pipeline_str = ( f"pipewiresrc fd={pw_fd} ! " "videoconvert ! " "video/x-raw,format=RGB ! " "tee name=t " "t. ! queue ! videoconvert ! gtk4paintablesink name=sink " "t. ! queue max-size-buffers=1 leaky=downstream ! " "videoscale ! video/x-raw,width=640,height=480 ! " "videoconvert ! video/x-raw,format=GRAY8 ! " "appsink name=qrsink emit-signals=true max-buffers=1 drop=true" ) try: self._pipeline = Gst.parse_launch(pipeline_str) except GLib.Error as e: log.error(f"Failed to create GStreamer pipeline: {e.message}") self._show_error(_("Camera pipeline error: {}").format(e.message)) return # Connect the paintable sink to the Picture widget sink = self._pipeline.get_by_name("sink") paintable = sink.get_property("paintable") self._picture.set_paintable(paintable) # Connect to appsink for QR decoding appsink = self._pipeline.get_by_name("qrsink") appsink.connect("new-sample", self._on_new_sample) # Watch for pipeline errors bus = self._pipeline.get_bus() bus.add_signal_watch() bus.connect("message::error", self._on_pipeline_error) self._pipeline.set_state(Gst.State.PLAYING) self._dialog.present(self._window) self._scan_active = True self._scan_start_time = GLib.get_monotonic_time() self._hint_shown = False self._timeout_shown = False self._hint_timer_id = GLib.timeout_add(8000, self._on_scan_hint) self._timeout_timer_id = GLib.timeout_add(30000, self._on_scan_timeout) def _on_new_sample(self, appsink): """Pull a frame from appsink and try to decode QR codes.""" if not self._scan_active: return Gst.FlowReturn.OK sample = appsink.emit("pull-sample") if not sample: return Gst.FlowReturn.OK buf = sample.get_buffer() caps = sample.get_caps() struct = caps.get_structure(0) width = struct.get_value("width") height = struct.get_value("height") struct.get_string("format") success, mapinfo = buf.map(Gst.MapFlags.READ) if not success: return Gst.FlowReturn.OK try: from pyzbar import pyzbar # pyzbar can decode from raw (data, width, height) tuple for 'L' (grayscale) # or from PIL Image. For RGB data, use the tuple form with 3 channels. results = pyzbar.decode( (bytes(mapinfo.data), width, height), symbols=[pyzbar.ZBarSymbol.QRCODE], ) except Exception as e: log.debug(f"QR decode error: {e}") buf.unmap(mapinfo) return Gst.FlowReturn.OK buf.unmap(mapinfo) for qr in results: data = qr.data.decode("utf-8", errors="ignore").strip() log.info(f"QR code detected: {data[:80]}...") self._scan_active = False GLib.idle_add(self._on_qr_found, data) return Gst.FlowReturn.OK return Gst.FlowReturn.OK def _on_scan_hint(self): """Show a hint after 8 seconds of no detection.""" self._hint_timer_id = None if self._scan_active: self._status_label.set_label( _( "No QR code detected yet.\n" "Make sure the code is well-lit, sharp, and fills most of the frame." ) ) return False def _on_scan_timeout(self): """Show a give-up message after 30 seconds.""" self._timeout_timer_id = None if self._scan_active: self._status_label.set_label( _("Could not detect QR code.\n" "Try importing the contact via clipboard instead.") ) return False def _on_qr_found(self, data): """Handle a detected meshcore:// QR code.""" self._status_label.set_label(_("QR code found!")) self._stop_pipeline() if self._dialog: self._dialog.close() self._on_result(data) return False def _on_pipeline_error(self, bus, msg): err, debug = msg.parse_error() log.error(f"GStreamer error: {err.message} ({debug})") GLib.idle_add(lambda: self._show_error(_("Camera error: {}").format(err.message)) or False) def _on_dialog_closed(self, dialog): self._stop_pipeline() def _stop_pipeline(self): self._scan_active = False if getattr(self, "_hint_timer_id", None): GLib.source_remove(self._hint_timer_id) self._hint_timer_id = None if getattr(self, "_timeout_timer_id", None): GLib.source_remove(self._timeout_timer_id) self._timeout_timer_id = None if self._pipeline: self._pipeline.set_state(Gst.State.NULL) self._pipeline = None def _show_error(self, message): dialog = Adw.AlertDialog(heading=_("Camera Error"), body=message) dialog.add_response("ok", _("OK")) dialog.present(self._window) meshy/src/smaz.py000066400000000000000000000111031521052255700143160ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """SMAZ decompression for interoperability with meshcore-open clients. Meshcore-open can compress short text messages using the SMAZ algorithm (dictionary-based compression for short ASCII strings). Compressed messages are sent with an 's:' prefix followed by base64-encoded SMAZ data. """ import base64 _VERBATIM_SINGLE = 254 _VERBATIM_RUN = 255 _CODEBOOK = [ " ", "the", "e", "t", "a", "of", "o", "and", "i", "n", "s", "e ", "r", " th", " t", "in", "he", "th", "h", "he ", "to", "\r\n", "l", "s ", "d", " a", "an", "er", "c", " o", "d ", "on", " of", "re", "of ", "t ", ", ", "is", "u", "at", " ", "n ", "or", "which", "f", "m", "as", "it", "that", "\n", "was", "en", " ", " w", "es", " an", " i", "\r", "f ", "g", "p", "nd", " s", "nd ", "ed ", "w", "ed", "http://", "for", "te", "ing", "y ", "The", " c", "ti", "r ", "his", "st", " in", "ar", "nt", ",", " to", "y", "ng", " h", "with", "le", "al", "to ", "b", "ou", "be", "were", " b", "se", "o ", "ent", "ha", "ng ", "their", '"', "hi", "from", " f", "in ", "de", "ion", "me", "v", ".", "ve", "all", "re ", "ri", "ro", "is ", "co", "f t", "are", "ea", ". ", "her", " m", "er ", " p", "es ", "by", "they", "di", "ra", "ic", "not", "s, ", "d t", "at ", "ce", "la", "h ", "ne", "as ", "tio", "on ", "n t", "io", "we", " a ", "om", ", a", "s o", "ur", "li", "ll", "ch", "had", "this", "e t", "g ", "e\r\n", " wh", "ere", " co", "e o", "a ", "us", " d", "ss", "\n\r\n", "\r\n\r", '="', " be", " e", "s a", "ma", "one", "t t", "or ", "but", "el", "so", "l ", "e s", "s,", "no", "ter", " wa", "iv", "ho", "e a", " r", "hat", "s t", "ns", "ch ", "wh", "tr", "ut", "/", "have", "ly ", "ta", " ha", " on", "tha", "-", " l", "ati", "en ", "pe", " re", "there", "ass", "si", " fo", "wa", "ec", "our", "who", "its", "z", "fo", "rs", ">", "ot", "un", "<", "im", "th ", "nc", "ate", "><", "ver", "ad", " we", "ly", "ee", " n", "id", " cl", "ac", "il", " bytes: out = bytearray() i = 0 while i < len(data): code = data[i] if code == _VERBATIM_SINGLE: if i + 1 >= len(data): break out.append(data[i + 1]) i += 2 elif code == _VERBATIM_RUN: if i + 1 >= len(data): break length = data[i + 1] + 1 end = i + 2 + length if end > len(data): break out.extend(data[i + 2 : end]) i = end elif code < len(_CODEBOOK_BYTES): out.extend(_CODEBOOK_BYTES[code]) i += 1 else: break return bytes(out) def try_decode_prefixed(text: str) -> str: """If text starts with 's:', decode base64 + SMAZ. Otherwise return as-is.""" stripped = text.lstrip() if not stripped.startswith("s:") or len(stripped) <= 2: return text encoded = stripped[2:] try: compressed = base64.b64decode(encoded, validate=True) except Exception: try: normalized = encoded.replace("-", "+").replace("_", "/") pad = len(normalized) % 4 if pad: normalized += "=" * (4 - pad) compressed = base64.b64decode(normalized) except Exception: return text try: return _decompress(compressed).decode("utf-8", errors="replace") except Exception: return text meshy/src/storage.py000066400000000000000000000756041521052255700150300ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """SQLite storage for contacts, messages, and channels.""" import contextlib import os import sqlite3 from datetime import datetime from meshy.models import Channel, ChannelMessage, Contact, Message, MessageStatus, NotificationLevel class Storage: @staticmethod def db_path_for_device(device_address: str) -> str: data_dir = os.path.join( os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "meshy" ) safe_addr = device_address.replace(":", "_").replace("/", "_") return os.path.join(data_dir, f"device_{safe_addr}.db") @staticmethod def delete_device_db(device_address: str): path = Storage.db_path_for_device(device_address) for suffix in ("", "-wal", "-shm"): with contextlib.suppress(FileNotFoundError): os.remove(path + suffix) def __init__(self, db_path: str | None = None, device_address: str | None = None): if db_path is None: if device_address: db_path = self.db_path_for_device(device_address) os.makedirs(os.path.dirname(db_path), exist_ok=True) else: data_dir = os.path.join( os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")), "meshy" ) os.makedirs(data_dir, exist_ok=True) db_path = os.path.join(data_dir, "meshy.db") self._db = sqlite3.connect(db_path) self._db.row_factory = sqlite3.Row self._db.execute("PRAGMA journal_mode=WAL") self._batch_depth = 0 self._create_tables() def _create_tables(self): self._db.executescript(""" CREATE TABLE IF NOT EXISTS contacts ( public_key_hex TEXT PRIMARY KEY, name TEXT NOT NULL, type INTEGER NOT NULL DEFAULT 1, flags INTEGER NOT NULL DEFAULT 0, path_length INTEGER NOT NULL DEFAULT -1, path BLOB NOT NULL DEFAULT X'', latitude REAL, longitude REAL, last_seen REAL, last_message_at REAL, is_active INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, contact_key_hex TEXT NOT NULL, sender_key_hex TEXT NOT NULL, text TEXT NOT NULL, timestamp REAL NOT NULL, is_outgoing INTEGER NOT NULL, is_cli INTEGER NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, message_id TEXT, FOREIGN KEY (contact_key_hex) REFERENCES contacts(public_key_hex) ); CREATE TABLE IF NOT EXISTS channels ( idx INTEGER PRIMARY KEY, name TEXT NOT NULL DEFAULT '', psk BLOB NOT NULL DEFAULT X'00000000000000000000000000000000', unread_count INTEGER NOT NULL DEFAULT 0, notification_level INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS channel_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_index INTEGER NOT NULL, sender_name TEXT NOT NULL DEFAULT '', text TEXT NOT NULL, timestamp REAL NOT NULL, is_outgoing INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (channel_index) REFERENCES channels(idx) ); CREATE TABLE IF NOT EXISTS channel_message_paths ( id INTEGER PRIMARY KEY AUTOINCREMENT, channel_index INTEGER NOT NULL, sender_timestamp INTEGER NOT NULL, msg_text_hash TEXT NOT NULL, snr REAL, path_len INTEGER NOT NULL DEFAULT -1, path_bytes TEXT NOT NULL DEFAULT '' ); CREATE TABLE IF NOT EXISTS regions ( name TEXT PRIMARY KEY ); CREATE TABLE IF NOT EXISTS discovered_contacts ( public_key_hex TEXT PRIMARY KEY, name TEXT NOT NULL, type INTEGER NOT NULL DEFAULT 1, flags INTEGER NOT NULL DEFAULT 0, path_length INTEGER NOT NULL DEFAULT -1, path BLOB NOT NULL DEFAULT X'', path_hash_size INTEGER NOT NULL DEFAULT 1, latitude REAL, longitude REAL, last_seen REAL ); CREATE INDEX IF NOT EXISTS idx_messages_contact ON messages(contact_key_hex, timestamp); CREATE INDEX IF NOT EXISTS idx_channel_messages_channel ON channel_messages(channel_index, timestamp); CREATE INDEX IF NOT EXISTS idx_channel_message_paths ON channel_message_paths(channel_index, sender_timestamp, msg_text_hash); """) self._db.commit() self._migrate() def _migrate(self): """Add columns that may not exist in older databases.""" cursor = self._db.execute("PRAGMA table_info(channels)") ch_columns = {row[1] for row in cursor.fetchall()} if "notification_level" not in ch_columns: self._db.execute( "ALTER TABLE channels ADD COLUMN notification_level INTEGER NOT NULL DEFAULT 0" ) if "last_read_at" not in ch_columns: self._db.execute("ALTER TABLE channels ADD COLUMN last_read_at REAL") if "region" not in ch_columns: self._db.execute("ALTER TABLE channels ADD COLUMN region TEXT NOT NULL DEFAULT ''") cursor = self._db.execute("PRAGMA table_info(contacts)") ct_columns = {row[1] for row in cursor.fetchall()} if "last_read_at" not in ct_columns: self._db.execute("ALTER TABLE contacts ADD COLUMN last_read_at REAL") if "path_hash_size" not in ct_columns: self._db.execute( "ALTER TABLE contacts ADD COLUMN path_hash_size INTEGER NOT NULL DEFAULT 1" ) if "device_lastmod" not in ct_columns: self._db.execute("ALTER TABLE contacts ADD COLUMN device_lastmod REAL") cursor = self._db.execute("PRAGMA table_info(messages)") msg_columns = {row[1] for row in cursor.fetchall()} if "room_sender_name" not in msg_columns: self._db.execute( "ALTER TABLE messages ADD COLUMN room_sender_name TEXT NOT NULL DEFAULT ''" ) self._db.execute(""" CREATE TABLE IF NOT EXISTS room_passwords ( room_key_hex TEXT PRIMARY KEY, password TEXT NOT NULL ) """) self._db.execute(""" CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '' ) """) self._db.execute(""" CREATE TABLE IF NOT EXISTS discovered_contacts ( public_key_hex TEXT PRIMARY KEY, name TEXT NOT NULL, type INTEGER NOT NULL DEFAULT 1, flags INTEGER NOT NULL DEFAULT 0, path_length INTEGER NOT NULL DEFAULT -1, path BLOB NOT NULL DEFAULT X'', path_hash_size INTEGER NOT NULL DEFAULT 1, latitude REAL, longitude REAL, last_seen REAL ) """) self._db.commit() self._cleanup_old_data() def _cleanup_old_data(self): """Remove stale data that is no longer relevant.""" import time week_ago = time.time() - 7 * 86400 self._db.execute( "DELETE FROM channel_message_paths WHERE sender_timestamp < ?", (int(week_ago),) ) ninety_days_ago = time.time() - 90 * 86400 self._db.execute("DELETE FROM discovered_contacts WHERE last_seen < ?", (ninety_days_ago,)) self._db.commit() def _commit(self): if self._batch_depth == 0: self._db.commit() @contextlib.contextmanager def batch(self): self._batch_depth += 1 try: yield finally: self._batch_depth -= 1 if self._batch_depth == 0: self._commit() def close(self): self._db.close() # ─── Contacts ────────────────────────────────────────────── def save_contact(self, contact: Contact): self._db.execute( """ INSERT INTO contacts (public_key_hex, name, type, flags, path_length, path, path_hash_size, latitude, longitude, last_seen, device_lastmod, last_message_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(public_key_hex) DO UPDATE SET name = excluded.name, type = excluded.type, flags = excluded.flags, path_length = excluded.path_length, path = excluded.path, path_hash_size = excluded.path_hash_size, latitude = excluded.latitude, longitude = excluded.longitude, last_seen = excluded.last_seen, device_lastmod = excluded.device_lastmod, last_message_at = excluded.last_message_at, is_active = excluded.is_active """, ( contact.public_key_hex, contact.name, contact.type, contact.flags, contact.path_length, contact.path, contact.path_hash_size, contact.latitude, contact.longitude, contact.last_seen.timestamp() if contact.last_seen else None, contact.device_lastmod.timestamp() if contact.device_lastmod else None, contact.last_message_at.timestamp() if contact.last_message_at else None, int(contact.is_active), ), ) self._commit() def deactivate_contact(self, public_key_hex: str): """Mark a contact as inactive (no longer on the device).""" self._db.execute( "UPDATE contacts SET is_active = 0 WHERE public_key_hex = ?", (public_key_hex,) ) self._commit() def get_contacts(self) -> list[Contact]: rows = self._db.execute( "SELECT * FROM contacts WHERE is_active = 1 ORDER BY last_seen DESC" ).fetchall() return [self._row_to_contact(r) for r in rows] def get_max_device_lastmod(self) -> int | None: """Return the highest device_lastmod timestamp among active contacts.""" row = self._db.execute( "SELECT MAX(device_lastmod) FROM contacts WHERE is_active = 1" ).fetchone() val = row[0] if row else None return int(val) if val else None def get_contact(self, public_key_hex: str) -> Contact | None: row = self._db.execute( "SELECT * FROM contacts WHERE public_key_hex = ?", (public_key_hex,) ).fetchone() return self._row_to_contact(row) if row else None def remove_contact(self, public_key_hex: str): self._db.execute("DELETE FROM contacts WHERE public_key_hex = ?", (public_key_hex,)) self._db.execute("DELETE FROM messages WHERE contact_key_hex = ?", (public_key_hex,)) self._commit() def find_contact_by_prefix(self, prefix: bytes) -> Contact | None: """Find a contact by the first 6 bytes of its public key.""" prefix_hex = prefix.hex() upper = prefix_hex[:-1] + chr(ord(prefix_hex[-1]) + 1) row = self._db.execute( "SELECT * FROM contacts WHERE public_key_hex >= ? AND public_key_hex < ?", (prefix_hex, upper), ).fetchone() return self._row_to_contact(row) if row else None def _row_to_contact(self, row) -> Contact: return Contact( public_key=bytes.fromhex(row["public_key_hex"]), name=row["name"], type=row["type"], flags=row["flags"], path_length=row["path_length"], path=bytes(row["path"]), path_hash_size=row["path_hash_size"], latitude=row["latitude"], longitude=row["longitude"], last_seen=datetime.fromtimestamp(row["last_seen"]) if row["last_seen"] else None, device_lastmod=datetime.fromtimestamp(row["device_lastmod"]) if row["device_lastmod"] else None, last_message_at=datetime.fromtimestamp(row["last_message_at"]) if row["last_message_at"] else None, is_active=bool(row["is_active"]), ) def get_oldest_unread_contact_timestamp(self, contact_key_hex: str) -> float | None: """Return the timestamp of the oldest unread incoming message for a contact.""" last_read = self.get_contact_last_read_at(contact_key_hex) if last_read is None: row = self._db.execute( """ SELECT MIN(timestamp) as ts FROM messages WHERE contact_key_hex = ? AND is_outgoing = 0 """, (contact_key_hex,), ).fetchone() else: row = self._db.execute( """ SELECT MIN(timestamp) as ts FROM messages WHERE contact_key_hex = ? AND timestamp > ? AND is_outgoing = 0 """, (contact_key_hex, last_read), ).fetchone() return row["ts"] if row and row["ts"] else None def get_contact_last_read_at(self, public_key_hex: str) -> float | None: row = self._db.execute( "SELECT last_read_at FROM contacts WHERE public_key_hex = ?", (public_key_hex,) ).fetchone() return row["last_read_at"] if row and row["last_read_at"] else None def set_contact_last_read_at(self, public_key_hex: str, timestamp: float): self._db.execute( "UPDATE contacts SET last_read_at = ? WHERE public_key_hex = ?", (timestamp, public_key_hex), ) self._commit() # ─── Discovered Contacts ───────────────────────────────── def save_discovered_contact(self, contact: Contact): self._db.execute( """ INSERT INTO discovered_contacts (public_key_hex, name, type, flags, path_length, path, path_hash_size, latitude, longitude, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(public_key_hex) DO UPDATE SET name = excluded.name, type = excluded.type, flags = excluded.flags, path_length = excluded.path_length, path = excluded.path, path_hash_size = excluded.path_hash_size, latitude = excluded.latitude, longitude = excluded.longitude, last_seen = excluded.last_seen """, ( contact.public_key_hex, contact.name, contact.type, contact.flags, contact.path_length, contact.path, contact.path_hash_size, contact.latitude, contact.longitude, contact.last_seen.timestamp() if contact.last_seen else None, ), ) self._commit() def get_discovered_contacts(self) -> list[Contact]: rows = self._db.execute( "SELECT * FROM discovered_contacts ORDER BY last_seen DESC" ).fetchall() return [self._row_to_discovered_contact(r) for r in rows] def _row_to_discovered_contact(self, row) -> Contact: return Contact( public_key=bytes.fromhex(row["public_key_hex"]), name=row["name"], type=row["type"], flags=row["flags"], path_length=row["path_length"], path=bytes(row["path"]), path_hash_size=row["path_hash_size"], latitude=row["latitude"], longitude=row["longitude"], last_seen=datetime.fromtimestamp(row["last_seen"]) if row["last_seen"] else None, is_active=True, ) def count_unread_messages(self, contact_key_hex: str) -> int: """Count incoming messages newer than last_read_at for a contact.""" last_read = self.get_contact_last_read_at(contact_key_hex) if last_read is None: row = self._db.execute( """ SELECT COUNT(*) as cnt FROM messages WHERE contact_key_hex = ? AND is_outgoing = 0 """, (contact_key_hex,), ).fetchone() else: row = self._db.execute( """ SELECT COUNT(*) as cnt FROM messages WHERE contact_key_hex = ? AND timestamp > ? AND is_outgoing = 0 """, (contact_key_hex, last_read), ).fetchone() return row["cnt"] if row else 0 def count_unread_channel_messages(self, channel_index: int) -> int: """Count channel messages newer than last_read_at.""" last_read = self.get_channel_last_read_at(channel_index) if last_read is None: row = self._db.execute( """ SELECT COUNT(*) as cnt FROM channel_messages WHERE channel_index = ? AND is_outgoing = 0 """, (channel_index,), ).fetchone() else: row = self._db.execute( """ SELECT COUNT(*) as cnt FROM channel_messages WHERE channel_index = ? AND timestamp > ? AND is_outgoing = 0 """, (channel_index, last_read), ).fetchone() return row["cnt"] if row else 0 # ─── Messages ────────────────────────────────────────────── def save_message(self, contact_key_hex: str, message: Message): self._db.execute( """ INSERT INTO messages (contact_key_hex, sender_key_hex, text, timestamp, is_outgoing, is_cli, status, message_id, room_sender_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( contact_key_hex, message.sender_key_hex, message.text, message.timestamp.timestamp(), int(message.is_outgoing), int(message.is_cli), int(message.status), message.message_id, message.room_sender_name, ), ) # Update contact's last_message_at self._db.execute( """ UPDATE contacts SET last_message_at = ? WHERE public_key_hex = ? """, (message.timestamp.timestamp(), contact_key_hex), ) self._commit() def get_messages(self, contact_key_hex: str, limit: int = 30, offset: int = 0) -> list[Message]: rows = self._db.execute( """ SELECT * FROM messages WHERE contact_key_hex = ? ORDER BY timestamp DESC LIMIT ? OFFSET ? """, (contact_key_hex, limit, offset), ).fetchall() return [self._row_to_message(r) for r in reversed(rows)] def has_duplicate_message(self, contact_key_hex: str, timestamp: float, text: str) -> bool: """Check if an incoming message is a duplicate. Matches same sender + text within a 5-second window around the timestamp to catch retries where the timestamp may differ slightly. """ row = self._db.execute( """ SELECT 1 FROM messages WHERE contact_key_hex = ? AND text = ? AND is_outgoing = 0 AND abs(timestamp - ?) <= 5 LIMIT 1 """, (contact_key_hex, text, timestamp), ).fetchone() return row is not None def delete_message(self, message_id: str): self._db.execute("DELETE FROM messages WHERE message_id = ?", (message_id,)) self._commit() def update_message_status(self, message_id: str, status: MessageStatus): self._db.execute( "UPDATE messages SET status = ? WHERE message_id = ?", (int(status), message_id) ) self._commit() def fail_orphaned_messages(self): """Mark any PENDING or SENT outgoing messages as FAILED. Called on disconnect to clean up messages that were in flight when the connection dropped. """ self._db.execute( "UPDATE messages SET status = ? WHERE is_outgoing = 1 AND status IN (?, ?)", (int(MessageStatus.FAILED), int(MessageStatus.PENDING), int(MessageStatus.SENT)), ) self._commit() def _row_to_message(self, row) -> Message: return Message( sender_key=bytes.fromhex(row["sender_key_hex"]), text=row["text"], timestamp=datetime.fromtimestamp(row["timestamp"]), is_outgoing=bool(row["is_outgoing"]), is_cli=bool(row["is_cli"]), status=MessageStatus(row["status"]), message_id=row["message_id"], room_sender_name=row["room_sender_name"], ) # ─── Room Passwords ─────────────────────────────────────── def save_room_password(self, room_key_hex: str, password: str): self._db.execute( "INSERT OR REPLACE INTO room_passwords (room_key_hex, password) VALUES (?, ?)", (room_key_hex, password), ) self._commit() def get_room_password(self, room_key_hex: str) -> str | None: row = self._db.execute( "SELECT password FROM room_passwords WHERE room_key_hex = ?", (room_key_hex,) ).fetchone() return row["password"] if row else None def remove_room_password(self, room_key_hex: str): self._db.execute("DELETE FROM room_passwords WHERE room_key_hex = ?", (room_key_hex,)) self._commit() # ─── Channels ────────────────────────────────────────────── def save_channel(self, channel: Channel): self._db.execute( """ INSERT INTO channels (idx, name, psk, unread_count, notification_level, region) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(idx) DO UPDATE SET name = excluded.name, psk = excluded.psk, unread_count = excluded.unread_count, notification_level = excluded.notification_level, region = excluded.region """, ( channel.index, channel.name, channel.psk, channel.unread_count, int(channel.notification_level), channel.region, ), ) self._commit() def get_channels(self) -> list[Channel]: rows = self._db.execute("SELECT * FROM channels ORDER BY idx").fetchall() return [self._row_to_channel(r) for r in rows] def get_channel(self, index: int) -> Channel | None: row = self._db.execute("SELECT * FROM channels WHERE idx = ?", (index,)).fetchone() return self._row_to_channel(row) if row else None def _row_to_channel(self, row) -> Channel: return Channel( index=row["idx"], name=row["name"], psk=bytes(row["psk"]), unread_count=row["unread_count"], notification_level=NotificationLevel(row["notification_level"]), region=row["region"].split(",")[0].strip(), ) def remove_channel(self, index: int): self._db.execute("DELETE FROM channels WHERE idx = ?", (index,)) self._db.execute("DELETE FROM channel_messages WHERE channel_index = ?", (index,)) self._commit() def set_channel_notification_level(self, index: int, level: NotificationLevel): self._db.execute( "UPDATE channels SET notification_level = ? WHERE idx = ?", (int(level), index) ) self._commit() def set_channel_region(self, index: int, region: str): self._db.execute("UPDATE channels SET region = ? WHERE idx = ?", (region, index)) self._commit() # ─── Regions ────────────────────────────────────────────── def get_regions(self) -> list[str]: rows = self._db.execute("SELECT name FROM regions ORDER BY name").fetchall() return [r["name"] for r in rows] def add_region(self, name: str): self._db.execute("INSERT OR IGNORE INTO regions (name) VALUES (?)", (name,)) self._commit() def remove_region(self, name: str): self._db.execute("DELETE FROM regions WHERE name = ?", (name,)) # Clear region from channels that used it self._db.execute("UPDATE channels SET region = '' WHERE region = ?", (name,)) # Clear default scope if it was this region row = self._db.execute("SELECT value FROM settings WHERE key = 'default_scope'").fetchone() if row and row["value"] == name: self._db.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES ('default_scope', '')" ) self._commit() def get_default_scope(self) -> str: row = self._db.execute("SELECT value FROM settings WHERE key = 'default_scope'").fetchone() return row["value"] if row else "" def set_default_scope(self, scope: str): self._db.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES ('default_scope', ?)", (scope,) ) self._commit() def get_oldest_unread_channel_timestamp(self, channel_index: int) -> float | None: """Return the timestamp of the oldest unread incoming channel message.""" last_read = self.get_channel_last_read_at(channel_index) if last_read is None: row = self._db.execute( """ SELECT MIN(timestamp) as ts FROM channel_messages WHERE channel_index = ? AND is_outgoing = 0 """, (channel_index,), ).fetchone() else: row = self._db.execute( """ SELECT MIN(timestamp) as ts FROM channel_messages WHERE channel_index = ? AND timestamp > ? AND is_outgoing = 0 """, (channel_index, last_read), ).fetchone() return row["ts"] if row and row["ts"] else None def get_channel_last_read_at(self, index: int) -> float | None: row = self._db.execute( "SELECT last_read_at FROM channels WHERE idx = ?", (index,) ).fetchone() return row["last_read_at"] if row and row["last_read_at"] else None def get_max_channel_message_timestamp(self, channel_index: int) -> float | None: """Return the newest message timestamp for a channel.""" row = self._db.execute( "SELECT MAX(timestamp) as ts FROM channel_messages WHERE channel_index = ?", (channel_index,), ).fetchone() return row["ts"] if row and row["ts"] else None def set_channel_last_read_at(self, index: int, timestamp: float): self._db.execute("UPDATE channels SET last_read_at = ? WHERE idx = ?", (timestamp, index)) self._commit() # ─── Channel Messages ────────────────────────────────────── def save_channel_message(self, msg: ChannelMessage): self._db.execute( """ INSERT INTO channel_messages (channel_index, sender_name, text, timestamp, is_outgoing) VALUES (?, ?, ?, ?, ?) """, ( msg.channel_index, msg.sender_name, msg.text, msg.timestamp.timestamp(), int(msg.is_outgoing), ), ) self._commit() def delete_channel_message(self, channel_index: int, timestamp: float, text: str): self._db.execute( """ DELETE FROM channel_messages WHERE channel_index = ? AND abs(timestamp - ?) < 1 AND text = ? """, (channel_index, timestamp, text), ) self._commit() def get_channel_messages( self, channel_index: int, limit: int = 30, offset: int = 0 ) -> list[ChannelMessage]: rows = self._db.execute( """ SELECT * FROM channel_messages WHERE channel_index = ? ORDER BY timestamp DESC LIMIT ? OFFSET ? """, (channel_index, limit, offset), ).fetchall() return [ ChannelMessage( channel_index=r["channel_index"], sender_name=r["sender_name"], text=r["text"], timestamp=datetime.fromtimestamp(r["timestamp"]), is_outgoing=bool(r["is_outgoing"]), ) for r in reversed(rows) ] # ─── Channel Message Paths ──────────────────────────────── @staticmethod def _msg_text_hash(text: str) -> str: import hashlib return hashlib.md5(text.encode("utf-8")).hexdigest()[:16] def save_channel_message_path( self, channel_index: int, sender_timestamp: int, text: str, path_info: dict ): text_hash = self._msg_text_hash(text) self._db.execute( """ INSERT INTO channel_message_paths (channel_index, sender_timestamp, msg_text_hash, snr, path_len, path_bytes) VALUES (?, ?, ?, ?, ?, ?) """, ( channel_index, sender_timestamp, text_hash, path_info.get("snr"), path_info.get("path_len", -1), path_info.get("path_bytes", ""), ), ) self._commit() def get_channel_message_paths( self, channel_index: int, sender_timestamp: int, text: str ) -> list[dict]: text_hash = self._msg_text_hash(text) rows = self._db.execute( """ SELECT snr, path_len, path_bytes FROM channel_message_paths WHERE channel_index = ? AND sender_timestamp = ? AND msg_text_hash = ? """, (channel_index, sender_timestamp, text_hash), ).fetchall() return [ {"snr": r["snr"], "path_len": r["path_len"], "path_bytes": r["path_bytes"]} for r in rows ] meshy/src/tcp.py000066400000000000000000000106601521052255700141410ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """TCP transport for MeshCore companion devices over WiFi. Uses the same frame format as USB serial. """ import contextlib import logging import socket from gi.repository import GLib from meshy.transport_base import ( RX_MARKER, TX_MARKER, ConnectionState, FrameTransport, ) log = logging.getLogger(__name__) TCP_DEFAULT_PORT = 5000 TCP_CONNECT_TIMEOUT = 10 # seconds class TcpDevice: """Represents a TCP endpoint for connection view compatibility.""" def __init__(self, host: str, port: int): self.host = host self.port = port self.path = f"{host}:{port}" self.address = self.path self.name = f"TCP {host}:{port}" def __repr__(self): return f"TcpDevice({self.path})" class TcpTransport(FrameTransport): """TCP transport for MeshCore companion devices over WiFi.""" _LABEL = "TCP" # Accept both markers — meshcore-proxy sends responses framed with 0x3C _RX_MARKERS = frozenset((RX_MARKER, TX_MARKER)) def __init__(self): super().__init__() self._socket: socket.socket | None = None def get_paired_devices(self) -> list: """TCP doesn't have paired devices.""" return [] def connect_device(self, device_path: str): """Connect to a TCP endpoint. device_path is 'host:port'.""" if self._state == ConnectionState.CONNECTED: self.disconnect() self._set_state(ConnectionState.CONNECTING) try: host, port_str = device_path.rsplit(":", 1) port = int(port_str) except (ValueError, AttributeError): log.error(f"Invalid TCP address: {device_path}") self._set_state(ConnectionState.DISCONNECTED) return try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TCP_CONNECT_TIMEOUT) sock.connect((host, port)) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.settimeout(None) # blocking reads in thread self._socket = sock self._connected_address = device_path log.info(f"Connected to TCP {host}:{port}") self._start_read_thread() self._set_state(ConnectionState.CONNECTED) except Exception as e: log.error(f"TCP connect failed ({device_path}): {e}") if self._socket: with contextlib.suppress(Exception): self._socket.close() self._socket = None self._set_state(ConnectionState.DISCONNECTED) def disconnect(self): """Disconnect from the TCP endpoint.""" if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING): return self._set_state(ConnectionState.DISCONNECTING) self._running = False if self._socket: with contextlib.suppress(Exception): self._socket.shutdown(socket.SHUT_RDWR) self._close_io() self._connected_address = None self._set_state(ConnectionState.DISCONNECTED) # ─── FrameTransport implementation ──────────────────────── def _is_io_ready(self) -> bool: return self._socket is not None def _write_bytes(self, frame: bytes): self._socket.sendall(frame) def _close_io(self): if self._socket: with contextlib.suppress(Exception): self._socket.close() self._socket = None def _read_loop(self, gen: int): """Background thread that reads from the TCP socket.""" sock = self._socket while self._running and gen == self._connect_gen and sock: try: data = sock.recv(4096) if not data: if self._running and gen == self._connect_gen: log.info("TCP: remote closed connection") GLib.idle_add(self._handle_disconnect, gen) break self._rx_buffer.extend(data) self._process_rx_buffer() except OSError as e: if self._running and gen == self._connect_gen: log.error(f"TCP read error: {e}") GLib.idle_add(self._handle_disconnect, gen) break meshy/src/theme_chooser_dialog.py000066400000000000000000000070701521052255700175170ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import gi gi.require_version("Gtk", "4.0") gi.require_version("Gdk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gtk from meshy.theme_manager import THEMES, ThemeManager @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/theme-chooser-dialog.ui") class ThemeChooserDialog(Adw.Dialog): __gtype_name__ = "MeshyThemeChooserDialog" flow_box = Gtk.Template.Child() def __init__(self, settings, on_theme_changed_cb, **kwargs): super().__init__(**kwargs) self._settings = settings self._on_theme_changed_cb = on_theme_changed_cb current = settings.get_string("custom-theme") self._buttons = {} self._tile_provider = Gtk.CssProvider() Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), self._tile_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) css_parts = [] for theme_id, theme in THEMES.items(): btn = self._create_tile(theme_id, theme, theme_id == current) self.flow_box.append(btn) self._buttons[theme_id] = btn css_parts.append(self._tile_css(theme_id, theme)) self._tile_provider.load_from_string("\n".join(css_parts)) def _create_tile(self, theme_id, theme, selected): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) color_box = Gtk.Box(hexpand=True, vexpand=True) color_box.set_size_request(100, 50) color_box.add_css_class(f"theme-preview-{theme_id}") color_box.add_css_class("theme-tile") if theme.get("accent"): accent_bar = Gtk.Box(hexpand=True, height_request=6) accent_bar.add_css_class(f"theme-accent-{theme_id}") accent_bar.set_valign(Gtk.Align.END) overlay = Gtk.Overlay() overlay.set_child(color_box) overlay.add_overlay(accent_bar) box.append(overlay) else: box.append(color_box) label = Gtk.Label(label=theme["name"]) label.add_css_class("caption") box.append(label) btn = Gtk.ToggleButton() btn.set_child(box) btn.add_css_class("flat") btn.set_active(selected) btn.connect("toggled", self._on_toggled, theme_id) return btn def _on_toggled(self, button, theme_id): if not button.get_active(): return for tid, btn in self._buttons.items(): if tid != theme_id: btn.set_active(False) ThemeManager.get_default().set_custom_theme(theme_id) if self._on_theme_changed_cb: self._on_theme_changed_cb() @staticmethod def _tile_css(theme_id, theme): if theme.get("background"): r, g, b = theme["background"][:3] bg = f"rgb({int(r*255)},{int(g*255)},{int(b*255)})" else: bg = "@window_bg_color" if theme.get("foreground"): r, g, b = theme["foreground"][:3] fg = f"rgb({int(r*255)},{int(g*255)},{int(b*255)})" else: fg = "@window_fg_color" css = f".theme-preview-{theme_id} {{ background-color: {bg}; color: {fg}; }}\n" if theme.get("accent"): r, g, b = theme["accent"][:3] accent = f"rgb({int(r*255)},{int(g*255)},{int(b*255)})" css += f".theme-accent-{theme_id} {{ background-color: {accent}; border-radius: 0 0 12px 12px; }}\n" return css meshy/src/theme_manager.py000066400000000000000000000154571521052255700161600ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gio, Gtk THEMES = { "meshcore": { "name": "MeshCore Dark", "foreground": (1.0, 1.0, 1.0, 0.9), "background": (0.067, 0.094, 0.153), "accent": (0.231, 0.510, 0.965), "is_dark": True, }, "meshcore-light": { "name": "MeshCore Light", "foreground": (0.067, 0.094, 0.153, 0.85), "background": (0.945, 0.961, 0.976), "accent": (0.231, 0.510, 0.965), "is_dark": False, }, "spring": { "name": "Spring", "foreground": (0.250, 0.368, 0.501), "background": (0.972, 1.0, 0.949), "accent": (0.2, 0.65, 0.4), "is_dark": False, }, "midnight": { "name": "Midnight", "foreground": (1.0, 1.0, 1.0, 0.6), "background": (0.137, 0.207, 0.250), "accent": (0.160, 0.470, 0.650), "is_dark": True, }, "gruvbox": { "name": "Gruvbox", "foreground": (0.984, 0.945, 0.780, 0.8), "background": (0.156, 0.156, 0.156), "accent": (0.407, 0.615, 0.415), "is_dark": True, }, "ocean": { "name": "Ocean", "foreground": (0.741, 0.901, 0.984), "background": (0.117, 0.145, 0.160), "accent": (0.3, 0.7, 0.8), "is_dark": True, }, } COLOR_SCHEME_MAP = { "system": Adw.ColorScheme.DEFAULT, "light": Adw.ColorScheme.FORCE_LIGHT, "dark": Adw.ColorScheme.FORCE_DARK, } class ThemeManager: _instance = None def __init__(self): self._recoloring_provider = Gtk.CssProvider() Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), self._recoloring_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1, ) self._settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") @classmethod def get_default(cls): if cls._instance is None: cls._instance = cls() return cls._instance def apply_from_settings(self): mode = self._settings.get_string("color-scheme") if mode == "custom": theme_id = self._settings.get_string("custom-theme") self.apply_custom_theme(theme_id) else: self.apply_color_scheme(mode) def apply_color_scheme(self, mode): scheme = COLOR_SCHEME_MAP.get(mode, Adw.ColorScheme.DEFAULT) Adw.StyleManager.get_default().set_color_scheme(scheme) self._recoloring_provider.load_from_string("") def apply_custom_theme(self, theme_id): theme = THEMES.get(theme_id) if not theme: Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.DEFAULT) self._recoloring_provider.load_from_string("") return if theme["is_dark"]: Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK) else: Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_LIGHT) css = self._generate_css(theme) self._recoloring_provider.load_from_string(css) def set_color_scheme(self, mode): self._settings.set_string("color-scheme", mode) if mode != "custom": self.apply_color_scheme(mode) def set_custom_theme(self, theme_id): self._settings.set_string("custom-theme", theme_id) self._settings.set_string("color-scheme", "custom") self.apply_custom_theme(theme_id) @staticmethod def _rgba_str(components): if len(components) == 4: r, g, b, a = components return f"rgba({int(r*255)},{int(g*255)},{int(b*255)},{a:.2f})" r, g, b = components[:3] return f"rgb({int(r*255)},{int(g*255)},{int(b*255)})" @staticmethod def _rgba_opaque_str(components): r, g, b = components[:3] return f"rgb({int(r*255)},{int(g*255)},{int(b*255)})" def _generate_css(self, theme): fg = theme["foreground"] bg = theme["background"] accent = theme["accent"] is_dark = theme["is_dark"] alt = "white" if is_dark else "black" mix_level = 0.07 if is_dark else 0.25 fg_str = self._rgba_str(fg) bg_str = self._rgba_opaque_str(bg) lines = [] lines.append(f"@define-color window_bg_color {bg_str};") lines.append(f"@define-color window_fg_color {fg_str};") lines.append(f"@define-color headerbar_bg_color mix({bg_str},white,{mix_level});") lines.append(f"@define-color headerbar_fg_color {fg_str};") lines.append(f"@define-color popover_bg_color mix({bg_str},white,{mix_level});") lines.append(f"@define-color popover_fg_color {fg_str};") lines.append( f'@define-color view_bg_color mix({bg_str},{"black" if is_dark else "white"},{0.1 if is_dark else 0.3});' ) lines.append(f"@define-color view_fg_color {fg_str};") lines.append(f"@define-color card_fg_color {fg_str};") lines.append(f"@define-color headerbar_border_color {fg_str};") lines.append(f"@define-color dialog_fg_color {fg_str};") lines.append("@define-color dark_fill_bg_color @headerbar_bg_color;") if is_dark: lines.append(f"@define-color dialog_bg_color mix({bg_str},white,0.07);") lines.append("@define-color card_bg_color alpha(white,0.08);") else: lines.append(f"@define-color dialog_bg_color {bg_str};") lines.append("@define-color card_bg_color alpha(white,0.8);") lines.append(f"@define-color sidebar_bg_color mix({bg_str},{alt},0.05);") lines.append(f"@define-color sidebar_backdrop_color mix({bg_str},{alt},0.1);") lines.append(f"@define-color sidebar_fg_color mix({fg_str},{alt},0.05);") lines.append(f"@define-color sidebar_shade_color mix({bg_str},black,0.1);") lines.append(f"@define-color sidebar_border_color mix({bg_str},black,0.1);") lines.append(f"@define-color secondary_sidebar_bg_color mix({bg_str},{alt},0.02);") lines.append(f"@define-color secondary_sidebar_backdrop_color mix({bg_str},{alt},0.05);") lines.append(f"@define-color secondary_sidebar_fg_color mix({fg_str},{alt},0.02);") lines.append(f"@define-color secondary_sidebar_shade_color mix({bg_str},black,0.1);") lines.append(f"@define-color secondary_sidebar_border_color mix({bg_str},black,0.1);") if accent: accent_str = self._rgba_opaque_str(accent) lines.append(f"@define-color accent_color {accent_str};") lines.append(f"@define-color accent_bg_color {accent_str};") lines.append("@define-color accent_fg_color white;") return "\n".join(lines) meshy/src/transport_base.py000066400000000000000000000201441521052255700163770ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Shared base for frame-oriented MeshCore transports (USB, TCP). Frame format: start_marker(1) + length_le(2) + payload(N) Host→Device: 0x3C + length + payload Device→Host: 0x3E + length + payload """ import logging import threading from collections.abc import Callable from enum import IntEnum, auto from gi.repository import GLib log = logging.getLogger(__name__) TX_MARKER = 0x3C # Host → Device RX_MARKER = 0x3E # Device → Host HEADER_SIZE = 3 MAX_RX_PAYLOAD = 176 # accept frames up to 176 bytes (firmware 1.16+) MAX_TX_PAYLOAD = 172 # send at most 172 bytes for backward compat with older firmware class ConnectionState(IntEnum): DISCONNECTED = auto() SCANNING = auto() CONNECTING = auto() CONNECTED = auto() DISCONNECTING = auto() class FrameTransport: """Base class for frame-oriented transports (USB serial, TCP). Subclasses must implement: - _write_bytes(frame: bytes) — send raw bytes over the wire - _close_io() — close the underlying port/socket - _read_loop(gen: int) — background read thread body - connect_device(path: str) — establish the connection """ # Subclass label for log messages _LABEL = "Transport" # Set of valid RX markers — override in TCP to also accept 0x3C _RX_MARKERS = frozenset((RX_MARKER,)) def __init__(self): self._state = ConnectionState.DISCONNECTED self._connected_address: str | None = None self._read_thread: threading.Thread | None = None self._running = False self._rx_buffer = bytearray() self._connect_gen = 0 # Callbacks — same interface as BleTransport self.on_state_changed: Callable | None = None self.on_data_received: Callable[[bytes], None] | None = None self.on_device_discovered: Callable | None = None self.on_passkey_requested: Callable | None = None # ─── Properties ─────────────────────────────────────────── @property def state(self): return self._state @property def is_connected(self) -> bool: return self._state == ConnectionState.CONNECTED @property def connected_address(self) -> str | None: return self._connected_address @property def discovered_devices(self) -> list: return [] # ─── State management ───────────────────────────────────── def _set_state(self, state): if self._state != state: old = self._state self._state = state log.info( f"{self._LABEL} state: {ConnectionState(old).name} -> " f"{ConnectionState(state).name}" ) if self.on_state_changed: self.on_state_changed(state) # ─── Sending ────────────────────────────────────────────── def send_data(self, data: bytes): """Send a protocol frame wrapped in the wire framing.""" if not self._is_io_ready() or self._state != ConnectionState.CONNECTED: log.warning(f"{self._LABEL}: cannot send, state={self._state}") return if len(data) > MAX_TX_PAYLOAD: log.error(f"Frame too large: {len(data)} > {MAX_TX_PAYLOAD}") return frame = bytearray(HEADER_SIZE + len(data)) frame[0] = TX_MARKER frame[1] = len(data) & 0xFF frame[2] = (len(data) >> 8) & 0xFF frame[3:] = data try: self._write_bytes(bytes(frame)) log.debug(f"{self._LABEL} TX [{len(data)} bytes]: {data[:20].hex()}...") except Exception as e: log.error(f"{self._LABEL} write failed: {e}") GLib.idle_add(self._handle_disconnect) # ─── Receiving ──────────────────────────────────────────── def _process_rx_buffer(self): """Parse complete frames from the receive buffer.""" while len(self._rx_buffer) >= HEADER_SIZE: marker = self._rx_buffer[0] if marker not in self._RX_MARKERS: idx = None for i, b in enumerate(self._rx_buffer): if b in self._RX_MARKERS: idx = i break if idx is None: self._rx_buffer.clear() return log.debug(f"{self._LABEL}: skipping {idx} invalid bytes") del self._rx_buffer[:idx] continue payload_len = self._rx_buffer[1] | (self._rx_buffer[2] << 8) if payload_len > MAX_RX_PAYLOAD: log.debug(f"{self._LABEL}: invalid payload length {payload_len}, skipping") del self._rx_buffer[0] continue frame_size = HEADER_SIZE + payload_len if len(self._rx_buffer) < frame_size: return payload = bytes(self._rx_buffer[HEADER_SIZE:frame_size]) del self._rx_buffer[:frame_size] log.debug(f"{self._LABEL} RX [{len(payload)} bytes]: {payload[:20].hex()}...") if self.on_data_received: GLib.idle_add(self._deliver_data, payload) def _deliver_data(self, data: bytes): """Deliver received data on the main thread.""" if self.on_data_received: self.on_data_received(data) return False # ─── Disconnect handling ────────────────────────────────── def _handle_disconnect(self, gen: int = -1): """Handle unexpected disconnect. Ignore stale callbacks.""" if gen != -1 and gen != self._connect_gen: log.debug( f"{self._LABEL}: ignoring stale disconnect " f"(gen {gen} != {self._connect_gen})" ) return if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING): return log.info(f"{self._LABEL} disconnected") self._running = False self._close_io() self._connected_address = None self._set_state(ConnectionState.DISCONNECTED) def _start_read_thread(self): """Start the background read thread (call after IO is ready).""" self._running = True self._rx_buffer.clear() self._connect_gen += 1 gen = self._connect_gen self._read_thread = threading.Thread( target=self._read_loop, args=(gen,), daemon=True, ) self._read_thread.start() # ─── Stubs / no-ops ─────────────────────────────────────── def connect_by_address(self, address: str): self.connect_device(address) def pair_device(self, device_path: str): self.connect_device(device_path) def start_scan(self): pass def stop_scan(self): pass # ─── Abstract (must be implemented by subclass) ─────────── def _is_io_ready(self) -> bool: """Return True if the underlying IO object is available.""" raise NotImplementedError def _write_bytes(self, frame: bytes): """Write raw bytes to the transport.""" raise NotImplementedError def _close_io(self): """Close the underlying port/socket.""" raise NotImplementedError def _read_loop(self, gen: int): """Background thread body — read data and feed _rx_buffer.""" raise NotImplementedError def connect_device(self, device_path: str): """Establish connection to the given device/endpoint.""" raise NotImplementedError def disconnect(self): """Gracefully disconnect.""" raise NotImplementedError meshy/src/usb_serial.py000066400000000000000000000112221521052255700154760ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """USB Serial transport for MeshCore companion devices. Uses pyserial to communicate with MeshCore devices over USB serial ports. """ import contextlib import glob import logging import time from gi.repository import GLib from meshy.transport_base import ConnectionState, FrameTransport log = logging.getLogger(__name__) SERIAL_BAUD_RATE = 115200 class UsbDevice: """Represents a discovered USB serial device.""" def __init__(self, path: str, name: str = ""): self.path = path self.address = path # Use path as address for storage key self.name = name or path def __repr__(self): return f"UsbDevice({self.path})" class UsbSerialTransport(FrameTransport): """USB serial transport using pyserial.""" _LABEL = "USB" def __init__(self): super().__init__() self._port = None self._connection_error: str | None = None self._is_permission_error: bool = False def get_serial_devices(self) -> list[UsbDevice]: """Scan for available USB serial devices.""" devices = [] patterns = ["/dev/ttyACM*", "/dev/ttyUSB*"] for pattern in patterns: for path in sorted(glob.glob(pattern)): name = path.split("/")[-1] devices.append(UsbDevice(path=path, name=name)) return devices def get_paired_devices(self) -> list[UsbDevice]: """Return available serial devices (USB doesn't have pairing).""" return self.get_serial_devices() def connect_device(self, device_path: str): """Connect to a USB serial device.""" import serial if self._state == ConnectionState.CONNECTED: self.disconnect() self._set_state(ConnectionState.CONNECTING) self._connected_address = device_path try: self._port = serial.Serial( device_path, baudrate=SERIAL_BAUD_RATE, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, xonxoff=False, rtscts=False, timeout=None, ) self._port.rts = False self._port.reset_input_buffer() self._port.reset_output_buffer() log.info(f"Opened serial port {device_path} at {SERIAL_BAUD_RATE} baud") self._start_read_thread() self._set_state(ConnectionState.CONNECTED) except Exception as e: error_msg = str(e) log.error(f"Failed to open serial port {device_path}: {error_msg}") self._connection_error = error_msg self._is_permission_error = "Permission denied" in error_msg or "Errno 13" in error_msg self._connected_address = None self._set_state(ConnectionState.DISCONNECTED) def disconnect(self): """Disconnect from the serial device.""" if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING): return self._set_state(ConnectionState.DISCONNECTING) self._running = False self._close_io() self._connected_address = None self._set_state(ConnectionState.DISCONNECTED) # ─── FrameTransport implementation ──────────────────────── def _is_io_ready(self) -> bool: return self._port is not None def _write_bytes(self, frame: bytes): self._port.write(frame) def _close_io(self): if self._port: with contextlib.suppress(Exception): self._port.close() self._port = None def _read_loop(self, gen: int): """Background thread that reads from the serial port.""" port = self._port while self._running and gen == self._connect_gen and port: try: waiting = port.in_waiting if waiting > 0: data = port.read(waiting) if data: self._rx_buffer.extend(data) self._process_rx_buffer() else: time.sleep(0.05) except OSError as e: if self._running and gen == self._connect_gen: log.error(f"USB read error: {e}") GLib.idle_add(self._handle_disconnect, gen) break except Exception as e: if self._running and gen == self._connect_gen: log.error(f"USB read error: {e}") break meshy/src/utils.py000066400000000000000000000044721521052255700145170ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later import math import re import unicodedata from gi.repository import GLib _URL_RE = re.compile(r'(https?://[^\s<>"\')\]]+)') _SHARED_CONTACT_RE = re.compile(r"^<([0-9a-fA-F]{64}):(\d+):(.+)>$") _SHARED_LOCATION_RE = re.compile(r"^(-?\d+\.\d+),\s*(-?\d+\.\d+)$") def linkify(text: str) -> str: escaped = GLib.markup_escape_text(text) def _replace(m): url = m.group(1) trailing = "" while url and url[-1] in ".,;:!?": trailing = url[-1] + trailing url = url[:-1] return f'{url}{trailing}' return _URL_RE.sub(_replace, escaped) def parse_shared_contact(text: str) -> tuple[str, int, str] | None: m = _SHARED_CONTACT_RE.match(text.strip()) if not m: return None return (m.group(1), int(m.group(2)), m.group(3)) def parse_shared_location(text: str) -> tuple[float, float] | None: m = _SHARED_LOCATION_RE.match(text.strip()) if not m: return None lat, lon = float(m.group(1)), float(m.group(2)) if -90 <= lat <= 90 and -180 <= lon <= 180: return (lat, lon) return None def haversine_distance(lat1, lon1, lat2, lon2) -> float: """Return the great-circle distance in km between two points.""" R = 6371.0 dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = ( math.sin(dlat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2 ) return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) def extract_emoji(name: str) -> str | None: if not name: return None for char in name: cat = unicodedata.category(char) if cat == "So": return char cp = ord(char) if 0x1F600 <= cp <= 0x1F64F: return char if 0x1F300 <= cp <= 0x1F5FF: return char if 0x1F680 <= cp <= 0x1F6FF: return char if 0x1F900 <= cp <= 0x1F9FF: return char if 0x1FA00 <= cp <= 0x1FAFF: return char if 0x2600 <= cp <= 0x27BF: return char if 0xFE00 <= cp <= 0xFE0F: continue if cp == 0x200D: continue return None meshy/src/views/000077500000000000000000000000001521052255700141335ustar00rootroot00000000000000meshy/src/views/__init__.py000066400000000000000000000240671521052255700162550ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Shared view utilities.""" import json import threading import weakref from datetime import datetime, timedelta from urllib.request import Request, urlopen import gi gi.require_version("Adw", "1") gi.require_version("Gtk", "4.0") gi.require_version("Shumate", "1.0") from gi.repository import Adw, GLib, GObject, Gtk, Shumate _OFM_BASE = "https://tiles.openfreemap.org" _OFM_TILEJSON_URL = f"{_OFM_BASE}/planet" _OFM_STYLE_URLS = { "bright": f"{_OFM_BASE}/styles/bright", "dark": f"{_OFM_BASE}/styles/dark", } _source_cache = {} _source_failed = set() _tile_urls_cache = None _map_widgets = [] _theme_listener_connected = False def _format_date_text(dt): """Format a datetime as a human-friendly date string for dividers.""" today = datetime.now().date() d = dt.date() if isinstance(dt, datetime) else dt if d == today: return "Today" if d == today - timedelta(days=1): return "Yesterday" if d.year == today.year: return d.strftime("%A, %B %-d") return d.strftime("%A, %B %-d, %Y") def _get_osm_source(): registry = Shumate.MapSourceRegistry.new_with_defaults() return registry.get_by_id(Shumate.MAP_SOURCE_OSM_MAPNIK) def _fix_interpolations(obj): """Fix ["linear", N] -> ["linear"] for Shumate compatibility.""" if isinstance(obj, list): if ( len(obj) >= 2 and obj[0] == "interpolate" and isinstance(obj[1], list) and obj[1][0] == "linear" and len(obj[1]) > 1 ): obj[1] = ["linear"] for item in obj: _fix_interpolations(item) elif isinstance(obj, dict): for v in obj.values(): _fix_interpolations(v) def _patch_style_json(style_text, tile_urls): """Patch OpenFreeMap style JSON for Shumate compatibility.""" style = json.loads(style_text) sources = style.get("sources", {}) vector_key = None for key, src in sources.items(): if src.get("type") == "vector": vector_key = key break if not vector_key: return None vector_src = sources[vector_key] vector_src.pop("url", None) vector_src["tiles"] = tile_urls style["sources"] = {vector_key: vector_src} style.pop("sprite", None) style.pop("glyphs", None) for layer in style.get("layers", []): _fix_interpolations(layer) return json.dumps(style) def _fetch_and_create_ofm_source(style, callback): """Fetch an OFM style + TileJSON and create a VectorRenderer asynchronously.""" if style in _source_cache: callback(_source_cache[style]) return if style in _source_failed: callback(None) return def _worker(): global _tile_urls_cache try: ua = "Meshy/1.0 (https://codeberg.org/sesivany/meshy)" style_url = _OFM_STYLE_URLS[style] style_text = ( urlopen(Request(style_url, headers={"User-Agent": ua}), timeout=10) .read() .decode("utf-8") ) if _tile_urls_cache is None: tilejson_text = ( urlopen(Request(_OFM_TILEJSON_URL, headers={"User-Agent": ua}), timeout=10) .read() .decode("utf-8") ) tilejson = json.loads(tilejson_text) _tile_urls_cache = tilejson.get("tiles", []) if not _tile_urls_cache: raise ValueError("No tile URLs") patched = _patch_style_json(style_text, _tile_urls_cache) if not patched: raise ValueError("Style patching failed") def _finish(): try: renderer = Shumate.VectorRenderer.new(f"ofm-{style}", patched) renderer.set_license("© OpenFreeMap © OpenStreetMap (ODbL)") renderer.set_license_uri("https://www.openstreetmap.org/copyright") _source_cache[style] = renderer callback(renderer) except Exception: _source_failed.add(style) callback(None) return False GLib.idle_add(_finish) except Exception: _source_failed.add(style) GLib.idle_add(lambda: callback(None) or False) threading.Thread(target=_worker, daemon=True).start() _MAX_ZOOM = 14 def _apply_map_source(map_widget, source): if source: map_widget.set_map_source(source) map_widget.get_viewport().set_max_zoom_level(_MAX_ZOOM) def _is_dark(): return Adw.StyleManager.get_default().get_dark() def _on_theme_changed(*_args): live = [(ref, ref()) for ref in _map_widgets] _map_widgets[:] = [ref for ref, w in live if w is not None] widgets = [w for _, w in live if w is not None] if not widgets: return style = "dark" if _is_dark() else "bright" def _apply(source): s = source if source else _get_osm_source() for w in widgets: _apply_map_source(w, s) _fetch_and_create_ofm_source(style, _apply) def create_shumate_map(zoom_level=14, lat=0.0, lon=0.0): """Create a Shumate SimpleMap with theme-aware tiles.""" global _theme_listener_connected map_widget = Shumate.SimpleMap() map_widget.set_vexpand(True) osm_source = _get_osm_source() if osm_source: _apply_map_source(map_widget, osm_source) viewport = map_widget.get_viewport() viewport.set_zoom_level(zoom_level) viewport.set_location(lat, lon) marker_layer = Shumate.MarkerLayer.new(viewport) map_widget.get_map().add_layer(marker_layer) _map_widgets.append(weakref.ref(map_widget)) style = "dark" if _is_dark() else "bright" def _apply_ofm(source): if source: _apply_map_source(map_widget, source) _fetch_and_create_ofm_source(style, _apply_ofm) if not _theme_listener_connected: Adw.StyleManager.get_default().connect("notify::dark", _on_theme_changed) _theme_listener_connected = True return map_widget, marker_layer, viewport def deregister_map_widget(map_widget): """Remove a map widget from the theme-tracking list.""" _map_widgets[:] = [ref for ref in _map_widgets if ref() is not None and ref() is not map_widget] def open_map_picker(window, title, button_label, init_lat, init_lon, on_pick): """Open a map dialog with crosshair, call on_pick(lat, lon) on confirm.""" dialog = Adw.Dialog() dialog.set_title(title) win_w = window.get_width() or 800 win_h = window.get_height() or 600 dialog.set_content_width(max(500, win_w - 60)) dialog.set_content_height(max(400, win_h - 60)) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() confirm_btn = Gtk.Button(label=button_label) confirm_btn.add_css_class("suggested-action") header.pack_end(confirm_btn) toolbar_view.add_top_bar(header) init_zoom = 14 if (abs(init_lat) > 1e-6 or abs(init_lon) > 1e-6) else 2 map_widget, _ml, viewport = create_shumate_map(zoom_level=init_zoom, lat=init_lat, lon=init_lon) map_widget.set_hexpand(True) crosshair = Gtk.Label() crosshair.set_markup('') crosshair.set_halign(Gtk.Align.CENTER) crosshair.set_valign(Gtk.Align.CENTER) overlay = Gtk.Overlay() overlay.set_child(map_widget) overlay.add_overlay(crosshair) toolbar_view.set_content(overlay) dialog.set_child(toolbar_view) def _on_confirm(*_args): on_pick(viewport.get_latitude(), viewport.get_longitude()) dialog.close() confirm_btn.connect("clicked", _on_confirm) dialog.connect("closed", lambda _d: deregister_map_widget(map_widget)) dialog.present(window) def _add_hover_swap(row, button, badge, get_count, on_long_press): """Add hover show/hide behavior: show button and hide badge on hover, reverse on leave. Also add long-press handler for touchscreens.""" motion = Gtk.EventControllerMotion() motion.connect("enter", lambda *_: (button.set_visible(True), badge.set_visible(False))) motion.connect( "leave", lambda *_: (button.set_visible(False), badge.set_visible(get_count() > 0)) ) row.add_controller(motion) long_press = Gtk.GestureLongPress() long_press.connect("pressed", lambda g, x, y: on_long_press()) row.add_controller(long_press) _CONTACT_TYPE_ICONS = { 1: "avatar-default-symbolic", 2: "network-server-symbolic", 3: "user-home-symbolic", 4: "weather-clear-symbolic", } class _ContactPickItem(GObject.Object): def __init__(self, contact): super().__init__() self.contact = contact def _contact_pick_factory_setup(factory, list_item, button_label): row = Adw.ActionRow() icon = Gtk.Image() row.add_prefix(icon) btn = Gtk.Button(label=button_label, valign=Gtk.Align.CENTER) btn.add_css_class("suggested-action") btn.set_visible(False) row.add_suffix(btn) row._icon = icon row._action_btn = btn row._action_handler_id = None hover = Gtk.EventControllerMotion() hover.connect("enter", lambda *_a: btn.set_visible(True)) hover.connect("leave", lambda *_a: btn.set_visible(False)) row.add_controller(hover) list_item.set_child(row) def _contact_pick_factory_bind(factory, list_item, on_click, subtitle_func=None): row = list_item.get_child() contact = list_item.get_item().contact row.set_title(GLib.markup_escape_text(contact.name) if contact.name else _("Unknown")) subtitle = subtitle_func(contact) if subtitle_func else contact.type_label row.set_subtitle(subtitle) row._icon.set_from_icon_name(_CONTACT_TYPE_ICONS.get(contact.type, "avatar-default-symbolic")) row._action_handler_id = row._action_btn.connect("clicked", lambda _b: on_click(contact)) def _contact_pick_factory_unbind(factory, list_item): row = list_item.get_child() if row._action_handler_id is not None: row._action_btn.disconnect(row._action_handler_id) row._action_handler_id = None meshy/src/views/channels_view.py000066400000000000000000002573471521052255700173540ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Channels list and chat view.""" import hashlib import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Shumate", "1.0") from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Pango, Shumate from meshy.models import Channel, ChannelMessage, NotificationLevel from meshy.protocol import max_channel_text_bytes from meshy.utils import extract_emoji, linkify, parse_shared_contact, parse_shared_location from meshy.views import ( _add_hover_swap, _contact_pick_factory_bind, _contact_pick_factory_setup, _contact_pick_factory_unbind, _ContactPickItem, _format_date_text, create_shumate_map, ) from meshy.views.chat_mixin import ChatScrollMixin _BATCH_SIZE = 30 class ChannelChatItem(GObject.Object): """Model object for one row in the channel chat ListView.""" KIND_MESSAGE = 0 KIND_UNREAD_DIVIDER = 1 KIND_DATE_DIVIDER = 2 def __init__(self, kind=0, message=None, date_text=""): super().__init__() self.kind = kind self.message = message self.date_text = date_text self._repeat_count_val = 0 self._resend_count_val = 0 @GObject.Property(type=int) def repeat_count(self): return self._repeat_count_val @repeat_count.setter def repeat_count(self, value): self._repeat_count_val = value @GObject.Property(type=int) def resend_count(self): return self._resend_count_val @resend_count.setter def resend_count(self, value): self._resend_count_val = value @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/channel-chat-widget.ui") class ChannelChatWidget(ChatScrollMixin, Gtk.Box): """Channel chat content widget - shown in the content panel.""" __gtype_name__ = "MeshyChannelChatWidget" _stack = Gtk.Template.Child("stack") _scroll = Gtk.Template.Child("chat_scroll") _list_view = Gtk.Template.Child("list_view") _scroll_down_revealer = Gtk.Template.Child("scroll_down_revealer") _unread_btn_revealer = Gtk.Template.Child("unread_btn_revealer") _char_label = Gtk.Template.Child("char_label") _emoji_btn = Gtk.Template.Child("emoji_btn") _text_entry = Gtk.Template.Child("text_entry") _send_btn = Gtk.Template.Child("send_btn") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._current_channel: Channel | None = None # Track outgoing items for repeat detection: (channel_idx, timestamp, text) -> item self._outgoing_items: dict[tuple, ChannelChatItem] = {} # Repeat info per outgoing message key: list of {path_hops, path_len} self._repeats: dict[tuple, list] = {} # Track incoming message path variants: msg_key -> list of path_info dicts self._incoming_paths: dict[tuple, list] = {} self._seen_channel_msgs: set[tuple] = set() self._incoming_items: dict[tuple, ChannelChatItem] = {} # Map resend timestamps to original outgoing message keys # so echoes of resent messages can be matched back self._resend_ts_map: dict[tuple, tuple] = {} self._mention_popover = None self._mention_listbox = None # Pango attributes for char counter attrs = Pango.AttrList() attrs.insert(Pango.attr_scale_new(0.85)) self._char_label.set_attributes(attrs) # ListView model and factory self._store = Gio.ListStore(item_type=ChannelChatItem) selection = Gtk.NoSelection(model=self._store) self._factory = Gtk.SignalListItemFactory() self._factory.connect("setup", self._on_factory_setup) self._factory.connect("bind", self._on_factory_bind) self._factory.connect("unbind", self._on_factory_unbind) self._list_view.set_model(selection) self._list_view.set_factory(self._factory) # Context menus (single popover on the ListView, menu switches per item) self._context_popover = Gtk.PopoverMenu() self._context_popover.set_parent(self._list_view) self._context_popover.set_has_arrow(False) self._context_popover.connect("closed", self._on_context_popover_closed) self._context_menu_open = False self._frozen_scroll_value = 0.0 self._context_item = None self._outgoing_menu = Gio.Menu() self._outgoing_menu.append(_("Heard Repeats"), "msg.repeats") self._outgoing_menu.append(_("Send Again"), "msg.resend") self._outgoing_menu.append(_("Delete"), "msg.delete") self._incoming_menu = Gio.Menu() self._incoming_menu.append(_("Reply"), "msg.reply") self._incoming_menu.append(_("Copy Text"), "msg.copy") self._incoming_menu.append(_("View Message Paths"), "msg.paths") self._incoming_menu.append(_("Delete"), "msg.delete") ctx_group = Gio.SimpleActionGroup() for name, cb in [ ("repeats", lambda a, p: self._show_repeats_context()), ("resend", lambda a, p: self._resend_context()), ("delete", lambda a, p: self._delete_context()), ("reply", lambda a, p: self._reply_context()), ("copy", lambda a, p: self._copy_context()), ("paths", lambda a, p: self._show_paths_context()), ]: action = Gio.SimpleAction.new(name, None) action.connect("activate", cb) ctx_group.add_action(action) self._list_view.insert_action_group("msg", ctx_group) # Gesture controllers right_click = Gtk.GestureClick(button=3) right_click.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) right_click.connect("pressed", self._on_list_right_click) self._list_view.add_controller(right_click) long_press = Gtk.GestureLongPress() long_press.set_touch_only(True) long_press.set_exclusive(True) long_press.connect("pressed", self._on_list_long_press) self._list_view.add_controller(long_press) self._is_sticky = True self._scroll_positions: dict[int, float] = {} self._restoring_scroll = False self._unread_pos = -1 self._messages_offset = 0 self._all_loaded = False self._loading_more = False # Signal connections self._scroll_down_revealer.get_child().connect("clicked", self._on_scroll_to_bottom_clicked) self._unread_btn_revealer.get_child().connect("clicked", self._on_scroll_to_unread_clicked) self._emoji_btn.connect("clicked", self._on_emoji_clicked) self._text_entry.connect("activate", self._on_send) self._text_entry.connect("changed", self._on_text_changed) mention_key_ctrl = Gtk.EventControllerKey() mention_key_ctrl.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) mention_key_ctrl.connect("key-pressed", self._on_entry_key_pressed) self._text_entry.add_controller(mention_key_ctrl) self._send_btn.connect("clicked", self._on_send) # Extra context menu on text entry extra_menu = Gio.Menu() extra_menu.append(_("Share Contact"), "entry.share-contact") extra_menu.append(_("Share Location"), "entry.share-location") extra_menu.append(_("Share Location from Map"), "entry.share-location-map") self._text_entry.set_extra_menu(extra_menu) entry_group = Gio.SimpleActionGroup() share_a = Gio.SimpleAction.new("share-contact", None) share_a.connect("activate", self._on_share_contact) entry_group.add_action(share_a) loc_a = Gio.SimpleAction.new("share-location", None) loc_a.connect("activate", self._on_share_location) entry_group.add_action(loc_a) loc_map_a = Gio.SimpleAction.new("share-location-map", None) loc_map_a.connect("activate", self._on_share_location_map) entry_group.add_action(loc_map_a) self._text_entry.insert_action_group("entry", entry_group) # Scroll state tracking adj = self._scroll.get_vadjustment() adj.connect("value-changed", self._on_scroll_value_changed) adj.connect("notify::upper", self._on_scroll_upper_changed) adj.connect("notify::page-size", self._on_scroll_upper_changed) self._scroll.connect("edge-overshot", self._on_edge_overshot) self._stack.set_visible_child_name("placeholder") # ─── Factory callbacks ─────────────────────────────────── def _on_factory_setup(self, factory, list_item): """Create the widget tree for one list item (recycled).""" list_item.set_activatable(False) list_item.set_selectable(False) list_item.set_focusable(False) outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # ── Avatar ── avatar = Adw.Avatar(size=32, show_initials=True) avatar.set_valign(Gtk.Align.START) emoji_lbl = Gtk.Label() emoji_lbl.add_css_class("emoji-avatar") emoji_lbl.set_valign(Gtk.Align.START) avatar_stack = Gtk.Stack() avatar_stack.set_transition_type(Gtk.StackTransitionType.NONE) avatar_stack.add_named(avatar, "initials") avatar_stack.add_named(emoji_lbl, "emoji") avatar_stack.set_visible(False) # ── Message bubble ── msg_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=2, margin_end=12, margin_top=2, margin_bottom=2, ) content_box = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL, spacing=8, margin_start=12, ) content_box.append(avatar_stack) content_box.append(msg_box) # Sender name (incoming only) sender_lbl = Gtk.Label(xalign=0) sender_lbl.add_css_class("dim-label") sender_lbl.add_css_class("caption") sender_lbl.set_visible(False) msg_box.append(sender_lbl) # Card frame with text text_lbl = Gtk.Label( wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, xalign=0, max_width_chars=40, ) text_lbl.set_margin_start(12) text_lbl.set_margin_end(12) text_lbl.set_margin_top(8) text_lbl.set_margin_bottom(8) frame = Gtk.Frame() frame.add_css_class("card") content_stack = Gtk.Stack() content_stack.set_transition_type(Gtk.StackTransitionType.NONE) content_stack.set_vhomogeneous(False) content_stack.set_hhomogeneous(False) content_stack.add_named(text_lbl, "text") sc_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=6, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) sc_header_lbl = Gtk.Label(xalign=0, wrap=True) sc_header_lbl.add_css_class("dim-label") sc_header_lbl.add_css_class("caption") sc_box.append(sc_header_lbl) sc_name_box = Gtk.Box(spacing=6) sc_icon = Gtk.Image.new_from_icon_name("avatar-default-symbolic") sc_name_lbl = Gtk.Label(xalign=0) attrs = Pango.AttrList() attrs.insert(Pango.attr_weight_new(Pango.Weight.BOLD)) sc_name_lbl.set_attributes(attrs) sc_name_box.append(sc_icon) sc_name_box.append(sc_name_lbl) sc_box.append(sc_name_box) sc_key_lbl = Gtk.Label(xalign=0) sc_key_lbl.add_css_class("dim-label") sc_key_lbl.add_css_class("caption") sc_box.append(sc_key_lbl) sc_add_btn = Gtk.Button(label=_("Add Contact")) sc_add_btn.add_css_class("suggested-action") sc_add_btn.set_halign(Gtk.Align.START) sc_add_btn.set_margin_top(4) sc_box.append(sc_add_btn) content_stack.add_named(sc_box, "contact") # ── Shared location card ── loc_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=6, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) loc_header_lbl = Gtk.Label(xalign=0, wrap=True) loc_header_lbl.add_css_class("dim-label") loc_header_lbl.add_css_class("caption") loc_box.append(loc_header_lbl) loc_coords_lbl = Gtk.Label(xalign=0, selectable=True) loc_coords_lbl.add_css_class("caption") loc_box.append(loc_coords_lbl) loc_map_frame = Gtk.Frame() loc_map_frame.add_css_class("card") loc_map_frame.set_overflow(Gtk.Overflow.HIDDEN) loc_map_frame.set_cursor(Gdk.Cursor.new_from_name("pointer")) loc_map_gesture = Gtk.GestureClick() loc_map_frame.add_controller(loc_map_gesture) loc_box.append(loc_map_frame) loc_open_btn = Gtk.Button(label=_("Open in Maps App"), margin_top=4) loc_box.append(loc_open_btn) content_stack.add_named(loc_box, "location") frame.set_child(content_stack) msg_box.append(frame) # Meta line: time + resend + repeat meta_box = Gtk.Box(spacing=4) time_lbl = Gtk.Label() time_lbl.add_css_class("dim-label") time_lbl.add_css_class("caption") meta_box.append(time_lbl) resend_lbl = Gtk.Label(label="") resend_lbl.add_css_class("dim-label") resend_lbl.add_css_class("caption") resend_lbl.set_visible(False) meta_box.append(resend_lbl) repeat_lbl = Gtk.Label(label="") repeat_lbl.add_css_class("dim-label") repeat_lbl.add_css_class("caption") repeat_lbl.set_visible(False) meta_box.append(repeat_lbl) msg_box.append(meta_box) outer.append(content_box) # ── Divider ── div_box = Gtk.Box( spacing=8, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) line_left = Gtk.Separator(hexpand=True, valign=Gtk.Align.CENTER) div_box.append(line_left) div_lbl = Gtk.Label() div_box.append(div_lbl) line_right = Gtk.Separator(hexpand=True, valign=Gtk.Align.CENTER) div_box.append(line_right) div_box.set_visible(False) outer.append(div_box) # Store widget references outer._content_box = content_box outer._avatar = avatar outer._emoji_lbl = emoji_lbl outer._avatar_stack = avatar_stack outer._msg_box = msg_box outer._sender_lbl = sender_lbl outer._text_lbl = text_lbl outer._frame = frame outer._time_lbl = time_lbl outer._resend_lbl = resend_lbl outer._repeat_lbl = repeat_lbl outer._meta_box = meta_box outer._div_box = div_box outer._div_lbl = div_lbl outer._div_line_left = line_left outer._div_line_right = line_right outer._content_stack = content_stack outer._sc_header_lbl = sc_header_lbl outer._sc_icon = sc_icon outer._sc_name_lbl = sc_name_lbl outer._sc_key_lbl = sc_key_lbl outer._sc_add_btn = sc_add_btn outer._sc_add_handler_id = None outer._loc_header_lbl = loc_header_lbl outer._loc_coords_lbl = loc_coords_lbl outer._loc_map_frame = loc_map_frame outer._loc_open_btn = loc_open_btn outer._loc_map_gesture = loc_map_gesture outer._loc_map_click_handler = None outer._loc_open_handler = None outer._chat_item = None outer._handler_ids = [] # Wrap in per-item Clamp for width limiting clamp = Adw.Clamp(maximum_size=600, child=outer) clamp._outer = outer list_item.set_child(clamp) def _on_factory_bind(self, factory, list_item): """Populate widgets from the model item.""" item = list_item.get_item() clamp = list_item.get_child() outer = clamp._outer outer._chat_item = item if item.kind == ChannelChatItem.KIND_MESSAGE: msg = item.message outer._msg_box.set_visible(True) outer._div_box.set_visible(False) # Alignment if msg.is_outgoing: outer._content_box.set_halign(Gtk.Align.END) outer._meta_box.set_halign(Gtk.Align.END) outer._avatar_stack.set_visible(False) else: outer._content_box.set_halign(Gtk.Align.START) outer._meta_box.set_halign(Gtk.Align.START) # Avatar emoji = extract_emoji(msg.sender_name) if msg.sender_name else None if emoji: outer._emoji_lbl.set_label(emoji) outer._avatar_stack.set_visible_child_name("emoji") else: outer._avatar.set_text(msg.sender_name or "") outer._avatar_stack.set_visible_child_name("initials") outer._avatar_stack.set_visible(True) # Sender name if not msg.is_outgoing and msg.sender_name: outer._sender_lbl.set_label(msg.sender_name) outer._sender_lbl.set_visible(True) else: outer._sender_lbl.set_visible(False) # Text / shared contact sc = parse_shared_contact(msg.text) if sc: pub_key_hex, contact_type, contact_name = sc outer._content_stack.set_visible_child_name("contact") if msg.is_outgoing: outer._sc_header_lbl.set_label(_("You shared a contact:")) else: sender = msg.sender_name or "?" outer._sc_header_lbl.set_label( _("{name} shared a contact with you:").format(name=sender) ) icon_map = { 1: "avatar-default-symbolic", 2: "network-server-symbolic", 3: "user-home-symbolic", 4: "weather-clear-symbolic", } outer._sc_icon.set_from_icon_name( icon_map.get(contact_type, "avatar-default-symbolic") ) outer._sc_name_lbl.set_label(contact_name) outer._sc_key_lbl.set_label(f"{pub_key_hex[:8]}...{pub_key_hex[-8:]}") if msg.is_outgoing: outer._sc_add_btn.set_visible(False) else: outer._sc_add_btn.set_visible(True) is_known = self._window.is_contact_known(pub_key_hex) outer._sc_add_btn.set_sensitive(not is_known) outer._sc_add_btn.set_label( _("Already added") if is_known else _("Add Contact") ) if outer._sc_add_handler_id is not None: outer._sc_add_btn.disconnect(outer._sc_add_handler_id) outer._sc_add_handler_id = outer._sc_add_btn.connect( "clicked", self._on_shared_contact_add, pub_key_hex, contact_type, contact_name, ) else: loc = parse_shared_location(msg.text) if loc: lat, lon = loc outer._content_stack.set_visible_child_name("location") if msg.is_outgoing: outer._loc_header_lbl.set_label(_("You shared a location:")) else: sender = msg.sender_name or "?" outer._loc_header_lbl.set_label( _("{name} shared a location:").format(name=sender) ) outer._loc_coords_lbl.set_label(f"{lat:.6f}, {lon:.6f}") self._ensure_loc_map(outer, lat, lon) outer._loc_map_click_handler = outer._loc_map_gesture.connect( "released", lambda g, n, x, y, la=lat, lo=lon: self._on_show_location_map(la, lo), ) outer._loc_open_handler = outer._loc_open_btn.connect( "clicked", self._on_open_location_external, lat, lon ) else: outer._content_stack.set_visible_child_name("text") outer._text_lbl.set_markup(linkify(msg.text)) # Frame style if msg.is_outgoing: outer._frame.add_css_class("outgoing-message") else: outer._frame.remove_css_class("outgoing-message") # Timestamp outer._time_lbl.set_label(msg.timestamp.strftime("%H:%M")) # Outgoing: resend + repeat labels if msg.is_outgoing: outer._resend_lbl.set_visible(True) outer._repeat_lbl.set_visible(True) self._update_resend_label_widget(outer, item) self._update_repeat_label_widget(outer, item) h1 = item.connect( "notify::repeat-count", lambda obj, pspec, w=outer: self._update_repeat_label_widget(w, obj), ) h2 = item.connect( "notify::resend-count", lambda obj, pspec, w=outer: self._update_resend_label_widget(w, obj), ) outer._handler_ids = [h1, h2] else: outer._resend_lbl.set_visible(False) outer._repeat_lbl.set_visible(False) outer._handler_ids = [] elif item.kind == ChannelChatItem.KIND_DATE_DIVIDER: outer._msg_box.set_visible(False) outer._div_box.set_visible(True) outer._div_lbl.set_label(item.date_text) outer._div_lbl.remove_css_class("unread-divider") outer._div_lbl.add_css_class("dim-label") outer._div_line_left.remove_css_class("unread-divider-line") outer._div_line_left.add_css_class("date-divider-line") outer._div_line_right.remove_css_class("unread-divider-line") outer._div_line_right.add_css_class("date-divider-line") elif item.kind == ChannelChatItem.KIND_UNREAD_DIVIDER: outer._msg_box.set_visible(False) outer._div_box.set_visible(True) outer._div_lbl.set_label(_("New Messages")) outer._div_lbl.remove_css_class("dim-label") outer._div_lbl.add_css_class("unread-divider") outer._div_line_left.remove_css_class("date-divider-line") outer._div_line_left.add_css_class("unread-divider-line") outer._div_line_right.remove_css_class("date-divider-line") outer._div_line_right.add_css_class("unread-divider-line") def _on_factory_unbind(self, factory, list_item): """Clean up signal connections when the widget is recycled.""" clamp = list_item.get_child() outer = clamp._outer for hid in outer._handler_ids: if outer._chat_item: outer._chat_item.disconnect(hid) outer._handler_ids = [] if outer._sc_add_handler_id is not None: outer._sc_add_btn.disconnect(outer._sc_add_handler_id) outer._sc_add_handler_id = None if outer._loc_map_click_handler is not None: outer._loc_map_gesture.disconnect(outer._loc_map_click_handler) outer._loc_map_click_handler = None if outer._loc_open_handler is not None: outer._loc_open_btn.disconnect(outer._loc_open_handler) outer._loc_open_handler = None outer._loc_map_frame.set_child(None) outer._chat_item = None # ─── Shared contact helpers ───────────────────────────── def _on_shared_contact_add(self, btn, pub_key_hex, contact_type, name): self._window.add_contact(bytes.fromhex(pub_key_hex), contact_type, name) btn.set_sensitive(False) btn.set_label(_("Already added")) # ─── Shared location helpers ────────────────────────────── @staticmethod def _ensure_loc_map(outer, lat, lon): from meshy.views.map_view import MapView loc_map, loc_marker_layer, loc_viewport = create_shumate_map(zoom_level=14) loc_map.set_vexpand(False) loc_map.set_hexpand(True) loc_map.set_size_request(100, 150) loc_map.get_scale().set_visible(False) loc_map.set_show_zoom_buttons(False) loc_map.get_license().set_visible(False) loc_marker_widget = MapView._build_marker_widget( 0.26, 0.52, 0.96, "find-location-symbolic", None, show_label=False ) loc_marker = Shumate.Marker() loc_marker.set_child(loc_marker_widget) loc_marker_layer.add_marker(loc_marker) loc_viewport.set_zoom_level(14) loc_viewport.set_location(lat, lon) loc_marker.set_location(lat, lon) outer._loc_map_frame.set_child(loc_map) def _on_show_location_map(self, lat, lon): from meshy.views.map_view import MapView dialog, map_w, m_layer, vp = MapView.open_map_dialog(self._window, _("Shared Location")) marker = MapView.create_marker( 0.26, 0.52, 0.96, "find-location-symbolic", _("Shared"), lat, lon ) m_layer.add_marker(marker) dev = self._window.device_info if dev and dev.latitude is not None and dev.longitude is not None: self_marker = MapView.create_marker( 0.90, 0.16, 0.22, "avatar-default-symbolic", dev.name or _("My Device"), dev.latitude, dev.longitude, ) m_layer.add_marker(self_marker) vp.set_zoom_level(14) vp.set_location(lat, lon) dialog.present(self._window) def _on_open_location_external(self, btn, lat, lon): launcher = Gtk.UriLauncher.new(f"geo:{lat},{lon}") launcher.launch(self._window, None, None, None) # ─── Label update helpers ──────────────────────────────── @staticmethod def _update_repeat_label_widget(outer, item): count = item.repeat_count if count > 0: outer._repeat_lbl.set_label(f'\u00b7 {count} repeat{"s" if count != 1 else ""}') else: outer._repeat_lbl.set_label("") @staticmethod def _update_resend_label_widget(outer, item): count = item.resend_count if count > 0: outer._resend_lbl.set_label(f'\u00b7 {count} resend{"s" if count != 1 else ""}') else: outer._resend_lbl.set_label("") # ─── Context menu ─────────────────────────────────────── def _on_list_right_click(self, gesture, n_press, x, y): outer = self._find_outer_at(x, y) if not outer: return item = outer._chat_item if item and item.kind == ChannelChatItem.KIND_MESSAGE and item.message: if gesture: gesture.set_state(Gtk.EventSequenceState.CLAIMED) self._context_item = item self._context_menu_open = True self._is_sticky = False self._frozen_scroll_value = self._scroll.get_vadjustment().get_value() if item.message.is_outgoing: self._context_popover.set_menu_model(self._outgoing_menu) else: self._context_popover.set_menu_model(self._incoming_menu) rect = Gdk.Rectangle() rect.x, rect.y, rect.width, rect.height = int(x), int(y), 1, 1 self._context_popover.set_pointing_to(rect) self._context_popover.popup() def _show_repeats_context(self): item = self._context_item if not item or not item.message: return msg = item.message key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) self._show_repeats_dialog(key) def _resend_context(self): item = self._context_item if not item or not item.message: return self._resend_message(item.message, item) def _delete_context(self): item = self._context_item if not item or not item.message: return self._delete_message_item(item) def _reply_context(self): item = self._context_item if not item or not item.message: return self._reply_to_message(item.message) def _copy_context(self): item = self._context_item if not item or not item.message: return self._copy_message_text(item.message) def _show_paths_context(self): item = self._context_item if not item or not item.message: return msg = item.message key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) self._show_message_paths(key, msg) # ─── Emoji ─────────────────────────────────────────────── def _on_share_contact(self, action, param): if not self._current_channel: return contacts = self._window.contacts if not contacts: return builder = Gtk.Builder.new_from_resource( "/page/codeberg/sesivany/Meshy/ui/share-contact-dialog.ui" ) dialog = builder.get_object("share_dialog") search_btn = builder.get_object("share_search_btn") search_entry = builder.get_object("share_search_entry") list_view = builder.get_object("share_list_view") status_label = builder.get_object("share_status_label") search_btn.bind_property( "active", search_entry, "visible", GObject.BindingFlags.SYNC_CREATE ) search_entry.connect("stop-search", lambda _e: search_btn.set_active(False)) store = Gio.ListStore(item_type=_ContactPickItem) for c in sorted(contacts, key=lambda c: c.name.lower()): store.append(_ContactPickItem(c)) custom_filter = Gtk.CustomFilter.new(self._share_filter_func, search_entry) def _on_search_changed(*_args): custom_filter.changed(Gtk.FilterChange.DIFFERENT) search_entry.connect("search-changed", _on_search_changed) filter_model = Gtk.FilterListModel(model=store, filter=custom_filter) selection = Gtk.NoSelection(model=filter_model) list_view.set_model(selection) def _on_share(contact): text = f"<{contact.public_key_hex}:{contact.type}:{contact.name}>" self._window.send_channel_message(self._current_channel.index, text) dialog.close() factory = Gtk.SignalListItemFactory() factory.connect("setup", lambda f, li: _contact_pick_factory_setup(f, li, _("Share"))) factory.connect("bind", lambda f, li: _contact_pick_factory_bind(f, li, _on_share)) factory.connect("unbind", _contact_pick_factory_unbind) list_view.set_factory(factory) def _on_activate(lv, pos): item = filter_model.get_item(pos) if item: _on_share(item.contact) list_view.connect("activate", _on_activate) total = store.get_n_items() status_label.set_label( _("{total} contacts").format(total=total) if total else _("No contacts") ) dialog._refs = (store, custom_filter, filter_model, selection, factory) dialog.present(self._window) @staticmethod def _share_filter_func(item, search_entry): q = search_entry.get_text().lower() return not q or q in item.contact.name.lower() def _on_share_location(self, action, param): if not self._current_channel: return dev = self._window.device_info if not dev or dev.latitude is None or dev.longitude is None: dialog = Adw.AlertDialog( heading=_("Location Not Available"), body=_("Your companion does not have a GPS location."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return text = f"{dev.latitude:.6f},{dev.longitude:.6f}" self._window.send_channel_message(self._current_channel.index, text) def _on_share_location_map(self, action, param): if not self._current_channel: return from meshy.views import open_map_picker dev = self._window.device_info lat = dev.latitude if dev and dev.latitude is not None else 0.0 lon = dev.longitude if dev and dev.longitude is not None else 0.0 channel_idx = self._current_channel.index open_map_picker( self._window, _("Share Location"), _("Share"), lat, lon, lambda la, lo: self._window.send_channel_message(channel_idx, f"{la:.6f},{lo:.6f}"), ) # ─── @Mention autocomplete ───────────────────────────────── def _get_channel_senders(self) -> list[str]: my_name = self._window.device_info.name or "" seen = set() result = [] for i in range(self._store.get_n_items() - 1, -1, -1): item = self._store.get_item(i) if item.kind == ChannelChatItem.KIND_MESSAGE and item.message: name = item.message.sender_name if name and name != my_name and name not in seen: seen.add(name) result.append(name) return result def _update_mention_popover(self, prefix: str, at_char_idx: int): senders = self._get_channel_senders() filtered = ( [n for n in senders if n.lower().startswith(prefix.lower())] if prefix else senders ) if not filtered: if self._mention_popover: self._mention_popover.popdown() return if not self._mention_popover: self._mention_popover = Gtk.Popover() self._mention_popover.set_parent(self._text_entry) self._mention_popover.set_autohide(False) self._mention_popover.set_has_arrow(False) self._mention_popover.set_position(Gtk.PositionType.TOP) self._mention_popover.set_halign(Gtk.Align.START) self._mention_listbox = Gtk.ListBox() self._mention_listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) self._mention_listbox.connect("row-activated", self._on_mention_selected) scrolled = Gtk.ScrolledWindow( propagate_natural_height=True, max_content_height=280, hscrollbar_policy=Gtk.PolicyType.NEVER, ) scrolled.set_child(self._mention_listbox) self._mention_popover.set_child(scrolled) try: delegate = self._text_entry.get_delegate() layout = delegate.get_layout() index_bytes = len(self._text_entry.get_text()[:at_char_idx].encode("utf-8")) strong_pos, _ = layout.get_cursor_pos(index_bytes) layout_x, _ = delegate.get_layout_offsets() cursor_x = layout_x + strong_pos.x // Pango.SCALE except Exception: cursor_x = 0 rect = Gdk.Rectangle() rect.x = cursor_x rect.y = 0 rect.width = 1 rect.height = self._text_entry.get_height() self._mention_popover.set_pointing_to(rect) while row := self._mention_listbox.get_first_child(): self._mention_listbox.remove(row) for name in filtered[:20]: label = Gtk.Label( label=name, xalign=0, margin_start=8, margin_end=8, margin_top=4, margin_bottom=4 ) self._mention_listbox.append(label) self._mention_popover.popup() def _check_mention_trigger(self, text: str, cursor_pos: int): text_before = text[:cursor_pos] at_idx = text_before.rfind("@") if at_idx == -1: if self._mention_popover: self._mention_popover.popdown() return if at_idx > 0 and text_before[at_idx - 1] not in (" ", "\t"): if self._mention_popover: self._mention_popover.popdown() return after_at = text_before[at_idx + 1 :] if "[" in after_at: if self._mention_popover: self._mention_popover.popdown() return self._update_mention_popover(after_at, at_idx) def _on_mention_selected(self, listbox, row): label = row.get_child() name = label.get_label() text = self._text_entry.get_text() cursor_pos = self._text_entry.get_position() text_before = text[:cursor_pos] at_idx = text_before.rfind("@") if at_idx == -1: return mention = f"@[{name}] " new_text = text[:at_idx] + mention + text[cursor_pos:] self._text_entry.set_text(new_text) self._text_entry.set_position(at_idx + len(mention)) self._mention_popover.popdown() def _on_entry_key_pressed(self, ctrl, keyval, keycode, state): if not self._mention_popover or not self._mention_popover.get_visible(): return False if keyval == Gdk.KEY_Escape: self._mention_popover.popdown() return True if keyval in (Gdk.KEY_Down, Gdk.KEY_Up): selected = self._mention_listbox.get_selected_row() if keyval == Gdk.KEY_Down: if selected: next_row = selected.get_next_sibling() if next_row: self._mention_listbox.select_row(next_row) else: first = self._mention_listbox.get_first_child() if first: self._mention_listbox.select_row(first) elif keyval == Gdk.KEY_Up and selected: prev_row = selected.get_prev_sibling() if prev_row: self._mention_listbox.select_row(prev_row) return True if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter, Gdk.KEY_Tab): selected = self._mention_listbox.get_selected_row() if selected: self._on_mention_selected(self._mention_listbox, selected) return True return False # ─── Channel & messages ────────────────────────────────── def get_last_message_time(self, channel_index: int) -> float | None: try: if not self._window.storage: return None return self._window.storage.get_max_channel_message_timestamp(channel_index) except Exception: return None def set_channel(self, channel: Channel): if self._mention_popover: self._mention_popover.popdown() if self._current_channel is not None: adj = self._scroll.get_vadjustment() self._scroll_positions[self._current_channel.index] = adj.get_value() self._current_channel = channel self._stack.set_visible_child_name("chat") self._load_messages(channel) def _load_messages(self, channel: Channel): self._outgoing_items.clear() self._repeats.clear() self._incoming_paths.clear() self._seen_channel_msgs.clear() self._incoming_items.clear() self._resend_ts_map.clear() self._unread_pos = -1 self._messages_offset = 0 self._all_loaded = False self._loading_more = False messages = self._window.get_channel_messages(channel.index, limit=_BATCH_SIZE) self._messages_offset = len(messages) self._all_loaded = len(messages) < _BATCH_SIZE last_read_at = self._window.get_channel_last_read_at(channel.index) self._preload_paths(messages) new_items = [] divider_inserted = False last_date = None for msg in messages: msg_date = msg.timestamp.date() if last_date is not None and msg_date != last_date: new_items.append( ChannelChatItem( kind=ChannelChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(msg.timestamp), ) ) last_date = msg_date if ( not divider_inserted and last_read_at is not None and not msg.is_outgoing and msg.timestamp.timestamp() > last_read_at ): new_items.append(ChannelChatItem(kind=ChannelChatItem.KIND_UNREAD_DIVIDER)) self._unread_pos = len(new_items) - 1 divider_inserted = True new_items.append(self._create_message_item(msg)) saved_pos = self._scroll_positions.get(channel.index) if saved_pos is not None: self._is_sticky = False self._restoring_scroll = True # Replace all items in a single atomic operation self._store.splice(0, self._store.get_n_items(), new_items) if saved_pos is not None: GLib.idle_add(lambda pos=saved_pos: self._restore_scroll_position(pos)) else: GLib.idle_add(self._scroll_to_bottom) if self._unread_pos >= 0: GLib.idle_add(lambda: self._update_unread_btn_visibility() or False) # Mark as read now self._window.mark_channel_read(channel.index) def _preload_paths(self, messages): if not self._window.storage: return for msg in messages: key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) if msg.is_outgoing: paths = self._window.storage.get_channel_message_paths( msg.channel_index, int(msg.timestamp.timestamp()), msg.text, ) if paths: self._repeats[key] = [ { "path_hops": self._split_path_hops( p.get("path_bytes", ""), p.get("path_len", 0), ), "path_len": p.get("path_len", 0), } for p in paths ] else: if key not in self._incoming_paths: paths = self._window.storage.get_channel_message_paths( msg.channel_index, int(msg.timestamp.timestamp()), msg.text, ) if paths: self._incoming_paths[key] = paths def _load_older_messages(self): if self._all_loaded or self._loading_more or not self._current_channel: return self._loading_more = True messages = self._window.get_channel_messages( self._current_channel.index, limit=_BATCH_SIZE, offset=self._messages_offset ) if not messages: self._all_loaded = True self._loading_more = False return self._preload_paths(messages) older_items = [] last_date = None for msg in messages: msg_date = msg.timestamp.date() if last_date is not None and msg_date != last_date: older_items.append( ChannelChatItem( kind=ChannelChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(msg.timestamp), ) ) last_date = msg_date older_items.append(self._create_message_item(msg)) # Add boundary date divider if the dates differ if last_date is not None: for i in range(self._store.get_n_items()): first = self._store.get_item(i) if first.kind == ChannelChatItem.KIND_MESSAGE: if first.message.timestamp.date() != last_date: older_items.append( ChannelChatItem( kind=ChannelChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(first.message.timestamp), ) ) break self._store.splice(0, 0, older_items) if self._unread_pos >= 0: self._unread_pos += len(older_items) self._messages_offset += len(messages) self._all_loaded = len(messages) < _BATCH_SIZE self._loading_more = False @staticmethod def _split_path_hops(path_bytes_hex: str, hop_count: int) -> list[str]: """Split concatenated path hex into individual hop prefixes. Supports 1-byte (2 hex), 2-byte (4 hex), and 3-byte (6 hex) hash sizes. The hash size is derived from total bytes / hop count. """ if not path_bytes_hex or hop_count <= 0: return [] total_bytes = len(path_bytes_hex) // 2 if total_bytes % hop_count == 0: hash_size = total_bytes // hop_count else: hash_size = 1 # fallback chars_per_hop = hash_size * 2 return [ path_bytes_hex[i : i + chars_per_hop].upper() for i in range(0, len(path_bytes_hex), chars_per_hop) ] def _make_msg_key(self, channel_index, timestamp, text): """Create a key for matching outgoing messages to repeats.""" ts = int(timestamp.timestamp()) if hasattr(timestamp, "timestamp") else int(timestamp) return (channel_index, ts, text) def _create_message_item(self, msg: ChannelMessage) -> ChannelChatItem: """Create a ChannelChatItem and register it in tracking dicts.""" item = ChannelChatItem(kind=ChannelChatItem.KIND_MESSAGE, message=msg) key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) if msg.is_outgoing: self._outgoing_items[key] = item if key not in self._repeats: self._repeats[key] = [] # Set initial repeat count from persisted data item._repeat_count_val = len(self._repeats.get(key, [])) else: self._incoming_items[key] = item return item def _add_message_item(self, msg: ChannelMessage): """Create a ChannelChatItem and add it to the store.""" item = self._create_message_item(msg) self._store.append(item) # ─── Dialogs (unchanged) ───────────────────────────────── def _show_message_paths(self, key, msg): """Show dialog with message path variants and repeater hops.""" paths = self._incoming_paths.get(key, []) # Fallback to storage if no in-memory data if not paths and self._window.storage: paths = self._window.storage.get_channel_message_paths( msg.channel_index, int(msg.timestamp.timestamp()), msg.text, ) if paths: self._incoming_paths[key] = paths if not paths: dialog = Adw.AlertDialog( heading=_("Message Paths"), body=_( "No path information available. Path data is only " "captured for messages received while connected." ), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return dialog = Adw.Dialog() dialog.set_title(_("Message Paths")) dialog.set_content_width(420) dialog.set_content_height(480) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) scroll = Gtk.ScrolledWindow(vexpand=True) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=12, margin_end=12, margin_top=12, margin_bottom=12, ) info_label = Gtk.Label( label=f'Message from {msg.sender_name or "unknown"} ' f'received via {len(paths)} path{"s" if len(paths) != 1 else ""}:', wrap=True, xalign=0, ) info_label.add_css_class("dim-label") content.append(info_label) for i, p in enumerate(paths, 1): snr = p.get("snr") path_len = p.get("path_len", -1) path_bytes_hex = p.get("path_bytes", "") if path_bytes_hex and path_len > 0: hops = self._split_path_hops(path_bytes_hex, path_len) num_hops = len(hops) if hops else path_len hops_str = f'{num_hops} hop{"s" if num_hops != 1 else ""}' elif path_len == 255 or path_len == -1: hops_str = "flood" elif path_len == 0: hops_str = "direct" else: hops_str = f'{path_len} hop{"s" if path_len != 1 else ""}' snr_str = f"SNR: {snr:.1f} dB" if snr is not None else "" title_parts = [f"Path {i}: {hops_str}"] if snr_str: title_parts.append(snr_str) path_group = Adw.PreferencesGroup( title=" \u00b7 ".join(title_parts), ) if path_bytes_hex and path_len > 0: hops = self._split_path_hops(path_bytes_hex, path_len) hop_contacts = self._window.resolve_hop_prefixes(hops) for hop_idx, prefix in enumerate(hops): contact = hop_contacts[hop_idx] name = contact.name if contact else _("Unknown repeater") subtitle = f"Hop {hop_idx + 1} \u00b7 prefix {prefix}" hop_row = Adw.ActionRow( title=name, subtitle=subtitle, ) hop_row.add_prefix(Gtk.Image.new_from_icon_name("network-server-symbolic")) path_group.add(hop_row) if any(c and c.has_location for c in hop_contacts): map_btn = Gtk.Button(icon_name="map-symbolic", valign=Gtk.Align.CENTER) map_btn.add_css_class("flat") map_btn.set_tooltip_text(_("Show on Map")) map_btn.connect( "clicked", lambda b, hc=hop_contacts, hp=hops: self._show_channel_path_on_map(hc, hp), ) path_group.set_header_suffix(map_btn) elif path_len == 0: direct_row = Adw.ActionRow( title=_("Direct message"), subtitle=_("No repeaters were used"), sensitive=False, ) direct_row.add_prefix(Gtk.Image.new_from_icon_name("network-wireless-symbolic")) path_group.add(direct_row) else: no_path_row = Adw.ActionRow( title=_("No repeater details"), subtitle=_("Path data is only captured via LOG_DATA while connected"), sensitive=False, ) path_group.add(no_path_row) content.append(path_group) scroll.set_child(content) toolbar_view.set_content(scroll) dialog.set_child(toolbar_view) dialog.present(self._window) def _show_channel_path_on_map(self, hop_contacts, hop_prefixes=None): """Show a channel message path on the map.""" import gi from meshy.views.map_view import ( _MARKER_COLORS, _MARKER_ICONS, _SELF_COLOR, _UNKNOWN_COLOR, MapView, ) gi.require_version("Shumate", "1.0") from gi.repository import Shumate device = self._window.device_info self_lat = getattr(device, "latitude", None) self_lon = getattr(device, "longitude", None) has_self = self_lat and self_lon and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) # Build full hop list including unknowns raw_hops = [] for i, c in enumerate(hop_contacts): prefix = hop_prefixes[i] if hop_prefixes and i < len(hop_prefixes) else None if c and c.has_location: raw_hops.append((c.latitude, c.longitude, c, prefix)) else: raw_hops.append((None, None, c, prefix)) if has_self: raw_hops.append((self_lat, self_lon, None, None)) # Interpolate unknown positions interp = [] for lat, lon, contact, prefix in raw_hops: interp.append((lat, lon, contact, prefix, None)) interp = MapView.interpolate_unknown_hops(interp) if not any(h[0] is not None for h in interp): return dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( self._window, title=_("Message Path") ) map_widget.get_map().remove_layer(marker_layer) path_layer = MapView.create_dashed_path_layer(viewport) map_widget.get_map().add_layer(path_layer) map_widget.get_map().add_layer(marker_layer) lats, lons = [], [] hop_num = 0 for lat, lon, contact, prefix, _snr in interp: if lat is None: continue node = Shumate.Marker() node.set_location(lat, lon) path_layer.add_node(node) if contact is None and prefix is None: # Self/device marker r, g, b = _SELF_COLOR icon_name = "network-wireless-symbolic" name = device.name or _("My Device") type_label = None badge = "" elif contact is None or not contact.has_location: # Unknown/interpolated hop r, g, b = _UNKNOWN_COLOR icon_name = "network-server-symbolic" name = prefix or _("Unknown") type_label = _("Unknown repeater") hop_num += 1 badge = str(hop_num) else: r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") name = contact.name type_label = contact.type_label hop_num += 1 badge = str(hop_num) is_interpolated = (contact is None or not contact.has_location) and prefix is not None detail_cb = (lambda c=contact: self._window.show_contact_detail(c)) if contact else None widget = MapView._build_marker_widget(r, g, b, icon_name, "", badge_text=badge) widget.set_tooltip_text(name) click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, w=widget, nm=name, la=lat, lo=lon, t=type_label, ip=is_interpolated, od=detail_cb: MapView.show_marker_popover( w, nm, la, lo, contact_type=t, interpolated=ip, on_detail=od ), ) widget.add_controller(click) hop_marker = Shumate.Marker() hop_marker.set_child(widget) hop_marker.set_location(lat, lon) marker_layer.add_marker(hop_marker) lats.append(lat) lons.append(lon) MapView.fit_viewport(viewport, lats, lons) dialog.present(self._window) def _reply_to_message(self, msg): """Insert @[sender_name] into the text entry.""" mention = f"@[{msg.sender_name}] " if msg.sender_name else "" self._text_entry.set_text(mention) self._text_entry.set_position(len(mention)) self._text_entry.grab_focus() def _copy_message_text(self, msg): """Copy message text to clipboard.""" clipboard = self._window.get_clipboard() clipboard.set(msg.text) def _show_repeats_dialog(self, key): repeats = self._repeats.get(key, []) if not repeats: dialog = Adw.AlertDialog( heading=_("Heard Repeats"), body=_("No repeats heard yet."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return dialog = Adw.AlertDialog(heading=_("Heard Repeats")) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) for i, rep in enumerate(repeats, 1): hops = rep.get("path_hops", []) if hops: hop_names = [] for prefix in hops: contact = self._window.find_contact_by_key_prefix(prefix.lower()) hop_names.append(contact.name if contact else prefix) path_str = " \u2192 ".join(hop_names) else: path_len = rep.get("path_len", 0) path_str = ( f'{path_len} hop{"s" if path_len != 1 else ""}' if path_len > 0 else "direct" ) label = Gtk.Label(label=f"{i}. {path_str}", xalign=0, wrap=True) content.append(label) dialog.set_extra_child(content) dialog.add_response("ok", _("OK")) dialog.present(self._window) # ─── Message actions ───────────────────────────────────── def _resend_message(self, msg, item): if self._current_channel: new_ts = self._window.resend_channel_message(self._current_channel.index, msg.text) original_key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) resend_key = (msg.channel_index, new_ts, msg.text) self._resend_ts_map[resend_key] = original_key item.props.resend_count = item.resend_count + 1 def _delete_message_item(self, item): msg = item.message # Find and remove from store for i in range(self._store.get_n_items()): if self._store.get_item(i) is item: self._store.remove(i) break # Clean up tracking dicts key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) self._outgoing_items.pop(key, None) self._incoming_items.pop(key, None) self._repeats.pop(key, None) self._incoming_paths.pop(key, None) self._seen_channel_msgs.discard(key) # Delete from storage self._window.delete_channel_message( msg.channel_index, msg.timestamp.timestamp(), msg.text, ) # ─── Input ─────────────────────────────────────────────── def _get_channel_limit(self) -> int: sender_name = self._window.device_info.name or "" return max_channel_text_bytes(sender_name) def _on_text_changed(self, entry): text = entry.get_text() used = len(text.encode("utf-8")) limit = self._get_channel_limit() if used == 0: self._char_label.set_label("") self._send_btn.set_sensitive(True) self._char_label.remove_css_class("error") else: self._char_label.set_label(f"{used}/{limit}") over = used > limit self._send_btn.set_sensitive(not over) if over: self._char_label.add_css_class("error") else: self._char_label.remove_css_class("error") self._check_mention_trigger(text, entry.get_position()) def _on_send(self, *args): if not self._current_channel: return text = self._text_entry.get_text().strip() if not text: return if len(text.encode("utf-8")) > self._get_channel_limit(): return self._text_entry.set_text("") self._window.send_channel_message(self._current_channel.index, text) # ─── External callbacks ────────────────────────────────── def on_channel_message_sent(self, msg: ChannelMessage, sender_ts: int): """Called when a channel message is sent by us.""" if ( self._current_channel and msg.channel_index == self._current_channel.index and self._stack.get_visible_child_name() == "chat" ): # Force sticky so notify::upper handler scrolls to the new message self._is_sticky = True self._add_message_item(msg) def on_path_variant_received(self, msg, path_info=None): """Check if this is a duplicate message via a different path. Returns True if it's a duplicate (already seen), False if new.""" key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) if key in self._seen_channel_msgs: if path_info: self._incoming_paths.setdefault(key, []).append(path_info) return True self._seen_channel_msgs.add(key) if path_info: self._incoming_paths.setdefault(key, []).append(path_info) return False def add_path_info(self, msg, path_info): """Append path_info to an existing message without affecting dedup.""" key = self._make_msg_key(msg.channel_index, msg.timestamp, msg.text) if key in self._incoming_paths: self._incoming_paths[key].append(path_info) else: self._incoming_paths[key] = [path_info] def on_channel_message_received(self, msg: ChannelMessage, path_info=None): if ( self._current_channel and msg.channel_index == self._current_channel.index and self._stack.get_visible_child_name() == "chat" ): # If sticky, the notify::upper handler will auto-scroll self._add_message_item(msg) def on_repeat_received(self, channel_idx, sender_ts, text, path_hops): """Called when an echo/repeat of our outgoing message is received. Returns the original message key (channel_idx, ts, text) if matched, so the caller can persist path info under the original timestamp. """ repeat_entry = { "path_hops": path_hops or [], "path_len": len(path_hops) if path_hops else 0, } # Check if this echo matches a resend timestamp resend_key = (channel_idx, sender_ts, text) matched_key = self._resend_ts_map.pop(resend_key, None) if matched_key is None: # Fall back to heuristic: same channel + timestamp within 30s + matching text for k in self._outgoing_items: if k[0] != channel_idx: continue ts_diff = abs(k[1] - sender_ts) if ts_diff > 30: continue if k[2] == text or text.startswith(k[2]) or k[2].startswith(text): matched_key = k break if matched_key is None: key = (channel_idx, sender_ts, text) if key not in self._repeats: self._repeats[key] = [] self._repeats[key].append(repeat_entry) return None if matched_key not in self._repeats: self._repeats[matched_key] = [] self._repeats[matched_key].append(repeat_entry) item = self._outgoing_items.get(matched_key) if item: item.props.repeat_count = len(self._repeats[matched_key]) return matched_key @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/channels-view.ui") class ChannelsView(Gtk.Box): """Channels list - shown in the sidebar.""" __gtype_name__ = "MeshyChannelsView" _channel_list = Gtk.Template.Child("channel_list") _search_entry = Gtk.Template.Child("search_entry") _status_label = Gtk.Template.Child("status_label") FILTER_ALL = "all" FILTER_PUBLIC = "public" FILTER_HASHTAG = "hashtag" FILTER_PRIVATE = "private" SORT_AZ = "az" SORT_MESSAGES = "messages" def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._chat_widget = ChannelChatWidget(window) self._unread_indices: set[int] = set() self._unread_counts: dict[int, int] = {} self._unread_dots: dict[int, Gtk.Label] = {} self._current_filter = self.FILTER_ALL settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") saved_sort = settings.get_string("channel-sort") self._current_sort = ( saved_sort if saved_sort in (self.SORT_AZ, self.SORT_MESSAGES) else self.SORT_MESSAGES ) self._total_channels = 0 self._channel_list.connect("row-activated", self._on_row_selected) self._search_entry.connect("search-changed", self._on_search_changed) self._channel_list.set_filter_func(self._filter_func) # Actions for the add channel menu and filter/sort action_group = Gio.SimpleActionGroup() action = Gio.SimpleAction.new("create-private", None) action.connect("activate", self._on_create_private_channel) action_group.add_action(action) action = Gio.SimpleAction.new("join-private", None) action.connect("activate", self._on_join_private_channel) action_group.add_action(action) action = Gio.SimpleAction.new("join-public", None) action.connect("activate", self._on_join_public_channel) action_group.add_action(action) action = Gio.SimpleAction.new("join-hashtag", None) action.connect("activate", self._on_join_hashtag_channel) action_group.add_action(action) filter_action = Gio.SimpleAction.new_stateful( "filter", GLib.VariantType.new("s"), GLib.Variant.new_string(self._current_filter) ) filter_action.connect("activate", self._on_filter_changed) action_group.add_action(filter_action) sort_action = Gio.SimpleAction.new_stateful( "sort", GLib.VariantType.new("s"), GLib.Variant.new_string(self._current_sort) ) sort_action.connect("activate", self._on_sort_changed) action_group.add_action(sort_action) self._action_group = action_group self.insert_action_group("channel", action_group) self._search_btn = None self._search_binding = None self.update_channels(self._window.channels) def get_chat_widget(self) -> ChannelChatWidget: return self._chat_widget def set_search_button(self, btn): if self._search_binding: self._search_binding.unbind() self._search_btn = btn self._search_binding = btn.bind_property( "active", self._search_entry, "visible", GObject.BindingFlags.SYNC_CREATE ) self._search_entry.connect("stop-search", lambda _e: btn.set_active(False)) def build_filter_menu(self): menu = Gio.Menu() for filter_id, label in ( ("all", _("All")), ("public", _("Public")), ("hashtag", _("Hashtag")), ("private", _("Private")), ): item = Gio.MenuItem.new(label, None) item.set_action_and_target_value("channel.filter", GLib.Variant.new_string(filter_id)) menu.append_item(item) return menu def build_sort_menu(self): menu = Gio.Menu() for sort_id, label in ( ("az", _("A–Z")), ("messages", _("Latest Messages")), ): item = Gio.MenuItem.new(label, None) item.set_action_and_target_value("channel.sort", GLib.Variant.new_string(sort_id)) menu.append_item(item) return menu def _filter_func(self, row) -> bool: if not hasattr(row, "_channel"): return True channel = row._channel f = self._current_filter if ( f == self.FILTER_PUBLIC and not channel.is_public_channel or f == self.FILTER_HASHTAG and not channel.is_hashtag_channel or f == self.FILTER_PRIVATE and (channel.is_public_channel or channel.is_hashtag_channel) ): return False search = self._search_entry.get_text().lower() return not (search and search not in (channel.name or "").lower()) def _update_status_count(self): total = self._total_channels if self._current_filter != self.FILTER_ALL or self._search_entry.get_text(): visible = 0 row = self._channel_list.get_first_child() while row: if hasattr(row, "_channel") and self._filter_func(row): visible += 1 row = row.get_next_sibling() self._status_label.set_label( _("{visible}/{total} channels").format(visible=visible, total=total) ) else: self._status_label.set_label( _("{total} channels").format(total=total) if total != 1 else _("{total} channel").format(total=total) ) def _on_search_changed(self, entry): self._channel_list.invalidate_filter() GLib.idle_add(self._update_status_count) def _on_filter_changed(self, action, param): self._current_filter = param.get_string() action.set_state(param) self._channel_list.invalidate_filter() GLib.idle_add(self._update_status_count) def _on_sort_changed(self, action, param): self._current_sort = param.get_string() action.set_state(param) settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") settings.set_string("channel-sort", self._current_sort) self.update_channels(self._window.channels) def _channel_sort_key(self, channel: Channel): type_order = 0 if channel.is_public_channel else (1 if channel.is_hashtag_channel else 2) if self._current_sort == self.SORT_AZ: return (type_order, (channel.name or "").lower()) else: last_msg = self._chat_widget.get_last_message_time(channel.index) return (type_order, -(last_msg or 0)) def _on_row_selected(self, listbox, row): if row and hasattr(row, "_channel"): self._chat_widget.set_channel(row._channel) # Navigate to content panel (matters in collapsed/phone mode) title = row._channel.name or _("Channel {}").format(row._channel.index) if row._channel.region: title = _("{name} ({region})").format(name=title, region=row._channel.region) self._window.content_page.set_title(title) self._window.split_view.set_show_content(True) self.mark_unread(row._channel.index, False) def update_channels(self, channels: list[Channel]): child = self._channel_list.get_first_child() while child: next_child = child.get_next_sibling() self._channel_list.remove(child) child = next_child non_empty = [c for c in channels if not c.is_empty] self._total_channels = len(non_empty) sorted_channels = sorted(non_empty, key=self._channel_sort_key) for channel in sorted_channels: row = Adw.ActionRow( title=channel.name or _("Channel {}").format(channel.index), subtitle=channel.region, ) if channel.is_hashtag_channel: icon_name = "hashtag-symbolic" elif channel.is_public_channel: icon_name = "system-users-symbolic" else: icon_name = "padlock2-symbolic" row.add_prefix(Gtk.Image.new_from_icon_name(icon_name)) row.set_activatable(True) row._channel = channel # Unread badge — visible by default when count > 0, hidden on hover count = self._unread_counts.get(channel.index, 0) unread_badge = Gtk.Label(label=str(count) if count else "") unread_badge.add_css_class("unread-badge") unread_badge.set_valign(Gtk.Align.CENTER) unread_badge.set_visible(count > 0) row.add_suffix(unread_badge) self._unread_dots[channel.index] = unread_badge # "More" button + hover behavior + long press info_btn = Gtk.Button(icon_name="view-more-symbolic", valign=Gtk.Align.CENTER) info_btn.add_css_class("flat") info_btn.set_visible(False) on_detail = lambda *_, ch=channel: self._show_channel_detail(ch) info_btn.connect("clicked", on_detail) row.add_suffix(info_btn) _add_hover_swap( row, info_btn, unread_badge, lambda _idx=channel.index: self._unread_counts.get(_idx, 0), on_detail, ) self._channel_list.append(row) GLib.idle_add(self._update_status_count) def _get_visible_rows(self): """Return visible rows in display order.""" rows = [] row = self._channel_list.get_first_child() while row: if row.get_visible() and hasattr(row, "_channel"): rows.append(row) row = row.get_next_sibling() return rows def navigate(self, direction: int, unread_only: bool = False): """Move selection up (-1) or down (+1). If unread_only, skip to next unread.""" rows = self._get_visible_rows() if not rows: return if unread_only: rows = [r for r in rows if r._channel.index in self._unread_indices] if not rows: return selected = self._channel_list.get_selected_row() if selected and selected in rows: idx = rows.index(selected) idx = (idx + direction) % len(rows) else: idx = 0 if direction > 0 else len(rows) - 1 self._channel_list.select_row(rows[idx]) rows[idx].grab_focus() def on_path_variant_received(self, msg, path_info=None): return self._chat_widget.on_path_variant_received(msg, path_info) def add_path_info(self, msg, path_info): self._chat_widget.add_path_info(msg, path_info) def on_channel_message_received(self, msg: ChannelMessage, path_info=None): self._chat_widget.on_channel_message_received(msg, path_info) def on_channel_message_sent(self, msg: ChannelMessage, sender_ts: int): self._chat_widget.on_channel_message_sent(msg, sender_ts) def on_repeat_received(self, channel_idx, sender_ts, text, path_hops): return self._chat_widget.on_repeat_received(channel_idx, sender_ts, text, path_hops) def mark_unread(self, channel_index: int, unread: bool): """Show or hide the unread badge for a channel.""" if unread: self._unread_indices.add(channel_index) self._unread_counts[channel_index] = self._unread_counts.get(channel_index, 0) + 1 else: self._unread_indices.discard(channel_index) self._unread_counts[channel_index] = 0 if channel_index in self._unread_dots: badge = self._unread_dots[channel_index] count = self._unread_counts.get(channel_index, 0) badge.set_label(str(count) if count else "") badge.set_visible(count > 0) def _show_channel_detail(self, channel: Channel): """Show channel detail dialog with rename, PSK, notifications, and remove.""" dialog = Adw.Dialog() dialog.set_title(channel.name or _("Channel {}").format(channel.index)) dialog.set_content_width(420) dialog.set_content_height(480) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) scroll = Gtk.ScrolledWindow(vexpand=True) clamp = Adw.Clamp(maximum_size=420) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=12, margin_end=12, margin_top=12, margin_bottom=12, ) # Info group info_group = Adw.PreferencesGroup(title=_("Channel Info")) if channel.is_public_channel: name_entry = Adw.EntryRow(title=_("Name")) name_entry.set_text(channel.name) name_entry.set_show_apply_button(True) def _on_rename_apply(*args): new_name = name_entry.get_text().strip() if new_name: self._window.set_channel(channel.index, new_name, channel.psk) name_entry.connect("apply", _on_rename_apply) info_group.add(name_entry) else: info_group.add( Adw.ActionRow( title=_("Name"), subtitle=channel.name or _("Channel {}").format(channel.index), ) ) info_group.add( Adw.ActionRow( title=_("Type"), subtitle=channel.channel_type, ) ) # Private Key row with copy button (only for private channels) if not channel.is_public_channel and not channel.is_hashtag_channel: psk_row = Adw.ActionRow( title=_("Private Key"), subtitle=channel.psk_hex, ) psk_row.set_subtitle_selectable(True) copy_btn = Gtk.Button(icon_name="edit-copy-symbolic", valign=Gtk.Align.CENTER) copy_btn.add_css_class("flat") copy_btn.set_tooltip_text("Copy private key to clipboard") def _on_copy_psk(*args): clipboard = self._window.get_clipboard() clipboard.set(channel.psk_hex) self._window.show_toast(_("Private key copied to clipboard"), timeout=2) copy_btn.connect("clicked", _on_copy_psk) psk_row.add_suffix(copy_btn) info_group.add(psk_row) content.append(info_group) # Notifications group notif_group = Adw.PreferencesGroup(title=_("Notifications")) notif_row = Adw.ComboRow(title=_("Notification Level")) notif_list = Gtk.StringList() for label in [_("Default"), _("All Messages"), _("Mentions Only"), _("None")]: notif_list.append(label) notif_row.set_model(notif_list) level_to_index = { NotificationLevel.DEFAULT: 0, NotificationLevel.ALL: 1, NotificationLevel.MENTIONS: 2, NotificationLevel.NONE: 3, } notif_row.set_selected(level_to_index.get(channel.notification_level, 0)) def _on_notif_changed(*args): selected = notif_row.get_selected() level = [ NotificationLevel.DEFAULT, NotificationLevel.ALL, NotificationLevel.MENTIONS, NotificationLevel.NONE, ][selected] self._window.set_channel_notification_level(channel, level) notif_row.connect("notify::selected", _on_notif_changed) notif_group.add(notif_row) content.append(notif_group) # Region group available_regions = self._window.get_regions() region_group = Adw.PreferencesGroup( title=_("Region"), description=_("Limit flood messages to a specific region"), ) if available_regions: region_row = Adw.ComboRow(title=_("Region")) region_list = Gtk.StringList() region_list.append(_("None")) for r in available_regions: region_list.append(r) region_row.set_model(region_list) if channel.region in available_regions: region_row.set_selected(available_regions.index(channel.region) + 1) else: region_row.set_selected(0) def _on_region_changed(*args): selected = region_row.get_selected() new_region = available_regions[selected - 1] if selected > 0 else "" channel.region = new_region if self._window.storage: self._window.storage.set_channel_region(channel.index, new_region) self.update_channels(self._window._channels) region_row.connect("notify::selected", _on_region_changed) region_group.add(region_row) else: hint_row = Adw.ActionRow( title=_("No regions defined"), subtitle=_("Add regions in Settings to assign them to channels"), ) hint_row.add_suffix(Gtk.Image(icon_name="go-next-symbolic")) hint_row.set_activatable(True) def _on_hint_activated(*args): dialog.connect( "closed", lambda *a: self._window._nav_buttons["settings"].set_active(True) ) dialog.close() hint_row.connect("activated", _on_hint_activated) region_group.add(hint_row) content.append(region_group) # Actions group actions_group = Adw.PreferencesGroup(title=_("Actions")) remove_row = Adw.ActionRow(title=_("Remove Channel")) remove_btn = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER) remove_btn.add_css_class("destructive-action") def _on_remove(*args): confirm = Adw.AlertDialog( heading=_("Remove Channel?"), body=_('Remove "{}" and all its messages?').format( channel.name or _("Channel {}").format(channel.index) ), ) confirm.add_response("cancel", _("Cancel")) confirm.add_response("remove", _("Remove")) confirm.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) confirm.connect( "response", lambda d, r: (self._window.remove_channel(channel), dialog.close()) if r == "remove" else None, ) confirm.present(self._window) remove_btn.connect("clicked", _on_remove) remove_row.add_suffix(remove_btn) remove_row.set_activatable_widget(remove_btn) actions_group.add(remove_row) content.append(actions_group) clamp.set_child(content) scroll.set_child(clamp) toolbar_view.set_content(scroll) dialog.set_child(toolbar_view) dialog.present(self._window) def _find_next_empty_index(self) -> int | None: """Find the next available channel index (0-39).""" used = {ch.index for ch in self._window.channels if not ch.is_empty} for i in range(40): if i not in used: return i return None def _show_no_slots_dialog(self): dialog = Adw.AlertDialog( heading=_("No Slots Available"), body=_("All 8 channel slots are in use."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) def _on_create_private_channel(self, *args): idx = self._find_next_empty_index() if idx is None: self._show_no_slots_dialog() return dialog = Adw.AlertDialog( heading=_("Create a Private Channel"), body=_("A random PSK will be generated. Share it with others so they can join."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) name_row = Adw.EntryRow(title=_("Channel Name")) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(name_row) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("create", _("Create")) dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "create": import os name = name_row.get_text().strip() if not name: return psk = os.urandom(16) self._window.set_channel(idx, name, psk) dialog.connect("response", _on_response) dialog.present(self._window) def _on_join_private_channel(self, *args): idx = self._find_next_empty_index() if idx is None: self._show_no_slots_dialog() return dialog = Adw.AlertDialog( heading=_("Join a Private Channel"), body=_("Enter the channel name and private key shared with you."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) name_row = Adw.EntryRow(title=_("Channel Name")) psk_row = Adw.EntryRow(title=_("Private Key (32 hex characters)")) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(name_row) fields_list.append(psk_row) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("join", _("Join")) dialog.set_response_appearance("join", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "join": try: name = name_row.get_text().strip() psk = bytes.fromhex(psk_row.get_text().strip()) if len(psk) == 16 and name: self._window.set_channel(idx, name, psk) except ValueError: pass dialog.connect("response", _on_response) dialog.present(self._window) def _on_join_public_channel(self, *args): # Check if already on the public channel for ch in self._window.channels: if ch.is_public_channel: dialog = Adw.AlertDialog( heading=_("Already Joined"), body=_("You are already on the public channel.") ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return idx = self._find_next_empty_index() if idx is None: self._show_no_slots_dialog() return psk = bytes.fromhex(Channel.PUBLIC_CHANNEL_PSK) self._window.set_channel(idx, _("Public"), psk) def _on_join_hashtag_channel(self, *args): idx = self._find_next_empty_index() if idx is None: self._show_no_slots_dialog() return dialog = Adw.AlertDialog( heading=_("Join a Hashtag Channel"), body=_("Enter a hashtag name. Anyone using the same hashtag can communicate."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) hashtag_row = Adw.EntryRow(title=_("Hashtag (e.g. #general)")) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(hashtag_row) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("join", _("Join")) dialog.set_response_appearance("join", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "join": tag = hashtag_row.get_text().strip() if not tag: return if not tag.startswith("#"): tag = f"#{tag}" h = hashlib.sha256(tag.encode()).digest() psk = h[:16] name = tag self._window.set_channel(idx, name, psk) dialog.connect("response", _on_response) dialog.present(self._window) meshy/src/views/chat_mixin.py000066400000000000000000000133171521052255700166350ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Shared behaviour for chat-style ListViews (DM chat and channel chat).""" from gi.repository import GLib, Gtk class ChatScrollMixin: """Scroll management, context-menu helpers, and emoji picker. Host class must provide: _scroll Gtk.ScrolledWindow _list_view Gtk.ListView _store Gio.ListStore _scroll_down_revealer Gtk.Revealer _unread_btn_revealer Gtk.Revealer _text_entry Gtk.Entry / Gtk.Text _context_popover Gtk.PopoverMenu _window main application window _is_sticky bool _restoring_scroll bool _context_menu_open bool _frozen_scroll_value float _unread_pos int _load_older_messages() method """ # ─── Scroll state ──────────────────────────────────────── def _is_at_bottom(self) -> bool: adj = self._scroll.get_vadjustment() return adj.get_value() + adj.get_page_size() >= adj.get_upper() - 1.0 def _on_edge_overshot(self, scrolled_window, pos): if pos == Gtk.PositionType.TOP: self._load_older_messages() def _on_scroll_value_changed(self, adj): if self._restoring_scroll: return if self._context_menu_open: adj.set_value(self._frozen_scroll_value) return at_bottom = self._is_at_bottom() self._is_sticky = at_bottom self._scroll_down_revealer.set_reveal_child(not at_bottom) self._update_unread_btn_visibility() def _on_scroll_upper_changed(self, adj, *args): if self._restoring_scroll: return if self._context_menu_open: adj.set_value(self._frozen_scroll_value) return if self._is_sticky: self._scroll_down() else: self._scroll_down_revealer.set_reveal_child(not self._is_at_bottom()) def _is_item_visible(self, position: int) -> bool: adj = self._scroll.get_vadjustment() top = adj.get_value() bottom = top + adj.get_page_size() n = self._store.get_n_items() if position < 0 or position >= n or n == 0: return False item_y = (position / n) * adj.get_upper() return top <= item_y < bottom def _update_unread_btn_visibility(self): if self._unread_pos < 0: self._unread_btn_revealer.set_reveal_child(False) return visible = self._is_item_visible(self._unread_pos) if visible: self._unread_pos = -1 self._unread_btn_revealer.set_reveal_child(False) else: self._unread_btn_revealer.set_reveal_child(True) # ─── Scroll actions ────────────────────────────────────── def _scroll_down(self): if self._context_menu_open or self._is_at_bottom(): return n = self._store.get_n_items() if n > 0: GLib.idle_add(self._do_scroll_down) def _do_scroll_down(self): if self._context_menu_open: return False n = self._store.get_n_items() if n > 0: self._list_view.scroll_to(n - 1, Gtk.ListScrollFlags.FOCUS, None) return False def _scroll_to_bottom(self): adj = self._scroll.get_vadjustment() adj.set_value(adj.get_upper()) return False def _restore_scroll_position(self, value): adj = self._scroll.get_vadjustment() upper = adj.get_upper() - adj.get_page_size() adj.set_value(min(value, upper)) self._restoring_scroll = False self._is_sticky = self._is_at_bottom() self._scroll_down_revealer.set_reveal_child(not self._is_sticky) return False def _scroll_to_item(self, position): n = self._store.get_n_items() if position < 0 or position >= n: return False self._list_view.scroll_to(position, Gtk.ListScrollFlags.FOCUS, None) return False def _on_scroll_to_bottom_clicked(self, btn): self._is_sticky = True self._scroll_down() def _on_scroll_to_unread_clicked(self, btn): if self._unread_pos >= 0: self._scroll_to_item(self._unread_pos) self._unread_pos = -1 self._unread_btn_revealer.set_reveal_child(False) # ─── Context menu helpers ───────────────────────────────── def _find_outer_at(self, x, y): target = self._list_view.pick(x, y, Gtk.PickFlags.DEFAULT) while target is not None and target is not self._list_view: if hasattr(target, "_chat_item"): return target target = target.get_parent() return None def _on_context_popover_closed(self, popover): self._context_menu_open = False self._is_sticky = self._is_at_bottom() def _on_list_long_press(self, gesture, x, y): self._on_list_right_click(None, 1, x, y) # ─── Emoji ─────────────────────────────────────────────── def _on_emoji_clicked(self, btn): chooser = Gtk.EmojiChooser() chooser.set_parent(btn) chooser.connect( "emoji-picked", lambda c, emoji: self._text_entry.insert_text(emoji, self._text_entry.get_position()), ) chooser.connect("closed", lambda c: c.unparent()) chooser.popup() meshy/src/views/chat_view.py000066400000000000000000001077461521052255700164750ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Chat conversation view - shown in the content panel.""" import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Shumate", "1.0") from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk, Pango, Shumate from meshy.models import Contact, Message, MessageStatus from meshy.protocol import max_dm_text_bytes from meshy.utils import linkify, parse_shared_contact, parse_shared_location from meshy.views import ( _contact_pick_factory_bind, _contact_pick_factory_setup, _contact_pick_factory_unbind, _ContactPickItem, _format_date_text, create_shumate_map, ) from meshy.views.chat_mixin import ChatScrollMixin _BATCH_SIZE = 30 class ChatItem(GObject.Object): """Model object for one row in the chat ListView.""" KIND_MESSAGE = 0 KIND_UNREAD_DIVIDER = 1 KIND_DATE_DIVIDER = 2 def __init__(self, kind=0, message=None, date_text=""): super().__init__() self.kind = kind self.message = message self.date_text = date_text self._status_val = int(message.status) if message else int(MessageStatus.PENDING) self.status_detail = "" @GObject.Property(type=int) def status(self): return self._status_val @status.setter def status(self, value): self._status_val = value @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/chat-view.ui") class ChatView(ChatScrollMixin, Gtk.Box): __gtype_name__ = "MeshyChatView" _stack = Gtk.Template.Child("stack") _scroll = Gtk.Template.Child("scroll") _list_view = Gtk.Template.Child("list_view") _scroll_down_revealer = Gtk.Template.Child("scroll_down_revealer") _unread_btn_revealer = Gtk.Template.Child("unread_btn_revealer") _char_label = Gtk.Template.Child("char_label") _emoji_btn = Gtk.Template.Child("emoji_btn") _text_entry = Gtk.Template.Child("text_entry") _send_btn = Gtk.Template.Child("send_btn") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._contact: Contact | None = None self._item_by_msg_id: dict[str, ChatItem] = {} # Pango attributes for char counter attrs = Pango.AttrList() attrs.insert(Pango.attr_scale_new(0.85)) self._char_label.set_attributes(attrs) # ListView model and factory self._store = Gio.ListStore(item_type=ChatItem) selection = Gtk.NoSelection(model=self._store) self._factory = Gtk.SignalListItemFactory() self._factory.connect("setup", self._on_factory_setup) self._factory.connect("bind", self._on_factory_bind) self._factory.connect("unbind", self._on_factory_unbind) self._list_view.set_model(selection) self._list_view.set_factory(self._factory) # Context menu for outgoing messages self._context_popover = Gtk.PopoverMenu() self._context_popover.set_parent(self._list_view) self._context_popover.set_has_arrow(False) self._context_popover.connect("closed", self._on_context_popover_closed) self._context_menu_open = False self._frozen_scroll_value = 0.0 ctx_menu = Gio.Menu() ctx_menu.append(_("Send Again"), "msg.resend") ctx_menu.append(_("Delete"), "msg.delete") self._context_popover.set_menu_model(ctx_menu) self._context_item = None ctx_group = Gio.SimpleActionGroup() resend_a = Gio.SimpleAction.new("resend", None) resend_a.connect("activate", lambda a, p: self._resend_context_item()) ctx_group.add_action(resend_a) delete_a = Gio.SimpleAction.new("delete", None) delete_a.connect("activate", lambda a, p: self._delete_context_item()) ctx_group.add_action(delete_a) self._list_view.insert_action_group("msg", ctx_group) # Gesture controllers right_click = Gtk.GestureClick(button=3) right_click.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) right_click.connect("pressed", self._on_list_right_click) self._list_view.add_controller(right_click) long_press = Gtk.GestureLongPress() long_press.set_touch_only(True) long_press.set_exclusive(True) long_press.connect("pressed", self._on_list_long_press) self._list_view.add_controller(long_press) self._is_sticky = True self._scroll_positions: dict[str, float] = {} self._restoring_scroll = False self._unread_pos = -1 self._messages_offset = 0 self._all_loaded = False self._loading_more = False # Signal connections self._scroll_down_revealer.get_child().connect("clicked", self._on_scroll_to_bottom_clicked) self._unread_btn_revealer.get_child().connect("clicked", self._on_scroll_to_unread_clicked) self._emoji_btn.connect("clicked", self._on_emoji_clicked) self._text_entry.connect("activate", self._on_send) self._text_entry.connect("changed", self._on_text_changed) self._send_btn.connect("clicked", self._on_send) # Extra context menu on text entry extra_menu = Gio.Menu() extra_menu.append(_("Share Contact"), "entry.share-contact") extra_menu.append(_("Share Location"), "entry.share-location") extra_menu.append(_("Share Location from Map"), "entry.share-location-map") self._text_entry.set_extra_menu(extra_menu) entry_group = Gio.SimpleActionGroup() share_a = Gio.SimpleAction.new("share-contact", None) share_a.connect("activate", self._on_share_contact) entry_group.add_action(share_a) loc_a = Gio.SimpleAction.new("share-location", None) loc_a.connect("activate", self._on_share_location) entry_group.add_action(loc_a) loc_map_a = Gio.SimpleAction.new("share-location-map", None) loc_map_a.connect("activate", self._on_share_location_map) entry_group.add_action(loc_map_a) self._text_entry.insert_action_group("entry", entry_group) # Scroll state tracking adj = self._scroll.get_vadjustment() adj.connect("value-changed", self._on_scroll_value_changed) adj.connect("notify::upper", self._on_scroll_upper_changed) adj.connect("notify::page-size", self._on_scroll_upper_changed) self._scroll.connect("edge-overshot", self._on_edge_overshot) self._stack.set_visible_child_name("placeholder") # ─── Factory callbacks ─────────────────────────────────── def _on_factory_setup(self, factory, list_item): """Create the widget tree for one list item (recycled).""" list_item.set_activatable(False) list_item.set_selectable(False) list_item.set_focusable(False) outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # ── Message bubble ── msg_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=2, margin_top=4, margin_bottom=4, margin_start=12, margin_end=12, ) # Sender name (for room messages) sender_lbl = Gtk.Label(xalign=0, max_width_chars=40) sender_lbl.add_css_class("caption") sender_lbl.add_css_class("accent") sender_lbl.set_margin_start(4) sender_lbl.set_visible(False) msg_box.append(sender_lbl) # Card frame with text text_lbl = Gtk.Label( wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, xalign=0, selectable=True, max_width_chars=40, ) text_lbl.set_margin_start(12) text_lbl.set_margin_end(12) text_lbl.set_margin_top(8) text_lbl.set_margin_bottom(8) frame = Gtk.Frame() frame.add_css_class("card") content_stack = Gtk.Stack() content_stack.set_transition_type(Gtk.StackTransitionType.NONE) content_stack.set_vhomogeneous(False) content_stack.set_hhomogeneous(False) content_stack.add_named(text_lbl, "text") sc_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=6, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) sc_header_lbl = Gtk.Label(xalign=0, wrap=True) sc_header_lbl.add_css_class("dim-label") sc_header_lbl.add_css_class("caption") sc_box.append(sc_header_lbl) sc_name_box = Gtk.Box(spacing=6) sc_icon = Gtk.Image.new_from_icon_name("avatar-default-symbolic") sc_name_lbl = Gtk.Label(xalign=0) attrs = Pango.AttrList() attrs.insert(Pango.attr_weight_new(Pango.Weight.BOLD)) sc_name_lbl.set_attributes(attrs) sc_name_box.append(sc_icon) sc_name_box.append(sc_name_lbl) sc_box.append(sc_name_box) sc_key_lbl = Gtk.Label(xalign=0) sc_key_lbl.add_css_class("dim-label") sc_key_lbl.add_css_class("caption") sc_box.append(sc_key_lbl) sc_add_btn = Gtk.Button(label=_("Add Contact")) sc_add_btn.add_css_class("suggested-action") sc_add_btn.set_halign(Gtk.Align.START) sc_add_btn.set_margin_top(4) sc_box.append(sc_add_btn) content_stack.add_named(sc_box, "contact") # ── Shared location card ── loc_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=6, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) loc_header_lbl = Gtk.Label(xalign=0, wrap=True) loc_header_lbl.add_css_class("dim-label") loc_header_lbl.add_css_class("caption") loc_box.append(loc_header_lbl) loc_coords_lbl = Gtk.Label(xalign=0, selectable=True) loc_coords_lbl.add_css_class("caption") loc_box.append(loc_coords_lbl) loc_map_frame = Gtk.Frame() loc_map_frame.add_css_class("card") loc_map_frame.set_overflow(Gtk.Overflow.HIDDEN) loc_map_frame.set_cursor(Gdk.Cursor.new_from_name("pointer")) loc_map_gesture = Gtk.GestureClick() loc_map_frame.add_controller(loc_map_gesture) loc_box.append(loc_map_frame) loc_open_btn = Gtk.Button(label=_("Open in Maps App"), margin_top=4) loc_box.append(loc_open_btn) content_stack.add_named(loc_box, "location") frame.set_child(content_stack) msg_box.append(frame) # Meta line: time + status meta_box = Gtk.Box(spacing=4) time_lbl = Gtk.Label() time_lbl.add_css_class("dim-label") time_lbl.add_css_class("caption") meta_box.append(time_lbl) status_lbl = Gtk.Label() status_lbl.add_css_class("caption") status_lbl.set_visible(False) meta_box.append(status_lbl) msg_box.append(meta_box) outer.append(msg_box) # ── Divider ── div_box = Gtk.Box( spacing=8, margin_start=12, margin_end=12, margin_top=8, margin_bottom=8, ) line_left = Gtk.Separator(hexpand=True, valign=Gtk.Align.CENTER) div_box.append(line_left) div_lbl = Gtk.Label() div_box.append(div_lbl) line_right = Gtk.Separator(hexpand=True, valign=Gtk.Align.CENTER) div_box.append(line_right) div_box.set_visible(False) outer.append(div_box) # Store widget references outer._msg_box = msg_box outer._sender_lbl = sender_lbl outer._text_lbl = text_lbl outer._frame = frame outer._time_lbl = time_lbl outer._status_lbl = status_lbl outer._meta_box = meta_box outer._div_box = div_box outer._div_lbl = div_lbl outer._div_line_left = line_left outer._div_line_right = line_right outer._content_stack = content_stack outer._sc_header_lbl = sc_header_lbl outer._sc_icon = sc_icon outer._sc_name_lbl = sc_name_lbl outer._sc_key_lbl = sc_key_lbl outer._sc_add_btn = sc_add_btn outer._sc_add_handler_id = None outer._loc_header_lbl = loc_header_lbl outer._loc_coords_lbl = loc_coords_lbl outer._loc_map_frame = loc_map_frame outer._loc_open_btn = loc_open_btn outer._loc_map_gesture = loc_map_gesture outer._loc_map_click_handler = None outer._loc_open_handler = None outer._chat_item = None outer._status_handler_id = None # Wrap in per-item Clamp for width limiting clamp = Adw.Clamp(maximum_size=600, child=outer) clamp._outer = outer list_item.set_child(clamp) def _on_factory_bind(self, factory, list_item): """Populate widgets from the model item.""" item = list_item.get_item() clamp = list_item.get_child() outer = clamp._outer outer._chat_item = item if item.kind == ChatItem.KIND_MESSAGE: msg = item.message outer._msg_box.set_visible(True) outer._div_box.set_visible(False) # Alignment if msg.is_outgoing: outer._msg_box.set_halign(Gtk.Align.END) outer._meta_box.set_halign(Gtk.Align.END) else: outer._msg_box.set_halign(Gtk.Align.START) outer._meta_box.set_halign(Gtk.Align.START) # Sender name (room messages) if msg.room_sender_name and not msg.is_outgoing: outer._sender_lbl.set_label(msg.room_sender_name) outer._sender_lbl.set_visible(True) else: outer._sender_lbl.set_visible(False) # Text / shared contact sc = parse_shared_contact(msg.text) if sc: pub_key_hex, contact_type, contact_name = sc outer._content_stack.set_visible_child_name("contact") if msg.is_outgoing: outer._sc_header_lbl.set_label(_("You shared a contact:")) else: sender = self._contact.name if self._contact else "?" outer._sc_header_lbl.set_label( _("{name} shared a contact with you:").format(name=sender) ) icon_map = { 1: "avatar-default-symbolic", 2: "network-server-symbolic", 3: "user-home-symbolic", 4: "weather-clear-symbolic", } outer._sc_icon.set_from_icon_name( icon_map.get(contact_type, "avatar-default-symbolic") ) outer._sc_name_lbl.set_label(contact_name) outer._sc_key_lbl.set_label(f"{pub_key_hex[:8]}...{pub_key_hex[-8:]}") if msg.is_outgoing: outer._sc_add_btn.set_visible(False) else: outer._sc_add_btn.set_visible(True) is_known = self._window.is_contact_known(pub_key_hex) outer._sc_add_btn.set_sensitive(not is_known) outer._sc_add_btn.set_label( _("Already added") if is_known else _("Add Contact") ) if outer._sc_add_handler_id is not None: outer._sc_add_btn.disconnect(outer._sc_add_handler_id) outer._sc_add_handler_id = outer._sc_add_btn.connect( "clicked", self._on_shared_contact_add, pub_key_hex, contact_type, contact_name, ) else: loc = parse_shared_location(msg.text) if loc: lat, lon = loc outer._content_stack.set_visible_child_name("location") if msg.is_outgoing: outer._loc_header_lbl.set_label(_("You shared a location:")) else: sender = self._contact.name if self._contact else "?" outer._loc_header_lbl.set_label( _("{name} shared a location:").format(name=sender) ) outer._loc_coords_lbl.set_label(f"{lat:.6f}, {lon:.6f}") self._ensure_loc_map(outer, lat, lon) outer._loc_map_click_handler = outer._loc_map_gesture.connect( "released", lambda g, n, x, y, la=lat, lo=lon: self._on_show_location_map(la, lo), ) outer._loc_open_handler = outer._loc_open_btn.connect( "clicked", self._on_open_location_external, lat, lon ) else: outer._content_stack.set_visible_child_name("text") outer._text_lbl.set_markup(linkify(msg.text)) # Frame style if msg.is_outgoing: outer._frame.add_css_class("outgoing-message") else: outer._frame.remove_css_class("outgoing-message") # Timestamp outer._time_lbl.set_label(msg.timestamp.strftime("%H:%M")) # Status (outgoing only) if msg.is_outgoing: outer._status_lbl.set_visible(True) self._apply_status( outer._status_lbl, MessageStatus(item.status), item.status_detail, ) hid = item.connect( "notify::status", lambda obj, pspec, w=outer: self._apply_status( w._status_lbl, MessageStatus(obj.status), obj.status_detail, ), ) outer._status_handler_id = hid else: outer._status_lbl.set_visible(False) elif item.kind == ChatItem.KIND_DATE_DIVIDER: outer._msg_box.set_visible(False) outer._div_box.set_visible(True) outer._div_lbl.set_label(item.date_text) # Date divider styling outer._div_lbl.remove_css_class("unread-divider") outer._div_lbl.add_css_class("dim-label") outer._div_line_left.remove_css_class("unread-divider-line") outer._div_line_left.add_css_class("date-divider-line") outer._div_line_right.remove_css_class("unread-divider-line") outer._div_line_right.add_css_class("date-divider-line") elif item.kind == ChatItem.KIND_UNREAD_DIVIDER: outer._msg_box.set_visible(False) outer._div_box.set_visible(True) outer._div_lbl.set_label(_("New Messages")) # Unread divider styling outer._div_lbl.remove_css_class("dim-label") outer._div_lbl.add_css_class("unread-divider") outer._div_line_left.remove_css_class("date-divider-line") outer._div_line_left.add_css_class("unread-divider-line") outer._div_line_right.remove_css_class("date-divider-line") outer._div_line_right.add_css_class("unread-divider-line") def _on_factory_unbind(self, factory, list_item): """Clean up signal connections when the widget is recycled.""" clamp = list_item.get_child() outer = clamp._outer if outer._status_handler_id is not None: outer._chat_item.disconnect(outer._status_handler_id) outer._status_handler_id = None if outer._sc_add_handler_id is not None: outer._sc_add_btn.disconnect(outer._sc_add_handler_id) outer._sc_add_handler_id = None if outer._loc_map_click_handler is not None: outer._loc_map_gesture.disconnect(outer._loc_map_click_handler) outer._loc_map_click_handler = None if outer._loc_open_handler is not None: outer._loc_open_btn.disconnect(outer._loc_open_handler) outer._loc_open_handler = None outer._loc_map_frame.set_child(None) outer._chat_item = None # ─── Shared contact helpers ───────────────────────────── def _on_shared_contact_add(self, btn, pub_key_hex, contact_type, name): self._window.add_contact(bytes.fromhex(pub_key_hex), contact_type, name) btn.set_sensitive(False) btn.set_label(_("Already added")) # ─── Shared location helpers ────────────────────────────── @staticmethod def _ensure_loc_map(outer, lat, lon): from meshy.views.map_view import MapView loc_map, loc_marker_layer, loc_viewport = create_shumate_map(zoom_level=14) loc_map.set_vexpand(False) loc_map.set_hexpand(True) loc_map.set_size_request(100, 150) loc_map.get_scale().set_visible(False) loc_map.set_show_zoom_buttons(False) loc_map.get_license().set_visible(False) loc_marker_widget = MapView._build_marker_widget( 0.26, 0.52, 0.96, "find-location-symbolic", None, show_label=False ) loc_marker = Shumate.Marker() loc_marker.set_child(loc_marker_widget) loc_marker_layer.add_marker(loc_marker) loc_viewport.set_zoom_level(14) loc_viewport.set_location(lat, lon) loc_marker.set_location(lat, lon) outer._loc_map_frame.set_child(loc_map) def _on_show_location_map(self, lat, lon): from meshy.views.map_view import MapView dialog, map_w, m_layer, vp = MapView.open_map_dialog(self._window, _("Shared Location")) marker = MapView.create_marker( 0.26, 0.52, 0.96, "find-location-symbolic", _("Shared"), lat, lon ) m_layer.add_marker(marker) dev = self._window.device_info if dev and dev.latitude is not None and dev.longitude is not None: self_marker = MapView.create_marker( 0.90, 0.16, 0.22, "avatar-default-symbolic", dev.name or _("My Device"), dev.latitude, dev.longitude, ) m_layer.add_marker(self_marker) vp.set_zoom_level(14) vp.set_location(lat, lon) dialog.present(self._window) def _on_open_location_external(self, btn, lat, lon): launcher = Gtk.UriLauncher.new(f"geo:{lat},{lon}") launcher.launch(self._window, None, None, None) # ─── Status helpers ────────────────────────────────────── @staticmethod def _apply_status(label, status, detail=""): symbol, css = { MessageStatus.PENDING: (_("Sending"), "dim-label"), MessageStatus.SENT: (_("Sending"), "dim-label"), MessageStatus.DELIVERED: ("\u2713", "success"), MessageStatus.FAILED: ("\u2717", "error"), }.get(status, ("...", "dim-label")) text = f"{symbol} {detail}".strip() if detail else symbol label.set_label(text) for c in ("dim-label", "success", "error"): label.remove_css_class(c) label.add_css_class(css) # ─── Context menu ─────────────────────────────────────── def _on_list_right_click(self, gesture, n_press, x, y): outer = self._find_outer_at(x, y) if not outer: return item = outer._chat_item if ( item and item.kind == ChatItem.KIND_MESSAGE and item.message and item.message.is_outgoing ): gesture.set_state(Gtk.EventSequenceState.CLAIMED) self._context_item = item self._context_menu_open = True self._is_sticky = False self._frozen_scroll_value = self._scroll.get_vadjustment().get_value() rect = Gdk.Rectangle() rect.x, rect.y, rect.width, rect.height = int(x), int(y), 1, 1 self._context_popover.set_pointing_to(rect) self._context_popover.popup() def _resend_context_item(self): if self._context_item and self._context_item.message: self.resend_message(self._context_item.message) def _delete_context_item(self): if self._context_item and self._context_item.message: self.delete_message(self._context_item.message) def _on_share_contact(self, action, param): if not self._contact: return contacts = [ c for c in self._window.contacts if c.public_key_hex != self._contact.public_key_hex ] if not contacts: return builder = Gtk.Builder.new_from_resource( "/page/codeberg/sesivany/Meshy/ui/share-contact-dialog.ui" ) dialog = builder.get_object("share_dialog") search_btn = builder.get_object("share_search_btn") search_entry = builder.get_object("share_search_entry") list_view = builder.get_object("share_list_view") status_label = builder.get_object("share_status_label") search_btn.bind_property( "active", search_entry, "visible", GObject.BindingFlags.SYNC_CREATE ) search_entry.connect("stop-search", lambda _e: search_btn.set_active(False)) store = Gio.ListStore(item_type=_ContactPickItem) for c in sorted(contacts, key=lambda c: c.name.lower()): store.append(_ContactPickItem(c)) custom_filter = Gtk.CustomFilter.new(self._share_filter_func, search_entry) def _on_search_changed(*_args): custom_filter.changed(Gtk.FilterChange.DIFFERENT) search_entry.connect("search-changed", _on_search_changed) filter_model = Gtk.FilterListModel(model=store, filter=custom_filter) selection = Gtk.NoSelection(model=filter_model) list_view.set_model(selection) def _on_share(contact): text = f"<{contact.public_key_hex}:{contact.type}:{contact.name}>" self._window.send_message(self._contact, text) dialog.close() factory = Gtk.SignalListItemFactory() factory.connect("setup", lambda f, li: _contact_pick_factory_setup(f, li, _("Share"))) factory.connect("bind", lambda f, li: _contact_pick_factory_bind(f, li, _on_share)) factory.connect("unbind", _contact_pick_factory_unbind) list_view.set_factory(factory) def _on_activate(lv, pos): item = filter_model.get_item(pos) if item: _on_share(item.contact) list_view.connect("activate", _on_activate) total = store.get_n_items() status_label.set_label( _("{total} contacts").format(total=total) if total else _("No contacts") ) dialog._refs = (store, custom_filter, filter_model, selection, factory) dialog.present(self._window) @staticmethod def _share_filter_func(item, search_entry): q = search_entry.get_text().lower() return not q or q in item.contact.name.lower() def _on_share_location(self, action, param): if not self._contact: return dev = self._window.device_info if not dev or dev.latitude is None or dev.longitude is None: dialog = Adw.AlertDialog( heading=_("Location Not Available"), body=_("Your companion does not have a GPS location."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return text = f"{dev.latitude:.6f},{dev.longitude:.6f}" self._window.send_message(self._contact, text) def _on_share_location_map(self, action, param): if not self._contact: return from meshy.views import open_map_picker dev = self._window.device_info lat = dev.latitude if dev and dev.latitude is not None else 0.0 lon = dev.longitude if dev and dev.longitude is not None else 0.0 contact = self._contact open_map_picker( self._window, _("Share Location"), _("Share"), lat, lon, lambda la, lo: self._window.send_message(contact, f"{la:.6f},{lo:.6f}"), ) # ─── Contact & messages ────────────────────────────────── def set_contact(self, contact: Contact): if self._contact: adj = self._scroll.get_vadjustment() self._scroll_positions[self._contact.public_key_hex] = adj.get_value() self._contact = contact self._stack.set_visible_child_name("chat") self._load_messages() def _load_messages(self): self._item_by_msg_id.clear() self._unread_pos = -1 self._messages_offset = 0 self._all_loaded = False self._loading_more = False if not self._contact: self._store.remove_all() return messages = self._window.get_messages_for_contact(self._contact, limit=_BATCH_SIZE) self._messages_offset = len(messages) self._all_loaded = len(messages) < _BATCH_SIZE last_read_at = self._window.get_contact_last_read_at(self._contact.public_key_hex) new_items = [] divider_inserted = False last_date = None for msg in messages: msg_date = msg.timestamp.date() if last_date is not None and msg_date != last_date: new_items.append( ChatItem( kind=ChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(msg.timestamp), ) ) last_date = msg_date # Insert unread divider before first unread incoming message if ( not divider_inserted and last_read_at is not None and not msg.is_outgoing and msg.timestamp.timestamp() > last_read_at ): new_items.append(ChatItem(kind=ChatItem.KIND_UNREAD_DIVIDER)) self._unread_pos = len(new_items) - 1 divider_inserted = True item = ChatItem(kind=ChatItem.KIND_MESSAGE, message=msg) new_items.append(item) if msg.message_id: self._item_by_msg_id[msg.message_id] = item saved_pos = self._scroll_positions.get(self._contact.public_key_hex) if saved_pos is not None: self._is_sticky = False self._restoring_scroll = True # Replace all items in a single atomic operation self._store.splice(0, self._store.get_n_items(), new_items) if saved_pos is not None: GLib.idle_add(lambda pos=saved_pos: self._restore_scroll_position(pos)) else: GLib.idle_add(self._scroll_to_bottom) if self._unread_pos >= 0: GLib.idle_add(lambda: self._update_unread_btn_visibility() or False) # Mark as read now self._window.mark_contact_read(self._contact.public_key_hex) def _load_older_messages(self): if self._all_loaded or self._loading_more or not self._contact: return self._loading_more = True messages = self._window.get_messages_for_contact( self._contact, limit=_BATCH_SIZE, offset=self._messages_offset ) if not messages: self._all_loaded = True self._loading_more = False return older_items = [] last_date = None for msg in messages: msg_date = msg.timestamp.date() if last_date is not None and msg_date != last_date: older_items.append( ChatItem( kind=ChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(msg.timestamp), ) ) last_date = msg_date item = ChatItem(kind=ChatItem.KIND_MESSAGE, message=msg) older_items.append(item) if msg.message_id: self._item_by_msg_id[msg.message_id] = item # Add boundary date divider if the dates differ if last_date is not None: for i in range(self._store.get_n_items()): first = self._store.get_item(i) if first.kind == ChatItem.KIND_MESSAGE: if first.message.timestamp.date() != last_date: older_items.append( ChatItem( kind=ChatItem.KIND_DATE_DIVIDER, date_text=_format_date_text(first.message.timestamp), ) ) break self._store.splice(0, 0, older_items) if self._unread_pos >= 0: self._unread_pos += len(older_items) self._messages_offset += len(messages) self._all_loaded = len(messages) < _BATCH_SIZE self._loading_more = False def _on_text_changed(self, entry): text = entry.get_text() used = len(text.encode("utf-8")) limit = max_dm_text_bytes() if used == 0: self._char_label.set_label("") self._send_btn.set_sensitive(True) self._char_label.remove_css_class("error") else: self._char_label.set_label(f"{used}/{limit}") over = used > limit self._send_btn.set_sensitive(not over) if over: self._char_label.add_css_class("error") else: self._char_label.remove_css_class("error") def _on_send(self, *args): if not self._contact: return text = self._text_entry.get_text().strip() if not text: return if len(text.encode("utf-8")) > max_dm_text_bytes(): return self._text_entry.set_text("") self._window.send_message(self._contact, text) def on_message_received(self, contact: Contact, message: Message): """Called when a new message is received.""" if self._contact and contact.public_key_hex == self._contact.public_key_hex: item = ChatItem(kind=ChatItem.KIND_MESSAGE, message=message) # If sticky, the notify::upper handler will auto-scroll self._store.append(item) def on_message_sent(self, message: Message): """Called when a message is sent.""" # Force sticky so notify::upper handler scrolls to the new message self._is_sticky = True item = ChatItem(kind=ChatItem.KIND_MESSAGE, message=message) self._store.append(item) if message.message_id: self._item_by_msg_id[message.message_id] = item def resend_message(self, message: Message): """Re-send a message to the current contact.""" if self._contact and message.message_id: self._window.resend_message(self._contact, message) def delete_message(self, message: Message): """Delete a message from the view and storage.""" if not message.message_id: return for i in range(self._store.get_n_items()): item = self._store.get_item(i) if ( item.kind == ChatItem.KIND_MESSAGE and item.message and item.message.message_id == message.message_id ): self._store.remove(i) break self._item_by_msg_id.pop(message.message_id, None) self._window.delete_message(message.message_id) def update_message_status(self, message_id: str, status: MessageStatus, detail: str = ""): """Update the status of a sent message.""" if message_id in self._item_by_msg_id: item = self._item_by_msg_id[message_id] item.status_detail = detail # Setting status triggers notify::status -> bound widget updates item.props.status = int(status) meshy/src/views/connection_view.py000066400000000000000000000604201521052255700177000ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Connection screen - shown when no device is connected.""" import os import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Gdk", "4.0") from gi.repository import Adw, Gdk, Gio, GLib, Gtk from meshy.ble import BleDevice from meshy.host_integration import is_flatpak, run_host_command from meshy.storage import Storage @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/connection-view.ui") class ConnectionView(Gtk.Box): """Connection screen with Bluetooth and USB connection options.""" __gtype_name__ = "MeshyConnectionView" _paired_list = Gtk.Template.Child("paired_list") _pair_btn = Gtk.Template.Child("pair_btn") _usb_list = Gtk.Template.Child("usb_list") _tcp_list = Gtk.Template.Child("tcp_list") _tcp_add_btn = Gtk.Template.Child("tcp_add_btn") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._bt_poll_id = None self._pair_btn.connect("clicked", self._on_pair_new) self._tcp_add_btn.connect("clicked", self._on_tcp_add_new) def _add_remove_context_menu(self, row, on_remove): """Add a right-click / long-press context menu with Remove option.""" menu_model = Gio.Menu() menu_model.append(_("Remove"), "row.remove") popover = Gtk.PopoverMenu(menu_model=menu_model) popover.set_parent(row) popover.set_has_arrow(False) row._context_popover = popover remove_action = Gio.SimpleAction.new("remove", None) remove_action.connect("activate", lambda *_: on_remove()) action_group = Gio.SimpleActionGroup() action_group.add_action(remove_action) row.insert_action_group("row", action_group) def _show_menu(x, y): rect = Gdk.Rectangle() rect.x, rect.y, rect.width, rect.height = int(x), int(y), 1, 1 popover.set_pointing_to(rect) popover.popup() right_click = Gtk.GestureClick(button=3) right_click.connect("pressed", lambda g, n, x, y: _show_menu(x, y)) row.add_controller(right_click) long_press = Gtk.GestureLongPress() long_press.connect("pressed", lambda g, x, y: _show_menu(x, y)) row.add_controller(long_press) def refresh_paired_devices(self): """Refresh the list of paired BLE devices, USB serial devices, and TCP companions.""" self._refresh_tcp_companions() # Refresh BLE paired devices self._clear_list(self._paired_list) bt_enabled = self._window.ble_transport.is_bluetooth_enabled() self._pair_btn.set_sensitive(bt_enabled) if not bt_enabled: placeholder = Adw.ActionRow( title=_("Bluetooth is disabled"), subtitle=_("Enable Bluetooth in system settings to connect"), sensitive=False, ) placeholder.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-disabled-symbolic")) self._paired_list.append(placeholder) else: devices = self._window.ble_transport.get_paired_devices() if not devices: placeholder = Adw.ActionRow( title=_("No paired devices"), subtitle=_("Use the button below to pair a new companion"), sensitive=False, ) placeholder.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-disabled-symbolic")) self._paired_list.append(placeholder) else: for device in devices: row = Adw.ActionRow( title=device.name, subtitle=device.address, ) row.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-active-symbolic")) connect_btn = Gtk.Button( label=_("Connect"), valign=Gtk.Align.CENTER, ) connect_btn.add_css_class("suggested-action") connect_btn.connect( "clicked", lambda b, d=device: self._window.connect_to_device(d), ) row.add_suffix(connect_btn) self._add_remove_context_menu( row, lambda d=device: self._remove_ble_device(d), ) self._paired_list.append(row) self._bt_was_enabled = bt_enabled self._start_bt_polling() # Refresh USB serial devices child = self._usb_list.get_first_child() while child: next_child = child.get_next_sibling() self._usb_list.remove(child) child = next_child from meshy.usb_serial import UsbSerialTransport usb_transport = UsbSerialTransport() usb_devices = usb_transport.get_serial_devices() if not usb_devices: placeholder = Adw.ActionRow( title=_("No USB devices detected"), subtitle=_("Connect a MeshCore companion via USB"), sensitive=False, ) placeholder.add_prefix(Gtk.Image.new_from_icon_name("drive-removable-media-symbolic")) self._usb_list.append(placeholder) else: for device in usb_devices: row = Adw.ActionRow( title=device.name, subtitle=device.path, ) row.add_prefix(Gtk.Image.new_from_icon_name("drive-removable-media-symbolic")) connect_btn = Gtk.Button( label=_("Connect"), valign=Gtk.Align.CENTER, ) connect_btn.add_css_class("suggested-action") connect_btn.connect( "clicked", lambda b, d=device, t=usb_transport: self._connect_usb(d, t), ) row.add_suffix(connect_btn) self._usb_list.append(row) def _remove_ble_device(self, device): """Remove (unpair) a Bluetooth device with confirmation.""" dialog = Adw.AlertDialog( heading=_("Remove Companion"), body=_("Remove {} ({})? You will need to pair again to reconnect.").format( device.name, device.address ), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("remove", _("Remove")) dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) delete_check = Gtk.CheckButton(label=_("Delete stored data")) dialog.set_extra_child(delete_check) def _on_response(d, response): if response == "remove": if delete_check.get_active(): Storage.delete_device_db(device.address) self._window.ble_transport.remove_device(device.path) self.refresh_paired_devices() dialog.connect("response", _on_response) dialog.present(self._window) def _connect_usb(self, device, transport): """Connect to a USB device with error handling.""" self._window.connect_to_device(device, transport=transport) # Check if connection failed with an error if hasattr(transport, "_connection_error") and transport._connection_error: error_msg = transport._connection_error is_perm = getattr(transport, "_is_permission_error", False) transport._connection_error = None transport._is_permission_error = False if is_perm: self._show_permission_dialog(device, transport) else: dialog = Adw.AlertDialog( heading=_("USB Connection Failed"), body=error_msg, ) dialog.add_response("ok", _("OK")) dialog.present(self._window) def _can_spawn_host(self): """Check if we can use flatpak-spawn --host (not in sandbox, or have permission).""" try: kf = GLib.KeyFile() kf.load_from_file("/.flatpak-info", GLib.KeyFileFlags.NONE) try: talk_names = kf.get_string_list("Session Bus Policy", "org.freedesktop.Flatpak") return "talk" in talk_names or "own" in talk_names except GLib.Error: return False except GLib.Error: return True def _show_permission_dialog(self, device, transport): """Show permission error with option to install udev rule.""" if self._can_spawn_host(): self._show_install_rule_dialog(device, transport) else: self._show_manual_rule_dialog(device) def _show_install_rule_dialog(self, device, transport): """Show dialog with automatic udev rule installation.""" dialog = Adw.AlertDialog( heading=_("USB Permission Denied"), body=_( "Cannot access {}.\n\n" "A udev rule is needed to grant access to USB serial " 'devices. Click "Install Rule" to install it ' "(requires administrator password)." ).format(device.path), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("install", _("Install Rule")) dialog.set_response_appearance("install", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "install": self._install_udev_rule(device, transport) dialog.connect("response", _on_response) dialog.present(self._window) def _show_manual_rule_dialog(self, device): """Show dialog with manual udev rule installation instructions.""" command = ( 'echo \'SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", MODE="0666"\n' 'SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-9]*", MODE="0666"\'' " | sudo tee /etc/udev/rules.d/60-meshy-serial.rules" " && sudo udevadm control --reload-rules" " && sudo udevadm trigger" ) dialog = Adw.AlertDialog( heading=_("USB Permission Denied"), body=_( "Cannot access {}.\n\n" "A udev rule is needed to grant access to USB serial " "devices. Run the following command in a terminal:\n\n" "{}" ).format(device.path, command), ) dialog.add_response("close", _("Close")) dialog.add_response("copy", _("Copy Command")) dialog.set_response_appearance("copy", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "copy": clipboard = self._window.get_clipboard() clipboard.set(command) clipboard.store_async(None, None) self._window.show_toast(_("Command copied to clipboard")) dialog.connect("response", _on_response) dialog.present(self._window) def _install_udev_rule(self, device, transport): """Install udev rule via pkexec and retry connection.""" import tempfile rule_content = ( "# Meshy - MeshCore companion USB serial access\n" 'SUBSYSTEM=="tty", KERNEL=="ttyACM[0-9]*", MODE="0666"\n' 'SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-9]*", MODE="0666"\n' ) tmp_path = None try: # In Flatpak, /tmp is sandbox-private and invisible to host # commands run via flatpak-spawn. Use the cache dir which is # shared at the same absolute path. tmp_dir = GLib.get_user_cache_dir() if is_flatpak() else None with tempfile.NamedTemporaryFile( mode="w", suffix=".rules", delete=False, dir=tmp_dir, ) as tmp: tmp.write(rule_content) tmp_path = tmp.name dest = "/etc/udev/rules.d/60-meshy-serial.rules" result = run_host_command( [ "pkexec", "bash", "-c", f'cp "$1" {dest}' " && udevadm control --reload-rules" " && udevadm trigger", "--", tmp_path, ], capture_output=True, text=True, timeout=60, ) if result.returncode == 0: # Wait a moment then retry connection def _retry(): self._window.connect_to_device(device, transport=transport) return False GLib.timeout_add(2000, _retry) else: dialog = Adw.AlertDialog( heading=_("Installation Failed"), body=_("Could not install udev rule:\n{}").format(result.stderr.strip()), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) except Exception as e: dialog = Adw.AlertDialog( heading=_("Installation Failed"), body=str(e), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) finally: if tmp_path and os.path.exists(tmp_path): os.unlink(tmp_path) def _clear_list(self, list_widget): child = list_widget.get_first_child() while child: next_child = child.get_next_sibling() popover = getattr(child, "_context_popover", None) if popover: popover.unparent() list_widget.remove(child) child = next_child def _refresh_tcp_companions(self): """Refresh the list of saved TCP companions.""" self._clear_list(self._tcp_list) from gi.repository import Gio settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") companions = settings.get_strv("tcp-companions") if not companions: placeholder = Adw.ActionRow( title=_("No saved companions"), subtitle=_("Use the button below to add one"), sensitive=False, ) placeholder.add_prefix(Gtk.Image.new_from_icon_name("network-wireless-symbolic")) self._tcp_list.append(placeholder) else: for entry in companions: if "|" not in entry: continue name, address = entry.split("|", 1) row = Adw.ActionRow(title=name, subtitle=address) row.add_prefix(Gtk.Image.new_from_icon_name("network-wireless-symbolic")) connect_btn = Gtk.Button( label=_("Connect"), valign=Gtk.Align.CENTER, ) connect_btn.add_css_class("suggested-action") connect_btn.connect( "clicked", lambda b, a=address: self._connect_tcp_address(a), ) row.add_suffix(connect_btn) self._add_remove_context_menu( row, lambda a=address: self._remove_tcp_companion(a), ) self._tcp_list.append(row) def _connect_tcp_address(self, address: str): """Connect to a saved TCP companion by address (host:port).""" from meshy.tcp import TcpDevice, TcpTransport host, port_str = address.rsplit(":", 1) transport = TcpTransport() device = TcpDevice(host, int(port_str)) self._window.connect_to_device(device, transport=transport) def _remove_tcp_companion(self, address: str): """Remove a saved TCP companion with confirmation.""" dialog = Adw.AlertDialog( heading=_("Remove Companion"), body=_("Remove TCP companion {}?").format(address), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("remove", _("Remove")) dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) delete_check = Gtk.CheckButton(label=_("Delete stored data")) dialog.set_extra_child(delete_check) def _on_response(d, response): if response == "remove": if delete_check.get_active(): Storage.delete_device_db(address) self._window.remove_tcp_companion(address) self._refresh_tcp_companions() dialog.connect("response", _on_response) dialog.present(self._window) def _on_tcp_add_new(self, *args): """Show dialog to add a new TCP companion.""" dialog = Adw.AlertDialog( heading=_("Add TCP Companion"), body=_("Enter the IP address and port of your MeshCore device."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) host_entry = Adw.EntryRow(title=_("Hostname / IP Address")) host_entry.set_input_purpose(Gtk.InputPurpose.URL) port_entry = Adw.EntryRow(title=_("Port")) port_entry.set_input_purpose(Gtk.InputPurpose.DIGITS) port_entry.set_text("5000") fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(host_entry) fields_list.append(port_entry) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("connect", _("Connect")) dialog.set_response_appearance("connect", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response != "connect": return host = host_entry.get_text().strip() port_str = port_entry.get_text().strip() or "5000" if not host: return try: port = int(port_str) if not 1 <= port <= 65535: raise ValueError except ValueError: return from meshy.tcp import TcpDevice, TcpTransport transport = TcpTransport() device = TcpDevice(host, port) self._window.connect_to_device(device, transport=transport) dialog.connect("response", _on_response) dialog.present(self._window) def _start_bt_polling(self): """Poll for Bluetooth state changes every 2 seconds.""" if self._bt_poll_id is not None: return def _check_bt(): bt_enabled = self._window.ble_transport.is_bluetooth_enabled() if bt_enabled != self._bt_was_enabled: self.refresh_paired_devices() return True self._bt_poll_id = GLib.timeout_add(2000, _check_bt) def stop_polling(self): """Stop polling for Bluetooth state.""" if self._bt_poll_id is not None: GLib.source_remove(self._bt_poll_id) self._bt_poll_id = None def _on_pair_new(self, *args): """Scan for new MeshCore devices and pair.""" dialog = Adw.Dialog() dialog.set_title(_("Pair New Companion")) dialog.set_content_width(420) dialog.set_content_height(480) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=8, margin_end=8, margin_bottom=8, ) # Status with spinner status_box = Gtk.Box(spacing=8, halign=Gtk.Align.CENTER) spinner = Gtk.Spinner(spinning=True) status_box.append(spinner) status_label = Gtk.Label(label=_("Scanning for MeshCore devices...")) status_label.add_css_class("dim-label") status_box.append(status_label) content.append(status_box) # Device list scroll = Gtk.ScrolledWindow(vexpand=True) list_wrapper = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) list_box = Gtk.ListBox() list_box.set_selection_mode(Gtk.SelectionMode.NONE) list_box.add_css_class("boxed-list") placeholder = Adw.StatusPage( icon_name="bluetooth-active-symbolic", title=_("Scanning..."), description=_("Looking for nearby MeshCore devices"), ) list_box.set_placeholder(placeholder) list_wrapper.append(list_box) scroll.set_child(list_wrapper) content.append(scroll) toolbar_view.set_content(content) dialog.set_child(toolbar_view) seen_addresses = set() paired_addresses = { d.address.upper() for d in self._window.ble_transport.get_paired_devices() } def on_device_found(device: BleDevice): if device.address in seen_addresses: return if device.address.upper() in paired_addresses: return seen_addresses.add(device.address) row = Adw.ActionRow( title=device.name, subtitle=device.address, ) row.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-active-symbolic")) pair_btn = Gtk.Button(label=_("Pair"), valign=Gtk.Align.CENTER) pair_btn.add_css_class("suggested-action") pair_btn.connect("clicked", lambda b, d=device: self._initiate_pairing(d, dialog)) row.add_suffix(pair_btn) list_box.append(row) # Save original callback and set ours orig_callback = self._window.ble_transport.on_device_discovered self._window.ble_transport.on_device_discovered = on_device_found def on_dialog_closed(d): self._window.ble_transport.on_device_discovered = orig_callback if self._window.ble_transport.state.name == "SCANNING": self._window.ble_transport.stop_scan() # Refresh paired list in case a new device was just paired self.refresh_paired_devices() dialog.connect("closed", on_dialog_closed) # Start scanning self._window.ble_transport.start_scan() dialog.present(self._window) def _initiate_pairing(self, device: BleDevice, scan_dialog: Adw.Dialog): """Start pairing with a device, showing passkey dialog if needed.""" # Stop scanning and close scan dialog if self._window.ble_transport.state.name == "SCANNING": self._window.ble_transport.stop_scan() scan_dialog.close() # Set up passkey handler def on_passkey_requested(device_path, reply_callback): GLib.idle_add(lambda: self._show_passkey_dialog(device_path, reply_callback) or False) self._window.ble_transport.on_passkey_requested = on_passkey_requested self._window.ble_transport.pair_device(device.path) def _show_passkey_dialog(self, device_path, reply_callback): """Show dialog asking user to enter the passkey displayed on the device.""" dialog = Adw.AlertDialog( heading=_("Enter Pairing PIN"), body=_("Enter the PIN shown on your MeshCore device."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) pin_entry = Adw.EntryRow(title=_("PIN")) pin_entry.set_input_purpose(Gtk.InputPurpose.DIGITS) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(pin_entry) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("pair", _("Pair")) dialog.set_response_appearance("pair", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "pair": try: passkey = int(pin_entry.get_text().strip()) reply_callback(passkey) except ValueError: reply_callback(0) else: reply_callback(0) dialog.connect("response", _on_response) dialog.present(self._window) meshy/src/views/contacts_view.py000066400000000000000000002201711521052255700173600ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Contacts list view - shown in the sidebar.""" import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Shumate", "1.0") from gi.repository import Adw, Gio, GLib, GObject, Gtk, Shumate from meshy.models import Contact, ContactType from meshy.protocol import build_sync_next_message from meshy.views import ( _add_hover_swap, _contact_pick_factory_bind, _contact_pick_factory_setup, _contact_pick_factory_unbind, _ContactPickItem, ) @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/contacts-view.ui") class ContactsView(Gtk.Box): __gtype_name__ = "MeshyContactsView" _search_entry = Gtk.Template.Child("search_entry") _list_box = Gtk.Template.Child("list_box") _status_label = Gtk.Template.Child("status_label") _add_btn = Gtk.Template.Child("add_btn") # Filter constants FILTER_ALL = "all" FILTER_FAVORITES = "favorites" FILTER_USERS = "users" FILTER_REPEATERS = "repeaters" FILTER_ROOMS = "rooms" FILTER_SENSORS = "sensors" # Sort constants SORT_AZ = "az" SORT_HEARD = "heard" SORT_MESSAGES = "messages" def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._contact_rows: dict[str, Adw.ActionRow] = {} self._unread_keys: set[str] = set() self._unread_counts: dict[str, int] = {} self._unread_dots: dict[str, Gtk.Label] = {} self._current_filter = self.FILTER_ALL self._current_sort = self.SORT_HEARD self._favorites_first = True self._login_dialog_open = False self._total_contacts = 0 # Register actions action_group = Gio.SimpleActionGroup() filter_action = Gio.SimpleAction.new_stateful( "filter", GLib.VariantType.new("s"), GLib.Variant.new_string(self._current_filter) ) filter_action.connect("activate", self._on_filter_changed) action_group.add_action(filter_action) sort_action = Gio.SimpleAction.new_stateful( "sort", GLib.VariantType.new("s"), GLib.Variant.new_string(self._current_sort) ) sort_action.connect("activate", self._on_sort_changed) action_group.add_action(sort_action) fav_action = Gio.SimpleAction.new_stateful( "favorites-first", None, GLib.Variant.new_boolean(True) ) fav_action.connect("change-state", self._on_favorites_first_toggled) action_group.add_action(fav_action) # Add contact menu (conditional QR item) add_menu = Gio.Menu() add_menu.append(_("Discover Contacts"), "contacts.discover") add_menu.append(_("Add Manually"), "contacts.add-manual") add_menu.append(_("Import from Clipboard"), "contacts.import-clipboard") import meshy as _meshy_pkg if _meshy_pkg.QR_SCANNER_ENABLED: add_menu.append(_("Scan QR Code"), "contacts.scan-qr") self._add_btn.set_menu_model(add_menu) add_action = Gio.SimpleAction.new("add-manual", None) add_action.connect("activate", self._on_add_manual) action_group.add_action(add_action) discover_action = Gio.SimpleAction.new("discover", None) discover_action.connect("activate", self._on_discover_contacts) action_group.add_action(discover_action) clipboard_action = Gio.SimpleAction.new("import-clipboard", None) clipboard_action.connect("activate", self._on_import_clipboard) action_group.add_action(clipboard_action) if _meshy_pkg.QR_SCANNER_ENABLED: qr_action = Gio.SimpleAction.new("scan-qr", None) qr_action.connect("activate", self._on_scan_qr) action_group.add_action(qr_action) self._action_group = action_group self.insert_action_group("contacts", action_group) self._search_btn = None self._search_binding = None # Signal connections self._search_entry.connect("search-changed", self._on_search_changed) self._list_box.set_filter_func(self._filter_func) self._list_box.connect("row-activated", self._on_row_selected) # Initial populate self.update_contacts(self._window.contacts) def set_search_button(self, btn): if self._search_binding: self._search_binding.unbind() self._search_btn = btn self._search_binding = btn.bind_property( "active", self._search_entry, "visible", GObject.BindingFlags.SYNC_CREATE ) self._search_entry.connect("stop-search", lambda _e: btn.set_active(False)) def build_filter_menu(self): menu = Gio.Menu() for filter_id, label in ( ("all", _("All")), ("favorites", _("Favourites")), ("users", _("Users")), ("repeaters", _("Repeaters")), ("rooms", _("Room Servers")), ("sensors", _("Sensors")), ): item = Gio.MenuItem.new(label, None) item.set_action_and_target_value("contacts.filter", GLib.Variant.new_string(filter_id)) menu.append_item(item) return menu def build_sort_menu(self): menu = Gio.Menu() section1 = Gio.Menu() for sort_id, label in ( ("az", _("A–Z")), ("heard", _("Heard Recently")), ("messages", _("Latest Messages")), ): item = Gio.MenuItem.new(label, None) item.set_action_and_target_value("contacts.sort", GLib.Variant.new_string(sort_id)) section1.append_item(item) menu.append_section(None, section1) section2 = Gio.Menu() section2.append(_("Favorites First"), "contacts.favorites-first") menu.append_section(None, section2) return menu def _filter_func(self, row) -> bool: if not hasattr(row, "_contact"): return True contact = row._contact # Type filter f = self._current_filter if ( f == self.FILTER_FAVORITES and not contact.is_favorite or f == self.FILTER_USERS and contact.type != ContactType.CHAT or f == self.FILTER_REPEATERS and contact.type != ContactType.REPEATER or f == self.FILTER_ROOMS and contact.type != ContactType.ROOM or f == self.FILTER_SENSORS and contact.type != ContactType.SENSOR ): return False # Search filter search = self._search_entry.get_text().lower() return not (search and search not in contact.name.lower()) def _update_status_count(self): """Update the status label with visible/total contact count.""" total = self._total_contacts if self._current_filter != self.FILTER_ALL or self._search_entry.get_text(): # Count rows that pass the filter visible = sum(1 for row in self._contact_rows.values() if self._filter_func(row)) self._status_label.set_label( _("{visible}/{total} contacts").format(visible=visible, total=total) ) else: self._status_label.set_label( _("{total} contacts").format(total=total) if total != 1 else _("{total} contact").format(total=total) ) def _on_search_changed(self, entry): self._list_box.invalidate_filter() GLib.idle_add(self._update_status_count) def _on_filter_changed(self, action, param): self._current_filter = param.get_string() action.set_state(param) self._list_box.invalidate_filter() GLib.idle_add(self._update_status_count) def _on_sort_changed(self, action, param): self._current_sort = param.get_string() action.set_state(param) self.update_contacts(self._window.contacts) def _on_favorites_first_toggled(self, action, value): action.set_state(value) self._favorites_first = value.get_boolean() self.update_contacts(self._window.contacts) def _on_discover_contacts(self, *args): discovered = [ c for c in self._window.discovered_contacts if not self._window.is_contact_known(c.public_key_hex) ] if not discovered: dialog = Adw.AlertDialog( heading=_("Discover Contacts"), body=_( "No new contacts have been heard yet. " "Leave the app running and contacts will appear " "as their adverts are received." ), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return builder = Gtk.Builder.new_from_resource( "/page/codeberg/sesivany/Meshy/ui/discover-dialog.ui" ) dialog = builder.get_object("discover_dialog") search_btn = builder.get_object("discover_search_btn") filter_btn = builder.get_object("discover_filter_btn") sort_btn = builder.get_object("discover_sort_btn") search_entry = builder.get_object("discover_search_entry") list_view = builder.get_object("discover_list_view") status_label = builder.get_object("discover_status_label") # Search toggle search_btn.bind_property( "active", search_entry, "visible", GObject.BindingFlags.SYNC_CREATE ) search_entry.connect("stop-search", lambda _e: search_btn.set_active(False)) # Filter menu filter_menu = Gio.Menu() for fid, label in ( ("all", _("All")), ("users", _("Users")), ("repeaters", _("Repeaters")), ("rooms", _("Room Servers")), ("sensors", _("Sensors")), ): item = Gio.MenuItem.new(label, f"discover.filter::{fid}") filter_menu.append_item(item) filter_btn.set_menu_model(filter_menu) # Sort menu dev = self._window.device_info has_self_loc = ( dev and dev.latitude is not None and dev.longitude is not None and (abs(dev.latitude) > 1e-6 or abs(dev.longitude) > 1e-6) ) sort_menu = Gio.Menu() for sid, label in ( ("name", _("Name")), ("last-seen", _("Last Seen")), ): sort_menu.append_item(Gio.MenuItem.new(label, f"discover.sort::{sid}")) if has_self_loc: sort_menu.append_item(Gio.MenuItem.new(_("Distance"), "discover.sort::distance")) sort_btn.set_menu_model(sort_menu) action_group = Gio.SimpleActionGroup() filter_action = Gio.SimpleAction.new_stateful( "filter", GLib.VariantType.new("s"), GLib.Variant.new_string("all") ) sort_action = Gio.SimpleAction.new_stateful( "sort", GLib.VariantType.new("s"), GLib.Variant.new_string("name") ) action_group.add_action(filter_action) action_group.add_action(sort_action) dialog.insert_action_group("discover", action_group) # Model — prevent GC by storing refs on the dialog store = Gio.ListStore(item_type=_ContactPickItem) for c in sorted(discovered, key=lambda c: c.name.lower()): store.append(_ContactPickItem(c)) # Filter custom_filter = Gtk.CustomFilter.new( self._discover_filter_func, (search_entry, filter_action) ) def _on_filter_or_search_changed(*_args): custom_filter.changed(Gtk.FilterChange.DIFFERENT) self._update_discover_status(status_label, filter_model, store) def _on_filter_activated(_action, param): filter_action.set_state(param) _on_filter_or_search_changed() search_entry.connect("search-changed", _on_filter_or_search_changed) filter_action.connect("activate", _on_filter_activated) filter_model = Gtk.FilterListModel(model=store, filter=custom_filter) # Sorter self_lat = dev.latitude if dev else None self_lon = dev.longitude if dev else None custom_sorter = Gtk.CustomSorter.new( self._discover_sort_func, (sort_action, self_lat, self_lon) ) def _on_sort_activated(_action, param): sort_action.set_state(param) custom_sorter.changed(Gtk.SorterChange.DIFFERENT) sort_action.connect("activate", _on_sort_activated) sort_model = Gtk.SortListModel(model=filter_model, sorter=custom_sorter) selection = Gtk.NoSelection(model=sort_model) list_view.set_model(selection) # Factory def _on_add(contact): item = None for i in range(store.get_n_items()): it = store.get_item(i) if it.contact.public_key_hex == contact.public_key_hex: item = it break if item: self._add_discovered_lv(item, store, dialog, status_label, filter_model) def _subtitle(contact): subtitle = contact.type_label if has_self_loc and contact.has_location: from meshy.utils import haversine_distance dist = haversine_distance(self_lat, self_lon, contact.latitude, contact.longitude) if dist < 1.0: subtitle += f" · {dist * 1000:.0f} m" else: subtitle += f" · {dist:.1f} km" return subtitle factory = Gtk.SignalListItemFactory() factory.connect("setup", lambda f, li: _contact_pick_factory_setup(f, li, _("Add"))) factory.connect( "bind", lambda f, li: _contact_pick_factory_bind(f, li, _on_add, subtitle_func=_subtitle), ) factory.connect("unbind", _contact_pick_factory_unbind) list_view.set_factory(factory) def _on_activate(lv, pos): item = sort_model.get_item(pos) if item: self._add_discovered_lv(item, store, dialog, status_label, filter_model) list_view.connect("activate", _on_activate) # prevent Python GC from collecting these while the dialog is alive dialog._refs = ( store, custom_filter, filter_model, custom_sorter, sort_model, selection, factory, action_group, ) self._update_discover_status(status_label, filter_model, store) dialog.present(self._window) def _discover_filter_func(self, item, user_data): search_entry, filter_action = user_data contact = item.contact search = search_entry.get_text().lower() if search and search not in contact.name.lower(): return False f = filter_action.get_state().get_string() return not ( f == "users" and contact.type != ContactType.CHAT or f == "repeaters" and contact.type != ContactType.REPEATER or f == "rooms" and contact.type != ContactType.ROOM or f == "sensors" and contact.type != ContactType.SENSOR ) @staticmethod def _discover_sort_func(a, b, user_data=None): sort_action, self_lat, self_lon = user_data mode = sort_action.get_state().get_string() if mode == "last-seen": ta = a.contact.last_seen tb = b.contact.last_seen if ta is not None and tb is not None: return (tb > ta) - (tb < ta) if ta is not None: return -1 if tb is not None: return 1 na = a.contact.name.lower() nb = b.contact.name.lower() return (na > nb) - (na < nb) if mode == "distance" and self_lat is not None and self_lon is not None: from meshy.utils import haversine_distance da = ( haversine_distance(self_lat, self_lon, a.contact.latitude, a.contact.longitude) if a.contact.has_location else float("inf") ) db = ( haversine_distance(self_lat, self_lon, b.contact.latitude, b.contact.longitude) if b.contact.has_location else float("inf") ) if da != db: return (da > db) - (da < db) na = a.contact.name.lower() nb = b.contact.name.lower() return (na > nb) - (na < nb) def _add_discovered_lv(self, item, store, dialog, status_label, filter_model): self._window.add_discovered_contact(item.contact) found, pos = store.find(item) if found: store.remove(pos) self._update_discover_status(status_label, filter_model, store) if store.get_n_items() == 0: dialog.close() @staticmethod def _update_discover_status(label, filter_model, store): total = store.get_n_items() visible = filter_model.get_n_items() if total == 0: label.set_label(_("No contacts")) elif visible != total: label.set_label(_("{visible}/{total} discovered").format(visible=visible, total=total)) else: label.set_label(_("{total} discovered").format(total=total)) def _on_add_manual(self, *args): dialog = Adw.AlertDialog( heading=_("Add Contact"), body=_("Enter the contact details."), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) type_row = Adw.ComboRow(title=_("Contact Type")) type_list = Gtk.StringList() for label in [_("Chat"), _("Repeater"), _("Room Server"), _("Sensor")]: type_list.append(label) type_row.set_model(type_list) type_row.set_selected(0) name_row = Adw.EntryRow(title=_("Name")) key_row = Adw.EntryRow(title=_("Public Key (64 hex characters)")) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(type_row) fields_list.append(name_row) fields_list.append(key_row) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("add", _("Add")) dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "add": try: name = name_row.get_text().strip() key_text = key_row.get_text().strip() pub_key = bytes.fromhex(key_text) if len(pub_key) != 32 or not name: return if self._window.is_contact_known(key_text): self._show_import_error(_("This contact is already in your list.")) return contact_type = ( type_row.get_selected() + 1 ) # CHAT=1, REPEATER=2, ROOM=3, SENSOR=4 self._window.add_contact(pub_key, contact_type, name) except ValueError: pass dialog.connect("response", _on_response) dialog.present(self._window) def _on_import_clipboard(self, *args): clipboard = self._window.get_clipboard() clipboard.read_text_async(None, self._on_clipboard_read) def _on_clipboard_read(self, clipboard, result): try: text = clipboard.read_text_finish(result) except Exception: text = None if not text or not text.strip().startswith("meshcore://"): dialog = Adw.AlertDialog( heading=_("Import Failed"), body=_("No meshcore:// link found in clipboard."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return app = self._window.get_application() app._handle_meshcore_uri(text.strip()) def _on_scan_qr(self, *args): from meshy.qr_scanner import QRScannerDialog self._qr_scanner = QRScannerDialog(self._window, self._on_qr_result) self._qr_scanner.start() def _on_qr_result(self, data): """Handle a scanned QR code - meshcore:// URL or raw public key hex.""" from urllib.parse import parse_qs, urlparse # meshcore://contact/add?name=...&public_key=... if data.lower().startswith("meshcore://"): parsed = urlparse(data) params = parse_qs(parsed.query) full_path = f"{parsed.netloc}{parsed.path}".rstrip("/") if full_path == "channel/add" and "secret" in params: # meshcore://channel/add?name=...&secret=... ch_name = params.get("name", [""])[0] secret_hex = params["secret"][0].strip() try: secret = bytes.fromhex(secret_hex) except ValueError: self._show_import_error(_("Invalid channel secret in QR code.")) return if len(secret) != 16: self._show_import_error( _("Channel secret must be 16 bytes, got {}.").format(len(secret)) ) return self._window.add_channel_from_qr(ch_name, secret) return if "public_key" in params: pub_key_hex = params["public_key"][0].strip() name = params.get("name", [""])[0] # Extract contact type: 1=Companion, 2=Repeater, 3=RoomServer, 4=Sensor try: contact_type = int(params.get("type", ["1"])[0]) except ValueError: contact_type = 1 try: pub_key = bytes.fromhex(pub_key_hex) except ValueError: self._show_import_error(_("Invalid public key in QR code.")) return if len(pub_key) != 32: self._show_import_error( _("Public key must be 32 bytes, got {}.").format(len(pub_key)) ) return if self._window.is_contact_known(pub_key_hex): self._show_import_error(_("This contact is already in your list.")) return if name: self._window.add_contact(pub_key, contact_type, name) else: self._show_qr_contact_dialog(pub_key, pub_key_hex) return # Try as raw hex contact frame (old format) hex_data = data[len("meshcore://") :].strip() if "/" not in hex_data and "?" not in hex_data: try: contact_frame = bytes.fromhex(hex_data) self._window.import_contact(contact_frame) return except ValueError: pass # Try as raw hex public key (64 hex chars = 32 bytes) cleaned = data.strip().replace(" ", "").replace(":", "") try: pub_key = bytes.fromhex(cleaned) if len(pub_key) == 32: if self._window.is_contact_known(cleaned): self._show_import_error(_("This contact is already in your list.")) return self._show_qr_contact_dialog(pub_key, cleaned) return except ValueError: pass self._show_import_error(_("Could not parse QR code data:\n{}").format(data[:120])) def _show_import_error(self, message): dialog = Adw.AlertDialog(heading=_("Import Failed"), body=message) dialog.add_response("ok", _("OK")) dialog.present(self._window) def _show_qr_contact_dialog(self, pub_key, hex_str): """Show dialog to complete contact details from a scanned public key.""" dialog = Adw.AlertDialog( heading=_("QR Code Scanned"), body=_("Public key: {}...{}").format(hex_str[:16], hex_str[-16:]), ) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) type_row = Adw.ComboRow(title=_("Contact Type")) type_list = Gtk.StringList() for label in [_("Chat"), _("Repeater"), _("Room Server"), _("Sensor")]: type_list.append(label) type_row.set_model(type_list) type_row.set_selected(0) name_row = Adw.EntryRow(title=_("Name")) fields_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) fields_list.add_css_class("boxed-list") fields_list.append(type_row) fields_list.append(name_row) content.append(fields_list) dialog.set_extra_child(content) dialog.add_response("cancel", _("Cancel")) dialog.add_response("add", _("Add Contact")) dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED) def _on_response(d, response): if response == "add": name = name_row.get_text().strip() if not name: return contact_type = type_row.get_selected() + 1 self._window.add_contact(pub_key, contact_type, name) dialog.connect("response", _on_response) dialog.present(self._window) def _on_row_selected(self, listbox, row): import logging log = logging.getLogger(__name__) if self._login_dialog_open: log.debug("Login dialog already open, ignoring row selection") return if row and hasattr(row, "_contact"): contact = row._contact log.info( f"Contact selected: {contact.name} (type={contact.type}, logged_in={self._window.is_room_logged_in(contact.public_key_hex)})" ) if contact.type == ContactType.REPEATER: if self._window.is_room_logged_in(contact.public_key_hex): self._window.show_repeater( contact, is_admin=self._window.is_room_admin(contact.public_key_hex) ) else: self._show_room_login(contact, repeater=True) elif contact.type == ContactType.ROOM and not self._window.is_room_logged_in( contact.public_key_hex ): log.info("Room not logged in, showing login dialog") self._show_room_login(contact) else: self._window.show_chat(contact) def _show_room_login(self, contact: Contact, management: bool = False, repeater: bool = False): """Show a login dialog for a room/repeater contact.""" import logging log = logging.getLogger(__name__) log.info( f"Showing login dialog for {contact.name} (type={contact.type}, pub_key={contact.public_key_hex[:16]}...)" ) self._login_dialog_open = True dialog = Adw.Dialog() dialog.set_title(_("Login to {}").format(contact.name)) dialog.set_content_width(360) dialog.set_content_height(280) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=16, margin_end=16, margin_top=12, margin_bottom=12, ) password_entry = Adw.PasswordEntryRow(title=_("Password")) save_switch = Adw.SwitchRow(title=_("Save password")) login_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) login_list.add_css_class("boxed-list") login_list.append(password_entry) login_list.append(save_switch) content.append(login_list) # Pre-fill saved password if self._window.storage: saved = self._window.storage.get_room_password(contact.public_key_hex) if saved: password_entry.set_text(saved) save_switch.set_active(True) status_box = Gtk.Box(spacing=8, halign=Gtk.Align.CENTER, margin_top=4) spinner = Gtk.Spinner() status_box.append(spinner) status_label = Gtk.Label(label="") status_label.add_css_class("dim-label") status_box.append(status_label) content.append(status_box) login_btn = Gtk.Button(label=_("Login")) login_btn.add_css_class("suggested-action") login_btn.set_halign(Gtk.Align.CENTER) content.append(login_btn) toolbar_view.set_content(content) dialog.set_child(toolbar_view) timeout_id = [None] def on_login_result(success, login_timestamp=None, is_admin=True): spinner.set_spinning(False) if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None if success: status_label.set_label(_("Login successful")) # Save or remove password if self._window.storage: if save_switch.get_active(): self._window.storage.save_room_password( contact.public_key_hex, password_entry.get_text() ) else: self._window.storage.remove_room_password(contact.public_key_hex) self._window.mark_room_logged_in(contact.public_key_hex, is_admin) # Immediately sync to fetch pending room messages self._window.send_frame(build_sync_next_message()) dialog.close() if repeater: self._window.show_repeater(contact, login_timestamp, is_admin) elif management: self._show_room_management(contact) else: self._window.show_chat(contact) else: status_label.set_label(_("Login failed — wrong password")) status_label.remove_css_class("dim-label") status_label.add_css_class("error") login_btn.set_sensitive(True) def on_timeout(): spinner.set_spinning(False) status_label.set_label(_("Login timed out")) self._window.stop_login() login_btn.set_sensitive(True) timeout_id[0] = None return False def on_retry(): status_label.set_label(_("Retrying via flood...")) def on_login_clicked(*args): log.info(f"Login button clicked for {contact.name}") password = password_entry.get_text() log.info(f"Password length: {len(password)}") spinner.set_spinning(True) status_label.set_label(_("Logging in...")) status_label.remove_css_class("error") status_label.add_css_class("dim-label") login_btn.set_sensitive(False) timeout_id[0] = GLib.timeout_add(15000, on_timeout) log.info(f"Calling send_login with pub_key={contact.public_key.hex()[:16]}...") self._window.send_login(contact.public_key, password, on_login_result, on_retry) login_btn.connect("clicked", on_login_clicked) password_entry.connect("apply", on_login_clicked) def on_dialog_closed(d): self._login_dialog_open = False self._window.stop_login() if timeout_id[0]: GLib.source_remove(timeout_id[0]) dialog.connect("closed", on_dialog_closed) dialog.present(self._window) def _sort_key(self, c: Contact): """Compute sort key for a contact based on current sort setting.""" fav = self._favorites_first if self._current_sort == self.SORT_AZ: return (not c.is_favorite if fav else False, c.name.lower()) elif self._current_sort == self.SORT_MESSAGES: return ( not c.is_favorite if fav else False, -(c.last_message_at.timestamp() if c.last_message_at else 0), ) else: # SORT_HEARD return ( not c.is_favorite if fav else False, -(c.last_seen.timestamp() if c.last_seen else 0), ) def update_contacts(self, contacts: list[Contact]): sorted_contacts = sorted(contacts, key=self._sort_key) new_keys = [c.public_key_hex for c in sorted_contacts] old_keys = list(self._contact_rows.keys()) # Fast path: same contacts in same order — update in-place if new_keys == old_keys: for contact in sorted_contacts: key = contact.public_key_hex row = self._contact_rows.get(key) if row: row.set_title(GLib.markup_escape_text(contact.name)) row.set_subtitle(contact.path_label) row._contact = contact self._total_contacts = len(contacts) self._update_status_count() return # Full rebuild: contact set or order changed self._full_rebuild(sorted_contacts) def _full_rebuild(self, sorted_contacts: list[Contact]): """Remove all rows and recreate from scratch.""" selected_row = self._list_box.get_selected_row() selected_key = ( selected_row._contact.public_key_hex if selected_row and hasattr(selected_row, "_contact") else None ) child = self._list_box.get_first_child() while child: next_child = child.get_next_sibling() self._list_box.remove(child) child = next_child self._contact_rows.clear() new_selected = None for contact in sorted_contacts: row = self._create_contact_row(contact) self._list_box.append(row) self._contact_rows[contact.public_key_hex] = row if contact.public_key_hex == selected_key: new_selected = row if new_selected: self._list_box.select_row(new_selected) self._total_contacts = len(sorted_contacts) self._update_status_count() def _create_contact_row(self, contact: Contact) -> Gtk.ListBoxRow: row = Adw.ActionRow( title=GLib.markup_escape_text(contact.name), subtitle=contact.path_label, ) row._contact = contact # Type icon with favorite star icon_name = { ContactType.CHAT: "avatar-default-symbolic", ContactType.REPEATER: "network-server-symbolic", ContactType.ROOM: "user-home-symbolic", ContactType.SENSOR: "weather-clear-symbolic", }.get(contact.type, "avatar-default-symbolic") prefix_box = Gtk.Box(spacing=2) star = Gtk.Image.new_from_icon_name("starred-symbolic") star.set_pixel_size(10) if not contact.is_favorite: star.set_opacity(0) prefix_box.append(star) prefix_box.append(Gtk.Image.new_from_icon_name(icon_name)) row.add_prefix(prefix_box) # Unread badge — visible by default when count > 0, hidden on hover count = self._unread_counts.get(contact.public_key_hex, 0) unread_badge = Gtk.Label(label=str(count) if count else "") unread_badge.add_css_class("unread-badge") unread_badge.set_valign(Gtk.Align.CENTER) unread_badge.set_visible(count > 0) row.add_suffix(unread_badge) self._unread_dots[contact.public_key_hex] = unread_badge # "More" button + hover behavior + long press info_btn = Gtk.Button(icon_name="view-more-symbolic", valign=Gtk.Align.CENTER) info_btn.add_css_class("flat") info_btn.set_visible(False) on_detail = lambda *_, r=row: self._show_contact_detail(r._contact) info_btn.connect("clicked", on_detail) row.add_suffix(info_btn) _add_hover_swap( row, info_btn, unread_badge, lambda: self._unread_counts.get(contact.public_key_hex, 0), on_detail, ) row.set_activatable(True) return row def mark_unread(self, public_key_hex: str, unread: bool): """Show or hide the unread badge for a contact.""" if unread: self._unread_keys.add(public_key_hex) self._unread_counts[public_key_hex] = self._unread_counts.get(public_key_hex, 0) + 1 else: self._unread_keys.discard(public_key_hex) self._unread_counts[public_key_hex] = 0 if public_key_hex in self._unread_dots: badge = self._unread_dots[public_key_hex] count = self._unread_counts.get(public_key_hex, 0) badge.set_label(str(count) if count else "") badge.set_visible(count > 0) def select_contact(self, contact: Contact): """Programmatically select a contact in the list.""" key = contact.public_key_hex if key in self._contact_rows: self._list_box.select_row(self._contact_rows[key]) def _get_visible_rows(self): """Return visible rows in display order.""" rows = [] row = self._list_box.get_first_child() while row: if row.get_visible() and hasattr(row, "_contact"): rows.append(row) row = row.get_next_sibling() return rows def navigate(self, direction: int, unread_only: bool = False): """Move selection up (-1) or down (+1). If unread_only, skip to next unread.""" rows = self._get_visible_rows() if not rows: return if unread_only: rows = [r for r in rows if r._contact.public_key_hex in self._unread_keys] if not rows: return selected = self._list_box.get_selected_row() if selected and selected in rows: idx = rows.index(selected) idx = (idx + direction) % len(rows) else: idx = 0 if direction > 0 else len(rows) - 1 self._list_box.select_row(rows[idx]) rows[idx].grab_focus() def _apply_contact_update(self, contact, name_entry, out_path_entry, flood_switch): """Send the current contact settings to the device.""" new_name = name_entry.get_text().strip() or contact.name path_text = out_path_entry.get_text().strip() force_flood = flood_switch.get_active() if force_flood: self._window.set_contact_path(contact, 0xFF, b"", name=new_name) elif path_text: try: hops = [h.strip() for h in path_text.split(",") if h.strip()] # Infer hash size from the length of the first hop hash_size = len(bytes.fromhex(hops[0])) path_bytes = b"".join(bytes.fromhex(h) for h in hops) hash_mode = hash_size - 1 hop_count = len(hops) path_len_byte = (hash_mode << 6) | (hop_count & 0x3F) self._window.set_contact_path(contact, path_len_byte, path_bytes, name=new_name) except ValueError: pass else: self._window.reset_contact_path(contact) if new_name != contact.name: GLib.timeout_add( 500, lambda: self._window.set_contact_path(contact, 0xFF, b"", name=new_name) or False, ) # Update local contact name immediately so the UI reflects it if new_name != contact.name: contact.name = new_name self._window.storage.save_contact(contact) self._window.update_contacts_from_storage() self._window.refresh_chat_header() def _on_path_discovery(self, contact: Contact, btn: Gtk.Button): """Send a path discovery request for a contact.""" btn.set_sensitive(False) self._discover_path_label.set_label(_("Searching...")) timeout_id = [None] def _on_response(data): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None self._window.stop_path_discovery() btn.set_sensitive(True) # Parse the PATH_DISCOVERY_RESPONSE (0x8D) # Format: [code][reserved][prefix×6][out_path_len][out_path...][in_path_len][in_path...] try: i = 2 # skip code + reserved i += 6 # skip prefix opl_byte = data[i] i += 1 opl_hlen = ((opl_byte & 0xC0) >> 6) + 1 opl = opl_byte & 0x3F out_hops = [] for _j in range(opl): out_hops.append(data[i : i + opl_hlen].hex().upper()) i += opl_hlen ipl_byte = data[i] i += 1 ipl_hlen = ((ipl_byte & 0xC0) >> 6) + 1 ipl = ipl_byte & 0x3F in_hops = [] for _j in range(ipl): in_hops.append(data[i : i + ipl_hlen].hex().upper()) i += ipl_hlen if opl == 0 and ipl == 0: self._discover_path_label.set_label(_("Direct")) else: parts = [] if out_hops: parts.append(",".join(out_hops)) self._discover_path_label.set_label( " → ".join(parts) if parts else f"{opl} hops" ) # Apply discovered outbound path to the contact if out_hops: path_bytes = b"".join(bytes.fromhex(h) for h in out_hops) hash_mode = opl_hlen - 1 path_len_byte = (hash_mode << 6) | (opl & 0x3F) self._window.set_contact_path(contact, path_len_byte, path_bytes) self._window.show_toast(f'Path set: {",".join(out_hops)}') except (IndexError, ValueError) as e: import logging logging.getLogger(__name__).warning(f"Path discovery parse error: {e}") self._discover_path_label.set_label(_("Path found")) self._window.refresh_contacts() def _on_timeout(): self._window.stop_path_discovery() btn.set_sensitive(True) self._discover_path_label.set_label(_("No response")) timeout_id[0] = None return False timeout_id[0] = GLib.timeout_add(30000, _on_timeout) self._window.send_path_discovery(contact.public_key, _on_response) def _on_get_advert_path(self, contact: Contact): """Get and display the inbound advertisement path for a contact.""" self._advert_path_label.set_label(_("Loading...")) # Set a timeout in case the response never comes (error consumed elsewhere) timeout_id = [None] def _on_response(result): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None if not result: self._advert_path_label.set_label(_("No path cached")) return hops = result.get("hops", []) path_len = result.get("path_length", -1) if path_len < 0 or not hops: self._advert_path_label.set_label(_("Flood")) elif path_len == 0: self._advert_path_label.set_label(_("Direct")) else: self._advert_path_label.set_label(" → ".join(hops)) def _on_timeout(): self._window.set_advert_path_callback(None) self._advert_path_label.set_label(_("Not supported")) timeout_id[0] = None return False timeout_id[0] = GLib.timeout_add(3000, _on_timeout) self._window.get_advert_path(contact.public_key, _on_response) def _ping_contact(self, contact: Contact, btn: Gtk.Button): """Ping a repeater/room via trace path to get RTT and SNR.""" # Trace protocol only supports 1B and 2B hashes (power-of-2 encoding) if self._window.device_info.path_hash_mode > 1: self._window.show_toast(_("Ping is not supported with 3-byte path hashes")) return import time btn.set_sensitive(False) start = time.monotonic() timeout_id = [None] hash_width = self._window.device_info.path_hash_mode + 1 hop_hash = contact.public_key[:hash_width] def on_trace(result): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None rtt = (time.monotonic() - start) * 1000 snr_values = result.get("snr_values", []) parts = [f"{rtt:.0f} ms"] if len(snr_values) >= 1: parts.append(f"SNR out: {snr_values[0]:+.1f} dB") if len(snr_values) >= 2: parts.append(f"SNR back: {snr_values[-1]:+.1f} dB") msg = _("Ping {}: {}").format(contact.name, ", ".join(parts)) self._window.show_toast(msg, timeout=5) btn.set_sensitive(True) def on_device_timeout(timeout_ms): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = GLib.timeout_add(timeout_ms, on_timeout) def on_timeout(): self._window.stop_trace_path() self._window.show_toast(_("Ping {}: timed out").format(contact.name), timeout=5) btn.set_sensitive(True) timeout_id[0] = None return False timeout_id[0] = GLib.timeout_add(15000, on_timeout) trace_flags = self._window.device_info.path_hash_mode self._window.send_trace_path(hop_hash, on_trace, on_device_timeout, flags=trace_flags) def _request_telemetry(self, contact: Contact, row: Adw.ActionRow): """Request and display telemetry from a contact.""" from meshy.protocol import parse_cayenne_lpp row.set_subtitle(_("Requesting...")) timeout_id = [None] def on_timeout(): row.set_subtitle(_("No response (timed out)")) timeout_id[0] = None return False def on_response(lpp_data): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None if not lpp_data: row.set_subtitle(_("No telemetry data")) return entries = parse_cayenne_lpp(lpp_data) if not entries: row.set_subtitle(_("No telemetry data")) return self._show_telemetry_dialog(contact, entries) row.set_subtitle(_("Telemetry received")) timeout_id[0] = GLib.timeout_add(15000, on_timeout) self._window.get_telemetry(contact.public_key, on_response, contact_type=contact.type) @staticmethod def _battery_percent(volts: float) -> int: from meshy.models import battery_percent_from_mv return battery_percent_from_mv(int(volts * 1000)) def _show_telemetry_dialog(self, contact: Contact, entries: list[dict]): """Show a dialog with parsed telemetry data and optional map.""" from meshy.protocol import LPP_GPS, LPP_VOLTAGE # Check for GPS data gps_entry = next((e for e in entries if e["type"] == LPP_GPS), None) dialog = Adw.Dialog() dialog.set_title(_("Telemetry — {}").format(contact.name)) dialog.set_content_width(420) dialog.set_content_height(550 if gps_entry else 400) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) scroll = Gtk.ScrolledWindow(vexpand=True) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=12, margin_end=12, margin_top=12, margin_bottom=12, ) channels = {} for entry in entries: channels.setdefault(entry["channel"], []).append(entry) for ch in sorted(channels): group = Adw.PreferencesGroup(title=_("Channel {}").format(ch)) for entry in channels[ch]: val = entry["value"] if isinstance(val, dict): alt_str = f', alt {val["alt"]:.0f} m' if abs(val["alt"]) > 0.01 else "" subtitle = f'{val["lat"]:.4f}, {val["lon"]:.4f}{alt_str}' elif entry["type"] == LPP_VOLTAGE and ch == 1: pct = self._battery_percent(val) subtitle = f"{pct}% ({val:.2f} V)" elif isinstance(val, float): subtitle = f'{val:.2f} {entry["unit"]}' else: subtitle = f'{val} {entry["unit"]}' title = _("Battery") if entry["type"] == LPP_VOLTAGE and ch == 1 else entry["name"] row = Adw.ActionRow(title=title, subtitle=subtitle) group.add(row) content.append(group) # Show map if GPS data is present if gps_entry: from meshy.views import create_shumate_map, deregister_map_widget lat = gps_entry["value"]["lat"] lon = gps_entry["value"]["lon"] from meshy.views.map_view import _MARKER_COLORS, _MARKER_ICONS, _SELF_COLOR, MapView map_widget, marker_layer, viewport = create_shumate_map(zoom_level=14, lat=lat, lon=lon) map_widget.set_vexpand(False) map_widget.set_size_request(-1, 250) r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") marker = MapView.create_marker(r, g, b, icon_name, contact.name, lat, lon) marker_layer.add_marker(marker) # Show our companion's position too lats, lons = [lat], [lon] si = self._window.self_info self_lat = si.get("adv_lat", 0.0) self_lon = si.get("adv_lon", 0.0) if abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6: self_name = si.get("name", "") or _("My Device") self_marker = MapView.create_marker( *_SELF_COLOR, "avatar-default-symbolic", self_name, self_lat, self_lon ) marker_layer.add_marker(self_marker) lats.append(self_lat) lons.append(self_lon) # Fit viewport to show all markers from meshy.views.map_view import MapView MapView.fit_viewport(viewport, lats, lons) map_frame = Gtk.Frame() map_frame.set_child(map_widget) content.append(map_frame) scroll.set_child(content) toolbar_view.set_content(scroll) dialog.set_child(toolbar_view) if gps_entry: dialog.connect("closed", lambda _d: deregister_map_widget(map_widget)) dialog.present(self._window) def _show_room_management(self, contact: Contact): """Show a CLI management dialog for a room/repeater.""" dialog = Adw.Dialog() dialog.set_title(_("{} — Management").format(contact.name)) dialog.set_content_width(500) dialog.set_content_height(500) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=4, margin_start=8, margin_end=8, margin_bottom=8, ) # Output area output_scroll = Gtk.ScrolledWindow(vexpand=True) output_view = Gtk.TextView(editable=False, wrap_mode=Gtk.WrapMode.WORD_CHAR) output_view.set_monospace(True) output_buf = output_view.get_buffer() output_scroll.set_child(output_view) content.append(output_scroll) # Command input cmd_entry = Gtk.Entry(placeholder_text="Type a CLI command...", hexpand=True) send_btn = Gtk.Button(icon_name="mail-send-symbolic") send_btn.add_css_class("suggested-action") input_box = Gtk.Box(spacing=8, margin_top=4) input_box.append(cmd_entry) input_box.append(send_btn) content.append(input_box) toolbar_view.set_content(content) dialog.set_child(toolbar_view) def append_output(text): end_iter = output_buf.get_end_iter() output_buf.insert(end_iter, text + "\n") GLib.idle_add( lambda: output_scroll.get_vadjustment().set_value( output_scroll.get_vadjustment().get_upper() ) or False ) def on_cli_response(cli_contact, message): """Handle CLI response messages from the room.""" if cli_contact.public_key_hex == contact.public_key_hex and message.is_cli: append_output(message.text) # Register CLI response listener self._window.set_cli_response_callback(on_cli_response) def on_send(*args): cmd = cmd_entry.get_text().strip() if not cmd: return append_output(f"> {cmd}") cmd_entry.set_text("") from meshy.protocol import build_send_cli_command self._window.send_frame(build_send_cli_command(contact.public_key, cmd)) send_btn.connect("clicked", on_send) cmd_entry.connect("activate", on_send) def on_dialog_closed(d): self._window.set_cli_response_callback(None) dialog.connect("closed", on_dialog_closed) # Send initial 'status' command append_output("> status") from meshy.protocol import build_send_cli_command self._window.send_frame(build_send_cli_command(contact.public_key, "status")) dialog.present(self._window) def _show_path_on_map(self, contact: Contact, hop_contacts: list, hop_prefixes: list = None): """Show the established path to a contact in a map dialog.""" from meshy.views.map_view import ( _MARKER_COLORS, _MARKER_ICONS, _SELF_COLOR, _UNKNOWN_COLOR, MapView, ) device = self._window.device_info self_lat = getattr(device, "latitude", None) self_lon = getattr(device, "longitude", None) has_self = self_lat and self_lon and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) # Build full hop list including unknowns: (lat, lon, contact, prefix, snr) raw_hops = [] if has_self: raw_hops.append((self_lat, self_lon, None, None, None)) for i, hop_contact in enumerate(hop_contacts): prefix = hop_prefixes[i] if hop_prefixes and i < len(hop_prefixes) else None if hop_contact and hop_contact.has_location: raw_hops.append( (hop_contact.latitude, hop_contact.longitude, hop_contact, prefix, None) ) else: raw_hops.append((None, None, hop_contact, prefix, None)) if contact.has_location: raw_hops.append((contact.latitude, contact.longitude, contact, None, None)) hops = MapView.interpolate_unknown_hops(raw_hops) if not any(h[0] is not None for h in hops): return dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( self._window, title=_("Path to {}").format(contact.name) ) map_widget.get_map().remove_layer(marker_layer) path_layer = MapView.create_dashed_path_layer(viewport) map_widget.get_map().add_layer(path_layer) map_widget.get_map().add_layer(marker_layer) lats, lons = [], [] hop_num = 0 for lat, lon, hop_contact, prefix, _snr in hops: if lat is None: continue node = Shumate.Marker() node.set_location(lat, lon) path_layer.add_node(node) if hop_contact is None and prefix is None: # Self/device marker r, g, b = _SELF_COLOR icon_name = "network-wireless-symbolic" name = device.name or _("My Device") badge = "" elif hop_contact is None or not hop_contact.has_location: # Unknown/interpolated hop r, g, b = _UNKNOWN_COLOR icon_name = "network-server-symbolic" name = prefix or _("Unknown") hop_num += 1 badge = str(hop_num) else: r, g, b = _MARKER_COLORS.get(hop_contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(hop_contact.type, "avatar-default-symbolic") name = hop_contact.name hop_num += 1 badge = str(hop_num) widget = MapView._build_marker_widget(r, g, b, icon_name, "", badge_text=badge) widget.set_tooltip_text(name) if hop_contact is not None: click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, c=hop_contact: self._show_contact_detail(c) ) widget.add_controller(click) elif prefix is not None: click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, w=widget, nm=name, la=lat, lo=lon: MapView.show_marker_popover( w, nm, la, lo, contact_type=_("Unknown repeater"), interpolated=True ), ) widget.add_controller(click) hop_marker = Shumate.Marker() hop_marker.set_child(widget) hop_marker.set_location(lat, lon) marker_layer.add_marker(hop_marker) lats.append(lat) lons.append(lon) MapView.fit_viewport(viewport, lats, lons) dialog.present(self._window) def _show_on_map(self, contact: Contact): """Show a contact's location in a map dialog.""" from meshy.views.map_view import _MARKER_COLORS, _MARKER_ICONS, MapView dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( self._window, title=contact.name ) r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") marker = MapView.create_marker( r, g, b, icon_name, contact.name, contact.latitude, contact.longitude ) marker_layer.add_marker(marker) viewport.set_zoom_level(14) viewport.set_location(contact.latitude, contact.longitude) dialog.present(self._window) def _show_contact_detail(self, contact: Contact): """Show contact detail dialog with name editing and path management.""" dialog = Adw.Dialog() dialog.set_title(contact.name) dialog.set_content_width(420) dialog.set_content_height(520) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) scroll = Gtk.ScrolledWindow(vexpand=True) clamp = Adw.Clamp(maximum_size=420) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=12, margin_end=12, margin_top=12, margin_bottom=12, ) # Info group info_group = Adw.PreferencesGroup(title=_("Contact Info")) name_entry = Adw.EntryRow(title=_("Name")) name_entry.set_text(contact.name) name_entry.set_show_apply_button(True) info_group.add(name_entry) favorite_row = Adw.SwitchRow( title=_("Favorite"), subtitle=_("Pin to top of contacts list"), ) favorite_row.set_active(contact.is_favorite) def _on_favorite_toggled(*args): from meshy.models import CONTACT_FLAG_FAVORITE if favorite_row.get_active(): contact.flags |= CONTACT_FLAG_FAVORITE else: contact.flags &= ~CONTACT_FLAG_FAVORITE self._window.storage.save_contact(contact) self._window.update_contact_flags(contact) self._window.update_contacts_from_storage() favorite_row.connect("notify::active", _on_favorite_toggled) info_group.add(favorite_row) info_group.add(Adw.ActionRow(title=_("Type"), subtitle=contact.type_label)) key_row = Adw.ActionRow(title=_("Public Key"), subtitle=contact.public_key_hex) key_row.set_subtitle_selectable(True) copy_btn = Gtk.Button(icon_name="edit-copy-symbolic", valign=Gtk.Align.CENTER) copy_btn.add_css_class("flat") copy_btn.set_tooltip_text("Copy to clipboard") def _copy_key(*_args): clipboard = self._window.get_clipboard() clipboard.set(contact.public_key_hex) clipboard.store_async(None, None) self._window.show_toast(_("Public key copied")) copy_btn.connect("clicked", _copy_key) key_row.add_suffix(copy_btn) info_group.add(key_row) last_seen_str = ( contact.last_seen.strftime("%Y-%m-%d %H:%M") if contact.last_seen else _("Never") ) info_group.add(Adw.ActionRow(title=_("Last Seen"), subtitle=last_seen_str)) if contact.has_location: loc_row = Adw.ActionRow( title=_("Location"), subtitle=f"{contact.latitude:.6f}, {contact.longitude:.6f}", ) map_btn = Gtk.Button( icon_name="map-symbolic", valign=Gtk.Align.CENTER, ) map_btn.add_css_class("flat") map_btn.set_tooltip_text("Show on Map") map_btn.connect("clicked", lambda *_, c=contact: self._show_on_map(c)) loc_row.add_suffix(map_btn) info_group.add(loc_row) content.append(info_group) # Out Path group path_group = Adw.PreferencesGroup( title=_("Out Path"), description=_("Comma-separated hex hops (e.g. 9a,74 or 9a3b,744c for 2-byte)"), ) # Out Path entry out_path_entry = Adw.EntryRow(title=_("Out Path")) out_path_entry.set_show_apply_button(True) if contact.path_hops: out_path_entry.set_text(",".join(h.lower() for h in contact.path_hops)) path_group.add(out_path_entry) reset_path_row = Adw.ActionRow( title=_("Reset Path"), subtitle=_("Clear the established path and switch to flood"), ) reset_path_btn = Gtk.Button( icon_name="edit-clear-symbolic", valign=Gtk.Align.CENTER, ) reset_path_btn.connect( "clicked", lambda *_: ( self._window.reset_contact_path(contact), out_path_entry.set_text(""), ), ) reset_path_row.add_suffix(reset_path_btn) reset_path_row.set_activatable_widget(reset_path_btn) path_group.add(reset_path_row) # Force Flood toggle — always off on open, toggling it on resets the path flood_switch = Adw.SwitchRow( title=_("Force Flood"), subtitle=_("Always broadcast, never use an established path"), ) flood_switch.set_active(False) path_group.add(flood_switch) _updating = [False] def _on_path_changed(*args): if _updating[0]: return if out_path_entry.get_text().strip(): _updating[0] = True flood_switch.set_active(False) _updating[0] = False def _on_flood_toggled(*args): if _updating[0]: return if flood_switch.get_active(): _updating[0] = True out_path_entry.set_text("") _updating[0] = False out_path_entry.connect("changed", _on_path_changed) flood_switch.connect("notify::active", _on_flood_toggled) # Apply on confirm def _apply(*args): self._apply_contact_update(contact, name_entry, out_path_entry, flood_switch) name_entry.connect("apply", _apply) out_path_entry.connect("apply", _apply) flood_switch.connect("notify::active", lambda *a: _apply() if not _updating[0] else None) # Show Path on Map — only if there are hops and at least one is locatable if contact.path_hops: hop_contacts = self._window.resolve_hop_prefixes(contact.path_hops) has_located = any(c and c.has_location for c in hop_contacts) if has_located or contact.has_location: path_map_row = Adw.ActionRow( title=_("Show Path on Map"), subtitle=_("Visualize the message route on the map"), ) path_map_btn = Gtk.Button( icon_name="map-symbolic", valign=Gtk.Align.CENTER, ) path_map_btn.add_css_class("flat") path_map_btn.connect( "clicked", lambda *_, c=contact, hc=hop_contacts, hp=contact.path_hops: self._show_path_on_map(c, hc, hp), ) path_map_row.add_suffix(path_map_btn) path_map_row.set_activatable_widget(path_map_btn) path_group.add(path_map_row) content.append(path_group) # Telemetry group tele_group = Adw.PreferencesGroup(title=_("Telemetry")) from meshy.models import ( CONTACT_FLAG_TELE_BASE, CONTACT_FLAG_TELE_ENV, CONTACT_FLAG_TELE_LOC, ) def _make_tele_switch(title, subtitle, flag): sw = Adw.SwitchRow(title=title, subtitle=subtitle) sw.set_active(bool(contact.flags & flag)) def _on_toggle(*args): if sw.get_active(): contact.flags |= flag else: contact.flags &= ~flag self._window.storage.save_contact(contact) self._window.update_contact_flags(contact) sw.connect("notify::active", _on_toggle) return sw if contact.type == ContactType.CHAT: tele_group.add( _make_tele_switch( _("Allow Telemetry Requests"), _("Allow this contact to query battery, voltage, temperature"), CONTACT_FLAG_TELE_BASE, ) ) tele_group.add( _make_tele_switch( _("Include Location"), _("Include GPS coordinates in telemetry responses"), CONTACT_FLAG_TELE_LOC, ) ) tele_group.add( _make_tele_switch( _("Include Environment Sensors"), _("Include humidity, pressure, air quality data"), CONTACT_FLAG_TELE_ENV, ) ) # Request telemetry button req_tele_row = Adw.ActionRow( title=_("Request Telemetry"), subtitle=_("Query this contact's telemetry data"), ) req_tele_btn = Gtk.Button( icon_name="view-refresh-symbolic", valign=Gtk.Align.CENTER, ) req_tele_btn.connect("clicked", lambda *_: self._request_telemetry(contact, req_tele_row)) req_tele_row.add_suffix(req_tele_btn) req_tele_row.set_activatable_widget(req_tele_btn) tele_group.add(req_tele_row) content.append(tele_group) # Actions group (only for repeaters/rooms) actions_group = Adw.PreferencesGroup(title=_("Actions")) actions_count = 0 if contact.type == ContactType.ROOM: mgmt_row = Adw.ActionRow( title=_("Room Management"), subtitle=_("Login and access CLI, status, and settings"), ) mgmt_btn = Gtk.Button( icon_name="utilities-terminal-symbolic", valign=Gtk.Align.CENTER, ) mgmt_btn.connect( "clicked", lambda *_: ( dialog.close(), self._show_room_login(contact, management=True), ), ) mgmt_row.add_suffix(mgmt_btn) mgmt_row.set_activatable_widget(mgmt_btn) actions_group.add(mgmt_row) actions_count += 1 if contact.type == ContactType.REPEATER: mgmt_row = Adw.ActionRow( title=_("Repeater Management"), subtitle=_("Login and access status, CLI, neighbors, and settings"), ) mgmt_btn = Gtk.Button( icon_name="utilities-terminal-symbolic", valign=Gtk.Align.CENTER, ) mgmt_btn.connect( "clicked", lambda *_: ( dialog.close(), self._show_room_login(contact, repeater=True), ), ) mgmt_row.add_suffix(mgmt_btn) mgmt_row.set_activatable_widget(mgmt_btn) actions_group.add(mgmt_row) actions_count += 1 if contact.type in (ContactType.REPEATER, ContactType.ROOM): ping_row = Adw.ActionRow( title=_("Ping"), subtitle=_("Measure round-trip time to this node"), ) ping_btn = Gtk.Button( icon_name="network-transmit-receive-symbolic", valign=Gtk.Align.CENTER, ) ping_btn.connect("clicked", lambda *_: self._ping_contact(contact, ping_btn)) ping_row.add_suffix(ping_btn) ping_row.set_activatable_widget(ping_btn) actions_group.add(ping_row) actions_count += 1 # Path Discovery — works with repeaters and rooms only if contact.type in (ContactType.REPEATER, ContactType.ROOM): discover_row = Adw.ActionRow( title=_("Discover Paths"), subtitle=_("Find routes to this node via flood"), ) self._discover_path_label = Gtk.Label(label="") self._discover_path_label.add_css_class("dim-label") self._discover_path_label.set_valign(Gtk.Align.CENTER) discover_row.add_suffix(self._discover_path_label) discover_btn = Gtk.Button( icon_name="system-search-symbolic", valign=Gtk.Align.CENTER, ) discover_btn.add_css_class("flat") discover_btn.connect( "clicked", lambda *_: self._on_path_discovery(contact, discover_btn) ) discover_row.add_suffix(discover_btn) discover_row.set_activatable_widget(discover_btn) actions_group.add(discover_row) actions_count += 1 if actions_count > 0: content.append(actions_group) # Danger Zone danger_group = Adw.PreferencesGroup(title=_("Danger Zone")) remove_row = Adw.ActionRow(title=_("Remove Contact")) remove_btn = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER) remove_btn.add_css_class("destructive-action") def _on_remove(*args): confirm = Adw.AlertDialog( heading=_("Remove Contact?"), body=_("Remove {} and all messages?").format(contact.name), ) confirm.add_response("cancel", _("Cancel")) confirm.add_response("remove", _("Remove")) confirm.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) confirm.connect( "response", lambda d, r: (self._window.remove_contact(contact), dialog.close()) if r == "remove" else None, ) confirm.present(self._window) remove_btn.connect("clicked", _on_remove) remove_row.add_suffix(remove_btn) remove_row.set_activatable_widget(remove_btn) danger_group.add(remove_row) content.append(danger_group) clamp.set_child(content) scroll.set_child(clamp) toolbar_view.set_content(scroll) dialog.set_child(toolbar_view) dialog.present(self._window) meshy/src/views/device_view.py000066400000000000000000001465421521052255700170120ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Device information view.""" import json import logging import threading import urllib.request import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Shumate", "1.0") import contextlib from gi.repository import Adw, Gdk, GLib, Gtk, Pango, Shumate from meshy.models import DeviceInfo, snr_to_icon log = logging.getLogger(__name__) _RELEASES_URL = "https://flasher.meshcore.io/releases" _FLASHER_URL = "https://flasher.meshcore.io" @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/device-view.ui") class DeviceView(Gtk.Box): __gtype_name__ = "MeshyDeviceView" _scroll = Gtk.Template.Child("scroll") _status_group = Gtk.Template.Child("status_group") _status_row = Gtk.Template.Child("status_row") _status_icon = Gtk.Template.Child("status_icon") _info_group = Gtk.Template.Child("info_group") _name_row = Gtk.Template.Child("name_row") _board_row = Gtk.Template.Child("board_row") _fw_row = Gtk.Template.Child("fw_row") _fw_update_btn = Gtk.Template.Child("fw_update_btn") _key_row = Gtk.Template.Child("key_row") _copy_key_btn = Gtk.Template.Child("copy_key_btn") _telemetry_row = Gtk.Template.Child("telemetry_row") _telemetry_btn = Gtk.Template.Child("telemetry_btn") _location_row = Gtk.Template.Child("location_row") _location_map_btn = Gtk.Template.Child("location_map_btn") _batt_group = Gtk.Template.Child("batt_group") _batt_row = Gtk.Template.Child("batt_row") _batt_bar = Gtk.Template.Child("batt_bar") _storage_row = Gtk.Template.Child("storage_row") _storage_bar = Gtk.Template.Child("storage_bar") _radio_group = Gtk.Template.Child("radio_group") _freq_row = Gtk.Template.Child("freq_row") _bw_row = Gtk.Template.Child("bw_row") _sf_row = Gtk.Template.Child("sf_row") _cr_row = Gtk.Template.Child("cr_row") _tx_row = Gtk.Template.Child("tx_row") _path_hash_row = Gtk.Template.Child("path_hash_row") _stats_group = Gtk.Template.Child("stats_group") _uptime_row = Gtk.Template.Child("uptime_row") _queue_row = Gtk.Template.Child("queue_row") _noise_row = Gtk.Template.Child("noise_row") _rssi_row = Gtk.Template.Child("rssi_row") _snr_row = Gtk.Template.Child("snr_row") _airtime_row = Gtk.Template.Child("airtime_row") _pkt_row = Gtk.Template.Child("pkt_row") _pkt_flood_row = Gtk.Template.Child("pkt_flood_row") _pkt_direct_row = Gtk.Template.Child("pkt_direct_row") _refresh_stats_btn = Gtk.Template.Child("refresh_stats_btn") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._location_lat = None self._location_lon = None self._location_pending_lat = None self._location_pending_lon = None self._gps_enabled = None # None = unknown, True/False from custom_vars self._section_widgets = [ ("Connection", self._status_group), ("Device Information", self._info_group), ("Battery & Storage", self._batt_group), ("Radio Configuration", self._radio_group), ("Statistics", self._stats_group), ] self._latest_fw_version = None # Connect signals that reference self._window self._copy_key_btn.connect("clicked", self._on_copy_public_key) self._telemetry_btn.connect("clicked", self._on_request_telemetry) self._location_map_btn.connect("clicked", self._on_show_location_map) self._fw_update_btn.connect("clicked", self._on_fw_update_clicked) self._refresh_stats_btn.connect("clicked", lambda *_: self._window.refresh_stats()) def _build_actions_group(self) -> Adw.PreferencesGroup: """Build the Actions preference group from UI template.""" builder = Gtk.Builder.new_from_resource( "/page/codeberg/sesivany/Meshy/ui/device-actions.ui" ) actions_group = builder.get_object("actions_group") builder.get_object("zero_hop_btn").connect( "clicked", lambda *_: self._window.send_advert(flood=False) ) builder.get_object("flood_btn").connect( "clicked", lambda *_: self._window.send_advert(flood=True) ) builder.get_object("clipboard_btn").connect("clicked", self._on_advert_to_clipboard) builder.get_object("qr_btn").connect("clicked", self._on_show_qr_code) builder.get_object("trace_btn").connect("clicked", self._on_trace_path) builder.get_object("discover_btn").connect("clicked", self._on_discover_nearby) builder.get_object("rx_log_btn").connect("clicked", self._on_rx_log) builder.get_object("reboot_btn").connect("clicked", self._on_reboot) return actions_group def build_sidebar_actions(self) -> Gtk.Widget: """Build a fresh Actions group for placement in the sidebar.""" return self._build_actions_group() @property def sections(self) -> list[str]: return [name for name, _w in self._section_widgets] def scroll_to_section(self, section_name: str): for name, widget in self._section_widgets: if name == section_name: widget.grab_focus() # Scroll the widget into view GLib.idle_add(self._do_scroll_to, widget) break def _do_scroll_to(self, widget): # Get widget position relative to the scrolled window success, x, y = widget.translate_coordinates(self._scroll.get_child(), 0, 0) if success: adj = self._scroll.get_vadjustment() adj.set_value(y) return False def _on_copy_public_key(self, *args): key = self._key_row.get_subtitle() if key and key != "--": clipboard = self._window.get_clipboard() clipboard.set(key) self._window.show_toast(_("Public key copied")) def _on_advert_to_clipboard(self, *args): """Export self contact and copy to clipboard.""" self._window.export_self_to_clipboard() def _on_show_qr_code(self, *args): """Show a QR code dialog with structured meshcore:// URL.""" info = self._window.device_info if not info.public_key: self._window.show_toast(_("Device public key not available")) return self._show_qr_dialog(None) def _show_qr_dialog(self, _unused=None): """Display a QR code dialog for sharing this device as a contact.""" import io try: import segno except ImportError: self._window.show_toast(_("QR code library not available")) return dialog = Adw.Dialog() dialog.set_title(_("Your Contact QR Code")) dialog.set_content_width(380) dialog.set_content_height(440) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=24, margin_end=24, margin_top=12, margin_bottom=24, ) # Device name info = self._window.device_info name = info.name or "My Device" name_label = Gtk.Label(label=name) name_label.add_css_class("title-2") content.append(name_label) # Build structured URL (accepted by official client) from urllib.parse import quote structured_uri = ( f"meshcore://contact/add?name={quote(name)}" f"&public_key={info.public_key}&type=1" ) qr = segno.make(structured_uri) png_buffer = io.BytesIO() qr.save(png_buffer, kind="png", scale=8, border=2) texture = Gdk.Texture.new_from_bytes(GLib.Bytes.new(png_buffer.getvalue())) picture = Gtk.Picture.new_for_paintable(texture) picture.set_content_fit(Gtk.ContentFit.CONTAIN) picture.set_size_request(280, 280) content.append(picture) # Instructions hint = Gtk.Label(label=_("Scan this QR code to add this contact")) hint.add_css_class("dim-label") content.append(hint) toolbar_view.set_content(content) dialog.set_child(toolbar_view) dialog.present(self._window) def _on_factory_reset(self, *args): dialog = Adw.AlertDialog( heading=_("Factory Reset?"), body=_( "This will permanently erase ALL data on the companion " "device, including contacts, messages, channels, settings, " "and the device identity (keys). " "This action cannot be undone." ), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("reset", _("Factory Reset")) dialog.set_response_appearance("reset", Adw.ResponseAppearance.DESTRUCTIVE) def _on_response(d, response): if response == "reset": self._window.factory_reset_device() dialog.connect("response", _on_response) dialog.present(self._window) def _on_reboot(self, *args): dialog = Adw.AlertDialog( heading=_("Reboot Device?"), body=_("The device will disconnect and restart."), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("reboot", _("Reboot")) dialog.set_response_appearance("reboot", Adw.ResponseAppearance.DESTRUCTIVE) def _on_response(d, response): if response == "reboot": self._window.reboot_device() dialog.connect("response", _on_response) dialog.present(self._window) def update_device_info(self, info: DeviceInfo): self._name_row.set_subtitle(info.name or "--") self._board_row.set_subtitle(info.model or "--") self._fw_row.set_subtitle( f"{info.ver} ({info.fw_build})" if info.ver and info.fw_build else info.ver or "--" ) if info.ver: self._check_firmware_update(info.ver) if info.public_key: self._key_row.set_subtitle(info.public_key) # Battery if info.battery_mv > 0: pct = info.battery_percent self._batt_row.set_subtitle(f"{pct}% ({info.battery_mv} mV)") self._batt_bar.set_value(pct) # Storage if info.storage_total_kb > 0: spct = info.storage_percent self._storage_row.set_subtitle( f"{spct}% ({info.storage_used_kb} / {info.storage_total_kb} KB)" ) self._storage_bar.set_value(spct) # Radio - radio_freq is in MHz, radio_bw is in kHz if info.radio_freq: self._freq_row.set_subtitle(f"{info.radio_freq:.3f} MHz") if info.radio_bw: self._bw_row.set_subtitle(f"{info.radio_bw:.1f} kHz") if info.radio_sf: self._sf_row.set_subtitle(f"SF{info.radio_sf}") if info.radio_cr: self._cr_row.set_subtitle(f"4/{info.radio_cr}") if info.tx_power: self._tx_row.set_subtitle(f"{info.tx_power} dBm") hash_labels = { 0: _("1-byte (max 64 hops)"), 1: _("2-byte (max 32 hops)"), 2: _("3-byte (max 21 hops)"), } self._path_hash_row.set_subtitle(hash_labels.get(info.path_hash_mode, "--")) def update_location(self, lat: float, lon: float): """Update the location row from the device's advertised position.""" if self._gps_enabled is None: # Custom vars not yet received — store for later self._location_pending_lat = lat self._location_pending_lon = lon return self._apply_location(lat, lon) def update_gps_enabled(self, enabled: bool): """Update GPS enabled state from custom_vars.""" self._gps_enabled = enabled # Apply pending location now that we know GPS state if self._location_pending_lat is not None: self._apply_location(self._location_pending_lat, self._location_pending_lon) self._location_pending_lat = None self._location_pending_lon = None else: self._update_location_title() def _apply_location(self, lat: float, lon: float): has_pos = abs(lat) > 1e-6 or abs(lon) > 1e-6 if has_pos: self._location_lat = lat self._location_lon = lon self._location_row.set_subtitle(self._format_location(lat, lon)) else: self._location_lat = None self._location_lon = None self._location_row.set_subtitle(_("Not set")) self._location_map_btn.set_visible(has_pos) self._update_location_title() def _update_location_title(self): has_pos = self._location_lat is not None if self._gps_enabled is None: # No GPS hardware on companion self._location_row.set_title(_("Location (manually set)") if has_pos else _("Location")) elif self._gps_enabled: self._location_row.set_title( _("Location (GPS)") if has_pos else _("Location (GPS on, no fix)") ) else: # GPS off but has position — set manually or via system location self._location_row.set_title( _("Location (manually set)") if has_pos else _("Location (GPS off)") ) @staticmethod def _format_location(lat: float, lon: float) -> str: lat_dir = "N" if lat >= 0 else "S" lon_dir = "E" if lon >= 0 else "W" return f"{abs(lat):.6f}° {lat_dir}, {abs(lon):.6f}° {lon_dir}" def _on_request_telemetry(self, btn): """Request telemetry from the connected companion device.""" from meshy.protocol import ( LPP_ALTITUDE, LPP_GPS, LPP_TEMPERATURE, LPP_VOLTAGE, parse_cayenne_lpp, ) pub_key_hex = self._window.device_info.public_key if not pub_key_hex: self._telemetry_row.set_subtitle(_("Not connected")) return btn.set_sensitive(False) self._telemetry_row.set_subtitle(_("Requesting…")) timeout_id = [None] def on_timeout(): btn.set_sensitive(True) self._telemetry_row.set_subtitle(_("No response (timed out)")) timeout_id[0] = None return False def on_response(lpp_data): btn.set_sensitive(True) if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None if not lpp_data: self._telemetry_row.set_subtitle(_("No telemetry data")) return entries = parse_cayenne_lpp(lpp_data) if not entries: self._telemetry_row.set_subtitle(_("No telemetry data")) return # Build summary for the subtitle parts = [] for e in entries: if e["type"] == LPP_TEMPERATURE: parts.append(f"{e['value']:.1f} °C") elif e["type"] == LPP_ALTITUDE: parts.append(f"{e['value']} m alt") elif e["type"] == LPP_VOLTAGE: parts.append(f"{e['value']:.2f} V") elif e["type"] == LPP_GPS: val = e["value"] parts.append(f"{val['alt']:.0f} m alt") self._telemetry_row.set_subtitle(", ".join(parts) if parts else _("Telemetry received")) timeout_id[0] = GLib.timeout_add(15000, on_timeout) self._window.get_self_telemetry(on_response) def _on_show_location_map(self, btn): if self._location_lat is None or self._location_lon is None: return lat, lon = self._location_lat, self._location_lon name = self._window.device_info.name or "My Device" dialog = Adw.Dialog() dialog.set_title(_("Device Location")) dialog.set_content_width(400) dialog.set_content_height(450) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=12, margin_end=12, margin_bottom=12, ) # Coordinates label coords = Gtk.Label(label=self._format_location(lat, lon)) coords.add_css_class("caption") content.append(coords) # Map from meshy.views import create_shumate_map, deregister_map_widget from meshy.views.map_view import _SELF_COLOR, MapView map_widget, marker_layer, viewport = create_shumate_map(zoom_level=14, lat=lat, lon=lon) map_widget.set_size_request(-1, 350) marker = MapView.create_marker(*_SELF_COLOR, "avatar-default-symbolic", name, lat, lon) marker_layer.add_marker(marker) map_frame = Gtk.Frame() map_frame.set_child(map_widget) content.append(map_frame) toolbar_view.set_content(content) dialog.set_child(toolbar_view) dialog.connect("closed", lambda _d: deregister_map_widget(map_widget)) dialog.present(self._window) def update_stats(self, stats: dict): """Update statistics display from CMD_GET_STATS responses.""" from meshy.protocol import STATS_TYPE_CORE, STATS_TYPE_PACKETS, STATS_TYPE_RADIO core = stats.get(STATS_TYPE_CORE, {}) if core: secs = core.get("uptime_secs", 0) days, rem = divmod(secs, 86400) hours, rem = divmod(rem, 3600) mins, _secs = divmod(rem, 60) parts = [] if days: parts.append(_("{days}d").format(days=days)) if hours: parts.append(_("{hours}h").format(hours=hours)) parts.append(_("{mins}m").format(mins=mins)) self._uptime_row.set_subtitle(" ".join(parts)) self._queue_row.set_subtitle(_("{} messages").format(core.get("queue_len", 0))) radio = stats.get(STATS_TYPE_RADIO, {}) if radio: self._noise_row.set_subtitle(f'{radio.get("noise_floor", 0)} dBm') self._rssi_row.set_subtitle(f'{radio.get("last_rssi", 0)} dBm') self._snr_row.set_subtitle(f'{radio.get("last_snr", 0):.1f} dB') tx_s = radio.get("tx_air_secs", 0) rx_s = radio.get("rx_air_secs", 0) self._airtime_row.set_subtitle(f"TX: {tx_s}s / RX: {rx_s}s") pkts = stats.get(STATS_TYPE_PACKETS, {}) if pkts: recv = pkts.get("recv", 0) sent = pkts.get("sent", 0) errs = pkts.get("recv_errors") sub = f"RX: {recv} / TX: {sent}" if errs is not None: sub += " / " + _("{count} errors").format(count=errs) self._pkt_row.set_subtitle(sub) self._pkt_flood_row.set_subtitle( f'TX: {pkts.get("flood_tx", 0)} / RX: {pkts.get("flood_rx", 0)}' ) self._pkt_direct_row.set_subtitle( f'TX: {pkts.get("direct_tx", 0)} / RX: {pkts.get("direct_rx", 0)}' ) def update_connection_status(self, connected: bool, transport: str = ""): """Update the connection status icon and text.""" if connected: transport_labels = {"ble": "Bluetooth", "usb": "USB", "tcp": "TCP"} label = transport_labels.get(transport, "") if label: self._status_row.set_subtitle("{} \u00b7 {}".format(label, _("Connected"))) else: self._status_row.set_subtitle(_("Connected")) self._status_icon.set_from_icon_name("charge-symbolic") else: self._status_row.set_subtitle(_("Disconnected")) self._status_icon.set_from_icon_name("network-offline-symbolic") def _on_trace_path(self, *args): """Open the Trace Path dialog.""" from meshy.models import ContactType # Trace protocol only supports 1B and 2B hashes (power-of-2 encoding) if self._window.device_info.path_hash_mode > 1: self._window.show_toast(_("Trace Path is not supported with 3-byte path hashes")) return dialog = Adw.Dialog() dialog.set_title(_("Trace Path")) dialog.set_content_width(460) dialog.set_content_height(500) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=8, margin_end=8, margin_bottom=8, ) # Path hash size: mode 0→1 byte, mode 1→2 bytes, mode 2→3 bytes hash_width = self._window.device_info.path_hash_mode + 1 examples = {1: _("e.g. aa,bb,cc"), 2: _("e.g. aabb,ccdd"), 3: _("e.g. aabbcc,ddeeff")} # Own node hash for bookending the trace self_pub_key = ( bytes.fromhex(self._window.device_info.public_key) if self._window.device_info.public_key else b"" ) self_hash = self_pub_key[:hash_width] if len(self_pub_key) >= hash_width else b"" self_name = self._window.device_info.name or "This device" # Add from Contacts button (above path entry) add_contact_btn = Gtk.MenuButton(label=_("Add from Contacts")) add_contact_btn.add_css_class("flat") add_contact_btn.set_halign(Gtk.Align.START) popover = Gtk.Popover() popover.set_size_request(300, -1) pop_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) pop_box.set_margin_start(8) pop_box.set_margin_end(8) pop_box.set_margin_top(8) pop_box.set_margin_bottom(8) search_entry = Gtk.SearchEntry(placeholder_text="Search repeaters...") pop_box.append(search_entry) repeater_list = Gtk.ListBox() repeater_list.set_selection_mode(Gtk.SelectionMode.NONE) repeater_list.add_css_class("boxed-list") repeaters = [ c for c in self._window.contacts if c.type in (ContactType.REPEATER, ContactType.ROOM) ] repeater_rows = [] for contact in repeaters: hop_hex = contact.public_key[:hash_width].hex() row = Adw.ActionRow(title=GLib.markup_escape_text(contact.name), subtitle=hop_hex) row.set_activatable(True) row._hop_hex = hop_hex row._search_text = contact.name.lower() repeater_list.append(row) repeater_rows.append(row) if not repeaters: empty = Gtk.Label(label=_("No repeaters or rooms in contacts")) empty.add_css_class("dim-label") empty.set_margin_top(8) empty.set_margin_bottom(8) pop_box.append(empty) # Pick on Map button pick_map_btn = Gtk.Button(label=_("Pick on Map")) pick_map_btn.add_css_class("flat") has_located = any(c.has_location for c in repeaters) pick_map_btn.set_sensitive(has_located) if not has_located: pick_map_btn.set_tooltip_text(_("No repeaters with known location")) # Path input path_entry = Adw.EntryRow(title=_("Path")) path_entry.set_text("") btn_box = Gtk.Box(spacing=8) btn_box.append(add_contact_btn) pick_map_btn.set_hexpand(True) pick_map_btn.set_halign(Gtk.Align.END) btn_box.append(pick_map_btn) content.append(btn_box) content.append(path_entry) placeholder_label = Gtk.Label(label=examples.get(hash_width, examples[1])) placeholder_label.add_css_class("dim-label") placeholder_label.add_css_class("caption") placeholder_label.set_halign(Gtk.Align.START) placeholder_label.set_margin_start(12) content.append(placeholder_label) def on_search_changed(entry): query = entry.get_text().strip().lower() for row in repeater_rows: row.set_visible(query in row._search_text or query in row._hop_hex) search_entry.connect("search-changed", on_search_changed) def on_row_activated(lb, row): current = path_entry.get_text().strip() if current: path_entry.set_text(f"{current},{row._hop_hex}") else: path_entry.set_text(row._hop_hex) popover.popdown() repeater_list.connect("row-activated", on_row_activated) repeater_scroll = Gtk.ScrolledWindow() repeater_scroll.set_child(repeater_list) repeater_scroll.set_min_content_height(120) repeater_scroll.set_max_content_height(300) pop_box.append(repeater_scroll) popover.set_child(pop_box) add_contact_btn.set_popover(popover) def on_pick_map(*args): from meshy.views.map_view import open_trace_picker_map def _on_map_done(hex_string): current = path_entry.get_text().strip() if current: path_entry.set_text(f"{current},{hex_string}") else: path_entry.set_text(hex_string) open_trace_picker_map(self._window, self._window.contacts, hash_width, _on_map_done) pick_map_btn.connect("clicked", on_pick_map) # Run button run_btn = Gtk.Button(label=_("Run Trace")) run_btn.add_css_class("suggested-action") run_btn.set_halign(Gtk.Align.CENTER) run_btn.set_margin_top(4) content.append(run_btn) # Separator content.append(Gtk.Separator()) # Status status_box = Gtk.Box(spacing=8, halign=Gtk.Align.CENTER) spinner = Gtk.Spinner() status_box.append(spinner) status_label = Gtk.Label(label=_("Enter path hashes and press Run Trace")) status_label.add_css_class("dim-label") status_box.append(status_label) status_map_btn = Gtk.Button(icon_name="map-symbolic") status_map_btn.add_css_class("flat") status_map_btn.set_tooltip_text("Show on Map") status_map_btn.set_visible(False) status_box.append(status_map_btn) content.append(status_box) # Results list results_scroll = Gtk.ScrolledWindow(vexpand=True) results_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) results_scroll.set_child(results_box) content.append(results_scroll) toolbar_view.set_content(content) dialog.set_child(toolbar_view) timeout_id = [None] trace_start = [0.0] def clear_results(): child = results_box.get_first_child() while child: next_c = child.get_next_sibling() results_box.remove(child) child = next_c def add_node_row(label): """Add a node row to results.""" row = Gtk.Label(label=label, xalign=0) row.set_margin_start(16) row.set_margin_top(6) row.set_margin_bottom(2) results_box.append(row) def add_snr_row(snr): """Add an SNR indicator row between nodes.""" snr_text = f"\u2502 {snr:+.1f} dB" label = Gtk.Label(label=snr_text, xalign=0) label.set_margin_start(20) label.set_margin_top(2) label.set_margin_bottom(2) label.add_css_class("caption") if snr >= 0: label.add_css_class("success") elif snr >= -5: label.add_css_class("dim-label") else: label.add_css_class("error") results_box.append(label) def show_results(result): """Display trace results in the results box.""" spinner.set_spinning(False) if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None hops = result["path_hops"] # list of bytes objects (per-hop hashes) snr_values = result["snr_values"] clear_results() import time duration = time.monotonic() - trace_start[0] status_label.set_label( _("Trace complete: {} hops, {:.1f}s").format(len(hops), duration) ) hop_hexes = [h.hex() for h in hops] hop_contacts = self._window.resolve_hop_prefixes(hop_hexes) # Start with companion node add_node_row(f"\u25cf {self_name} ({self_hash.hex()})") for i, hop_bytes in enumerate(hops): # SNR between previous node and this one if i < len(snr_values): add_snr_row(snr_values[i]) hop_hex = hop_bytes.hex() contact = hop_contacts[i] if contact: add_node_row(f"\u25cf {contact.name} ({hop_hex})") else: add_node_row(f"\u25cb {hop_hex}") # End with companion node (return path) if len(snr_values) > len(hops): add_snr_row(snr_values[len(hops)]) add_node_row(f"\u25cf {self_name} ({self_hash.hex()})") # Show on Map button in status bar if any hop has a known location has_any_location = any(c and c.has_location for c in hop_contacts) if has_any_location: status_map_btn.set_visible(True) # Reconnect handler for new results with contextlib.suppress(AttributeError, TypeError): status_map_btn.disconnect_by_func(status_map_btn._handler) def _on_map_clicked( *_, hc=hop_contacts, snr=snr_values, hw=hash_width, hp=hop_hexes ): self._show_trace_on_map(hc, snr, hw, hp) status_map_btn._handler = _on_map_clicked status_map_btn.connect("clicked", _on_map_clicked) else: status_map_btn.set_visible(False) run_btn.set_sensitive(True) def on_timeout(): spinner.set_spinning(False) status_label.set_label(_("Trace timed out")) self._window.stop_trace_path() run_btn.set_sensitive(True) timeout_id[0] = None return False def on_run(*args): text = path_entry.get_text().strip() if not text: return parts = [p.strip() for p in text.split(",") if p.strip()] try: path_bytes = b"" for p in parts: path_bytes += bytes.fromhex(p) except ValueError: status_label.set_label(_("Invalid hex in path")) return trace_bytes = path_bytes clear_results() spinner.set_spinning(True) status_label.set_label(_("Tracing...")) status_map_btn.set_visible(False) run_btn.set_sensitive(False) import time trace_start[0] = time.monotonic() def on_device_timeout(timeout_ms): """Device told us how long to wait.""" if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = GLib.timeout_add(timeout_ms, on_timeout) # Start with a fallback timeout; device may override with its own timeout_id[0] = GLib.timeout_add(30000, on_timeout) # Flag encodes hash size: mode 0→flag 0 (1B), mode 1→flag 1 (2B) trace_flags = self._window.device_info.path_hash_mode self._window.send_trace_path( trace_bytes, show_results, on_device_timeout, flags=trace_flags ) run_btn.connect("clicked", on_run) def on_dialog_closed(d): self._window.stop_trace_path() if timeout_id[0]: GLib.source_remove(timeout_id[0]) dialog.connect("closed", on_dialog_closed) dialog.present(self._window) def _show_trace_on_map(self, hop_contacts, snr_values, hash_width, hop_prefixes=None): """Show trace path results in a map dialog.""" from meshy.views.map_view import ( _MARKER_COLORS, _MARKER_ICONS, _SELF_COLOR, _UNKNOWN_COLOR, MapView, ) device = self._window.device_info self_lat = getattr(device, "latitude", None) self_lon = getattr(device, "longitude", None) has_self = self_lat and self_lon and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) # Build full hop list: (lat, lon, contact, prefix, snr) raw_hops = [] if has_self: raw_hops.append((self_lat, self_lon, None, None, None)) for i, contact in enumerate(hop_contacts): snr = snr_values[i] if i < len(snr_values) else None prefix = hop_prefixes[i] if hop_prefixes and i < len(hop_prefixes) else None if contact and contact.has_location: raw_hops.append((contact.latitude, contact.longitude, contact, prefix, snr)) else: raw_hops.append((None, None, contact, prefix, snr)) hops = MapView.interpolate_unknown_hops(raw_hops) if not any(h[0] is not None for h in hops): return dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( self._window, title=_("Trace Path") ) map_widget.get_map().remove_layer(marker_layer) path_layer = MapView.create_dashed_path_layer(viewport) map_widget.get_map().add_layer(path_layer) map_widget.get_map().add_layer(marker_layer) lats, lons = [], [] hop_num = 0 for lat, lon, contact, prefix, snr in hops: if lat is None: continue node = Shumate.Marker() node.set_location(lat, lon) path_layer.add_node(node) if contact is None and prefix is None: r, g, b = _SELF_COLOR icon_name = "network-wireless-symbolic" name = device.name or _("My Device") type_label = None badge = "" elif contact is None or not contact.has_location: r, g, b = _UNKNOWN_COLOR icon_name = "network-server-symbolic" name = prefix or _("Unknown") type_label = _("Unknown repeater") hop_num += 1 badge = str(hop_num) else: r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") name = contact.name type_label = contact.type_label hop_num += 1 badge = str(hop_num) is_interpolated = (contact is None or not contact.has_location) and prefix is not None detail_cb = (lambda c=contact: self._window.show_contact_detail(c)) if contact else None widget = MapView._build_marker_widget(r, g, b, icon_name, "", badge_text=badge) widget.set_tooltip_text(name) click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, w=widget, nm=name, la=lat, lo=lon, s=snr, t=type_label, ip=is_interpolated, od=detail_cb: MapView.show_marker_popover( w, nm, la, lo, s, t, interpolated=ip, on_detail=od ), ) widget.add_controller(click) hop_marker = Shumate.Marker() hop_marker.set_child(widget) hop_marker.set_location(lat, lon) marker_layer.add_marker(hop_marker) lats.append(lat) lons.append(lon) # SNR labels at midpoints for i in range(1, len(hops)): if hops[i][0] is None or hops[i - 1][0] is None: continue snr = hops[i][4] if snr is None: continue mid_lat = (hops[i - 1][0] + hops[i][0]) / 2 mid_lon = (hops[i - 1][1] + hops[i][1]) / 2 snr_widget = MapView.build_snr_widget(snr) snr_marker = Shumate.Marker() snr_marker.set_child(snr_widget) snr_marker.set_location(mid_lat, mid_lon) marker_layer.add_marker(snr_marker) MapView.fit_viewport(viewport, lats, lons) dialog.present(self._window) def _on_discover_nearby(self, *args): """Send a NODE_DISCOVER_REQ and show responding nodes live.""" from meshy.models import ContactType dialog = Adw.Dialog() dialog.set_title(_("Discover Nearby Nodes")) dialog.set_content_width(420) dialog.set_content_height(480) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=8, margin_end=8, margin_bottom=8, ) # Status with spinner status_box = Gtk.Box(spacing=8, halign=Gtk.Align.CENTER) spinner = Gtk.Spinner(spinning=True) status_box.append(spinner) status_label = Gtk.Label(label=_("Scanning... {}s remaining").format(30)) status_label.add_css_class("dim-label") status_box.append(status_label) content.append(status_box) # List of responding nodes scroll = Gtk.ScrolledWindow(vexpand=True) list_wrapper = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) list_box = Gtk.ListBox() list_box.set_selection_mode(Gtk.SelectionMode.NONE) list_box.add_css_class("boxed-list") placeholder = Adw.StatusPage( icon_name="edit-find-symbolic", title=_("Listening..."), description=_("Waiting for nearby nodes to respond"), ) list_box.set_placeholder(placeholder) list_wrapper.append(list_box) scroll.set_child(list_wrapper) content.append(scroll) toolbar_view.set_content(content) dialog.set_child(toolbar_view) seen = set() seconds_left = [30] timer_id = [None] type_icons = { ContactType.CHAT: "avatar-default-symbolic", ContactType.REPEATER: "network-server-symbolic", ContactType.ROOM: "user-home-symbolic", ContactType.SENSOR: "weather-clear-symbolic", } type_labels = { ContactType.CHAT: _("Chat"), ContactType.REPEATER: _("Repeater"), ContactType.ROOM: _("Room Server"), ContactType.SENSOR: _("Sensor"), } # Track rows by key prefix so adverts can update discover rows rows_by_prefix = {} # key_prefix → row # Companion location for distance calculation dev = self._window.device_info self_lat = dev.latitude if dev else None self_lon = dev.longitude if dev else None has_self_loc = ( self_lat is not None and self_lon is not None and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) ) log.debug("Discover: companion loc=%s/%s has_self_loc=%s", self_lat, self_lon, has_self_loc) def _distance_str(contact): if not has_self_loc or not contact or not contact.has_location: return "" from meshy.utils import haversine_distance dist = haversine_distance(self_lat, self_lon, contact.latitude, contact.longitude) if dist < 1.0: return f"{dist * 1000:.0f} m" return f"{dist:.1f} km" def _add_node_button(row): """Create an Add to Contacts button for unknown nodes.""" btn = Gtk.Button(icon_name="list-add-symbolic", valign=Gtk.Align.CENTER) btn.set_tooltip_text(_("Add to Contacts")) def on_add(_btn): info = row._node_info self._window.add_contact(info["pub_key"], info["type"], info["name"]) _btn.set_sensitive(False) _btn.set_icon_name("object-select-symbolic") self._window.show_toast(_("Added {}").format(info["name"])) btn.connect("clicked", on_add) row.add_suffix(btn) def on_discover_response(result): """Handle a NODE_DISCOVER_RESP.""" pub_key_hex = result.get("pub_key_hex", "") if not pub_key_hex: return if pub_key_hex in seen: return seen.add(pub_key_hex) snr = result.get("snr", 0) node_type = result.get("node_type", 1) snr_str = f"SNR: {snr:.1f} dB" contact = self._window.find_contact_by_key_prefix(pub_key_hex) is_known = contact is not None and self._window.is_contact_known(contact.public_key_hex) if contact: name = contact.name label = f"{contact.type_label} \u00b7 {snr_str}" icon = type_icons.get(contact.type, "avatar-default-symbolic") log.debug( "Discover resp: %s found has_loc=%s", pub_key_hex[:16], contact.has_location ) else: name = f"0x{pub_key_hex[:16].upper()}" label = "{} \u00b7 {}".format(type_labels.get(node_type, _("Node")), snr_str) icon = type_icons.get(node_type, "network-server-symbolic") log.debug("Discover resp: %s not found", pub_key_hex[:16]) dist = _distance_str(contact) if dist: label += f" \u00b7 {dist}" row = Adw.ActionRow(title=name, subtitle=label) row.add_prefix(Gtk.Image.new_from_icon_name(icon)) is_discovered = contact is not None and not is_known if is_discovered: row._node_info = {"pub_key": contact.public_key, "type": contact.type, "name": name} _add_node_button(row) rows_by_prefix[pub_key_hex] = row list_box.append(row) def on_advert(contact): """Also listen for adverts triggered by the discover.""" # Check if we already have a row for this node from discover response existing_row = None for prefix, row in rows_by_prefix.items(): if contact.public_key_hex.startswith(prefix) or prefix.startswith( contact.public_key_hex[:16] ): existing_row = row break if existing_row: # Update existing row with advert data (name, distance) adv_name = contact.name or existing_row.get_title() existing_row.set_title(adv_name) dist = _distance_str(contact) if dist: cur_sub = existing_row.get_subtitle() or "" if "km" not in cur_sub and " m" not in cur_sub: existing_row.set_subtitle(f"{cur_sub} · {dist}") is_known = self._window.is_contact_known(contact.public_key_hex) if hasattr(existing_row, "_node_info"): existing_row._node_info["name"] = adv_name existing_row._node_info["pub_key"] = contact.public_key existing_row._node_info["type"] = contact.type elif not is_known: existing_row._node_info = { "pub_key": contact.public_key, "type": contact.type, "name": adv_name, } _add_node_button(existing_row) seen.add(contact.public_key_hex) return if contact.public_key_hex in seen: return seen.add(contact.public_key_hex) icon = type_icons.get(contact.type, "avatar-default-symbolic") is_known = self._window.is_contact_known(contact.public_key_hex) log.debug( "Discover advert: %s has_loc=%s", contact.public_key_hex[:16], contact.has_location ) subtitle = contact.type_label dist = _distance_str(contact) if dist: subtitle += f" · {dist}" row = Adw.ActionRow( title=contact.name or _("Unknown"), subtitle=subtitle, ) row.add_prefix(Gtk.Image.new_from_icon_name(icon)) if not is_known: row._node_info = { "pub_key": contact.public_key, "type": contact.type, "name": contact.name or _("Unknown"), } _add_node_button(row) rows_by_prefix[contact.public_key_hex] = row list_box.append(row) def on_tick(): seconds_left[0] -= 1 if seconds_left[0] <= 0: stop_scanning() return False status_label.set_label(_("Scanning... {}s remaining").format(seconds_left[0])) return True def stop_scanning(): self._window.stop_discover() self._window.set_advert_listener(None) spinner.set_spinning(False) count = len(seen) status_label.set_label( _("Done. {} nodes found.").format(count) if count != 1 else _("Done. {} node found.").format(count) ) if timer_id[0]: GLib.source_remove(timer_id[0]) timer_id[0] = None def on_dialog_closed(d): stop_scanning() dialog.connect("closed", on_dialog_closed) # Start scanning - send both discover req and zero-hop advert self._window.set_advert_listener(on_advert) self._window.send_discover_req(on_discover_response) # Also send a zero-hop advert - this is the proven method # that triggers nearby nodes to respond with their adverts GLib.timeout_add(200, lambda: self._window.send_advert(flood=False, silent=True) or False) timer_id[0] = GLib.timeout_add_seconds(1, on_tick) dialog.present(self._window) def _on_rx_log(self, *args): """Open the Rx Log dialog showing received radio packets.""" builder = Gtk.Builder.new_from_resource("/page/codeberg/sesivany/Meshy/ui/rx-log-dialog.ui") dialog = builder.get_object("rx_log_dialog") list_box = builder.get_object("rx_log_list") clear_btn = builder.get_object("clear_btn") placeholder = builder.get_object("rx_log_placeholder") list_box.set_placeholder(placeholder) sf = self._window.device_info.radio_sf or 12 def _make_row(entry): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) box.set_margin_start(12) box.set_margin_end(12) box.set_margin_top(8) box.set_margin_bottom(8) header_box = Gtk.Box(spacing=8) title = Gtk.Label( label=f"{entry.route_type_name} {entry.payload_type_name}", xalign=0, hexpand=True ) title.add_css_class("heading") header_box.append(title) snr_icon = Gtk.Image.new_from_icon_name(snr_to_icon(entry.snr, sf)) header_box.append(snr_icon) snr_label = Gtk.Label(label=f"{entry.snr:.2f}dB", xalign=1) snr_label.add_css_class("dim-label") header_box.append(snr_label) box.append(header_box) def _detail(text, wrap=False): lbl = Gtk.Label(label=text, xalign=0) lbl.add_css_class("caption") lbl.add_css_class("dim-label") if wrap: lbl.set_wrap(True) lbl.set_wrap_mode(Pango.WrapMode.WORD_CHAR) box.append(lbl) _detail( f'{entry.timestamp.strftime("%H:%M:%S")} \u00b7 ' f'{_("Size")}: {entry.size} {_("bytes")}' ) if entry.pkt_hash: _detail(f"Hash: {entry.pkt_hash.upper()}") path_csv = ",".join(entry.path) if entry.path else "" _detail(f'{_("Path")}: {len(entry.path)} {_("hops")} ' f'[{path_csv}]', wrap=True) _detail(f'{_("Path Hashes")}: {entry.path_hash_size}-{_("byte per hop")}') if entry.transport_from and entry.transport_to: _detail(f"From: <{entry.transport_from}> " f"To: <{entry.transport_to}>") return box for entry in reversed(self._window.rx_log): list_box.append(_make_row(entry)) def on_new_entry(entry): list_box.prepend(_make_row(entry)) self._window.set_rx_log_callback(on_new_entry) def on_clear(*_): self._window.rx_log.clear() child = list_box.get_first_child() while child: next_c = child.get_next_sibling() list_box.remove(child) child = next_c clear_btn.connect("clicked", on_clear) def on_closed(d): self._window.set_rx_log_callback(None) dialog.connect("closed", on_closed) dialog.present(self._window) # ─── Firmware update check ────────────────────────────── @staticmethod def _parse_version(ver_str): ver = ver_str.strip().lstrip("v") ver = ver.split("-")[0] try: parts = [int(x) for x in ver.split(".")] while len(parts) < 3: parts.append(0) return tuple(parts) except (ValueError, AttributeError): return (0, 0, 0) def _check_firmware_update(self, current_ver): if self._latest_fw_version is not None: self._show_update_if_needed(current_ver, self._latest_fw_version) return def fetch(): try: req = urllib.request.Request(_RELEASES_URL, headers={"User-Agent": "Meshy"}) with urllib.request.urlopen(req, timeout=10) as resp: releases = json.loads(resp.read()) for entry in releases: if entry.get("type") == "companion": latest = entry["version"] GLib.idle_add(self._on_releases_fetched, current_ver, latest) return except Exception as e: log.debug("Firmware update check failed: %s", e) threading.Thread(target=fetch, daemon=True).start() def _on_releases_fetched(self, current_ver, latest_ver): self._latest_fw_version = latest_ver self._show_update_if_needed(current_ver, latest_ver) return False def _show_update_if_needed(self, current_ver, latest_ver): current = self._parse_version(current_ver) latest = self._parse_version(latest_ver) if latest > current: self._fw_update_btn.set_visible(True) self._fw_update_btn.set_tooltip_text( _("Update available: {version}").format(version=latest_ver) ) else: self._fw_update_btn.set_visible(False) def _on_fw_update_clicked(self, btn): launcher = Gtk.UriLauncher.new(_FLASHER_URL) launcher.launch(self._window, None, None) meshy/src/views/map_view.py000066400000000000000000001336041521052255700163230ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Map view - shows contacts with known locations on an OpenStreetMap.""" import math import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Shumate", "1.0") from gi.repository import Adw, Gdk, GLib, Gtk, Pango, Shumate from meshy.models import Contact, ContactType # Marker colors by contact type (R, G, B) _MARKER_COLORS = { ContactType.CHAT: (0.26, 0.52, 0.96), # Blue ContactType.REPEATER: (0.30, 0.69, 0.31), # Green ContactType.ROOM: (0.61, 0.32, 0.88), # Purple ContactType.SENSOR: (1.0, 0.60, 0.0), # Orange } _MARKER_ICONS = { ContactType.CHAT: "avatar-default-symbolic", ContactType.REPEATER: "network-server-symbolic", ContactType.ROOM: "user-home-symbolic", ContactType.SENSOR: "weather-clear-symbolic", } # Self marker color (Red) - MeshCore companion GPS _SELF_COLOR = (0.90, 0.16, 0.22) # Unknown/interpolated hop color (grey) _UNKNOWN_COLOR = (0.55, 0.55, 0.55) # Cluster color (dark grey) _CLUSTER_COLOR = (0.35, 0.35, 0.40) # Minimum pixel distance between markers before clustering _CLUSTER_RADIUS_PX = 60 _MAX_LATITUDE = 85.0511287798 def _lat_lon_to_pixels(lat, lon, zoom): """Convert lat/lon to pixel coordinates at a given zoom level.""" lat = max(-_MAX_LATITUDE, min(_MAX_LATITUDE, lat)) n = 2.0**zoom x = (lon + 180.0) / 360.0 * n * 256 lat_rad = math.radians(lat) y = (1.0 - math.log(math.tan(lat_rad) + 1.0 / math.cos(lat_rad)) / math.pi) / 2.0 * n * 256 return x, y class MapView(Gtk.Box): """Full map widget showing contacts with known locations.""" def __init__(self, window): super().__init__(orientation=Gtk.Orientation.VERTICAL) self._window = window self._located_contacts: list[Contact] = [] self._self_lat = None self._self_lon = None self._last_zoom = -1 self._zoom_handler_id = None self._recluster_timeout_id = None self._expanded_clusters: list[list[Contact]] = [] # clusters forced open self._all_contacts: list[Contact] = [] # all contacts with location (unfiltered) self._discovered_contacts: list[Contact] = [] self._show_discovered: bool = False self._trace_path_layer: Shumate.PathLayer | None = None self._trace_marker_layer: Shumate.MarkerLayer | None = None self._trace_anim_id: int | None = None self._visible_types: set[int] = { ContactType.CHAT, ContactType.REPEATER, ContactType.ROOM, ContactType.SENSOR, } self._build_ui() def _build_ui(self): from meshy.views import create_shumate_map self._map, self._marker_layer, viewport = create_shumate_map(zoom_level=3) self._map.set_hexpand(True) # Listen for zoom changes to recluster self._zoom_handler_id = viewport.connect("notify::zoom-level", self._on_zoom_changed) self.append(self._map) # Legend overlay at bottom legend = Gtk.Box(spacing=12, halign=Gtk.Align.CENTER, margin_bottom=8, margin_top=4) legend.add_css_class("dim-label") legend.add_css_class("caption") for ct, label in [ (ContactType.CHAT, _("User")), (ContactType.REPEATER, _("Repeater")), (ContactType.ROOM, _("Room")), (ContactType.SENSOR, _("Sensor")), ]: item = Gtk.Box(spacing=4) dot = Gtk.DrawingArea() dot.set_content_width(10) dot.set_content_height(10) r, g, b = _MARKER_COLORS[ct] dot.set_draw_func( lambda area, cr, w, h, _r=r, _g=g, _b=b: ( cr.set_source_rgb(_r, _g, _b), cr.arc(w / 2, h / 2, min(w, h) / 2, 0, 6.2832), cr.fill(), ) ) item.append(dot) item.append(Gtk.Label(label=label)) legend.append(item) self.append(legend) def update_contacts(self, contacts: list[Contact]): """Refresh markers from the current contact list.""" self._all_contacts = [c for c in contacts if c.has_location] # Check self location device_info = self._window.device_info self._self_lat = getattr(device_info, "latitude", None) self._self_lon = getattr(device_info, "longitude", None) if self._self_lat and self._self_lon: if abs(self._self_lat) < 1e-6 and abs(self._self_lon) < 1e-6: self._self_lat = None self._self_lon = None self._apply_filters(fit_bounds=True) def update_discovered(self, contacts: list[Contact]): """Set the list of discovered contacts available for map display.""" for c in contacts: c.is_discovered = True self._discovered_contacts = [c for c in contacts if c.has_location] if self._show_discovered: self._apply_filters() def _apply_filters(self, fit_bounds=False): """Filter contacts by visible types and refresh the map.""" self._located_contacts = [c for c in self._all_contacts if c.type in self._visible_types] if self._show_discovered: contact_keys = {c.public_key_hex for c in self._located_contacts} self._located_contacts += [ c for c in self._discovered_contacts if c.type in self._visible_types and c.public_key_hex not in contact_keys ] self._expanded_clusters.clear() self.clear_trace_path() if fit_bounds and (self._located_contacts or (self._self_lat and self._self_lon)): self._fit_bounds() self._last_zoom = -1 # force recluster self._recluster() self._update_status_label() def _update_status_label(self): """Update the status label with contact counts.""" if hasattr(self, "_status_label"): visible = len(self._located_contacts) total = len(self._all_contacts) all_contacts = len(self._window.contacts) if self._window.contacts else 0 if self._show_discovered: disc_count = len( [c for c in self._discovered_contacts if c.type in self._visible_types] ) visible_contacts = visible - disc_count else: visible_contacts = visible if visible_contacts == total: text = _("{} of {} contacts on map").format(visible_contacts, all_contacts) else: text = _("{} of {} located on map").format(visible_contacts, total) if self._show_discovered: text += f"\n+ {disc_count} discovered" self._status_label.set_label(text) def build_sidebar_filters(self) -> Gtk.Widget: """Build the sidebar filter controls and contact count.""" box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=12, margin_end=12, margin_top=8, ) # Filter group filter_group = Adw.PreferencesGroup() filter_items = [ (ContactType.CHAT, _("Users"), "avatar-default-symbolic"), (ContactType.REPEATER, _("Repeaters"), "network-server-symbolic"), (ContactType.ROOM, _("Rooms"), "user-home-symbolic"), (ContactType.SENSOR, _("Sensors"), "weather-clear-symbolic"), ] for ct, label, icon_name in filter_items: row = Adw.SwitchRow(title=label) row.add_prefix(Gtk.Image.new_from_icon_name(icon_name)) row.set_active(ct in self._visible_types) def _on_toggled(switch_row, pspec, contact_type=ct): if switch_row.get_active(): self._visible_types.add(contact_type) else: self._visible_types.discard(contact_type) self._apply_filters() row.connect("notify::active", _on_toggled) filter_group.add(row) box.append(filter_group) # Status label at bottom (same style as contacts view) self._status_label = Gtk.Label(label="") self._status_label.add_css_class("dim-label") self._status_label.add_css_class("caption") self._status_label.set_margin_top(4) self._status_label.set_margin_bottom(2) # Discovered nodes toggle disc_group = Adw.PreferencesGroup() disc_row = Adw.SwitchRow(title=_("Discovered Nodes")) disc_row.add_prefix(Gtk.Image.new_from_icon_name("edit-find-symbolic")) disc_row.set_active(self._show_discovered) def _on_disc_toggled(switch_row, pspec): self._show_discovered = switch_row.get_active() self._apply_filters() disc_row.connect("notify::active", _on_disc_toggled) disc_group.add(disc_row) box.append(disc_group) self._update_status_label() box.append(self._status_label) return box def _on_zoom_changed(self, viewport, pspec): """Debounce zoom changes to avoid reclustering on every frame.""" self._expanded_clusters.clear() if self._recluster_timeout_id: GLib.source_remove(self._recluster_timeout_id) self._recluster_timeout_id = GLib.timeout_add(150, self._recluster) def _recluster(self): """Recompute clusters at the current zoom level and rebuild markers.""" self._recluster_timeout_id = None viewport = self._map.get_viewport() zoom = viewport.get_zoom_level() _LABEL_ZOOM = 10 labels_changed = (self._last_zoom < _LABEL_ZOOM) != (zoom < _LABEL_ZOOM) if abs(zoom - self._last_zoom) < 0.3 and not self._expanded_clusters and not labels_changed: return False self._last_zoom = zoom self._marker_layer.remove_all() # Contacts that belong to expanded clusters get individual markers expanded_keys = set() for group in self._expanded_clusters: for c in group: expanded_keys.add(c.public_key_hex) # Cluster only the non-expanded contacts to_cluster = [c for c in self._located_contacts if c.public_key_hex not in expanded_keys] clusters = self._compute_clusters(to_cluster, zoom) for cluster in clusters: if len(cluster) == 1: marker = self._create_contact_marker(cluster[0]) else: marker = self._create_cluster_marker(cluster, self._zoom_or_expand_cluster) self._marker_layer.add_marker(marker) # Add expanded contacts as individual markers spread in a circle for group in self._expanded_clusters: avg_lat = sum(c.latitude for c in group) / len(group) sum(c.longitude for c in group) / len(group) for i, contact in enumerate(group): angle = 2 * math.pi * i / len(group) # Offset in degrees (~30m radius at equator, scales with zoom) offset = 0.0003 * (2 ** (15 - zoom)) offset = max(offset, 0.00005) # minimum offset dlat = offset * math.cos(angle) dlon = offset * math.sin(angle) / max(math.cos(math.radians(avg_lat)), 0.01) marker = self._create_contact_marker(contact) marker.set_location(contact.latitude + dlat, contact.longitude + dlon) self._marker_layer.add_marker(marker) # Add self marker (MeshCore device GPS) if self._self_lat and self._self_lon: self._marker_layer.add_marker(self._create_self_marker(self._self_lat, self._self_lon)) return False # don't repeat timeout @staticmethod def _compute_clusters(contacts: list[Contact], zoom: float) -> list[list[Contact]]: """Group contacts that are too close at the current zoom into clusters.""" if not contacts: return [] # Convert all contacts to pixel positions points = [] for c in contacts: px, py = _lat_lon_to_pixels(c.latitude, c.longitude, zoom) points.append((px, py, c)) clustered = [False] * len(points) clusters: list[list[Contact]] = [] radius_sq = _CLUSTER_RADIUS_PX**2 for i, (px, py, contact) in enumerate(points): if clustered[i]: continue group = [contact] clustered[i] = True # Find all unclustered points within radius for j in range(i + 1, len(points)): if clustered[j]: continue dx = points[j][0] - px dy = points[j][1] - py if dx * dx + dy * dy < radius_sq: group.append(points[j][2]) clustered[j] = True clusters.append(group) return clusters def _create_contact_marker(self, contact: Contact) -> Shumate.Marker: """Create a colored circle marker with icon for a contact.""" discovered = getattr(contact, "is_discovered", False) if discovered: r, g, b = _UNKNOWN_COLOR else: r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") zoom = self._map.get_viewport().get_zoom_level() widget = self._build_marker_widget( r, g, b, icon_name, contact.name, boxed_label=True, show_label=zoom >= 10 ) widget.set_tooltip_text( f"{contact.name}\n{contact.type_label}\n{contact.latitude:.5f}, {contact.longitude:.5f}" ) click = Gtk.GestureClick() if discovered: click.connect( "pressed", lambda g, n, x, y, w=widget, c=contact: MapView.show_marker_popover( w, c.name, c.latitude, c.longitude, contact_type=c.type_label ), ) else: click.connect("pressed", lambda g, n, x, y, c=contact: self._on_marker_clicked(c)) widget.add_controller(click) marker = Shumate.Marker() marker.set_child(widget) marker.set_location(contact.latitude, contact.longitude) return marker @staticmethod def _create_cluster_marker(contacts: list[Contact], on_cluster_click=None) -> Shumate.Marker: """Create a cluster marker showing the count of grouped contacts.""" avg_lat = sum(c.latitude for c in contacts) / len(contacts) avg_lon = sum(c.longitude for c in contacts) / len(contacts) count = len(contacts) size = min(48, 32 + count) r, g, b = _CLUSTER_COLOR box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0, halign=Gtk.Align.CENTER) circle = Gtk.DrawingArea() circle.set_content_width(size) circle.set_content_height(size) radius = size / 2 - 1 def draw_cluster(area, cr, w, h): cx, cy = w / 2, h / 2 cr.set_source_rgba(r, g, b, 0.3) cr.arc(cx, cy, radius, 0, 6.2832) cr.fill() cr.set_source_rgb(r, g, b) cr.arc(cx, cy, radius * 0.75, 0, 6.2832) cr.fill() cr.set_source_rgb(1, 1, 1) cr.set_line_width(2) cr.arc(cx, cy, radius * 0.75, 0, 6.2832) cr.stroke() circle.set_draw_func(draw_cluster) overlay = Gtk.Overlay() overlay.set_child(circle) count_label = Gtk.Label(label=str(count)) count_label.add_css_class("map-marker-icon") attrs = Pango.AttrList() attrs.insert(Pango.attr_weight_new(Pango.Weight.BOLD)) count_label.set_attributes(attrs) count_label.set_halign(Gtk.Align.CENTER) count_label.set_valign(Gtk.Align.CENTER) overlay.add_overlay(count_label) box.append(overlay) names = sorted(c.name for c in contacts) if len(names) > 8: tooltip = "\n".join(names[:8]) + "\n\u2026 " + _("and {} more").format(len(names) - 8) else: tooltip = "\n".join(names) box.set_tooltip_text(tooltip) if on_cluster_click: click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, cs=contacts, lat=avg_lat, lon=avg_lon: on_cluster_click( cs, lat, lon ), ) box.add_controller(click) marker = Shumate.Marker() marker.set_child(box) marker.set_location(avg_lat, avg_lon) return marker def _zoom_or_expand_cluster(self, contacts, lat, lon): """Zoom in to split a cluster, or expand it if zooming won't help.""" viewport = self._map.get_viewport() current_zoom = viewport.get_zoom_level() max_zoom = viewport.get_max_zoom_level() # Check if zooming in by 2 would split this cluster test_zoom = min(current_zoom + 2, max_zoom) test_clusters = self._compute_clusters(contacts, test_zoom) would_split = len(test_clusters) > 1 if would_split and current_zoom < max_zoom: # Zooming will help — zoom in viewport.set_zoom_level(test_zoom) viewport.set_location(lat, lon) else: # Zooming won't help — expand the cluster into individual markers self._expanded_clusters.append(contacts) self._last_zoom = -1 # force rebuild self._recluster() def _create_self_marker(self, lat: float, lon: float) -> Shumate.Marker: """Create the self/device location marker.""" r, g, b = _SELF_COLOR name = self._window.device_info.name or _("My Device") zoom = self._map.get_viewport().get_zoom_level() widget = self._build_marker_widget( r, g, b, "avatar-default-symbolic", name, boxed_label=True, show_label=zoom >= 10 ) widget.set_tooltip_text(f"{name}\n{lat:.5f}, {lon:.5f}") marker = Shumate.Marker() marker.set_child(widget) marker.set_location(lat, lon) return marker @staticmethod def _build_marker_widget( r, g, b, icon_name, label_text, badge_text="", boxed_label=False, show_label=True ): """Build a marker widget: colored circle with icon + name label below.""" box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1, halign=Gtk.Align.CENTER) circle = Gtk.DrawingArea() circle.set_content_width(32) circle.set_content_height(32) def draw_circle(area, cr, w, h): cr.set_source_rgb(r, g, b) cr.arc(w / 2, h / 2, 14, 0, 6.2832) cr.fill() cr.set_source_rgb(1, 1, 1) cr.set_line_width(2) cr.arc(w / 2, h / 2, 14, 0, 6.2832) cr.stroke() circle.set_draw_func(draw_circle) overlay = Gtk.Overlay() overlay.set_child(circle) icon = Gtk.Image.new_from_icon_name(icon_name) icon.set_pixel_size(16) icon.add_css_class("map-marker-icon") icon.set_halign(Gtk.Align.CENTER) icon.set_valign(Gtk.Align.CENTER) overlay.add_overlay(icon) if badge_text: badge = Gtk.Label(label=badge_text) badge.add_css_class("map-marker-badge") badge.set_halign(Gtk.Align.END) badge.set_valign(Gtk.Align.START) overlay.add_overlay(badge) box.append(overlay) if label_text and show_label: if boxed_label: name_widget = MapView.build_map_label_widget(label_text) name_widget.get_first_child().set_max_width_chars(12) name_widget.get_first_child().set_ellipsize(3) else: name_widget = Gtk.Label(label=label_text) name_widget.add_css_class("caption") name_widget.set_max_width_chars(12) name_widget.set_ellipsize(3) # PANGO_ELLIPSIZE_END box.append(name_widget) return box @staticmethod def create_marker( r, g, b, icon_name, label_text, lat, lon, boxed_label=True, show_label=True, badge_text="" ): """Create a positioned Shumate.Marker with standard styling.""" widget = MapView._build_marker_widget( r, g, b, icon_name, label_text, badge_text=badge_text, boxed_label=boxed_label, show_label=show_label, ) marker = Shumate.Marker() marker.set_child(widget) marker.set_location(lat, lon) return marker @staticmethod def create_dashed_path_layer(viewport): """Create a consistently styled dashed PathLayer for map overlays.""" layer = Shumate.PathLayer.new(viewport) layer.set_stroke_width(2) layer.set_dash([6, 4]) color = Gdk.RGBA() color.parse("rgba(100, 100, 100, 0.9)") layer.set_stroke_color(color) return layer @staticmethod def build_map_label_widget(text): """Build a label with opaque background for map overlays.""" label = Gtk.Label(label=text) label.add_css_class("caption") label.set_margin_start(6) label.set_margin_end(6) label.set_margin_top(2) label.set_margin_bottom(2) box = Gtk.Box() box.append(label) css = Gtk.CssProvider() css.load_from_string( "box { background: alpha(@window_bg_color, 0.85);" " border-radius: 6px; }" ) box.get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) return box @staticmethod def build_snr_widget(snr): """Build a styled SNR label with opaque background for map overlays.""" box = MapView.build_map_label_widget(f"{snr:+.1f} dB") label = box.get_first_child() if snr >= 0: label.add_css_class("success") elif snr >= -5: label.add_css_class("dim-label") else: label.add_css_class("error") return box @staticmethod def show_marker_popover( widget, name, lat, lon, snr=None, contact_type=None, interpolated=False, on_detail=None ): """Show a detail popover anchored to a map marker widget.""" popover = Gtk.Popover() box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=4, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) if on_detail is not None: name_btn = Gtk.Button(label=name) name_btn.add_css_class("flat") name_btn.add_css_class("heading") name_btn.set_halign(Gtk.Align.START) name_btn.connect("clicked", lambda *_: (popover.popdown(), on_detail())) box.append(name_btn) else: name_label = Gtk.Label(label=name, xalign=0) name_label.add_css_class("heading") box.append(name_label) if contact_type is not None: type_label = Gtk.Label(label=contact_type, xalign=0) type_label.add_css_class("dim-label") box.append(type_label) if interpolated: note = Gtk.Label(label=_("Position is illustrative — real location unknown"), xalign=0) note.add_css_class("dim-label") box.append(note) else: coords = Gtk.Label(label=f"{lat:.5f}, {lon:.5f}", xalign=0) coords.add_css_class("dim-label") box.append(coords) if snr is not None: snr_text = Gtk.Label(label=_("SNR: {:.1f} dB").format(snr), xalign=0) box.append(snr_text) popover.set_child(box) popover.set_parent(widget) popover.popup() def _on_marker_clicked(self, contact: Contact): """Navigate to the contact's detail view.""" self._window.show_contact_detail(contact) @staticmethod def interpolate_unknown_hops(hops): """Fill in coordinates for hops without a known location. Input/output: list of (lat_or_none, lon_or_none, contact_or_none, prefix_or_none, snr_or_none) Unknown hops between two located hops are linearly interpolated. Edge unknowns get a small offset from the nearest known hop. """ n = len(hops) if n == 0: return hops result = list(hops) # Find runs of unknown-location hops and interpolate i = 0 while i < n: if result[i][0] is not None: i += 1 continue # Found an unknown hop — find the run run_start = i while i < n and result[i][0] is None: i += 1 run_end = i # exclusive # Find the nearest known hops before and after prev_lat, prev_lon = None, None for j in range(run_start - 1, -1, -1): if result[j][0] is not None: prev_lat, prev_lon = result[j][0], result[j][1] break next_lat, next_lon = None, None for j in range(run_end, n): if result[j][0] is not None: next_lat, next_lon = result[j][0], result[j][1] break run_len = run_end - run_start if prev_lat is not None and next_lat is not None: # Interpolate between two known points for k, idx in enumerate(range(run_start, run_end)): frac = (k + 1) / (run_len + 1) lat = prev_lat + frac * (next_lat - prev_lat) lon = prev_lon + frac * (next_lon - prev_lon) result[idx] = (lat, lon) + result[idx][2:] elif prev_lat is not None: # Edge unknowns at the end — offset from previous for k, idx in enumerate(range(run_start, run_end)): lat = prev_lat + (k + 1) * 0.001 lon = prev_lon + (k + 1) * 0.001 result[idx] = (lat, lon) + result[idx][2:] elif next_lat is not None: # Edge unknowns at the start — offset from next for k, idx in enumerate(range(run_start, run_end)): lat = next_lat - (run_len - k) * 0.001 lon = next_lon - (run_len - k) * 0.001 result[idx] = (lat, lon) + result[idx][2:] return result def show_trace_path(self, hop_contacts, snr_values, hop_prefixes=None): """Draw a trace path on the map from trace results (with SNR).""" device = self._window.device_info self_lat = getattr(device, "latitude", None) self_lon = getattr(device, "longitude", None) has_self = self_lat and self_lon and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) hops = [] if has_self: hops.append((self_lat, self_lon, None, None, None)) for i, contact in enumerate(hop_contacts): snr = snr_values[i] if i < len(snr_values) else None prefix = hop_prefixes[i] if hop_prefixes and i < len(hop_prefixes) else None if contact and contact.has_location: hops.append((contact.latitude, contact.longitude, contact, prefix, snr)) else: hops.append((None, None, contact, prefix, snr)) hops = self.interpolate_unknown_hops(hops) self._draw_path_on_map(hops) def show_trace_path_from_hops(self, located_hops): """Draw a path on the map from pre-built hop list (established paths). Each hop is (lat, lon, name, snr_or_none, is_self). Converts to (lat, lon, contact_or_none, prefix, snr) format. """ converted = [] for lat, lon, name, snr, *rest in located_hops: is_self = rest[0] if rest else False if is_self: converted.append((lat, lon, None, None, snr)) else: contact = next( (c for c in self._all_contacts if c.name == name and c.has_location), None ) converted.append((lat, lon, contact, None, snr)) self._draw_path_on_map(converted) def _draw_path_on_map(self, hops): """Draw a path overlay: polyline + circle markers + SNR at midpoints. hops: list of (lat, lon, contact_or_none, prefix_or_none, snr_or_none) contact=None + prefix=None means self/device node. contact=None + prefix!=None means unknown/interpolated hop. """ self.clear_trace_path() if not hops: return viewport = self._map.get_viewport() # Create path layer for the line self._trace_path_layer = MapView.create_dashed_path_layer(viewport) self._map.get_map().add_layer(self._trace_path_layer) # Create marker layer self._trace_marker_layer = Shumate.MarkerLayer.new(viewport) self._map.get_map().add_layer(self._trace_marker_layer) # Add path nodes and markers lats, lons = [], [] hop_num = 0 for lat, lon, contact, prefix, snr in hops: node = Shumate.Marker() node.set_location(lat, lon) self._trace_path_layer.add_node(node) if contact is None and prefix is None: # Self/device marker r, g, b = _SELF_COLOR icon_name = "network-wireless-symbolic" name = self._window.device_info.name or _("My Device") badge = "" elif contact is None: # Unknown/interpolated hop r, g, b = _UNKNOWN_COLOR icon_name = "network-server-symbolic" name = prefix hop_num += 1 badge = str(hop_num) else: r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") name = contact.name hop_num += 1 badge = str(hop_num) widget = self._build_marker_widget(r, g, b, icon_name, "", badge_text=badge) widget.set_tooltip_text(name) if contact is not None: click = Gtk.GestureClick() click.connect("pressed", lambda g, n, x, y, c=contact: self._on_marker_clicked(c)) widget.add_controller(click) elif prefix is not None: click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y, w=widget, nm=name, la=lat, lo=lon: MapView.show_marker_popover( w, nm, la, lo, contact_type=_("Unknown repeater"), interpolated=True ), ) widget.add_controller(click) hop_marker = Shumate.Marker() hop_marker.set_child(widget) hop_marker.set_location(lat, lon) self._trace_marker_layer.add_marker(hop_marker) lats.append(lat) lons.append(lon) # Add SNR labels at midpoints between consecutive hops for i in range(1, len(hops)): snr = hops[i][4] if snr is None: continue mid_lat = (hops[i - 1][0] + hops[i][0]) / 2 mid_lon = (hops[i - 1][1] + hops[i][1]) / 2 snr_label = Gtk.Label(label=f"{snr:+.1f} dB") snr_label.add_css_class("caption") if snr >= 0: snr_label.add_css_class("success") elif snr >= -5: snr_label.add_css_class("dim-label") else: snr_label.add_css_class("error") snr_marker = Shumate.Marker() snr_marker.set_child(snr_label) snr_marker.set_location(mid_lat, mid_lon) self._trace_marker_layer.add_marker(snr_marker) # Fit map to path bounds MapView.fit_viewport(viewport, lats, lons) def clear_trace_path(self): """Remove any trace path overlay from the map.""" if self._trace_anim_id: GLib.source_remove(self._trace_anim_id) self._trace_anim_id = None if self._trace_path_layer: self._map.get_map().remove_layer(self._trace_path_layer) self._trace_path_layer = None if self._trace_marker_layer: self._map.get_map().remove_layer(self._trace_marker_layer) self._trace_marker_layer = None @staticmethod def fit_viewport(viewport, lats, lons): """Zoom and center a viewport to fit the given coordinates.""" if not lats: return if len(lats) == 1: viewport.set_zoom_level(14) viewport.set_location(lats[0], lons[0]) return min_lat, max_lat = min(lats), max(lats) min_lon, max_lon = min(lons), max(lons) span = max(max_lat - min_lat, max_lon - min_lon) if span < 0.005: zoom = 14 elif span < 0.05: zoom = 13 elif span < 0.1: zoom = 12 elif span < 0.5: zoom = 10 elif span < 1.0: zoom = 9 else: zoom = 7 viewport.set_zoom_level(zoom) viewport.set_location((min_lat + max_lat) / 2, (min_lon + max_lon) / 2) @staticmethod def open_map_dialog(window, title=None): """Create and return (dialog, map_widget, marker_layer, viewport). The caller can add markers, path layers, etc. and then call dialog.present(window) to show it. """ dialog = Adw.Dialog() dialog.set_title(title or _("Map")) # Size to nearly fill the parent window win_width = window.get_width() or 800 win_height = window.get_height() or 600 dialog.set_content_width(max(500, win_width - 60)) dialog.set_content_height(max(400, win_height - 60)) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) from meshy.views import create_shumate_map, deregister_map_widget map_widget, marker_layer, viewport = create_shumate_map() map_widget.set_hexpand(True) toolbar_view.set_content(map_widget) dialog.set_child(toolbar_view) dialog.connect("closed", lambda _d: deregister_map_widget(map_widget)) return dialog, map_widget, marker_layer, viewport @staticmethod def _filter_outliers(lats, lons): """Remove geographic outliers using median + MAD (robust to skew).""" if len(lats) < 3: return lats, lons from math import sqrt from statistics import median med_lat, med_lon = median(lats), median(lons) dists = [ sqrt((la - med_lat) ** 2 + (lo - med_lon) ** 2) for la, lo in zip(lats, lons, strict=False) ] med_dist = median(dists) or 0.01 filtered = [ (la, lo) for la, lo, d in zip(lats, lons, dists, strict=False) if d <= 3 * med_dist ] if not filtered: return lats, lons return [la for la, _ in filtered], [lo for _, lo in filtered] def _fit_bounds(self): """Zoom and center the map to show all located contacts.""" lats = [c.latitude for c in self._located_contacts] lons = [c.longitude for c in self._located_contacts] if self._self_lat and self._self_lon: lats.append(self._self_lat) lons.append(self._self_lon) if not lats: return viewport = self._map.get_viewport() if len(lats) == 1: viewport.set_zoom_level(14) viewport.set_location(lats[0], lons[0]) return lats, lons = self._filter_outliers(lats, lons) min_lat, max_lat = min(lats), max(lats) min_lon, max_lon = min(lons), max(lons) center_lat = (min_lat + max_lat) / 2 center_lon = (min_lon + max_lon) / 2 lat_span = max_lat - min_lat lon_span = max_lon - min_lon span = max(lat_span, lon_span) if span < 0.01: zoom = 14 elif span < 0.05: zoom = 13 elif span < 0.1: zoom = 12 elif span < 0.5: zoom = 10 elif span < 1.0: zoom = 9 elif span < 5.0: zoom = 7 elif span < 20.0: zoom = 5 else: zoom = 3 viewport.set_zoom_level(zoom) viewport.set_location(center_lat, center_lon) def open_trace_picker_map(window, contacts, hash_width, on_done): """Open a map dialog for selecting trace route waypoints by clicking. on_done(hex_string) is called with comma-separated hop hashes when the user confirms the selection. """ located = [ c for c in contacts if c.type in (ContactType.REPEATER, ContactType.ROOM) and c.has_location ] if not located: return selected = [] # list of Contact objects (ordered, may repeat) dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( window, title=_("Pick Trace Route") ) toolbar_view = dialog.get_child() path_layer = MapView.create_dashed_path_layer(viewport) map_widget.get_map().add_layer(path_layer) badge_layer = Shumate.MarkerLayer.new(viewport) map_widget.get_map().add_layer(badge_layer) # Track badge labels per contact for multi-selection display badge_markers = {} # pub_key_hex -> (Shumate.Marker, Gtk.Label) def _update_path_and_badges(): # Update polyline (companion → waypoints → companion) path_layer.remove_all() if selected and self_node: path_layer.add_node(self_node) for c in selected: pt = Shumate.Coordinate() pt.set_location(c.latitude, c.longitude) path_layer.add_node(pt) if selected and self_node: end = Shumate.Coordinate() end.set_location(self_lat, self_lon) path_layer.add_node(end) # Rebuild badge markers for m, _lbl in badge_markers.values(): badge_layer.remove_marker(m) badge_markers.clear() # Collect indices per contact indices = {} for i, c in enumerate(selected): indices.setdefault(c.public_key_hex, []).append(i + 1) for key_hex, nums in indices.items(): contact = next(c for c in selected if c.public_key_hex == key_hex) text = ",".join(str(n) for n in nums) badge_widget = MapView.build_map_label_widget(text) marker = Shumate.Marker() marker.set_child(badge_widget) marker.set_location(contact.latitude, contact.longitude) badge_layer.add_marker(marker) badge_markers[key_hex] = (marker, badge_widget) # Update route label if selected: names = [c.name for c in selected] route_label.set_label(" → ".join(names)) route_label.set_visible(True) done_btn.set_sensitive(True) else: route_label.set_label("") route_label.set_visible(False) done_btn.set_sensitive(False) def _on_contact_clicked(contact): selected.append(contact) _update_path_and_badges() # Companion (self) marker device = window._device_info if hasattr(window, "_device_info") else None self_lat = getattr(device, "latitude", None) self_lon = getattr(device, "longitude", None) has_self = self_lat and self_lon and (abs(self_lat) > 1e-6 or abs(self_lon) > 1e-6) self_node = None if has_self: self_node = Shumate.Coordinate() self_node.set_location(self_lat, self_lon) # Clustering state expanded_clusters = [] last_zoom = [-1] recluster_timeout = [None] def _create_picker_contact_marker(contact, zoom): r, g, b = _MARKER_COLORS.get(contact.type, (0.5, 0.5, 0.5)) icon_name = _MARKER_ICONS.get(contact.type, "avatar-default-symbolic") widget = MapView._build_marker_widget( r, g, b, icon_name, contact.name, boxed_label=True, show_label=zoom >= 10 ) widget.set_tooltip_text(contact.name) widget.set_cursor(Gdk.Cursor.new_from_name("pointer")) click = Gtk.GestureClick() click.connect("pressed", lambda g, n, x, y, c=contact: _on_contact_clicked(c)) widget.add_controller(click) marker = Shumate.Marker() marker.set_child(widget) marker.set_location(contact.latitude, contact.longitude) return marker def _zoom_or_expand(contacts, lat, lon): current_zoom = viewport.get_zoom_level() test_zoom = min(current_zoom + 2, 20) test_clusters = MapView._compute_clusters(contacts, test_zoom) if len(test_clusters) > 1 and current_zoom < 18: viewport.set_zoom_level(test_zoom) viewport.set_location(lat, lon) else: expanded_clusters.append(contacts) last_zoom[0] = -1 _rebuild_markers() def _rebuild_markers(): zoom = viewport.get_zoom_level() _LABEL_ZOOM = 10 labels_changed = (last_zoom[0] < _LABEL_ZOOM) != (zoom < _LABEL_ZOOM) if abs(zoom - last_zoom[0]) < 0.3 and not expanded_clusters and not labels_changed: return False last_zoom[0] = zoom marker_layer.remove_all() expanded_keys = set() for group in expanded_clusters: for c in group: expanded_keys.add(c.public_key_hex) to_cluster = [c for c in located if c.public_key_hex not in expanded_keys] clusters = MapView._compute_clusters(to_cluster, zoom) for cluster in clusters: if len(cluster) == 1: marker = _create_picker_contact_marker(cluster[0], zoom) else: marker = MapView._create_cluster_marker(cluster, _zoom_or_expand) marker_layer.add_marker(marker) for group in expanded_clusters: avg_lat = sum(c.latitude for c in group) / len(group) sum(c.longitude for c in group) / len(group) for i, contact in enumerate(group): angle = 2 * math.pi * i / len(group) offset = 0.0003 * (2 ** (15 - zoom)) offset = max(offset, 0.00005) dlat = offset * math.cos(angle) dlon = offset * math.sin(angle) / max(math.cos(math.radians(avg_lat)), 0.01) marker = _create_picker_contact_marker(contact, zoom) marker.set_location(contact.latitude + dlat, contact.longitude + dlon) marker_layer.add_marker(marker) if has_self: self_name = getattr(device, "name", "") or _("My Device") r, g, b = _SELF_COLOR widget = MapView._build_marker_widget( r, g, b, "avatar-default-symbolic", self_name, boxed_label=True, show_label=zoom >= 10, ) widget.set_tooltip_text(self_name) sm = Shumate.Marker() sm.set_child(widget) sm.set_location(self_lat, self_lon) marker_layer.add_marker(sm) return False def _on_zoom_changed(vp, pspec): expanded_clusters.clear() if recluster_timeout[0]: GLib.source_remove(recluster_timeout[0]) recluster_timeout[0] = GLib.timeout_add(150, _rebuild_markers) viewport.connect("notify::zoom-level", _on_zoom_changed) _rebuild_markers() # Bottom bar with route info and controls bottom_bar = Gtk.Box(spacing=8, margin_start=8, margin_end=8, margin_top=6, margin_bottom=6) route_label = Gtk.Label( label="", xalign=0, hexpand=True, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR ) route_label.add_css_class("caption") route_label.set_visible(False) bottom_bar.append(route_label) undo_btn = Gtk.Button(icon_name="edit-undo-symbolic", tooltip_text=_("Undo")) undo_btn.add_css_class("flat") def _on_undo(*_): if selected: selected.pop() _update_path_and_badges() undo_btn.connect("clicked", _on_undo) bottom_bar.append(undo_btn) clear_btn = Gtk.Button(icon_name="edit-clear-all-symbolic", tooltip_text=_("Clear")) clear_btn.add_css_class("flat") def _on_clear(*_): selected.clear() _update_path_and_badges() clear_btn.connect("clicked", _on_clear) bottom_bar.append(clear_btn) done_btn = Gtk.Button(label=_("Done")) done_btn.add_css_class("suggested-action") done_btn.set_sensitive(False) def _on_done(*_): hex_parts = [c.public_key[:hash_width].hex() for c in selected] on_done(",".join(hex_parts)) dialog.close() done_btn.connect("clicked", _on_done) bottom_bar.append(done_btn) toolbar_view.add_bottom_bar(bottom_bar) # Main content: map + instruction hint — unparent map from toolbar_view first toolbar_view.set_content(None) content_overlay = Gtk.Overlay() content_overlay.set_child(map_widget) hint_label = Gtk.Label(label=_("Tap repeaters to build the trace route")) hint_label.add_css_class("caption") hint_label.set_halign(Gtk.Align.CENTER) hint_label.set_valign(Gtk.Align.START) hint_label.set_margin_top(8) hint_css = Gtk.CssProvider() hint_css.load_from_string( "label { background: alpha(@window_bg_color, 0.85);" " border-radius: 6px; padding: 4px 12px; }" ) hint_label.get_style_context().add_provider(hint_css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) content_overlay.add_overlay(hint_label) toolbar_view.set_content(content_overlay) # Fit map to show all repeaters + companion lats = [c.latitude for c in located] lons = [c.longitude for c in located] if has_self: lats.append(self_lat) lons.append(self_lon) MapView.fit_viewport(viewport, lats, lons) dialog.present(window) meshy/src/views/repeater_view.py000066400000000000000000001714671521052255700173660ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Repeater management view — shown in the content panel for repeater contacts.""" import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") import contextlib import copy from gi.repository import Adw, Gio, GLib, Gtk from meshy.models import Contact from meshy.protocol import ( build_send_cli_command, ) def _fmt_uptime(secs): d, secs = divmod(secs, 86400) h, secs = divmod(secs, 3600) m, secs = divmod(secs, 60) parts = [] if d: parts.append(_("{d}d").format(d=d)) if h: parts.append(_("{h}h").format(h=h)) if m: parts.append(_("{m}m").format(m=m)) parts.append(_("{secs}s").format(secs=secs)) return " ".join(parts) def _fmt_ago(secs): if secs < 60: return _("{}s ago").format(secs) if secs < 3600: return _("{}m ago").format(secs // 60) if secs < 86400: return _("{}h ago").format(secs // 3600) return _("{}d ago").format(secs // 86400) def _battery_pct(mv): if mv >= 4100: return 100 if mv <= 3300: return 0 return int((mv - 3300) / 8) @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/repeater-view.ui") class RepeaterView(Gtk.Box): __gtype_name__ = "MeshyRepeaterView" REQUEST_TIMEOUT_MS = 15000 FETCH_CMD_TIMEOUT_MS = 8000 # Main _stack = Gtk.Template.Child("stack") # Status page _st_clock = Gtk.Template.Child("st_clock") _clock_drift_label = Gtk.Template.Child("clock_drift_label") _clock_sync_btn = Gtk.Template.Child("clock_sync_btn") _clock_reset_btn = Gtk.Template.Child("clock_reset_btn") _st_battery = Gtk.Template.Child("st_battery") _st_uptime = Gtk.Template.Child("st_uptime") _st_queue = Gtk.Template.Child("st_queue") _st_errors = Gtk.Template.Child("st_errors") _st_rssi = Gtk.Template.Child("st_rssi") _st_snr = Gtk.Template.Child("st_snr") _st_noise = Gtk.Template.Child("st_noise") _st_tx_air = Gtk.Template.Child("st_tx_air") _st_rx_air = Gtk.Template.Child("st_rx_air") _st_channel_util = Gtk.Template.Child("st_channel_util") _st_duty_cycle = Gtk.Template.Child("st_duty_cycle") _st_sent = Gtk.Template.Child("st_sent") _st_recv = Gtk.Template.Child("st_recv") _st_recv_errors = Gtk.Template.Child("st_recv_errors") _st_flood = Gtk.Template.Child("st_flood") _st_direct = Gtk.Template.Child("st_direct") _st_dups = Gtk.Template.Child("st_dups") _status_refresh_btn = Gtk.Template.Child("status_refresh_btn") # CLI page _cli_quick_box = Gtk.Template.Child("cli_quick_box") _cli_view = Gtk.Template.Child("cli_view") _cli_entry = Gtk.Template.Child("cli_entry") _cli_send_btn = Gtk.Template.Child("cli_send_btn") # Neighbors page _nb_spinner = Gtk.Template.Child("nb_spinner") _nb_status = Gtk.Template.Child("nb_status") _nb_list = Gtk.Template.Child("nb_list") _nb_refresh_btn = Gtk.Template.Child("nb_refresh_btn") _nb_load_more = Gtk.Template.Child("nb_load_more") _nb_map_btn = Gtk.Template.Child("nb_map_btn") # Access page _acl_spinner = Gtk.Template.Child("acl_spinner") _acl_status = Gtk.Template.Child("acl_status") _acl_list = Gtk.Template.Child("acl_list") _acl_refresh_btn = Gtk.Template.Child("acl_refresh_btn") _acl_add_btn = Gtk.Template.Child("acl_add_btn") # Regions page _reg_loading_box = Gtk.Template.Child("reg_loading_box") _reg_spinner = Gtk.Template.Child("reg_spinner") _reg_status = Gtk.Template.Child("reg_status") _reg_retry_btn = Gtk.Template.Child("reg_retry_btn") _reg_content_box = Gtk.Template.Child("reg_content_box") _reg_default_entry = Gtk.Template.Child("reg_default_entry") _reg_list = Gtk.Template.Child("reg_list") _reg_add_btn = Gtk.Template.Child("reg_add_btn") _reg_apply_btn = Gtk.Template.Child("reg_apply_btn") # Settings page – fetch spinners _basic_fetch_spinner = Gtk.Template.Child("basic_fetch_spinner") _loc_fetch_spinner = Gtk.Template.Child("loc_fetch_spinner") _radio_fetch_spinner = Gtk.Template.Child("radio_fetch_spinner") _advert_fetch_spinner = Gtk.Template.Child("advert_fetch_spinner") # Settings page _set_name = Gtk.Template.Child("set_name") _set_repeat = Gtk.Template.Child("set_repeat") _set_admin_pwd = Gtk.Template.Child("set_admin_pwd") _admin_pwd_save_btn = Gtk.Template.Child("admin_pwd_save_btn") _set_guest_pwd = Gtk.Template.Child("set_guest_pwd") _guest_pwd_save_btn = Gtk.Template.Child("guest_pwd_save_btn") _set_lat = Gtk.Template.Child("set_lat") _set_lon = Gtk.Template.Child("set_lon") _set_freq = Gtk.Template.Child("set_freq") _set_bw = Gtk.Template.Child("set_bw") _set_sf = Gtk.Template.Child("set_sf") _set_cr = Gtk.Template.Child("set_cr") _set_tx = Gtk.Template.Child("set_tx") _set_advert_int = Gtk.Template.Child("set_advert_int") _set_flood_int = Gtk.Template.Child("set_flood_int") _reboot_btn = Gtk.Template.Child("reboot_btn") _basic_fetch_btn = Gtk.Template.Child("basic_fetch_btn") _basic_save_btn = Gtk.Template.Child("basic_save_btn") _loc_fetch_btn = Gtk.Template.Child("loc_fetch_btn") _loc_save_btn = Gtk.Template.Child("loc_save_btn") _radio_fetch_btn = Gtk.Template.Child("radio_fetch_btn") _radio_save_btn = Gtk.Template.Child("radio_save_btn") _advert_fetch_btn = Gtk.Template.Child("advert_fetch_btn") _advert_save_btn = Gtk.Template.Child("advert_save_btn") # Telemetry _tele_btn = Gtk.Template.Child("tele_btn") # Bottom bar _logout_btn = Gtk.Template.Child("logout_btn") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._contact = None self._timeout_id = None self._nb_offset = 0 self._nb_total = 0 self._nb_neighbors = [] # list of (contact_or_None, snr) self._acl_entries = [] # list of {'key': hex, 'perm': int} self._region_entries = [] # list of {'name', 'indent', 'home', 'flood'} self._original_region_entries = [] self._original_default_scope = "" self._pending_region_cmds = [] self._reg_timeout_id = None self._reg_cmd_ids = set() self._regions_pending = False self._reg_prefixes = [] self._reg_popover_entry = None self._current_page = "placeholder" self._leaving_regions = False self._pending_cmd_ids = [] self._pending_prefixes = [] self._fetch_timeout_id = None self._fetch_btn = None self._fetch_spinner = None # CLI buffer reference self._cli_buf = self._cli_view.get_buffer() # Populate CLI quick command buttons for label, cmd in [ ("get name", "get name"), ("get radio", "get radio"), ("get tx", "get tx"), ("ver", "ver"), ("neighbors", "neighbors"), ("advert", "advert"), ("clock", "clock"), ]: btn = Gtk.Button(label=label) btn.add_css_class("flat") btn.connect("clicked", lambda b, c=cmd: self._send_cli(c)) self._cli_quick_box.append(btn) # Signal connections self._status_refresh_btn.connect("clicked", lambda *_: self._request_status()) self._tele_btn.connect("clicked", lambda *_: self._request_telemetry()) self._clock_sync_btn.connect("clicked", self._on_sync_time_clicked) self._clock_reset_btn.connect("clicked", self._on_clock_reset_clicked) self._cli_entry.connect( "activate", lambda *_: self._send_cli(self._cli_entry.get_text().strip()) ) self._cli_send_btn.connect( "clicked", lambda *_: self._send_cli(self._cli_entry.get_text().strip()) ) self._nb_refresh_btn.connect("clicked", lambda *_: self._request_neighbors(reset=True)) self._nb_load_more.connect("clicked", lambda *_: self._request_neighbors(reset=False)) self._nb_map_btn.connect("clicked", lambda *_: self._show_neighbors_map()) self._acl_refresh_btn.connect("clicked", lambda *_: self._request_acl()) self._acl_add_btn.connect("clicked", lambda *_: self._show_acl_add_dialog()) self._reg_retry_btn.connect("clicked", lambda *_: self._request_regions()) self._reg_add_btn.connect("clicked", lambda *_: self._show_region_add_dialog()) self._reg_apply_btn.connect("clicked", lambda *_: self._on_reg_apply()) self._reg_default_entry.connect("notify::text", lambda *_: self._on_reg_default_changed()) self._stack.connect("notify::visible-child-name", self._on_stack_page_changed) reg_action_group = Gio.SimpleActionGroup() for name, cb in [ ("toggle_flood", lambda *_: self._on_reg_toggle_flood()), ("set_home", lambda *_: self._on_reg_set_home()), ("remove", lambda *_: self._on_reg_remove()), ]: action = Gio.SimpleAction.new(name, None) action.connect("activate", cb) reg_action_group.add_action(action) self._reg_list.insert_action_group("reg", reg_action_group) self._basic_fetch_btn.connect( "clicked", lambda *_: self._fetch_section( ["get name", "get repeat"], self._parse_basic, self._basic_fetch_btn, self._basic_fetch_spinner, ), ) self._basic_save_btn.connect("clicked", lambda *_: self._save_basic()) self._admin_pwd_save_btn.connect("clicked", lambda *_: self._save_admin_password()) self._guest_pwd_save_btn.connect("clicked", lambda *_: self._save_guest_password()) self._loc_fetch_btn.connect( "clicked", lambda *_: self._fetch_section( ["get lat", "get lon"], self._parse_location, self._loc_fetch_btn, self._loc_fetch_spinner, ), ) self._loc_save_btn.connect("clicked", lambda *_: self._save_location()) self._radio_fetch_btn.connect( "clicked", lambda *_: self._fetch_section( ["get radio", "get tx"], self._parse_radio, self._radio_fetch_btn, self._radio_fetch_spinner, ), ) self._radio_save_btn.connect("clicked", lambda *_: self._save_radio()) self._advert_fetch_btn.connect( "clicked", lambda *_: self._fetch_section( ["get advert.interval", "get flood.advert.interval"], self._parse_advert, self._advert_fetch_btn, self._advert_fetch_spinner, ), ) self._advert_save_btn.connect("clicked", lambda *_: self._save_advert()) self._reboot_btn.connect("activated", self._on_reboot) self._logout_btn.connect("clicked", self._on_logout) def _switch_tab(self, name): """Programmatically switch to a tab.""" self._current_page = name self._stack.set_visible_child_name(name) def _start_timeout(self, on_timeout): """Start a request timeout. Cancels any previous one.""" self._cancel_timeout() self._timeout_id = GLib.timeout_add( self.REQUEST_TIMEOUT_MS, lambda: (on_timeout(), False)[1] ) def _cancel_timeout(self): if self._timeout_id: GLib.source_remove(self._timeout_id) self._timeout_id = None # ─── Status ──────────────────────────────────────────────── def _on_sync_time_clicked(self, btn): """Send the current system time to the repeater via CLI command.""" if not self._contact: return import time from datetime import datetime ts = int(time.time()) self._window.send_cli_command(self._contact, f"time {ts}") self._st_clock.set_subtitle(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) self._clock_drift_label.set_visible(False) self._clock_sync_btn.set_visible(False) self._clock_reset_btn.set_visible(False) self._window.show_toast(_("Time sync sent to repeater")) def _on_clock_reset_clicked(self, btn): """Reset the repeater's clock and reboot it (for when clock is ahead).""" if not self._contact: return dialog = Adw.AlertDialog( heading=_("Reset Clock & Reboot?"), body=_( "The repeater clock is ahead of the current time.\n" "Firmware does not allow setting time backwards.\n\n" "This will reset the clock and reboot the repeater." ), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("reset", _("Reset & Reboot")) dialog.set_response_appearance("reset", Adw.ResponseAppearance.DESTRUCTIVE) def _on_response(d, response): if response == "reset": self._window.send_cli_command(self._contact, "clkreboot") self._clock_drift_label.set_visible(False) self._clock_sync_btn.set_visible(False) self._clock_reset_btn.set_visible(False) self._st_clock.set_subtitle(_("Rebooting...")) self._window.show_toast(_("Clock reset & reboot sent to repeater")) dialog.connect("response", _on_response) dialog.present(self._window) def _request_status(self): if not self._contact: return self._start_timeout(self._on_status_timeout) self._window.send_status_req(self._contact.public_key, self._on_status_response) def _on_status_timeout(self): self._st_battery.set_subtitle(_("Request timed out")) self._window.set_status_callback(None) self._start_pending_regions() def _on_status_response(self, status): self._cancel_timeout() mv = status["battery_mv"] pct = _battery_pct(mv) self._st_battery.set_subtitle(f"{pct}% ({mv} mV)") self._st_uptime.set_subtitle(_fmt_uptime(status["uptime_secs"])) self._st_queue.set_subtitle(str(status["queue_len"])) self._st_errors.set_subtitle(str(status["error_events"])) self._st_rssi.set_subtitle(f'{status["last_rssi"]} dB') self._st_snr.set_subtitle(f'{status["last_snr"]:.1f} dB') self._st_noise.set_subtitle(f'{status["noise_floor"]} dB') self._st_tx_air.set_subtitle(_fmt_uptime(status["tx_air_secs"])) self._st_rx_air.set_subtitle(_fmt_uptime(status["rx_air_secs"])) uptime = status["uptime_secs"] if uptime > 0: util = (status["tx_air_secs"] + status["rx_air_secs"]) / uptime * 100 self._st_channel_util.set_subtitle(f"{util:.1f}%") duty = status["tx_air_secs"] / uptime * 100 self._st_duty_cycle.set_subtitle(f"{duty:.1f}%") self._st_sent.set_subtitle(str(status["packets_sent"])) self._st_recv.set_subtitle(str(status["packets_recv"])) recv_errors = status.get("recv_errors") if recv_errors is not None: self._st_recv_errors.set_visible(True) self._st_recv_errors.set_subtitle(str(recv_errors)) self._st_flood.set_subtitle(f'TX: {status["flood_tx"]} / RX: {status["flood_rx"]}') self._st_direct.set_subtitle(f'TX: {status["direct_tx"]} / RX: {status["direct_rx"]}') self._st_dups.set_subtitle(f'Flood: {status["dup_flood"]} / Direct: {status["dup_direct"]}') self._start_pending_regions() def _start_pending_regions(self): if self._regions_pending: self._regions_pending = False GLib.timeout_add(500, lambda: (self._request_regions(), False)[1]) # ─── Telemetry ───────────────────────────────────────────── def _request_telemetry(self): if not self._contact: return from meshy.models import ContactType self._tele_btn.set_sensitive(False) self._tele_btn.set_label(_("Requesting…")) timeout_id = [None] def _reset_btn(): self._tele_btn.set_sensitive(True) self._tele_btn.set_label(_("Request Telemetry")) def on_timeout(): _reset_btn() timeout_id[0] = None return False def on_response(lpp_data): if timeout_id[0]: GLib.source_remove(timeout_id[0]) timeout_id[0] = None _reset_btn() if not lpp_data: return from meshy.protocol import parse_cayenne_lpp entries = parse_cayenne_lpp(lpp_data) if entries: self._show_telemetry_dialog(entries) timeout_id[0] = GLib.timeout_add(self.REQUEST_TIMEOUT_MS, on_timeout) self._window.get_telemetry( self._contact.public_key, on_response, contact_type=ContactType.REPEATER ) def _show_telemetry_dialog(self, entries: list[dict]): from meshy.models import battery_percent_from_mv from meshy.protocol import LPP_GPS, LPP_VOLTAGE gps_entry = next((e for e in entries if e["type"] == LPP_GPS), None) dialog = Adw.Dialog() dialog.set_title(_("Telemetry — {}").format(self._contact.name if self._contact else "")) dialog.set_content_width(420) dialog.set_content_height(550 if gps_entry else 400) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) scroll = Gtk.ScrolledWindow(vexpand=True) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_start=12, margin_end=12, margin_top=12, margin_bottom=12, ) channels = {} for entry in entries: channels.setdefault(entry["channel"], []).append(entry) for ch in sorted(channels): group = Adw.PreferencesGroup(title=_("Channel {}").format(ch)) for entry in channels[ch]: val = entry["value"] if isinstance(val, dict): alt_str = f', alt {val["alt"]:.0f} m' if abs(val["alt"]) > 0.01 else "" subtitle = f'{val["lat"]:.4f}, {val["lon"]:.4f}{alt_str}' elif entry["type"] == LPP_VOLTAGE and ch == 1: pct = battery_percent_from_mv(int(val * 1000)) subtitle = f"{pct}% ({val:.2f} V)" elif isinstance(val, float): subtitle = f'{val:.2f} {entry["unit"]}' else: subtitle = f'{val} {entry["unit"]}' title = _("Battery") if entry["type"] == LPP_VOLTAGE and ch == 1 else entry["name"] row = Adw.ActionRow(title=title, subtitle=subtitle) group.add(row) content.append(group) if gps_entry: from meshy.views import create_shumate_map lat = gps_entry["value"]["lat"] lon = gps_entry["value"]["lon"] map_widget, marker_layer, viewport = create_shumate_map(zoom_level=14, lat=lat, lon=lon) if map_widget: map_widget.set_vexpand(False) map_widget.set_size_request(-1, 200) content.append(map_widget) scroll.set_child(content) toolbar_view.set_content(scroll) dialog.set_child(toolbar_view) dialog.present(self._window) # ─── CLI ─────────────────────────────────────────────────── def _send_cli(self, cmd): if not cmd or not self._contact: return self._cli_entry.set_text("") self._cli_append(f"> {cmd}") self._window.send_frame(build_send_cli_command(self._contact.public_key, cmd)) def _cli_append(self, text): end = self._cli_buf.get_end_iter() self._cli_buf.insert(end, text + "\n") GLib.idle_add( lambda: self._cli_view.get_parent() .get_vadjustment() .set_value(self._cli_view.get_parent().get_vadjustment().get_upper()) or False ) def on_cli_response(self, contact, message): """Called by window when a CLI response arrives.""" if ( self._contact and message.is_cli and contact.public_key_hex == self._contact.public_key_hex ): self._cli_append(message.text) # ─── Neighbors ───────────────────────────────────────────── def _request_neighbors(self, reset=True): if not self._contact: return if reset: self._nb_offset = 0 self._nb_neighbors = [] self._nb_map_btn.set_visible(False) child = self._nb_list.get_first_child() while child: next_c = child.get_next_sibling() self._nb_list.remove(child) child = next_c self._nb_spinner.set_spinning(True) self._nb_status.set_label(_("Requesting...")) self._nb_load_more.set_visible(False) self._start_timeout(self._on_neighbors_timeout) self._window.get_neighbors( self._contact.public_key, self._on_neighbors_response, offset=self._nb_offset ) def _on_neighbors_timeout(self): self._nb_spinner.set_spinning(False) self._nb_status.set_label(_("Request timed out")) self._nb_load_more.set_visible(self._nb_offset < self._nb_total) self._window.set_neighbors_callback(None) def _on_neighbors_response(self, result): self._cancel_timeout() self._nb_spinner.set_spinning(False) self._nb_total = result["total"] neighbors = result["neighbors"] self._nb_offset += len(neighbors) for nb in neighbors: prefix = nb.get("pub_key_hex", "") last_heard = nb.get("last_heard", 0) snr = nb.get("snr", 0) contact = self._window.find_contact_by_key_prefix(prefix) self._nb_neighbors.append((contact, snr)) title = contact.name if contact else prefix subtitle = f"{_fmt_ago(last_heard)} \u00b7 SNR: {snr:.1f} dB" row = Adw.ActionRow(title=title, subtitle=subtitle) row.add_prefix(Gtk.Image.new_from_icon_name("network-server-symbolic")) self._nb_list.append(row) self._nb_status.set_label(_("{} of {} neighbors").format(self._nb_offset, self._nb_total)) self._nb_load_more.set_visible(self._nb_offset < self._nb_total) center = self._contact has_center = center and center.has_location has_located_nb = any(c and c.has_location for c, _ in self._nb_neighbors) self._nb_map_btn.set_visible(has_center and has_located_nb) def _show_neighbors_map(self): from gi.repository import Shumate from meshy.views.map_view import MapView center = self._contact if not center or not center.has_location: return dialog, map_widget, marker_layer, viewport = MapView.open_map_dialog( self._window, title=_("Neighbor Map") ) # Remove marker_layer so we can re-add it on top of path layers map_widget.get_map().remove_layer(marker_layer) lats = [center.latitude] lons = [center.longitude] neighbors_located = [] # First pass: add path layers (lines) so they render below markers for contact, snr in self._nb_neighbors: if not contact or not contact.has_location: continue neighbors_located.append((contact, snr)) path_layer = MapView.create_dashed_path_layer(viewport) map_widget.get_map().add_layer(path_layer) center_node = Shumate.Marker() center_node.set_location(center.latitude, center.longitude) path_layer.add_node(center_node) nb_node = Shumate.Marker() nb_node.set_location(contact.latitude, contact.longitude) path_layer.add_node(nb_node) lats.append(contact.latitude) lons.append(contact.longitude) # Re-add marker layer on top of all path layers map_widget.get_map().add_layer(marker_layer) # Center repeater marker (red) center_widget = MapView._build_marker_widget( 0.90, 0.16, 0.22, "network-server-symbolic", "" ) center_widget.set_tooltip_text(center.name) center_marker = Shumate.Marker() center_marker.set_child(center_widget) center_marker.set_location(center.latitude, center.longitude) marker_layer.add_marker(center_marker) click = Gtk.GestureClick() click.connect( "pressed", lambda g, n, x, y: MapView.show_marker_popover( center_widget, center.name, center.latitude, center.longitude ), ) center_widget.add_controller(click) # Neighbor markers (green) and SNR labels for contact, snr in neighbors_located: nb_widget = MapView._build_marker_widget( 0.30, 0.69, 0.31, "network-server-symbolic", "" ) nb_widget.set_tooltip_text(contact.name) nb_click = Gtk.GestureClick() nb_click.connect( "pressed", lambda g, n, x, y, w=nb_widget, c=contact, s=snr: MapView.show_marker_popover( w, c.name, c.latitude, c.longitude, s ), ) nb_widget.add_controller(nb_click) nb_marker = Shumate.Marker() nb_marker.set_child(nb_widget) nb_marker.set_location(contact.latitude, contact.longitude) marker_layer.add_marker(nb_marker) mid_lat = (center.latitude + contact.latitude) / 2 mid_lon = (center.longitude + contact.longitude) / 2 snr_widget = MapView.build_snr_widget(snr) snr_marker = Shumate.Marker() snr_marker.set_child(snr_widget) snr_marker.set_location(mid_lat, mid_lon) marker_layer.add_marker(snr_marker) MapView.fit_viewport(viewport, lats, lons) dialog.present(self._window) # ─── Access Control (ACL) ───────────────────────────────── def _parse_acl_response(self, data): """Parse raw ACL binary response into list of {'key': hex, 'perm': int}.""" # Response: [code(1)][reserved(1)][tag(4)][payload...] # Payload: 7-byte entries: [pubkey_prefix(6)][perm(1)] buf = data[6:] # skip code + reserved + tag entries = [] i = 0 while i + 7 <= len(buf): key = buf[i : i + 6].hex() perm = buf[i + 6] if key != "000000000000": entries.append({"key": key, "perm": perm}) i += 7 return entries def _request_acl(self): if not self._contact: return # Clear list child = self._acl_list.get_first_child() while child: next_c = child.get_next_sibling() self._acl_list.remove(child) child = next_c self._acl_spinner.set_spinning(True) self._acl_status.set_label(_("Requesting...")) self._start_timeout(self._on_acl_timeout) self._window.get_acl(self._contact.public_key, self._on_acl_response) def _on_acl_timeout(self): self._acl_spinner.set_spinning(False) self._acl_status.set_label(_("Request timed out")) self._window.set_acl_callback(None) def _on_acl_response(self, data): self._cancel_timeout() self._acl_spinner.set_spinning(False) self._acl_entries = self._parse_acl_response(data) for entry in self._acl_entries: prefix = entry["key"] perm = entry["perm"] # Try to match to a known contact or own companion self_key = self._window.self_info.get("public_key", "") if self_key.startswith(prefix): name = self._window.self_info.get("name", "") or _("Me") else: contact = self._window.find_contact_by_key_prefix(prefix) name = contact.name if contact else None role = _("Admin") if (perm & 1) == 1 else _("Guest") title = name or prefix subtitle = f"{role} \u00b7 {prefix}" row = Adw.ActionRow(title=title, subtitle=subtitle) row.add_prefix(Gtk.Image.new_from_icon_name("avatar-default-symbolic")) # Remove button remove_btn = Gtk.Button(icon_name="user-trash-symbolic", valign=Gtk.Align.CENTER) remove_btn.add_css_class("destructive-action") remove_btn.connect("clicked", lambda b, k=prefix: self._on_acl_remove(k)) row.add_suffix(remove_btn) self._acl_list.append(row) count = len(self._acl_entries) self._acl_status.set_label( _("{} entry").format(count) if count == 1 else _("{} entries").format(count) ) def _on_acl_remove(self, key_hex): """Remove a node from ACL via setperm -1.""" if not self._contact: return # Find contact name for the confirmation message name = key_hex self_key = self._window.self_info.get("public_key", "") if self_key.startswith(key_hex): name = self._window.self_info.get("name", "") or _("Me") else: for c in self._window.contacts: if c.public_key_hex.startswith(key_hex): name = c.name break dialog = Adw.AlertDialog( heading=_("Remove from ACL?"), body=_("Remove {} from the access control list?").format(name), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("remove", _("Remove")) dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE) def _on_response(d, response): if response == "remove": self._send_cmds([f"setperm {key_hex} -1"]) # Refresh after a short delay to let the command take effect GLib.timeout_add(1000, lambda: (self._request_acl(), False)[1]) dialog.connect("response", _on_response) dialog.present(self._window) def _show_acl_add_dialog(self): """Show dialog to add a contact to the ACL.""" if not self._contact: return dialog = Adw.AlertDialog( heading=_("Add to Access Control"), body=_("Select a contact and role."), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("add", _("Add")) dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) content.set_size_request(300, -1) # Role selector role_row = Adw.ComboRow(title=_("Role")) role_model = Gtk.StringList.new([_("Admin"), _("Guest")]) role_row.set_model(role_model) role_row.set_selected(0) role_group = Adw.PreferencesGroup() role_group.add(role_row) content.append(role_group) # Contact list contacts_group = Adw.PreferencesGroup(title=_("Contact")) contact_scroll = Gtk.ScrolledWindow() contact_scroll.set_min_content_height(200) contact_scroll.set_max_content_height(300) contact_list = Gtk.ListBox() contact_list.set_selection_mode(Gtk.SelectionMode.SINGLE) contact_list.add_css_class("boxed-list") # Populate with contacts (exclude self repeater and non-chat contacts) acl_keys = {e["key"] for e in self._acl_entries} for c in self._window.contacts: # Skip the repeater itself if c.public_key_hex == self._contact.public_key_hex: continue # Skip contacts already in ACL if any(c.public_key_hex.startswith(k) for k in acl_keys): continue row = Adw.ActionRow( title=GLib.markup_escape_text(c.name), subtitle=c.public_key_hex[:12] + "..." ) row._contact = c contact_list.append(row) list_wrapper = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) list_wrapper.append(contact_list) contact_scroll.set_child(list_wrapper) contacts_group.add(contact_scroll) content.append(contacts_group) dialog.set_extra_child(content) def _on_response(d, response): if response != "add": return selected = contact_list.get_selected_row() if not selected or not hasattr(selected, "_contact"): return contact = selected._contact perm = 1 if role_row.get_selected() == 0 else 0 self._send_cmds([f"setperm {contact.public_key_hex} {perm}"]) # Refresh ACL after a delay GLib.timeout_add(1000, lambda: (self._request_acl(), False)[1]) dialog.connect("response", _on_response) dialog.present(self._window) # ─── Regions ─────────────────────────────────────────────── def _cancel_reg_timeout(self): if self._reg_timeout_id: GLib.source_remove(self._reg_timeout_id) self._reg_timeout_id = None def _request_regions(self): if not self._contact: return # Cancel previous region fetch self._cancel_reg_timeout() for src_id in list(self._reg_cmd_ids): GLib.source_remove(src_id) self._window.cancel_cli_prefixes(self._reg_prefixes) self._reg_cmd_ids = set() self._reg_prefixes = [] # Clear region list child = self._reg_list.get_first_child() while child: next_c = child.get_next_sibling() self._reg_list.remove(child) child = next_c # Show loading, hide content self._reg_loading_box.set_visible(True) self._reg_content_box.set_visible(False) self._reg_spinner.set_spinning(True) self._reg_status.set_label(_("Loading regions...")) self._reg_retry_btn.set_visible(False) commands = ["region", "region default"] responses = [None, None] remaining = [2] def make_cb(idx): def on_response(text): responses[idx] = text.strip() remaining[0] -= 1 if remaining[0] <= 0: self._cancel_reg_timeout() self._on_regions_loaded(responses) return on_response def on_timeout(): self._reg_timeout_id = None self._window.cancel_cli_prefixes(self._reg_prefixes) self._reg_prefixes = [] received = [r for r in responses if r is not None] if received: self._on_regions_loaded(responses) else: self._on_regions_timeout() self._reg_timeout_id = GLib.timeout_add( self.REQUEST_TIMEOUT_MS, lambda: (on_timeout(), False)[1] ) for i, cmd in enumerate(commands): holder = [0] def send(c=cmd, idx=i, h=holder): self._reg_cmd_ids.discard(h[0]) prefix = f"{self._window.cli_prefix_counter:02X}|" self._reg_prefixes.append(prefix) self._window.send_cli_with_prefix(self._contact.public_key, c, make_cb(idx)) return False holder[0] = GLib.timeout_add(i * 1500, send) self._reg_cmd_ids.add(holder[0]) def _on_regions_timeout(self): self._reg_spinner.set_spinning(False) self._reg_status.set_label(_("Request timed out")) self._reg_retry_btn.set_visible(True) def _on_regions_loaded(self, responses): self._region_entries = [] region_text = responses[0] if len(responses) > 0 and responses[0] else "" default_text = responses[1] if len(responses) > 1 and responses[1] else "" if not region_text: self._on_regions_timeout() return # Parse default scope default_name = "" if "default scope is" in default_text: val = default_text.split("default scope is", 1)[1].strip() if val and val != "": default_name = val self._reg_default_entry.set_text(default_name) self._original_default_scope = default_name # Parse region tree for line in region_text.split("\n"): if not line.strip(): continue indent = len(line) - len(line.lstrip(" ")) stripped = line.strip() flood = stripped.endswith(" F") if flood: stripped = stripped[:-2].strip() home = stripped.endswith("^") if home: stripped = stripped[:-1] self._region_entries.append( { "name": stripped, "indent": indent, "home": home, "flood": flood, } ) self._original_region_entries = copy.deepcopy(self._region_entries) self._pending_region_cmds = [] self._reg_apply_btn.set_sensitive(False) self._rebuild_region_rows() self._reg_loading_box.set_visible(False) self._reg_content_box.set_visible(True) def _rebuild_region_rows(self): child = self._reg_list.get_first_child() while child: next_c = child.get_next_sibling() self._reg_list.remove(child) child = next_c for entry in self._region_entries: is_wildcard = entry["name"] == "*" title = _("Global (wildcard)") if is_wildcard else entry["name"] parts = [] if entry["home"]: parts.append(_("Home")) parts.append(_("Flood allowed") if entry["flood"] else _("Flood denied")) subtitle = " · ".join(parts) row = Adw.ActionRow( title=GLib.markup_escape_text(title), subtitle=subtitle, ) prefix_box = Gtk.Box(spacing=0) visual_indent = max(0, entry["indent"] - 1) if visual_indent > 0: spacer = Gtk.Box() spacer.set_size_request(visual_indent * 16, -1) prefix_box.append(spacer) flood_icon = "check-plain-symbolic" if entry["flood"] else "circle-crossed-symbolic" prefix_box.append(Gtk.Image.new_from_icon_name(flood_icon)) row.add_prefix(prefix_box) menu_btn = Gtk.Button( icon_name="view-more-symbolic", valign=Gtk.Align.CENTER, tooltip_text=_("Actions"), ) menu_btn.add_css_class("flat") menu_btn.connect("clicked", lambda b, e=entry: self._show_reg_popover(b, e)) row.add_suffix(menu_btn) self._reg_list.append(row) def _show_reg_popover(self, button, entry): self._reg_popover_entry = entry is_wildcard = entry["name"] == "*" menu = Gio.Menu() flood_label = _("Deny Flood") if entry["flood"] else _("Allow Flood") menu.append(flood_label, "reg.toggle_flood") if not is_wildcard: if not entry["home"]: menu.append(_("Set as Home"), "reg.set_home") menu.append(_("Remove"), "reg.remove") popover = Gtk.PopoverMenu(menu_model=menu) popover.set_has_arrow(False) popover.set_parent(button) popover.connect("closed", lambda p: GLib.idle_add(p.unparent)) popover.popup() def _mark_regions_dirty(self): self._reg_apply_btn.set_sensitive(True) def _check_regions_dirty(self): if self._pending_region_cmds: return True return self._reg_default_entry.get_text().strip() != self._original_default_scope def _on_reg_default_changed(self): if self._check_regions_dirty(): self._mark_regions_dirty() else: self._reg_apply_btn.set_sensitive(False) def _on_reg_toggle_flood(self): entry = self._reg_popover_entry if not entry: return entry["flood"] = not entry["flood"] cmd = "region allowf" if entry["flood"] else "region denyf" self._pending_region_cmds.append(f'{cmd} {entry["name"]}') self._rebuild_region_rows() self._mark_regions_dirty() def _on_reg_set_home(self): entry = self._reg_popover_entry if not entry: return for e in self._region_entries: e["home"] = False entry["home"] = True self._pending_region_cmds.append(f'region home {entry["name"]}') self._rebuild_region_rows() self._mark_regions_dirty() def _on_reg_remove(self): entry = self._reg_popover_entry if not entry: return idx = self._region_entries.index(entry) if ( idx + 1 < len(self._region_entries) and self._region_entries[idx + 1]["indent"] > entry["indent"] ): self._window.show_toast(_("Remove child regions first")) return self._region_entries.remove(entry) self._pending_region_cmds.append(f'region remove {entry["name"]}') self._rebuild_region_rows() self._mark_regions_dirty() def _on_reg_apply(self): cmds = list(self._pending_region_cmds) current_default = self._reg_default_entry.get_text().strip() if current_default != self._original_default_scope: cmd = ( f"region default {current_default}" if current_default else "region default " ) cmds.append(cmd) if not cmds: return cmds.append("region save") self._send_cmds(cmds) self._pending_region_cmds = [] self._reg_apply_btn.set_sensitive(False) GLib.timeout_add(1500, lambda: (self._request_regions(), False)[1]) def _on_stack_page_changed(self, stack, pspec): new_page = stack.get_visible_child_name() if self._leaving_regions: self._leaving_regions = False self._current_page = new_page return prev_page = self._current_page if prev_page != "regions" or not self._check_regions_dirty(): self._current_page = new_page return self._leaving_regions = True stack.set_visible_child_name("regions") target_page = new_page dialog = Adw.AlertDialog( heading=_("Unsaved Changes"), body=_("You have unsaved region changes."), ) dialog.add_response("discard", _("Discard")) dialog.add_response("apply", _("Apply")) dialog.set_response_appearance("discard", Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_response_appearance("apply", Adw.ResponseAppearance.SUGGESTED) dialog.set_close_response("close") def _on_response(d, response): if response == "apply": self._on_reg_apply() self._leaving_regions = True stack.set_visible_child_name(target_page) elif response == "discard": self._pending_region_cmds = [] self._reg_apply_btn.set_sensitive(False) self._request_regions() self._leaving_regions = True stack.set_visible_child_name(target_page) else: self._leaving_regions = False dialog.connect("response", _on_response) dialog.present(self._window) def _show_region_add_dialog(self): if not self._contact: return dialog = Adw.AlertDialog( heading=_("Add Region"), body=_("Enter a name and optionally choose a parent region."), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("add", _("Add")) dialog.set_response_appearance("add", Adw.ResponseAppearance.SUGGESTED) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) content.set_size_request(300, -1) name_entry = Adw.EntryRow(title=_("Region Name")) name_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) name_list.add_css_class("boxed-list") name_list.append(name_entry) content.append(name_list) parent_names = ( [_("None")] + ["*"] + [e["name"] for e in self._region_entries if e["name"] != "*"] ) parent_row = Adw.ComboRow(title=_("Parent")) parent_model = Gtk.StringList.new(parent_names) parent_row.set_model(parent_model) parent_row.set_selected(0) parent_list = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE) parent_list.add_css_class("boxed-list") parent_list.append(parent_row) content.append(parent_list) dialog.set_extra_child(content) def _on_response(d, response): if response != "add": return name = name_entry.get_text().strip() if not name: return selected = parent_row.get_selected() if selected == 0: parent_entry = None else: parent_name = parent_names[selected] parent_entry = next( (e for e in self._region_entries if e["name"] == parent_name), None ) child_indent = (parent_entry["indent"] + 1) if parent_entry else 1 insert_idx = len(self._region_entries) if parent_entry: pi = self._region_entries.index(parent_entry) insert_idx = pi + 1 while ( insert_idx < len(self._region_entries) and self._region_entries[insert_idx]["indent"] > parent_entry["indent"] ): insert_idx += 1 self._region_entries.insert( insert_idx, { "name": name, "indent": child_indent, "home": False, "flood": True, }, ) if parent_entry: self._pending_region_cmds.append(f"region put {name} {parent_name}") else: self._pending_region_cmds.append(f"region put {name}") self._rebuild_region_rows() self._mark_regions_dirty() dialog.connect("response", _on_response) dialog.present(self._window) # ─── Settings ────────────────────────────────────────────── def _cancel_fetch(self): """Cancel any in-progress settings fetch.""" if self._fetch_timeout_id: GLib.source_remove(self._fetch_timeout_id) self._fetch_timeout_id = None for src_id in self._pending_cmd_ids: GLib.source_remove(src_id) self._window.cancel_cli_prefixes(self._pending_prefixes) self._pending_cmd_ids = [] self._pending_prefixes = [] if self._fetch_spinner: self._fetch_spinner.set_spinning(False) self._fetch_spinner.set_visible(False) if self._fetch_btn: self._fetch_btn.set_sensitive(True) self._fetch_btn = None self._fetch_spinner = None def _fetch_section(self, commands, parser, fetch_btn, spinner): """Fetch a section's settings via sequential CLI commands with retry.""" if not self._contact: return self._cancel_fetch() self._fetch_btn = fetch_btn self._fetch_spinner = spinner fetch_btn.set_sensitive(False) spinner.set_visible(True) spinner.set_spinning(True) responses = [None] * len(commands) self._fetch_next_cmd(commands, responses, 0, parser, False) def _fetch_next_cmd(self, commands, responses, idx, parser, retried): """Send command at idx; on response advance, on timeout retry once.""" if not self._contact: self._cancel_fetch() return if idx >= len(commands): self._cancel_fetch() parser(responses) return def on_response(text): if self._fetch_timeout_id: GLib.source_remove(self._fetch_timeout_id) self._fetch_timeout_id = None responses[idx] = text.strip() self._pending_prefixes = [] self._fetch_next_cmd(commands, responses, idx + 1, parser, False) def on_timeout(): self._fetch_timeout_id = None self._window.cancel_cli_prefixes(self._pending_prefixes) self._pending_prefixes = [] if not retried: self._fetch_next_cmd(commands, responses, idx, parser, True) else: self._fetch_next_cmd(commands, responses, idx + 1, parser, False) self._fetch_timeout_id = GLib.timeout_add( self.FETCH_CMD_TIMEOUT_MS, lambda: (on_timeout(), False)[1] ) prefix = f"{self._window.cli_prefix_counter:02X}|" self._pending_prefixes = [prefix] self._window.send_cli_with_prefix(self._contact.public_key, commands[idx], on_response) def _parse_basic(self, responses): for text in responses: if text is None: continue t = text.strip().lstrip("> ").strip() low = t.lower() if low in ("on", "off", "1", "0", "true", "false"): self._set_repeat.set_active(low in ("on", "1", "true")) elif t and "," not in t and not any(c in t for c in "{}[]"): self._set_name.set_text(t) def _parse_location(self, responses): for i, text in enumerate(responses): if text is None: continue t = text.strip().lstrip("> ").strip() try: val = float(t) if i == 0: self._set_lat.set_text(str(val)) else: self._set_lon.set_text(str(val)) except ValueError: pass def _parse_radio(self, responses): for text in responses: if text is None: continue t = text.strip().lstrip("> ").strip() if "," in t: parts = t.split(",") try: self._set_freq.set_text(parts[0].strip()) self._set_bw.set_text(parts[1].strip()) self._set_sf.set_value(int(float(parts[2].strip()))) if len(parts) > 3: self._set_cr.set_value(int(float(parts[3].strip()))) except (ValueError, IndexError): pass else: with contextlib.suppress(ValueError): self._set_tx.set_value(int(float(t))) def _parse_advert(self, responses): for i, text in enumerate(responses): if text is None: continue t = text.strip().lstrip("> ").strip() try: val = int(float(t)) if i == 0: self._set_advert_int.set_value(val) else: self._set_flood_int.set_value(val) except ValueError: pass def _save_admin_password(self): if not self._contact: return pwd = self._set_admin_pwd.get_text() if not pwd: return self._send_cmds([f"password {pwd}"]) self._set_admin_pwd.set_text("") self._window.show_toast(_("Admin password changed — re-login required")) def _save_guest_password(self): if not self._contact: return pwd = self._set_guest_pwd.get_text() self._send_cmds([f"set guest.password {pwd}"]) def _save_basic(self): if not self._contact: return cmds = [] name = self._set_name.get_text().strip() if name: cmds.append(f"set name {name}") cmds.append(f'set repeat {"on" if self._set_repeat.get_active() else "off"}') self._send_cmds(cmds) def _save_location(self): if not self._contact: return cmds = [] with contextlib.suppress(ValueError): cmds.append(f"set lat {float(self._set_lat.get_text())}") with contextlib.suppress(ValueError): cmds.append(f"set lon {float(self._set_lon.get_text())}") self._send_cmds(cmds) def _save_radio(self): if not self._contact: return cmds = [] freq = self._set_freq.get_text().strip() bw = self._set_bw.get_text().strip() if freq and bw: sf = int(self._set_sf.get_value()) cr = int(self._set_cr.get_value()) cmds.append(f"set radio {freq},{bw},{sf},{cr}") tx = int(self._set_tx.get_value()) if tx > 0: cmds.append(f"set tx {tx}") self._send_cmds(cmds) def _save_advert(self): if not self._contact: return cmds = [ f"set advert.interval {int(self._set_advert_int.get_value())}", f"set flood.advert.interval {int(self._set_flood_int.get_value())}", ] self._send_cmds(cmds) def _send_cmds(self, cmds): if not cmds: return for i, cmd in enumerate(cmds): GLib.timeout_add( i * 300, lambda c=cmd: ( self._window.send_frame(build_send_cli_command(self._contact.public_key, c)) or False ), ) name = self._contact.name if self._contact else "repeater" self._window.show_toast(_("Settings sent to {}").format(name)) def _on_reboot(self, *args): confirm = Adw.AlertDialog( heading=_("Reboot Repeater?"), body=_("Reboot {}? It will be offline briefly.").format(self._contact.name), ) confirm.add_response("cancel", _("Cancel")) confirm.add_response("reboot", _("Reboot")) confirm.set_response_appearance("reboot", Adw.ResponseAppearance.DESTRUCTIVE) confirm.connect( "response", lambda d, r: (self._send_cli("reboot")) if r == "reboot" else None ) confirm.present(self._window) # ─── Public API ──────────────────────────────────────────── def _reset_ui(self): """Reset all UI widgets to default/empty state.""" self._cancel_fetch() # Status rows for w in ( self._st_battery, self._st_uptime, self._st_queue, self._st_errors, self._st_rssi, self._st_snr, self._st_noise, self._st_tx_air, self._st_rx_air, self._st_sent, self._st_recv, self._st_flood, self._st_direct, self._st_dups, ): w.set_subtitle("--") self._st_recv_errors.set_visible(False) # Neighbors child = self._nb_list.get_first_child() while child: next_c = child.get_next_sibling() self._nb_list.remove(child) child = next_c self._nb_offset = 0 self._nb_total = 0 self._nb_neighbors = [] self._nb_spinner.set_spinning(False) self._nb_status.set_label("") self._nb_load_more.set_visible(False) self._nb_map_btn.set_visible(False) # ACL child = self._acl_list.get_first_child() while child: next_c = child.get_next_sibling() self._acl_list.remove(child) child = next_c self._acl_entries = [] self._acl_spinner.set_spinning(False) self._acl_status.set_label("") # Regions child = self._reg_list.get_first_child() while child: next_c = child.get_next_sibling() self._reg_list.remove(child) child = next_c self._region_entries = [] self._original_region_entries = [] self._original_default_scope = "" self._reg_default_entry.set_text("") self._reg_loading_box.set_visible(True) self._reg_content_box.set_visible(False) self._reg_spinner.set_spinning(False) self._reg_status.set_label("") self._reg_retry_btn.set_visible(False) # Settings self._set_name.set_text("") self._set_repeat.set_active(False) self._set_admin_pwd.set_text("") self._set_guest_pwd.set_text("") self._set_lat.set_text("") self._set_lon.set_text("") self._set_freq.set_text("") self._set_bw.set_text("") self._set_sf.set_value(5) self._set_cr.set_value(5) self._set_tx.set_value(1) self._set_advert_int.set_value(0) self._set_flood_int.set_value(0) # CLI self._cli_buf.set_text("") def set_repeater(self, contact: Contact, login_timestamp=None, is_admin=True): """Set the active repeater and switch to status view.""" # Clean up previous repeater state self._cancel_timeout() self._cancel_reg_timeout() self._pending_region_cmds = [] self._leaving_regions = False self._pending_cmd_ids = [] self._pending_prefixes = [] self._window.set_cli_response_callback(None) self._window.set_status_callback(None) self._window.set_neighbors_callback(None) self._window.set_acl_callback(None) self._reset_ui() self._contact = contact self._is_admin = is_admin # Show/hide admin-only tabs for name in ("cli", "access", "settings", "regions"): child = self._stack.get_child_by_name(name) if child: self._stack.get_page(child).set_visible(is_admin) # Show/hide clock sync controls (admin only) self._st_clock.set_visible(is_admin) # Display the repeater's clock time from the login response if is_admin and login_timestamp: import time from datetime import datetime dt = datetime.fromtimestamp(login_timestamp) self._st_clock.set_subtitle(dt.strftime("%Y-%m-%d %H:%M:%S")) now = int(time.time()) drift = now - login_timestamp # positive = behind, negative = ahead if abs(drift) > 300: # more than 5 minutes off self._clock_drift_label.set_label(_("Time is off")) self._clock_drift_label.set_visible(True) if drift > 0: # Repeater clock is behind — can set time forward self._clock_sync_btn.set_visible(True) self._clock_reset_btn.set_visible(False) else: # Repeater clock is ahead — firmware rejects setting time back self._clock_sync_btn.set_visible(False) self._clock_reset_btn.set_visible(True) else: self._clock_drift_label.set_visible(False) self._clock_sync_btn.set_visible(False) self._clock_reset_btn.set_visible(False) else: self._st_clock.set_subtitle("--") self._clock_drift_label.set_visible(False) self._clock_sync_btn.set_visible(False) self._clock_reset_btn.set_visible(False) self._switch_tab("status") # Clear CLI output self._cli_buf.set_text("") # Register CLI callback for admin if is_admin: self._window.set_cli_response_callback(self.on_cli_response) self._regions_pending = True # Request status (regions will load after status completes) self._request_status() def _on_logout(self, *args): if self._contact: self._window.logout_repeater(self._contact.public_key_hex) def clear(self): self._cancel_timeout() self._cancel_reg_timeout() self._cancel_fetch() self._pending_region_cmds = [] self._reg_apply_btn.set_sensitive(False) self._contact = None self._current_page = "placeholder" self._stack.set_visible_child_name("placeholder") self._window.set_cli_response_callback(None) self._window.set_status_callback(None) self._window.set_neighbors_callback(None) self._window.set_acl_callback(None) meshy/src/views/settings_view.py000066400000000000000000001473421521052255700174120ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Settings view for radio configuration and device settings.""" import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk from meshy.models import DeviceInfo # (name, freq_mhz, bw_hz, sf, cr, tx_dbm) RADIO_PRESETS = [ ("Australia", 915.8, 250000, 10, 5, 20), ("Australia (Narrow)", 916.575, 62500, 7, 5, 20), ("Australia SA, WA, QLD", 923.125, 62500, 8, 5, 20), ("Czech Republic", 869.432, 62500, 7, 5, 14), ("EU 433MHz", 433.650, 250000, 11, 5, 20), ("EU/UK (Long Range)", 869.525, 250000, 11, 5, 14), ("EU/UK (Medium Range)", 869.525, 250000, 10, 5, 14), ("EU/UK (Narrow)", 869.618, 62500, 8, 5, 14), ("New Zealand", 917.375, 250000, 11, 5, 20), ("New Zealand (Narrow)", 917.375, 62500, 7, 5, 20), ("Portugal 433", 433.375, 62500, 9, 5, 20), ("Portugal 869", 869.618, 62500, 7, 5, 14), ("Switzerland", 869.618, 62500, 8, 5, 14), ("USA Arizona", 908.205, 62500, 10, 5, 20), ("USA/Canada", 910.525, 62500, 7, 5, 20), ("Vietnam", 920.250, 250000, 11, 5, 20), ("Off-Grid 433", 433.0, 250000, 11, 5, 20), ("Off-Grid 869", 869.0, 250000, 11, 5, 14), ("Off-Grid 918", 918.0, 250000, 11, 5, 20), ] def _off_grid_freq_for(freq_mhz: float) -> float: """Pick the off-grid frequency matching the current band.""" if freq_mhz < 500: return 433.0 if freq_mhz < 900: return 869.0 return 918.0 def _off_grid_preset_for(freq_mhz: float) -> int: """Return RADIO_PRESETS index for the matching Off-Grid preset.""" target = _off_grid_freq_for(freq_mhz) for i, (name, freq, *_rest) in enumerate(RADIO_PRESETS): if name.startswith("Off-Grid") and abs(freq - target) < 0.01: return i return -1 @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/settings-view.ui") class SettingsView(Gtk.Box): __gtype_name__ = "MeshySettingsView" _pref_page = Gtk.Template.Child("pref_page") _app_group = Gtk.Template.Child("app_group") _style_row = Gtk.Template.Child("style_row") _theme_row = Gtk.Template.Child("theme_row") _channel_notif_row = Gtk.Template.Child("channel_notif_row") _background_switch = Gtk.Template.Child("background_switch") _autostart_switch = Gtk.Template.Child("autostart_switch") _advert_group = Gtk.Template.Child("advert_group") _name_entry = Gtk.Template.Child("name_entry") _advert_loc_switch = Gtk.Template.Child("advert_loc_switch") _loc_group = Gtk.Template.Child("loc_group") _gps_switch = Gtk.Template.Child("gps_switch") _lat_entry = Gtk.Template.Child("lat_entry") _lon_entry = Gtk.Template.Child("lon_entry") _locate_btn = Gtk.Template.Child("locate_btn") _pick_map_btn = Gtk.Template.Child("pick_map_btn") _radio_group = Gtk.Template.Child("radio_group") _preset_row = Gtk.Template.Child("preset_row") _repeat_switch = Gtk.Template.Child("repeat_switch") _radio_advanced = Gtk.Template.Child("radio_advanced") _freq_entry = Gtk.Template.Child("freq_entry") _bw_entry = Gtk.Template.Child("bw_entry") _sf_spin = Gtk.Template.Child("sf_spin") _cr_spin = Gtk.Template.Child("cr_spin") _tx_spin = Gtk.Template.Child("tx_spin") _path_hash_combo = Gtk.Template.Child("path_hash_combo") _routing_group = Gtk.Template.Child("routing_group") _multi_ack_spin = Gtk.Template.Child("multi_ack_spin") _auto_add_group = Gtk.Template.Child("auto_add_group") _auto_add_switch = Gtk.Template.Child("auto_add_switch") _auto_add_expander = Gtk.Template.Child("auto_add_expander") _auto_chat_switch = Gtk.Template.Child("auto_chat_switch") _auto_repeater_switch = Gtk.Template.Child("auto_repeater_switch") _auto_room_switch = Gtk.Template.Child("auto_room_switch") _auto_sensor_switch = Gtk.Template.Child("auto_sensor_switch") _auto_overwrite_switch = Gtk.Template.Child("auto_overwrite_switch") _auto_hop_limit_entry = Gtk.Template.Child("auto_hop_limit_entry") _tele_group = Gtk.Template.Child("tele_group") _tele_base_combo = Gtk.Template.Child("tele_base_combo") _tele_loc_combo = Gtk.Template.Child("tele_loc_combo") _tele_env_combo = Gtk.Template.Child("tele_env_combo") _regions_group = Gtk.Template.Child("regions_group") _add_region_entry = Gtk.Template.Child("add_region_entry") _default_scope_combo = Gtk.Template.Child("default_scope_combo") _bt_group = Gtk.Template.Child("bt_group") _pin_entry = Gtk.Template.Child("pin_entry") def __init__(self, window, **kwargs): super().__init__(**kwargs) self._window = window self._apply_btn: Gtk.Button | None = None self._clean_snapshot: dict | None = None self._suppressing_changes = False self._applying = False # True while Apply commands are in flight self._applying_preset = False # True while preset is populating fields self._pre_repeat_snapshot: dict | None = None # radio params before repeat on self._region_rows: dict[str, Adw.ActionRow] = {} self._section_widgets = [ (_("Application"), self._app_group), (_("Advert"), self._advert_group), (_("Routing and Messaging"), self._routing_group), (_("Location"), self._loc_group), (_("Radio"), self._radio_group), (_("Auto-Add Contacts"), self._auto_add_group), (_("Telemetry Privacy"), self._tele_group), (_("Regions"), self._regions_group), (_("Bluetooth"), self._bt_group), ] # Preset ComboRow model — built from RADIO_PRESETS data preset_model = Gtk.StringList() preset_model.append(_("Custom")) for name, *_rest in RADIO_PRESETS: preset_model.append(name) self._preset_row.set_model(preset_model) self._preset_row.connect("notify::selected", self._on_preset_selected) self._repeat_switch.connect("notify::active", self._on_repeat_toggled) # Telemetry combo models — shared label set tele_labels = Gtk.StringList.new([_("Deny All"), _("Allow per Contact"), _("Allow All")]) self._tele_base_combo.set_model(tele_labels) self._tele_loc_combo.set_model(tele_labels) self._tele_env_combo.set_model(tele_labels) # Default scope combo — starts with "None", regions added dynamically self._scope_model = Gtk.StringList() self._scope_model.append(_("None")) self._default_scope_combo.set_model(self._scope_model) self._default_scope_combo.connect("notify::selected", self._on_default_scope_changed) # Signal connections self._gps_switch.connect("notify::active", self._on_gps_toggled) self._locate_btn.connect("clicked", self._on_locate_clicked) self._pick_map_btn.connect("clicked", self._on_pick_map_clicked) self._add_region_entry.connect("apply", self._on_add_region) # Connect change signals for dirty tracking for entry in (self._name_entry, self._lat_entry, self._lon_entry): entry.connect("changed", lambda *_: self._check_dirty()) self._pin_entry.connect("changed", self._on_pin_changed) # Radio preset params — also reset preset to Custom when edited manually def _on_radio_param_changed(*_args): if ( not self._suppressing_changes and not self._applying_preset and self._preset_row.get_selected() != 0 ): self._preset_row.set_selected(0) self._check_dirty() for entry in (self._freq_entry, self._bw_entry): entry.connect("changed", _on_radio_param_changed) for spin in (self._sf_spin, self._cr_spin): spin.connect("notify::value", _on_radio_param_changed) self._tx_spin.connect("notify::value", lambda *_: self._check_dirty()) self._multi_ack_spin.connect("notify::value", lambda *_: self._check_dirty()) self._auto_add_switch.connect("notify::active", self._on_auto_add_toggled) for switch in ( self._advert_loc_switch, self._repeat_switch, self._gps_switch, self._auto_add_switch, self._auto_chat_switch, self._auto_repeater_switch, self._auto_room_switch, self._auto_sensor_switch, self._auto_overwrite_switch, ): switch.connect("notify::active", lambda *_: self._check_dirty()) self._auto_hop_limit_entry.connect("changed", lambda *_: self._check_dirty()) self._preset_row.connect("notify::selected", lambda *_: self._check_dirty()) self._path_hash_combo.connect("notify::selected", lambda *_: self._check_dirty()) for combo in (self._tele_base_combo, self._tele_loc_combo, self._tele_env_combo): combo.connect("notify::selected", lambda *_: self._check_dirty()) self._init_style_settings() def _init_style_settings(self): from meshy.theme_manager import THEMES settings = Gio.Settings(schema_id="page.codeberg.sesivany.Meshy") self._app_settings = settings mode = settings.get_string("color-scheme") scheme_options = ["system", "light", "dark", "custom"] idx = scheme_options.index(mode) if mode in scheme_options else 0 self._style_row.set_selected(idx) self._theme_row.set_visible(idx == 3) theme_id = settings.get_string("custom-theme") theme = THEMES.get(theme_id) if theme: self._theme_row.set_subtitle(theme["name"]) self._style_row.connect("notify::selected", self._on_style_changed) self._theme_row.connect("activated", self._on_theme_row_activated) notif_level = settings.get_int("default-channel-notification-level") self._channel_notif_row.set_selected(max(0, min(notif_level, 2))) self._channel_notif_row.connect("notify::selected", self._on_channel_notif_changed) self._background_switch.set_active(settings.get_boolean("run-in-background")) self._background_switch.connect("notify::active", self._on_background_changed) self._autostart_switch.set_active(settings.get_boolean("autostart")) self._autostart_switch.connect("notify::active", self._on_autostart_changed) def _on_background_changed(self, row, pspec): enabled = row.get_active() self._app_settings.set_boolean("run-in-background", enabled) app = self._window.get_application() if app: app.update_background_mode(enabled) def _on_autostart_changed(self, row, pspec): enabled = row.get_active() self._app_settings.set_boolean("autostart", enabled) app = self._window.get_application() if app: app.update_autostart(enabled) def _on_channel_notif_changed(self, row, pspec): self._app_settings.set_int("default-channel-notification-level", row.get_selected()) def _on_style_changed(self, row, pspec): from meshy.theme_manager import ThemeManager scheme_options = ["system", "light", "dark", "custom"] idx = row.get_selected() mode = scheme_options[idx] if idx < len(scheme_options) else "system" manager = ThemeManager.get_default() if mode == "custom": theme_id = self._app_settings.get_string("custom-theme") manager.set_custom_theme(theme_id) else: manager.set_color_scheme(mode) self._theme_row.set_visible(idx == 3) def _on_theme_row_activated(self, row): from meshy.theme_chooser_dialog import ThemeChooserDialog dialog = ThemeChooserDialog(self._app_settings, self._on_custom_theme_changed) dialog.present(self._window) def _on_custom_theme_changed(self): from meshy.theme_manager import THEMES theme_id = self._app_settings.get_string("custom-theme") theme = THEMES.get(theme_id) if theme: self._theme_row.set_subtitle(theme["name"]) def create_apply_button(self) -> Gtk.Button: """Create an Apply button to be placed in the headerbar.""" btn = Gtk.Button(label=_("Apply")) btn.add_css_class("suggested-action") btn.set_sensitive(False) btn.connect("clicked", self._on_apply_all) self._apply_btn = btn return btn @property def sections(self) -> list[str]: return [name for name, _w in self._section_widgets] def scroll_to_section(self, section_name: str): for name, widget in self._section_widgets: if name == section_name: GLib.idle_add(self._do_scroll_to, widget) break def _do_scroll_to(self, widget): scrolled = widget.get_ancestor(Gtk.ScrolledWindow) if scrolled: success, x, y = widget.translate_coordinates(scrolled.get_child(), 0, 0) if success: adj = scrolled.get_vadjustment() adj.set_value(y) return False def get_auto_add_config(self) -> dict: return { "auto_add_chat": self._auto_chat_switch.get_active(), "auto_add_repeater": self._auto_repeater_switch.get_active(), "auto_add_room_server": self._auto_room_switch.get_active(), "auto_add_sensor": self._auto_sensor_switch.get_active(), "overwrite_oldest": self._auto_overwrite_switch.get_active(), "max_hops": int(self._auto_hop_limit_entry.get_text() or "0"), } def restore_snapshot(self): """Revert all widgets to the last clean state (discard unapplied changes).""" snap = self._clean_snapshot if not snap: return self._suppressing_changes = True self._name_entry.set_text(snap.get("name", "")) self._advert_loc_switch.set_active(snap.get("advert_loc", False)) self._lat_entry.set_text(snap.get("lat", "")) self._lon_entry.set_text(snap.get("lon", "")) self._freq_entry.set_text(snap.get("freq", "")) self._bw_entry.set_text(snap.get("bw", "")) self._sf_spin.set_value(snap.get("sf", 7)) self._cr_spin.set_value(snap.get("cr", 5)) self._tx_spin.set_value(snap.get("tx", 20)) self._preset_row.set_selected(snap.get("preset", 0)) self._repeat_switch.set_active(snap.get("repeat", False)) self._path_hash_combo.set_selected(snap.get("path_hash", 0)) self._multi_ack_spin.set_value(snap.get("multi_ack", 1)) self._auto_add_switch.set_active(snap.get("auto_add", False)) self._auto_add_expander.set_sensitive(snap.get("auto_add", False)) self._auto_chat_switch.set_active(snap.get("auto_chat", False)) self._auto_repeater_switch.set_active(snap.get("auto_repeater", False)) self._auto_room_switch.set_active(snap.get("auto_room", False)) self._auto_sensor_switch.set_active(snap.get("auto_sensor", False)) self._auto_overwrite_switch.set_active(snap.get("auto_overwrite", False)) self._auto_hop_limit_entry.set_text(snap.get("auto_hop_limit", "")) self._tele_base_combo.set_selected(snap.get("tele_base", 0)) self._tele_loc_combo.set_selected(snap.get("tele_loc", 0)) self._tele_env_combo.set_selected(snap.get("tele_env", 0)) self._gps_switch.set_active(snap.get("gps", self._gps_switch.get_active())) self._pin_entry.set_text(snap.get("pin", "")) self._suppressing_changes = False if self._apply_btn: self._apply_btn.set_sensitive(False) def _take_snapshot(self): """Capture current widget values as the 'clean' state.""" self._clean_snapshot = { "name": self._name_entry.get_text(), "advert_loc": self._advert_loc_switch.get_active(), "lat": self._lat_entry.get_text(), "lon": self._lon_entry.get_text(), "freq": self._freq_entry.get_text(), "bw": self._bw_entry.get_text(), "sf": self._sf_spin.get_value(), "cr": self._cr_spin.get_value(), "tx": self._tx_spin.get_value(), "preset": self._preset_row.get_selected(), "repeat": self._repeat_switch.get_active(), "path_hash": self._path_hash_combo.get_selected(), "multi_ack": self._multi_ack_spin.get_value(), "auto_add": self._auto_add_switch.get_active(), "auto_chat": self._auto_chat_switch.get_active(), "auto_repeater": self._auto_repeater_switch.get_active(), "auto_room": self._auto_room_switch.get_active(), "auto_sensor": self._auto_sensor_switch.get_active(), "auto_overwrite": self._auto_overwrite_switch.get_active(), "auto_hop_limit": self._auto_hop_limit_entry.get_text(), "tele_base": self._tele_base_combo.get_selected(), "tele_loc": self._tele_loc_combo.get_selected(), "tele_env": self._tele_env_combo.get_selected(), "gps": self._gps_switch.get_active(), "pin": self._pin_entry.get_text(), } if self._apply_btn: self._apply_btn.set_sensitive(False) def _check_dirty(self): """Enable Apply button if any setting differs from the clean snapshot.""" if self._suppressing_changes or not self._clean_snapshot or not self._apply_btn: return snap = self._clean_snapshot dirty = ( self._name_entry.get_text() != snap["name"] or self._advert_loc_switch.get_active() != snap["advert_loc"] or self._lat_entry.get_text() != snap["lat"] or self._lon_entry.get_text() != snap["lon"] or self._freq_entry.get_text() != snap["freq"] or self._bw_entry.get_text() != snap["bw"] or self._sf_spin.get_value() != snap["sf"] or self._cr_spin.get_value() != snap["cr"] or self._tx_spin.get_value() != snap["tx"] or self._preset_row.get_selected() != snap["preset"] or self._repeat_switch.get_active() != snap.get("repeat", False) or self._path_hash_combo.get_selected() != snap.get("path_hash", 0) or self._multi_ack_spin.get_value() != snap.get("multi_ack", 1) or self._auto_add_switch.get_active() != snap.get("auto_add", False) or self._auto_chat_switch.get_active() != snap["auto_chat"] or self._auto_repeater_switch.get_active() != snap["auto_repeater"] or self._auto_room_switch.get_active() != snap["auto_room"] or self._auto_sensor_switch.get_active() != snap["auto_sensor"] or self._auto_overwrite_switch.get_active() != snap["auto_overwrite"] or self._auto_hop_limit_entry.get_text() != snap.get("auto_hop_limit", "") or self._tele_base_combo.get_selected() != snap.get("tele_base", 0) or self._tele_loc_combo.get_selected() != snap.get("tele_loc", 0) or self._tele_env_combo.get_selected() != snap.get("tele_env", 0) or self._gps_switch.get_active() != snap.get("gps", False) or self._pin_entry.get_text() != snap.get("pin", "") ) pin_ok = self._is_pin_valid(self._pin_entry.get_text().strip()) self._apply_btn.set_sensitive(dirty and pin_ok) def _detect_preset(self, info: DeviceInfo): """Set the preset dropdown to match current radio params, or Custom.""" if not info.radio_freq: return for i, (_, freq, bw, sf, cr, _tx) in enumerate(RADIO_PRESETS): if ( abs(info.radio_freq - freq) < 0.01 and abs(info.radio_bw - bw / 1000) < 0.01 and info.radio_sf == sf and info.radio_cr == cr ): self._preset_row.set_selected(i + 1) # +1 for "Custom" at index 0 return self._preset_row.set_selected(0) # Custom def _on_preset_selected(self, combo, *args): idx = combo.get_selected() if idx == 0: # "Custom" — don't change fields return self._applying_preset = True _, freq, bw, sf, cr, _tx = RADIO_PRESETS[idx - 1] self._freq_entry.set_text(str(freq)) self._bw_entry.set_text(str(bw / 1000)) # Hz to kHz for display self._sf_spin.set_value(sf) self._cr_spin.set_value(cr) self._applying_preset = False # TX power is intentionally not set — it's independent of the preset def _on_repeat_toggled(self, switch, *args): if self._suppressing_changes: return active = switch.get_active() if active: self._pre_repeat_snapshot = { "freq": self._freq_entry.get_text(), "bw": self._bw_entry.get_text(), "sf": self._sf_spin.get_value(), "cr": self._cr_spin.get_value(), "preset": self._preset_row.get_selected(), } try: cur_freq = float(self._freq_entry.get_text()) except ValueError: cur_freq = 918.0 og_idx = _off_grid_preset_for(cur_freq) if og_idx >= 0: self._suppressing_changes = True self._applying_preset = True _, freq, bw, sf, cr, _tx = RADIO_PRESETS[og_idx] self._freq_entry.set_text(str(freq)) self._bw_entry.set_text(str(bw / 1000)) self._sf_spin.set_value(sf) self._cr_spin.set_value(cr) self._preset_row.set_selected(og_idx + 1) self._applying_preset = False self._suppressing_changes = False else: snap = self._pre_repeat_snapshot if snap: self._suppressing_changes = True self._applying_preset = True self._freq_entry.set_text(snap["freq"]) self._bw_entry.set_text(snap["bw"]) self._sf_spin.set_value(snap["sf"]) self._cr_spin.set_value(snap["cr"]) self._preset_row.set_selected(snap["preset"]) self._applying_preset = False self._suppressing_changes = False self._pre_repeat_snapshot = None self._preset_row.set_sensitive(not active) self._radio_advanced.set_sensitive(not active) self._check_dirty() def _on_auto_add_toggled(self, switch, *args): if self._suppressing_changes: return self._auto_add_expander.set_sensitive(switch.get_active()) def _on_apply_all(self, *args): """Apply all settings to the device, sending only changed values.""" snap = self._clean_snapshot or {} cmds = [] # list of callables to send in sequence # Advert name = self._name_entry.get_text().strip() if name and name != snap.get("name", ""): cmds.append(lambda: self._window.set_advert_name(name)) # Location — send latlon first, then loc policy via set_other_params lat_text = self._lat_entry.get_text().strip() lon_text = self._lon_entry.get_text().strip() try: lat = float(lat_text) if lat_text else 0.0 lon = float(lon_text) if lon_text else 0.0 if lat_text != snap.get("lat", "") or lon_text != snap.get("lon", ""): cmds.append(lambda _lat=lat, _lon=lon: self._window.set_advert_latlon(_lat, _lon)) except ValueError: pass advert_loc = self._advert_loc_switch.get_active() if advert_loc != snap.get("advert_loc", False): policy = 1 if advert_loc else 0 cmds.append(lambda: self._window.set_advert_loc_policy(policy)) # Companion GPS gps = self._gps_switch.get_active() if gps != snap.get("gps", False): value = "1" if gps else "0" cmds.append(lambda: self._window.set_custom_var(f"gps:{value}")) # Telemetry privacy tele_base = self._tele_base_combo.get_selected() tele_loc = self._tele_loc_combo.get_selected() tele_env = self._tele_env_combo.get_selected() if ( tele_base != snap.get("tele_base", 0) or tele_loc != snap.get("tele_loc", 0) or tele_env != snap.get("tele_env", 0) ): cmds.append(lambda: self._window.set_telemetry_mode(tele_base, tele_loc, tele_env)) # Radio try: freq_mhz = float(self._freq_entry.get_text()) bw_khz = float(self._bw_entry.get_text()) sf = int(self._sf_spin.get_value()) cr = int(self._cr_spin.get_value()) power = int(self._tx_spin.get_value()) repeat = self._repeat_switch.get_active() repeat_changed = repeat != snap.get("repeat", False) radio_changed = ( self._freq_entry.get_text() != snap.get("freq", "") or self._bw_entry.get_text() != snap.get("bw", "") or sf != snap.get("sf", 0) or cr != snap.get("cr", 0) ) if radio_changed or repeat_changed: rpt = repeat if repeat_changed else None dev_cr = cr - 4 if self._window._device_info.cr_old_encoding else cr cmds.append( lambda: self._window.set_radio_params( int(freq_mhz * 1000), int(bw_khz * 1000), sf, dev_cr, rpt ) ) if power != snap.get("tx", 0): cmds.append(lambda: self._window.set_tx_power(power)) except ValueError: pass # Path hash mode if ( self._path_hash_combo.get_sensitive() and self._path_hash_combo.get_selected() != snap.get("path_hash", 0) ): mode = self._path_hash_combo.get_selected() cmds.append(lambda: self._window.set_path_hash_mode(mode)) # Multi ACK (display 1/2 → firmware 0/1) multi_ack_val = int(self._multi_ack_spin.get_value()) if multi_ack_val != int(snap.get("multi_ack", 1)): fw_val = multi_ack_val - 1 cmds.append(lambda: self._window.set_multi_acks(fw_val)) # Auto-add auto_add = self._auto_add_switch.get_active() hop_text = self._auto_hop_limit_entry.get_text().strip() hop_limit = int(hop_text) if hop_text.isdigit() else 0 if ( auto_add != snap.get("auto_add", False) or self._auto_chat_switch.get_active() != snap.get("auto_chat", False) or self._auto_repeater_switch.get_active() != snap.get("auto_repeater", False) or self._auto_room_switch.get_active() != snap.get("auto_room", False) or self._auto_sensor_switch.get_active() != snap.get("auto_sensor", False) or self._auto_overwrite_switch.get_active() != snap.get("auto_overwrite", False) or self._auto_hop_limit_entry.get_text() != snap.get("auto_hop_limit", "") ): from meshy.protocol import build_set_auto_add_config if auto_add: frame = build_set_auto_add_config( chat=self._auto_chat_switch.get_active(), repeater=self._auto_repeater_switch.get_active(), room=self._auto_room_switch.get_active(), sensor=self._auto_sensor_switch.get_active(), overwrite_oldest=self._auto_overwrite_switch.get_active(), max_hops=hop_limit, ) else: frame = build_set_auto_add_config() cmds.append(lambda: self._window.send_frame(frame)) # BLE PIN pin_text = self._pin_entry.get_text().strip() if pin_text != snap.get("pin", "") and pin_text.isdigit(): pin_val = int(pin_text) cmds.append(lambda: self._window.set_device_pin(pin_val)) if not cmds: return self._applying = True self._take_snapshot() self._send_next_setting(cmds, 0) def _send_next_setting(self, cmds, index): if index >= len(cmds): self._applying = False self._window.show_toast(_("Settings applied on device")) from meshy.protocol import build_device_query self._window.send_frame(build_device_query()) return self._window._frame_handler.expect_ok( on_ok=lambda: self._send_next_setting(cmds, index + 1), on_error=lambda err: self._on_setting_error(err), ) cmds[index]() def _on_setting_error(self, err): self._applying = False msg = err["message"] if err else _("Unknown error") self._window.show_toast(_("Settings error: {}").format(msg)) def update_device_info(self, info: DeviceInfo): """Update fields from device info.""" if self._applying: return self._suppressing_changes = True if info.radio_freq: self._freq_entry.set_text(str(info.radio_freq)) if info.radio_bw: self._bw_entry.set_text(str(info.radio_bw)) if info.radio_sf: self._sf_spin.set_value(info.radio_sf) if info.radio_cr: self._cr_spin.set_value(info.radio_cr) if info.max_tx_power: adj = self._tx_spin.get_adjustment() adj.set_upper(info.max_tx_power) self._tx_spin.set_title(_("TX Power (dBm, max {})").format(info.max_tx_power)) if info.tx_power: self._tx_spin.set_value(info.tx_power) if info.fw_ver >= 9: self._repeat_switch.set_visible(True) self._repeat_switch.set_active(info.repeat) self._preset_row.set_sensitive(not info.repeat) self._radio_advanced.set_sensitive(not info.repeat) if info.fw_ver >= 10: self._path_hash_combo.set_selected(info.path_hash_mode) self._path_hash_combo.set_sensitive(True) self._multi_ack_spin.set_value(info.multi_acks + 1) self._multi_ack_spin.set_sensitive(True) if info.name: self._name_entry.set_text(info.name) if info.ble_pin: self._pin_entry.set_text(str(info.ble_pin)) # Auto-detect matching preset from current radio params self._detect_preset(info) self._suppressing_changes = False self._take_snapshot() def update_transport_type(self, transport_type: str): """Show Bluetooth group only when connected via BLE.""" self._bt_group.set_visible(transport_type == "ble") def update_gps_settings(self, custom_vars: dict): """Update GPS toggle from custom vars.""" if "gps" in custom_vars: self._gps_switch.set_visible(True) self._suppressing_changes = True self._gps_switch.set_active(custom_vars.get("gps") == "1") self._suppressing_changes = False if self._clean_snapshot is not None: self._clean_snapshot["gps"] = self._gps_switch.get_active() def _on_gps_toggled(self, switch, *args): if self._suppressing_changes: return def _is_pin_valid(self, text: str) -> bool: if not text: return True if not text.isdigit(): return False val = int(text) return val == 0 or 100000 <= val <= 999999 def _on_pin_changed(self, entry, *args): text = entry.get_text().strip() if self._is_pin_valid(text): entry.remove_css_class("error") else: entry.add_css_class("error") self._check_dirty() def update_telemetry_modes(self, base: int, loc: int, env: int): """Populate the telemetry privacy combos from device self_info.""" if self._applying: return self._suppressing_changes = True self._tele_base_combo.set_selected(base) self._tele_loc_combo.set_selected(loc) self._tele_env_combo.set_selected(env) self._suppressing_changes = False self._take_snapshot() def update_multi_acks(self, value: int): if self._applying: return self._suppressing_changes = True self._multi_ack_spin.set_value(value + 1) self._suppressing_changes = False self._take_snapshot() def update_advert_location(self, lat: float, lon: float, loc_policy: int = 0): """Populate the lat/lon fields and location policy from the device.""" if self._applying: return self._suppressing_changes = True self._advert_loc_switch.set_active(loc_policy > 0) if abs(lat) > 1e-6 or abs(lon) > 1e-6: self._lat_entry.set_text(str(lat)) self._lon_entry.set_text(str(lon)) self._suppressing_changes = False self._take_snapshot() def _on_locate_clicked(self, btn): """Use GeoClue to get the computer's location and fill lat/lon fields.""" import gi gi.require_version("Geoclue", "2.0") from gi.repository import Geoclue btn.set_sensitive(False) def _on_ready(source, result, *args): try: geoclue = Geoclue.Simple.new_finish(result) loc = geoclue.get_location() lat = loc.get_property("latitude") lon = loc.get_property("longitude") self._lat_entry.set_text(f"{lat:.6f}") self._lon_entry.set_text(f"{lon:.6f}") self._window.show_toast(_("Location filled, click Apply to save")) except Exception: self._window.show_toast(_("Location unavailable")) btn.set_sensitive(True) Geoclue.Simple.new( "page.codeberg.sesivany.Meshy", Geoclue.AccuracyLevel.EXACT, None, _on_ready, ) def _on_pick_map_clicked(self, btn): """Open a map dialog where the user can click to pick a location.""" from meshy.views import open_map_picker init_lat, init_lon = 0.0, 0.0 try: lat = float(self._lat_entry.get_text()) lon = float(self._lon_entry.get_text()) if abs(lat) > 1e-6 or abs(lon) > 1e-6: init_lat, init_lon = lat, lon except ValueError: pass def _on_pick(lat, lon): self._lat_entry.set_text(f"{lat:.6f}") self._lon_entry.set_text(f"{lon:.6f}") open_map_picker( self._window, _("Pick Location"), _("Set Location"), init_lat, init_lon, _on_pick ) def load_regions(self): """Load regions from storage and populate the UI.""" if not self._window.storage: return for name in self._window.storage.get_regions(): self._add_region_row(name) def _on_add_region(self, entry): name = entry.get_text().strip() if not name: return # Strip leading # if user typed it — stored without hashtag if name.startswith("#"): name = name[1:] if not name: return if name in self._region_rows: return if self._window.storage: self._window.storage.add_region(name) self._add_region_row(name) entry.set_text("") def _add_region_row(self, name: str): if name in self._region_rows: return row = Adw.ActionRow(title=name) remove_btn = Gtk.Button( icon_name="edit-delete-symbolic", valign=Gtk.Align.CENTER, ) remove_btn.add_css_class("flat") remove_btn.connect("clicked", lambda *_, n=name: self._remove_region(n)) row.add_suffix(remove_btn) self._regions_group.add(row) self._region_rows[name] = row self._scope_model.append(name) def _remove_region(self, name: str): row = self._region_rows.pop(name, None) if row: self._regions_group.remove(row) # Remove from scope combo for i in range(self._scope_model.get_n_items()): if self._scope_model.get_string(i) == name: if self._default_scope_combo.get_selected() == i: self._default_scope_combo.set_selected(0) self._scope_model.remove(i) break if self._window.storage: self._window.storage.remove_region(name) # Clear region from in-memory channels too affected = False for ch in self._window.channels: if ch.region == name: ch.region = "" affected = True if affected: self._window._channels_view.update_channels(self._window.channels) def get_regions(self) -> list[str]: return list(self._region_rows.keys()) @property def default_scope(self) -> str: idx = self._default_scope_combo.get_selected() if idx == 0 or idx == Gtk.INVALID_LIST_POSITION: return "" return self._scope_model.get_string(idx) or "" def _on_default_scope_changed(self, combo, *args): if self._suppressing_changes: return scope = self.default_scope if self._window.storage: self._window.storage.set_default_scope(scope) if self._window.is_connected: from meshy.protocol import build_set_flood_scope self._window.send_frame(build_set_flood_scope(scope)) def restore_default_scope(self): """Restore default scope selection from storage.""" if not self._window.storage: return scope = self._window.storage.get_default_scope() if not scope: return self._suppressing_changes = True for i in range(self._scope_model.get_n_items()): if self._scope_model.get_string(i) == scope: self._default_scope_combo.set_selected(i) break self._suppressing_changes = False def _on_discover_regions(self, *args): """Query repeater contacts for their regions and let user pick.""" from meshy.models import ContactType if not self._window.is_connected: self._window.show_toast(_("Connect to a device first")) return repeaters = [ c for c in self._window.contacts if c.type in (ContactType.REPEATER, ContactType.ROOM) ] if not repeaters: self._window.show_toast(_("No repeaters in contacts")) return dialog = Adw.Dialog() dialog.set_title(_("Discover Regions")) dialog.set_content_width(420) dialog.set_content_height(480) toolbar_view = Adw.ToolbarView() header = Adw.HeaderBar() toolbar_view.add_top_bar(header) content = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=8, margin_start=8, margin_end=8, margin_bottom=8, ) status_box = Gtk.Box(spacing=8, halign=Gtk.Align.CENTER) spinner = Gtk.Spinner(spinning=True) status_box.append(spinner) status_label = Gtk.Label( label=( _("Querying {} repeaters...").format(len(repeaters)) if len(repeaters) != 1 else _("Querying {} repeater...").format(len(repeaters)) ) ) status_label.add_css_class("dim-label") status_box.append(status_label) content.append(status_box) scroll = Gtk.ScrolledWindow(vexpand=True) list_box = Gtk.ListBox() list_box.set_selection_mode(Gtk.SelectionMode.NONE) list_box.add_css_class("boxed-list") placeholder = Adw.StatusPage( icon_name="edit-find-symbolic", title=_("Waiting..."), description=_("Querying repeaters for regions"), ) list_box.set_placeholder(placeholder) list_wrapper = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, margin_start=8, margin_end=8, margin_top=8, margin_bottom=8, ) list_wrapper.append(list_box) scroll.set_child(list_wrapper) content.append(scroll) # Add selected button add_btn = Gtk.Button(label=_("Add Selected")) add_btn.add_css_class("suggested-action") add_btn.set_halign(Gtk.Align.CENTER) add_btn.set_sensitive(False) content.append(add_btn) toolbar_view.set_content(content) dialog.set_child(toolbar_view) discovered: dict[str, Gtk.CheckButton] = {} # region_name -> check button pending = [len(repeaters)] def _on_complete(): pending[0] -= 1 if pending[0] <= 0: spinner.set_spinning(False) count = len(discovered) if count: status_label.set_label( _("Found {} regions").format(count) if count != 1 else _("Found {} region").format(count) ) add_btn.set_sensitive(True) else: status_label.set_label(_("No regions found")) def _on_region_response(repeater_name, region_text): if not region_text: GLib.idle_add(_on_complete) return def _update_ui(): # Parse region text — regions are newline or comma separated for line in region_text.replace(",", "\n").split("\n"): name = line.strip().strip("#") if not name or name in discovered: continue already_added = name in self._region_rows check = Gtk.CheckButton(active=not already_added) check.set_valign(Gtk.Align.CENTER) subtitle = _("from {}").format(repeater_name) if already_added: subtitle += " " + _("(already added)") row = Adw.ActionRow( title=name, subtitle=subtitle, ) row.add_prefix(check) if already_added: check.set_sensitive(False) list_box.append(row) discovered[name] = check _on_complete() GLib.idle_add(_update_ui) for i, repeater in enumerate(repeaters): def _send_request(rep=repeater): self._window.request_regions( rep, lambda text, rn=rep.name: _on_region_response(rn, text), ) # Timeout for this request GLib.timeout_add(15000, lambda r=rep: _on_complete() or False) # Stagger requests 500ms apart to avoid overwhelming the firmware GLib.timeout_add(i * 500, lambda fn=_send_request: fn() or False) def _on_add(*_args): count = 0 for name, check in discovered.items(): if check.get_active() and name not in self._region_rows: if self._window.storage: self._window.storage.add_region(name) self._add_region_row(name) count += 1 dialog.close() if count: self._window.show_toast( _("Added {} regions").format(count) if count != 1 else _("Added {} region").format(count) ) add_btn.connect("clicked", _on_add) dialog.present(self._window) def update_auto_add_config(self, config: dict): """Update auto-add switches from device config.""" self._suppressing_changes = True flags = config.get("config", 0) any_type = bool(flags & 0x1E) self._auto_add_switch.set_active(any_type) self._auto_add_expander.set_sensitive(any_type) self._auto_chat_switch.set_active(bool(flags & 0x02)) self._auto_repeater_switch.set_active(bool(flags & 0x04)) self._auto_room_switch.set_active(bool(flags & 0x08)) self._auto_sensor_switch.set_active(bool(flags & 0x10)) self._auto_overwrite_switch.set_active(bool(flags & 0x01)) max_hops = config.get("max_hops", 0) self._auto_hop_limit_entry.set_text(str(max_hops) if max_hops > 0 else "") self._suppressing_changes = False self._take_snapshot() # ─── Backup & Restore ───────────────────────────────────── def build_backup_actions(self) -> Gtk.ListBox: """Build the Backup & Restore list for the sidebar.""" listbox = Gtk.ListBox() listbox.set_selection_mode(Gtk.SelectionMode.NONE) listbox.add_css_class("boxed-list") export_row = Adw.ActionRow( title=_("Export Backup"), subtitle=_("Save to a file"), ) export_btn = Gtk.Button( icon_name="document-save-symbolic", valign=Gtk.Align.CENTER, ) export_btn.add_css_class("flat") export_btn.connect("clicked", self._on_export_backup) export_row.add_suffix(export_btn) export_row.set_activatable_widget(export_btn) listbox.append(export_row) import_row = Adw.ActionRow( title=_("Import Backup"), subtitle=_("Restore from a file"), ) import_btn = Gtk.Button( icon_name="document-open-symbolic", valign=Gtk.Align.CENTER, ) import_btn.add_css_class("flat") import_btn.connect("clicked", self._on_import_backup) import_row.add_suffix(import_btn) import_row.set_activatable_widget(import_btn) listbox.append(import_row) return listbox def _on_export_backup(self, *args): """Export backup to a JSON file.""" import json from datetime import datetime if not self._window.is_connected: dialog = Adw.AlertDialog( heading=_("Not Connected"), body=_("Connect to a device first to export a backup."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return backup = self._window.export_backup() name = backup.get("name", "meshy").replace(" ", "_") ts = datetime.now().strftime("%Y-%m-%d-%H%M%S") default_name = f"{name}_meshcore_config_{ts}.json" file_dialog = Gtk.FileDialog() file_dialog.set_initial_name(default_name) json_filter = Gtk.FileFilter() json_filter.set_name(_("JSON files")) json_filter.add_pattern("*.json") filters = Gio.ListStore.new(Gtk.FileFilter) filters.append(json_filter) file_dialog.set_filters(filters) def on_save_response(dialog, result): try: gfile = dialog.save_finish(result) if gfile: path = gfile.get_path() with open(path, "w", encoding="utf-8") as f: json.dump(backup, f, indent=2, ensure_ascii=False) self._window.show_toast(_("Backup exported successfully")) except GLib.Error: pass # user cancelled except Exception as e: self._window.show_toast(_("Export failed: {}").format(e), timeout=5) file_dialog.save(self._window, None, on_save_response) def _on_import_backup(self, *args): """Import backup from a JSON file.""" if not self._window.is_connected: dialog = Adw.AlertDialog( heading=_("Not Connected"), body=_("Connect to a device first to import a backup."), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return file_dialog = Gtk.FileDialog() json_filter = Gtk.FileFilter() json_filter.set_name(_("JSON files")) json_filter.add_pattern("*.json") filters = Gio.ListStore.new(Gtk.FileFilter) filters.append(json_filter) file_dialog.set_filters(filters) def on_open_response(dialog, result): try: gfile = dialog.open_finish(result) if gfile: path = gfile.get_path() self._show_import_preview(path) except GLib.Error: pass # user cancelled file_dialog.open(self._window, None, on_open_response) def _show_import_preview(self, path: str): """Show a preview of what will be imported and let user confirm.""" import json try: with open(path, encoding="utf-8") as f: backup = json.load(f) except Exception as e: dialog = Adw.AlertDialog( heading=_("Import Failed"), body=_("Could not read backup file: {}").format(e), ) dialog.add_response("ok", _("OK")) dialog.present(self._window) return # Count what's in the backup n_contacts = len(backup.get("contacts", [])) n_channels = len(backup.get("channels", [])) n_messages = sum(len(msgs) for msgs in backup.get("messages", {}).values()) n_ch_messages = sum(len(msgs) for msgs in backup.get("channel_messages", {}).values()) n_passwords = len(backup.get("room_passwords", {})) source_name = backup.get("name", _("Unknown")) # Count what's new (not already on device) existing_contacts = {c.public_key_hex for c in self._window.contacts} new_contacts = sum( 1 for c in backup.get("contacts", []) if c.get("public_key", "") not in existing_contacts ) existing_channels = {c.psk_hex for c in self._window.channels if not c.is_empty} new_channels = sum( 1 for c in backup.get("channels", []) if c.get("secret", "") not in existing_channels ) has_settings = bool( backup.get("name") or backup.get("radio_settings") or backup.get("position_settings") or backup.get("other_settings") or backup.get("auto_add_settings") ) lines = [_("Source: {}").format(source_name)] if has_settings: lines.append(_("Device settings: name, radio, location")) if n_contacts: lines.append(_("Contacts: {} ({} new)").format(n_contacts, new_contacts)) if n_channels: lines.append(_("Channels: {} ({} new)").format(n_channels, new_channels)) if n_messages: lines.append(_("Messages: {}").format(n_messages)) if n_ch_messages: lines.append(_("Channel messages: {}").format(n_ch_messages)) if n_passwords: lines.append(_("Saved passwords: {}").format(n_passwords)) dialog = Adw.AlertDialog( heading=_("Import Backup"), body="\n".join(lines), ) dialog.add_response("cancel", _("Cancel")) dialog.add_response("device", _("Contacts & Channels Only")) dialog.add_response("all", _("Everything")) dialog.set_response_appearance("all", Adw.ResponseAppearance.SUGGESTED) def on_response(d, response): if response == "cancel": return restore_messages = response == "all" contacts, channels = self._window.import_backup( backup, restore_messages=restore_messages ) parts = [] if contacts: parts.append(_("{} contacts").format(contacts)) if channels: parts.append(_("{} channels").format(channels)) if restore_messages and n_messages: parts.append(_("messages")) if parts: self._window.show_toast(_("Imported: {}").format(", ".join(parts)), timeout=4) else: self._window.show_toast(_("Nothing new to import")) dialog.connect("response", on_response) dialog.present(self._window) meshy/src/window.py000066400000000000000000002146351521052255700146720ustar00rootroot00000000000000# Copyright 2026, Jiri Eischmann and the meshy contributors # SPDX-License-Identifier: GPL-3.0-or-later """Main application window with adaptive navigation.""" import collections import logging import time from datetime import datetime import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk, Pango from meshy.backup import BackupManager from meshy.ble import BleDevice, BleTransport from meshy.connection_controller import ConnectionController from meshy.frame_handler import FrameHandler from meshy.message_controller import MessageController from meshy.models import ( Channel, Contact, ContactType, DeviceInfo, Message, battery_icon_name, snr_to_icon, snr_to_label, ) from meshy.protocol import ( STATS_TYPE_CORE, STATS_TYPE_PACKETS, STATS_TYPE_RADIO, build_add_update_contact, build_anon_req_regions, build_export_contact, build_get_advert_path, build_get_batt_and_storage, build_get_channel, build_get_neighbors, build_get_stats, build_import_contact, build_path_discovery, build_reboot, build_remove_contact, build_reset_path, build_send_cli_command, build_send_discover_req, build_send_self_advert, build_send_status_req, build_set_advert_latlon, build_set_advert_name, build_set_channel, build_set_custom_var, build_set_device_pin, build_set_flood_scope, build_set_path_hash_mode, build_set_radio_params, build_set_radio_tx_power, build_trace_path, ) from meshy.storage import Storage from meshy.transport_base import ConnectionState from meshy.views.map_view import MapView log = logging.getLogger(__name__) @Gtk.Template(resource_path="/page/codeberg/sesivany/Meshy/ui/window.ui") class MeshyWindow(Adw.ApplicationWindow): __gtype_name__ = "MeshyWindow" _toast_overlay = Gtk.Template.Child("toast_overlay") _main_stack = Gtk.Template.Child("main_stack") _connection_box = Gtk.Template.Child("connection_box") _connection_screen_banner = Gtk.Template.Child("connection_screen_banner") _connecting_status = Gtk.Template.Child("connecting_status") _connecting_spinner = Gtk.Template.Child("connecting_spinner") _outer_split = Gtk.Template.Child("outer_split") _split_view = Gtk.Template.Child("split_view") _nav_device = Gtk.Template.Child("nav_device") _nav_contacts = Gtk.Template.Child("nav_contacts") _nav_channels = Gtk.Template.Child("nav_channels") _nav_map = Gtk.Template.Child("nav_map") _nav_settings = Gtk.Template.Child("nav_settings") _sidebar_header = Gtk.Template.Child("sidebar_header") _nav_rail_toggle = Gtk.Template.Child("nav_rail_toggle") _sidebar_banner = Gtk.Template.Child("sidebar_banner") _sidebar_page = Gtk.Template.Child("sidebar_page") _sidebar_box = Gtk.Template.Child("sidebar_box") _content_header = Gtk.Template.Child("content_header") _companion_box = Gtk.Template.Child("companion_box") _companion_name_label = Gtk.Template.Child("companion_name_label") _companion_separator = Gtk.Template.Child("companion_separator") _companion_battery_icon = Gtk.Template.Child("companion_battery_icon") _companion_signal_icon = Gtk.Template.Child("companion_signal_icon") _content_box = Gtk.Template.Child("content_box") _content_page = Gtk.Template.Child("content_page") def __init__(self, **kwargs): super().__init__(**kwargs) self._ble = BleTransport() # BLE transport (always available) self._usb = None # USB transport (created on demand) self._tcp = None # TCP transport (created on demand) self._transport = self._ble # Active transport self._storage: Storage | None = None self._device_info = DeviceInfo() self._device_stats: dict = {} # stats from CMD_GET_STATS self._key_store = None # meshcoredecoder key store for LOG_DATA self._channel_hash_map = {} # channel_hash_hex → channel_index self._self_info: dict = {} self._contacts: list[Contact] = [] self._contacts_by_key: dict[str, Contact] = {} # pub_key_hex -> Contact self._discovered: list[Contact] = [] # heard adverts not yet added self._channels: list[Channel] = [] self._msg_ctrl = MessageController(self) self._conn_ctrl = ConnectionController(self) self._pending_clipboard_export: bool = False self._neighbors_callback = None self._advert_listener = None # callback for live advert monitoring self._discover_callback = None # callback for discover responses self._trace_callback = None # callback for trace path responses self._trace_timeout_callback = None # callback for trace timeout from device self._advert_path_callback = None # callback for advert path responses self._path_discovery_callback = None # callback for path discovery responses self._acl_callback = None # callback for ACL responses self._telemetry_callback = None # callback for telemetry responses self._login_callback = None # callback for room login response self._login_target_prefix = None # 6-byte prefix of room/repeater being logged into self._login_is_admin = False # whether last login was admin self._login_retry_timeout_id = None self._login_pub_key = None self._login_password = None self._login_on_retry = None self._login_was_flood_retry = False self._logged_in_rooms: dict[str, bool] = {} # pub_key_hex -> is_admin self._cli_response_callback = None # callback for CLI responses from rooms/repeaters self._cli_prefix_counter = 0 # cycling 00-FF prefix for CLI correlation self._cli_pending_prefixes = {} # prefix_str -> callback(text) self._status_callback = None # callback for binary status responses self._region_callbacks: dict[str, callable] = {} # ack_hex -> callback for region responses self._pending_binary_requests: dict[str, dict] = {} # tag_hex -> {type, callback} self._ignore_next_err = False # suppress next ERR toast (e.g. time sync rejection) self._rx_log = collections.deque(maxlen=500) self._rx_log_callback = None # live-update callback when Rx Log dialog is open self._collapsed_handler_ids: list[int] = [] # split_view signal handlers to clean up self._recent_incoming: dict[str, float] = {} # "sender_hex:text" -> monotonic time self._custom_vars: dict = {} # key -> value from firmware self._background_mode = False self._direct_repeaters: dict = {} # key -> (snr, monotonic_time, miss_count) self._repeaters_refreshed: set = set() self._signal_icon_timer_id = None self._backup = BackupManager() self._frame_handler = FrameHandler(self) self._conn_ctrl.setup_transport_callbacks(self._ble) self._build_ui() self._restore_window_state() # Clear unread indicators when the window regains focus self.connect("notify::is-active", self._on_window_active_changed) # Auto-connect to last known device on startup GLib.timeout_add(500, self._conn_ctrl.auto_connect) @property def _connection_banner(self): page = self._main_stack.get_visible_child_name() if page == "connection": return self._connection_screen_banner return self._sidebar_banner def _restore_window_state(self): settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") self.set_default_size(settings.get_int("window-width"), settings.get_int("window-height")) if settings.get_boolean("window-maximized"): self.maximize() self.connect("close-request", self._on_close_request) def set_background_mode(self, enabled): self._background_mode = enabled def shutdown(self): """Full cleanup for application quit.""" self._conn_ctrl.manual_disconnect = True self._conn_ctrl.cancel_reconnect() self._transport.disconnect() if self._storage: self._storage.close() if self._conn_ctrl.batt_timer_id: GLib.source_remove(self._conn_ctrl.batt_timer_id) self._conn_ctrl.batt_timer_id = None if self._conn_ctrl.watchdog_timer_id: GLib.source_remove(self._conn_ctrl.watchdog_timer_id) self._conn_ctrl.watchdog_timer_id = None def _on_close_request(self, *args): settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") settings.set_int("window-width", self.get_width()) settings.set_int("window-height", self.get_height()) settings.set_boolean("window-maximized", self.is_maximized()) if self._background_mode and self.is_connected: self.set_visible(False) return True app = self.get_application() if app and hasattr(app, "_background_held") and app._background_held: app.release() app._background_held = False self.shutdown() def _build_ui(self): # Views are created lazily on first access to reduce startup memory. # Only ConnectionView is needed immediately for the connect screen. self._device_view_inst = None self._contacts_view_inst = None self._chat_view_inst = None self._channels_view_inst = None self._settings_view_inst = None self._repeater_view_inst = None self._map_view_inst = None from meshy.views.connection_view import ConnectionView self._connection_view = ConnectionView(self) self._current_view = "device" self._content_header_extra = None self._content_title_btn = None self._content_title_label = None self._sidebar_header_extras = [] # Nav button dict for toggling self._nav_buttons = { "device": self._nav_device, "contacts": self._nav_contacts, "channels": self._nav_channels, "map": self._nav_map, "settings": self._nav_settings, } for view_id, btn in self._nav_buttons.items(): btn.connect("toggled", self._on_nav_toggled, view_id) # Actions disconnect_action = Gio.SimpleAction.new("disconnect", None) disconnect_action.connect("activate", self._on_menu_disconnect) self.add_action(disconnect_action) quit_action = Gio.SimpleAction.new("quit", None) quit_action.connect("activate", lambda *_: self.get_application()._do_full_quit()) self.add_action(quit_action) about_action = Gio.SimpleAction.new("about", None) about_action.connect("activate", lambda *_: self.get_application()._on_about(None, None)) self.add_action(about_action) import meshy as _meshy_pkg if _meshy_pkg.SHORTCUTS_DIALOG_ENABLED: shortcuts_action = Gio.SimpleAction.new("show-help-overlay", None) shortcuts_action.connect("activate", self._on_show_shortcuts) self.add_action(shortcuts_action) for view_id in ("device", "contacts", "channels", "map", "settings"): action = Gio.SimpleAction.new(f"navigate-{view_id}", None) action.connect("activate", self._on_navigate_action, view_id) self.add_action(action) search_action = Gio.SimpleAction.new("search", None) search_action.connect("activate", self._on_search_action) self.add_action(search_action) new_msg_action = Gio.SimpleAction.new("new-message", None) new_msg_action.connect("activate", self._on_new_message_action) self.add_action(new_msg_action) for name, direction, unread in ( ("navigate-prev", -1, False), ("navigate-next", 1, False), ("navigate-prev-unread", -1, True), ("navigate-next-unread", 1, True), ): action = Gio.SimpleAction.new(name, None) action.connect("activate", self._on_list_navigate, direction, unread) self.add_action(action) # Signal connections for template widgets self._nav_rail_toggle.connect("clicked", self._on_nav_rail_toggle) self._sidebar_banner.connect("button-clicked", self._on_connect_clicked) self._connection_screen_banner.connect("button-clicked", self._on_connect_clicked) self._outer_split.connect("notify::collapsed", self._on_outer_split_collapsed_changed) self._outer_split.connect("notify::show-sidebar", self._on_outer_split_show_sidebar_changed) # Breakpoints bp_medium = Adw.Breakpoint.new(Adw.BreakpointCondition.parse("max-width: 860sp")) bp_medium.add_setter(self._outer_split, "collapsed", True) self.add_breakpoint(bp_medium) bp_narrow = Adw.Breakpoint.new(Adw.BreakpointCondition.parse("max-width: 600sp")) bp_narrow.add_setter(self._outer_split, "collapsed", True) bp_narrow.add_setter(self._split_view, "collapsed", True) self.add_breakpoint(bp_narrow) # Activate device view by default self._nav_buttons["device"].set_active(True) # Show connecting screen if we have a saved device, otherwise # show the connection screen last_addr = Gio.Settings.new("page.codeberg.sesivany.Meshy").get_string( "last-device-address" ) if last_addr: self._show_connecting_screen(last_addr) else: self._connection_box.append(self._connection_view) def _on_nav_rail_toggle(self, button): """Toggle the nav rail visibility when in collapsed mode.""" show = not self._outer_split.get_show_sidebar() self._outer_split.set_show_sidebar(show) def _on_outer_split_collapsed_changed(self, split_view, *args): """Show/hide the nav rail toggle button based on collapsed state.""" collapsed = split_view.get_collapsed() self._nav_rail_toggle.set_visible(collapsed) if not collapsed: split_view.set_show_sidebar(True) def _on_outer_split_show_sidebar_changed(self, split_view, *args): """Update toggle button icon based on sidebar visibility.""" if split_view.get_show_sidebar(): self._nav_rail_toggle.set_icon_name("sidebar-show-symbolic") else: self._nav_rail_toggle.set_icon_name("sidebar-show-right-symbolic") @property def _device_view(self): if self._device_view_inst is None: from meshy.views.device_view import DeviceView self._device_view_inst = DeviceView(self) return self._device_view_inst @property def _contacts_view(self): if self._contacts_view_inst is None: from meshy.views.contacts_view import ContactsView self._contacts_view_inst = ContactsView(self) return self._contacts_view_inst @property def _chat_view(self): if self._chat_view_inst is None: from meshy.views.chat_view import ChatView self._chat_view_inst = ChatView(self) return self._chat_view_inst @property def _channels_view(self): if self._channels_view_inst is None: from meshy.views.channels_view import ChannelsView self._channels_view_inst = ChannelsView(self) return self._channels_view_inst @property def _settings_view(self): if self._settings_view_inst is None: from meshy.views.settings_view import SettingsView self._settings_view_inst = SettingsView(self) return self._settings_view_inst @property def _map_view(self): if self._map_view_inst is None: self._map_view_inst = MapView(self) return self._map_view_inst @property def _repeater_view(self): if self._repeater_view_inst is None: from meshy.views.repeater_view import RepeaterView self._repeater_view_inst = RepeaterView(self) return self._repeater_view_inst def _on_nav_toggled(self, button, view_id): if not button.get_active(): return # Untoggle other buttons for vid, btn in self._nav_buttons.items(): if vid != view_id: btn.handler_block_by_func(self._on_nav_toggled) btn.set_active(False) btn.handler_unblock_by_func(self._on_nav_toggled) self._current_view = view_id self._switch_view(view_id) # Auto-hide nav rail overlay after selection if self._outer_split.get_collapsed(): self._outer_split.set_show_sidebar(False) def _on_show_shortcuts(self, action, param): builder = Gtk.Builder.new_from_resource("/page/codeberg/sesivany/Meshy/gtk/help-overlay.ui") dialog = builder.get_object("help_overlay") dialog.present(self) def _on_navigate_action(self, action, param, view_id): self._nav_buttons[view_id].set_active(True) def _on_search_action(self, action, param): if self._current_view == "contacts" and self._contacts_view_inst: if self._contacts_view_inst._search_btn: self._contacts_view_inst._search_btn.set_active(True) self._contacts_view_inst._search_entry.grab_focus() def _on_new_message_action(self, action, param): if self._current_view == "contacts" and self._chat_view_inst: self._chat_view_inst._text_entry.grab_focus() elif self._current_view == "channels" and self._channels_view_inst: self._channels_view_inst._chat_widget._text_entry.grab_focus() def _on_list_navigate(self, action, param, direction, unread_only): if self._current_view == "contacts" and self._contacts_view: self._contacts_view.navigate(direction, unread_only) elif self._current_view == "channels" and self._channels_view_inst: self._channels_view.navigate(direction, unread_only) def _clear_box(self, box): child = box.get_first_child() while child: next_child = child.get_next_sibling() box.remove(child) child = next_child def _add_sidebar_nav_button(self, label: str): """Create a navigation button for collapsed mode (e.g. 'Device Information ›').""" btn = Gtk.Button(label=f"{label} \u203a") btn.add_css_class("flat") btn.set_margin_start(12) btn.set_margin_end(12) btn.set_margin_top(8) btn.connect("clicked", lambda *_: self._split_view.set_show_content(True)) btn.set_visible(self._split_view.get_collapsed()) self._collapsed_handler_ids.append( self._split_view.connect( "notify::collapsed", lambda sv, *_: btn.set_visible(sv.get_collapsed()) ) ) self._sidebar_box.append(btn) return btn def _create_list_controls(self, view): """Create search/filter/sort buttons in the sidebar header for a list view.""" search_btn = Gtk.ToggleButton(icon_name="system-search-symbolic", tooltip_text=_("Search")) search_btn.add_css_class("flat") view.set_search_button(search_btn) filter_btn = Gtk.MenuButton(icon_name="filter-symbolic", tooltip_text=_("Filter")) filter_btn.set_menu_model(view.build_filter_menu()) filter_btn.add_css_class("flat") sort_btn = Gtk.MenuButton(icon_name="view-sort-descending-symbolic", tooltip_text=_("Sort")) sort_btn.set_menu_model(view.build_sort_menu()) sort_btn.add_css_class("flat") for btn in (sort_btn, filter_btn, search_btn): self._sidebar_header.pack_end(btn) self._sidebar_header_extras.append(btn) def _switch_view(self, view_id: str): """Switch the sidebar and content based on the selected navigation.""" # Discard unapplied settings changes when navigating away if self._settings_view_inst is not None and view_id != "settings": self._settings_view_inst.restore_snapshot() if self._repeater_view_inst is not None: self._repeater_view_inst.clear() self._clear_box(self._sidebar_box) self._clear_box(self._content_box) # Disconnect previous collapsed handlers to prevent leaks for handler_id in self._collapsed_handler_ids: self._split_view.disconnect(handler_id) self._collapsed_handler_ids.clear() # Remove any extra buttons from content headerbar if hasattr(self, "_content_header_extra") and self._content_header_extra: self._content_header.remove(self._content_header_extra) self._content_header_extra = None # Remove extra buttons from sidebar headerbar for btn in self._sidebar_header_extras: self._sidebar_header.remove(btn) self._sidebar_header_extras.clear() # Reset custom title widget and sidebar title visibility self._content_header.set_title_widget(None) self._sidebar_header.set_show_title(True) self._content_title_btn = None # Remove view-specific action groups from window for prefix in ("contacts", "channel"): self.insert_action_group(prefix, None) if view_id == "device": # Sidebar = Device Info button + Actions, content = device info self._sidebar_page.set_title(_("Actions")) self._content_page.set_title(_("Device Information")) self._add_sidebar_nav_button(_("Device Information")) actions = self._device_view.build_sidebar_actions() actions.set_margin_start(12) actions.set_margin_end(12) actions.set_margin_top(8) self._sidebar_box.append(actions) self._content_box.append(self._device_view) self._split_view.set_show_content(True) elif view_id == "contacts": self._sidebar_page.set_title(_("Contacts")) self._content_page.set_title(_("Chat")) self._sidebar_header.set_show_title(False) self._sidebar_box.append(self._contacts_view) self._content_box.append(self._chat_view) self.insert_action_group("contacts", self._contacts_view._action_group) self._create_list_controls(self._contacts_view) if self._chat_view_inst and self._chat_view_inst._contact: self._set_contact_title_widget(self._chat_view_inst._contact) self._split_view.set_show_content(False) elif view_id == "channels": self._sidebar_page.set_title(_("Channels")) self._content_page.set_title(_("Channel")) self._sidebar_header.set_show_title(False) self._sidebar_box.append(self._channels_view) self._content_box.append(self._channels_view.get_chat_widget()) self.insert_action_group("channel", self._channels_view._action_group) self._create_list_controls(self._channels_view) selected = self._channels_view._channel_list.get_selected_row() if selected and hasattr(selected, "_channel"): self._content_page.set_title( selected._channel.name or _("Channel {}").format(selected._channel.index) ) self._split_view.set_show_content(False) elif view_id == "map": self._sidebar_page.set_title(_("Filter")) self._content_page.set_title(_("Map")) # Navigation button for collapsed mode self._add_sidebar_nav_button(_("Map")) # Filter controls self._sidebar_box.append(self._map_view.build_sidebar_filters()) self._map_view.update_contacts(self._contacts) self._map_view.update_discovered(self._discovered) self._content_box.append(self._map_view) self._split_view.set_show_content(True) elif view_id == "settings": # Sidebar = Settings button + hint, content = settings with Apply in headerbar self._sidebar_page.set_title(_("Backup & Restore")) self._content_page.set_title(_("Settings")) self._add_sidebar_nav_button(_("Settings")) sb = Gtk.Builder.new_from_resource( "/page/codeberg/sesivany/Meshy/ui/settings-sidebar.ui" ) sb.get_object("backup_btn").connect("clicked", self._settings_view._on_export_backup) sb.get_object("restore_btn").connect("clicked", self._settings_view._on_import_backup) sb.get_object("factory_reset_btn").connect( "clicked", self._device_view._on_factory_reset ) self._sidebar_box.append(sb.get_object("backup_group")) self._sidebar_box.append(sb.get_object("spacer")) self._sidebar_box.append(sb.get_object("reset_group")) self._content_box.append(self._settings_view) self._settings_view.load_regions() self._settings_view.restore_default_scope() apply_btn = self._settings_view.create_apply_button() self._content_header.pack_end(apply_btn) self._content_header_extra = apply_btn self._split_view.set_show_content(True) def _build_section_nav(self, view) -> Gtk.Widget: """Build a section navigation list for views that have sections.""" box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) listbox = Gtk.ListBox() listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) listbox.add_css_class("navigation-sidebar") section_icons = { "Connection": "network-wireless-symbolic", "Device Information": "computer-symbolic", "Battery & Storage": "battery-symbolic", "Radio Configuration": "preferences-system-symbolic", "Radio": "preferences-system-symbolic", "Actions": "system-run-symbolic", "Advert": "network-wireless-symbolic", "Auto-Add Contacts": "avatar-default-symbolic", "Location": "find-location-symbolic", "Telemetry Privacy": "preferences-other-symbolic", "Regions": "network-workgroup-symbolic", } for section_name in view.sections: row = Adw.ActionRow(title=section_name) icon = section_icons.get(section_name, "go-next-symbolic") row.add_prefix(Gtk.Image.new_from_icon_name(icon)) row.set_activatable(True) row._section_name = section_name listbox.append(row) def on_row_activated(lb, row): if hasattr(row, "_section_name"): view.scroll_to_section(row._section_name) self._split_view.set_show_content(True) listbox.connect("row-activated", on_row_activated) scroll = Gtk.ScrolledWindow(vexpand=True) scroll.set_child(listbox) box.append(scroll) return box def _set_contact_title_widget(self, contact: Contact): """Set the content header to a clickable contact name title widget.""" self._content_page.set_title(contact.name) title_btn = Gtk.Button() title_btn.add_css_class("flat") title_box = Gtk.Box(spacing=4, halign=Gtk.Align.CENTER) self._content_title_label = Gtk.Label( label=_("{name} ({path})").format(name=contact.name, path=contact.path_label), ellipsize=Pango.EllipsizeMode.END, ) self._content_title_label.add_css_class("heading") title_box.append(self._content_title_label) title_box.append(Gtk.Image.new_from_icon_name("go-down-symbolic")) title_btn.set_child(title_box) contact_key = contact.public_key_hex title_btn.connect( "clicked", lambda *_, key=contact_key: self._contacts_view._show_contact_detail( self._contacts_by_key.get(key, contact) ), ) self._content_header.set_title_widget(title_btn) self._content_title_btn = title_btn def show_chat(self, contact: Contact): """Show chat view for a specific contact.""" # If repeater view is in content, swap it for chat if self._repeater_view_inst and self._repeater_view_inst.get_parent() == self._content_box: self._repeater_view_inst.clear() self._content_box.remove(self._repeater_view_inst) self._content_box.append(self._chat_view) self._chat_view.set_contact(contact) self._set_contact_title_widget(contact) self._split_view.set_show_content(True) self._contacts_view.mark_unread(contact.public_key_hex, False) def _refresh_chat_header(self): """Update the chat header title if the current contact's path changed.""" if not (self._chat_view_inst and self._chat_view_inst._contact): return key = self._chat_view_inst._contact.public_key_hex c = self._contacts_by_key.get(key) if c: self._chat_view_inst._contact = c if hasattr(self, "_content_title_label") and self._content_title_label: self._content_title_label.set_label( _("{name} ({path})").format(name=c.name, path=c.path_label) ) else: self._content_page.set_title( _("{name} ({path})").format(name=c.name, path=c.path_label) ) def show_repeater(self, contact: Contact, login_timestamp=None, is_admin=True): """Show repeater management view for a repeater contact.""" # Swap chat view for repeater view if needed if self._chat_view_inst and self._chat_view_inst.get_parent() == self._content_box: self._content_box.remove(self._chat_view_inst) self._content_box.append(self._repeater_view) self._repeater_view.set_repeater(contact, login_timestamp, is_admin) self._content_header.set_title_widget(None) self._content_title_btn = None role = _("Management") if is_admin else _("Guest") self._content_page.set_title(_("{name} \u2014 {role}").format(name=contact.name, role=role)) self._split_view.set_show_content(True) def mark_contact_read(self, public_key_hex: str): self._storage.set_contact_last_read_at(public_key_hex, datetime.now().timestamp()) def get_contact_last_read_at(self, public_key_hex: str) -> float | None: return self._storage.get_contact_last_read_at(public_key_hex) def mark_channel_read(self, channel_index: int): self._storage.set_channel_last_read_at(channel_index, datetime.now().timestamp()) def get_channel_last_read_at(self, channel_index: int) -> float | None: return self._storage.get_channel_last_read_at(channel_index) def _on_window_active_changed(self, window, pspec): """Reload messages and clear unread indicator for the currently visible chat when window gains focus.""" if not self.is_active(): return if ( self._current_view == "contacts" and self._chat_view_inst and self._chat_view_inst._contact ): self._contacts_view.mark_unread(self._chat_view_inst._contact.public_key_hex, False) self._chat_view_inst._load_messages() elif self._current_view == "channels" and self._channels_view_inst: chat = self._channels_view_inst.get_chat_widget() if chat._current_channel: self._channels_view_inst.mark_unread(chat._current_channel.index, False) chat._load_messages(chat._current_channel) # ─── BLE Connection ─────────────────────────────────────── def _setup_transport_callbacks(self, transport): self._conn_ctrl.setup_transport_callbacks(transport) def _setup_ble_callbacks(self): self._conn_ctrl.setup_transport_callbacks(self._ble) def _auto_connect(self): return self._conn_ctrl.auto_connect() def _update_companion_label(self): """Update the companion name + battery/signal icons in the header.""" name = self._device_info.name if not name: self._companion_box.set_visible(False) return Gio.Settings.new("page.codeberg.sesivany.Meshy").set_string("last-device-name", name) self._companion_name_label.set_label(name) has_battery = self._device_info.battery_mv > 0 if has_battery: pct = self._device_info.battery_percent icon = battery_icon_name(pct) self._companion_battery_icon.set_from_icon_name(icon) self._companion_battery_icon.set_tooltip_text(f"{pct}%") self._companion_battery_icon.set_visible(has_battery) has_icons = has_battery or self._companion_signal_icon.get_visible() self._companion_separator.set_visible(has_icons) self._companion_box.set_visible(True) def _update_signal_icon_tick(self): """Periodic callback to refresh the signal strength icon.""" self._update_signal_icon() for key in list(self._direct_repeaters): snr, ts, miss = self._direct_repeaters[key] if key in self._repeaters_refreshed: self._direct_repeaters[key] = (snr, ts, 0) else: miss += 1 if miss >= 5: del self._direct_repeaters[key] else: self._direct_repeaters[key] = (snr, ts, miss) self._repeaters_refreshed.clear() self._signal_icon_timer_id = GLib.timeout_add_seconds(60, self._update_signal_icon_tick) return GLib.SOURCE_REMOVE def _update_signal_icon(self): """Update the signal strength icon from repeater SNR data.""" import time now = time.monotonic() stale = [k for k, (_, ts, _mc) in self._direct_repeaters.items() if now - ts > 600] for k in stale: del self._direct_repeaters[k] if self._direct_repeaters: best_snr = max(snr for snr, _, _mc in self._direct_repeaters.values()) sf = self._device_info.radio_sf or 12 icon = snr_to_icon(best_snr, sf) self._companion_signal_icon.set_from_icon_name(icon) quality = { "Excellent": _("Excellent"), "Good": _("Good"), "Fair": _("Fair"), "Poor": _("Poor"), "Very poor": _("Very poor"), }.get(snr_to_label(best_snr, sf), "") self._companion_signal_icon.set_tooltip_text(f"{quality}, SNR: {best_snr:.1f} dB") self._companion_signal_icon.set_visible(True) else: self._companion_signal_icon.set_from_icon_name("strength-bars-none-symbolic") self._companion_signal_icon.set_tooltip_text(None) self._companion_signal_icon.set_visible(True) has_icons = ( self._companion_battery_icon.get_visible() or self._companion_signal_icon.get_visible() ) self._companion_separator.set_visible(has_icons) def _save_tcp_companion_if_needed(self): """Save or update the current TCP companion in GSettings.""" settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") if settings.get_string("last-transport") != "tcp": return addr = self._transport.connected_address name = self._device_info.name if not addr or not name: return entry = f"{name}|{addr}" companions = list(settings.get_strv("tcp-companions")) # Remove any existing entry for the same address companions = [c for c in companions if not c.endswith(f"|{addr}")] companions.append(entry) settings.set_strv("tcp-companions", companions) def _remove_tcp_companion(self, address: str): """Remove a saved TCP companion by address.""" settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") companions = list(settings.get_strv("tcp-companions")) companions = [c for c in companions if not c.endswith(f"|{address}")] settings.set_strv("tcp-companions", companions) def _show_connecting_screen(self, address=""): """Show the auto-connecting status screen.""" settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") name = settings.get_string("last-device-name") if name: self._connecting_status.set_description(f"{name} ({address})" if address else name) else: self._connecting_status.set_description(address) self._connecting_spinner.set_spinning(True) self._main_stack.set_visible_child_name("connecting") def _show_connection_screen(self): """Show the full-window connection screen.""" self._connecting_spinner.set_spinning(False) if self._connection_view.get_parent(): self._connection_view.get_parent().remove(self._connection_view) self._connection_box.append(self._connection_view) self._connection_screen_banner.set_revealed(False) self._main_stack.set_visible_child_name("connection") self._connection_view.refresh_paired_devices() def _hide_connection_screen(self): """Hide connection/connecting screen and switch to main app layout.""" from_connection = self._main_stack.get_visible_child_name() == "connection" self._connecting_spinner.set_spinning(False) self._connection_view.stop_polling() if self._connection_view.get_parent(): self._connection_view.get_parent().remove(self._connection_view) if from_connection: self._sidebar_banner.set_title(self._connection_screen_banner.get_title()) self._sidebar_banner.set_button_label(self._connection_screen_banner.get_button_label()) self._sidebar_banner.set_revealed(self._connection_screen_banner.get_revealed()) self._main_stack.set_visible_child_name("main") self._current_view = "device" self._switch_view("device") self._nav_buttons["device"].set_active(True) def connect_to_device(self, device, transport=None): self._conn_ctrl.connect_to_device(device, transport) def _on_menu_disconnect(self, *_args): """Handle Disconnect from hamburger menu.""" self._conn_ctrl.manual_disconnect = True if self._conn_ctrl.reconnect_timer_id is not None: self._conn_ctrl.cancel_reconnect() self._conn_ctrl.reconnect_attempts = 0 self._transport.disconnect() def _on_connect_clicked(self, banner): if self._conn_ctrl.reconnect_timer_id is not None: self._conn_ctrl.cancel_reconnect() self._conn_ctrl.reconnect_attempts = 0 self._conn_ctrl.manual_disconnect = True settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") settings.set_string("last-device-address", "") self._connection_banner.set_title(_("Not connected")) self._connection_banner.set_button_label(_("Connect")) self._show_connection_screen() return if self._transport.state == ConnectionState.CONNECTED: self._conn_ctrl.manual_disconnect = True self._transport.disconnect() elif self._transport.state == ConnectionState.DISCONNECTED: settings = Gio.Settings.new("page.codeberg.sesivany.Meshy") last_addr = settings.get_string("last-device-address") if last_addr: self._connection_banner.set_title(_("Connecting...")) self._transport.connect_by_address(last_addr) else: self._show_scan_dialog() elif self._transport.state == ConnectionState.SCANNING: self._transport.stop_scan() def _show_scan_dialog(self): self._conn_ctrl.show_scan_dialog() def _on_scan_device_selected(self, listbox, row): self._conn_ctrl._on_scan_device_selected(listbox, row) def _on_ble_device_discovered(self, device: BleDevice): self._conn_ctrl.on_ble_device_discovered(device) def _on_ble_state_changed(self, state: ConnectionState): self._conn_ctrl.on_ble_state_changed(state) def _on_connected(self): self._conn_ctrl.on_connected() def _apply_default_scope(self): scope = self._settings_view.default_scope self.send_frame(build_set_flood_scope(scope)) return False def _poll_battery(self) -> bool: return self._conn_ctrl.poll_battery() def _connection_watchdog(self) -> bool: return self._conn_ctrl.connection_watchdog() # ─── Data Handling ───────────────────────────────────────── def _on_ble_data_received(self, data: bytes): """Handle incoming data from the BLE device.""" self._conn_ctrl.last_device_rx = time.monotonic() GLib.idle_add(self._handle_frame, data) def _debug_log(self, msg: str): log.debug(msg) def _handle_frame(self, data: bytes): """Process a received protocol frame on the main thread.""" return self._frame_handler.handle_frame(data) def _send_notification(self, title: str, body: str, notif_id: str): """Send a desktop notification.""" notification = Gio.Notification.new(title) notification.set_body(body) self.get_application().send_notification(notif_id, notification) # ─── Public API for views ───────────────────────────────── def send_message(self, contact: Contact, text: str): self._msg_ctrl.send_message(contact, text) def resend_message(self, contact: Contact, message: Message): self._msg_ctrl.resend_message(contact, message) def send_channel_message(self, channel_index: int, text: str): self._msg_ctrl.send_channel_message(channel_index, text) def resend_channel_message(self, channel_index: int, text: str) -> int: return self._msg_ctrl.resend_channel_message(channel_index, text) def delete_message(self, message_id: str): self._msg_ctrl.delete_message(message_id) def delete_channel_message(self, channel_index: int, timestamp: float, text: str): self._msg_ctrl.delete_channel_message(channel_index, timestamp, text) def send_cli_command(self, contact: Contact, command: str): """Send a CLI command to a repeater.""" frame = build_send_cli_command(contact.public_key, command) self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(frame) def send_cli_with_prefix(self, contact_pub_key: bytes, command: str, callback): """Send a CLI command with a correlation prefix. The repeater firmware echoes the prefix back, allowing reliable matching of responses to commands. """ prefix = f"{self._cli_prefix_counter:02X}|" self._cli_prefix_counter = (self._cli_prefix_counter + 1) & 0xFF self._cli_pending_prefixes[prefix] = callback frame = build_send_cli_command(contact_pub_key, prefix + command) self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(frame) def cancel_cli_prefixes(self, prefixes): """Remove pending prefix callbacks (e.g. when cancelling a fetch).""" for p in prefixes: self._cli_pending_prefixes.pop(p, None) def send_advert(self, flood: bool = False, silent: bool = False): self.send_frame(build_send_self_advert(flood)) if not silent: mode = _("flood routed") if flood else _("zero-hop") self.show_toast(_("Advert sent ({})").format(mode)) @property def discovered_contacts(self) -> list: return self._discovered def _add_local_contact(self, contact: Contact): """Add a contact to the local list and UI immediately.""" key = contact.public_key_hex if key not in self._contacts_by_key: self._contacts.append(contact) self._contacts_by_key[key] = contact self._storage.save_contact(contact) self._contacts_view.update_contacts(self._contacts) def add_discovered_contact(self, contact: Contact): """Move a contact from discovered to the device's contact list.""" self.send_frame( build_add_update_contact( contact.public_key, contact_type=contact.type, name=contact.name, path_len=0xFF, lat=contact.latitude, lon=contact.longitude, ) ) # Remove from discovered list self._discovered = [ c for c in self._discovered if c.public_key_hex != contact.public_key_hex ] # Add to local list immediately self._add_local_contact(contact) # Also refresh from device to get firmware-assigned fields GLib.timeout_add(500, lambda: self._frame_handler.send_get_contacts() or False) def request_regions(self, contact, callback): """Send an anonymous zero-hop region request to a repeater. Only reaches directly reachable repeaters (zero-hop). Callback receives the region text string from the repeater. """ callback_key = contact.public_key_hex[:12] self._region_callbacks[callback_key] = callback frame = build_anon_req_regions(contact.public_key, 0, b"") self._msg_ctrl.sent_msg_queue.append(f"region:{callback_key}") self.send_frame(frame) def send_discover_req(self, callback): """Send a NODE_DISCOVER_REQ control packet. Callback receives each response.""" self._discover_callback = callback self.send_frame(build_send_discover_req(prefix_only=False)) def stop_discover(self): """Stop listening for discover responses.""" self._discover_callback = None def send_trace_path(self, path_bytes: bytes, callback, timeout_callback=None, flags: int = 0): """Send a SEND_TRACE_PATH command. Callback receives trace result dict.""" self._trace_callback = callback self._trace_timeout_callback = timeout_callback tag = int(datetime.now().timestamp()) self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(build_trace_path(tag, path_bytes, flags)) def stop_trace_path(self): """Stop listening for trace responses.""" self._trace_callback = None self._trace_timeout_callback = None def set_flood_scope(self, scope: str): """Set flood scope. Empty to disable, '#name' for hashtag scope.""" self.send_frame(build_set_flood_scope(scope)) def get_advert_path(self, pub_key: bytes, callback): """Get the advertisement path cached for a contact.""" self._advert_path_callback = callback self.send_frame(build_get_advert_path(pub_key)) def send_path_discovery(self, pub_key: bytes, callback): """Send a path discovery request. Callback receives raw response data.""" self._path_discovery_callback = callback self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(build_path_discovery(pub_key)) def stop_path_discovery(self): """Stop listening for path discovery responses.""" self._path_discovery_callback = None _LOGIN_FLOOD_RETRY_MS = 7000 def send_login(self, pub_key: bytes, password: str, callback, on_retry=None): """Send a room/repeater login. Callback receives True (success) or False (fail). If the first (directed) attempt gets no response within 7s, the cached path is reset and the login is retried via flood. *on_retry* is called (no args) when the flood retry fires, so the UI can update its status. """ from meshy.protocol import build_send_login self._login_callback = callback self._login_target_prefix = pub_key[:6] self._login_pub_key = pub_key self._login_password = password self._login_on_retry = on_retry frame = build_send_login(pub_key, password) log.info(f"Sending login to {pub_key.hex()[:16]}...") self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(frame) self._login_retry_timeout_id = GLib.timeout_add( self._LOGIN_FLOOD_RETRY_MS, self._on_login_flood_retry ) def _on_login_flood_retry(self): self._login_retry_timeout_id = None if not self._login_callback: return False log.info("Login directed attempt timed out, resetting path and retrying via flood") from meshy.protocol import build_reset_path, build_send_login self.send_frame(build_reset_path(self._login_pub_key)) frame = build_send_login(self._login_pub_key, self._login_password) self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(frame) self._login_was_flood_retry = True if self._login_on_retry: self._login_on_retry() return False def stop_login(self): """Stop listening for login responses.""" self._login_callback = None self._login_target_prefix = None self._login_pub_key = None self._login_password = None self._login_on_retry = None if self._login_retry_timeout_id: GLib.source_remove(self._login_retry_timeout_id) self._login_retry_timeout_id = None def is_room_logged_in(self, pub_key_hex: str) -> bool: return pub_key_hex in self._logged_in_rooms def is_room_admin(self, pub_key_hex: str) -> bool: return self._logged_in_rooms.get(pub_key_hex, False) def mark_room_logged_in(self, pub_key_hex: str, is_admin: bool = True): self._logged_in_rooms[pub_key_hex] = is_admin def logout_repeater(self, pub_key_hex: str): """Log out of a repeater/room — clear local state and return to contacts.""" self._logged_in_rooms.pop(pub_key_hex, None) if self._repeater_view_inst and self._repeater_view_inst.get_parent() == self._content_box: self._repeater_view_inst.clear() self._content_box.remove(self._repeater_view_inst) self._content_box.append(self._chat_view) self._split_view.set_show_content(False) def send_status_req(self, pub_key: bytes, callback): """Send a status request to a repeater. Callback receives parsed status dict.""" self._status_callback = callback self._msg_ctrl.sent_msg_queue.append(None) self.send_frame(build_send_status_req(pub_key)) def get_neighbors(self, repeater_pub_key: bytes, callback, offset: int = 0): """Request neighbor list from a repeater. Callback receives dict with 'total' and 'neighbors'.""" self._neighbors_callback = callback self._msg_ctrl.sent_msg_queue.append({"type": "neighbors", "callback": callback}) self.send_frame(build_get_neighbors(repeater_pub_key, offset=offset)) def get_acl(self, repeater_pub_key: bytes, callback): """Request ACL from a repeater. Callback receives raw binary response.""" from meshy.protocol import build_get_acl self._acl_callback = callback self._msg_ctrl.sent_msg_queue.append({"type": "acl", "callback": callback}) self.send_frame(build_get_acl(repeater_pub_key)) def get_telemetry(self, pub_key: bytes, callback, contact_type: int = None): """Request telemetry. Uses CMD_SEND_TELEMETRY_REQ for chat companions, CMD_SEND_BINARY_REQ for repeaters/rooms/sensors.""" from meshy.models import ContactType from meshy.protocol import build_get_telemetry, build_get_telemetry_chat if contact_type == ContactType.CHAT: self._telemetry_callback = callback self.send_frame(build_get_telemetry_chat(pub_key)) else: self._telemetry_callback = callback self._msg_ctrl.sent_msg_queue.append({"type": "telemetry", "callback": callback}) self.send_frame(build_get_telemetry(pub_key)) def get_self_telemetry(self, callback): """Request telemetry from the connected companion device itself.""" from meshy.protocol import build_get_telemetry_chat self._telemetry_callback = callback self.send_frame(build_get_telemetry_chat()) def find_contact_by_key_prefix(self, prefix_hex: str) -> Contact | None: """Find a contact whose public key starts with the given hex prefix.""" for c in self._contacts: if c.public_key_hex.startswith(prefix_hex): return c for c in self._discovered: if c.public_key_hex.startswith(prefix_hex): return c return None def resolve_hop_prefixes(self, hop_hexes: list[str]) -> list[Contact | None]: """Resolve hop hex prefixes to contacts using geographic proximity. When multiple contacts match a prefix, pick the one closest to the previous hop. """ import math def _distance_sq(lat1, lon1, lat2, lon2): dlat = lat1 - lat2 dlon = (lon1 - lon2) * math.cos(math.radians((lat1 + lat2) / 2)) return dlat * dlat + dlon * dlon ref_lat, ref_lon = None, None si = self._self_info ref_lat = si.get("adv_lat") ref_lon = si.get("adv_lon") if not ref_lat or not ref_lon or (abs(ref_lat) < 1e-6 and abs(ref_lon) < 1e-6): ref_lat, ref_lon = None, None all_contacts = list(self._contacts) + list(self._discovered) result = [] for hop_hex in hop_hexes: prefix = hop_hex.lower() candidates = [ c for c in all_contacts if ( c.type in (ContactType.REPEATER, ContactType.ROOM) and c.public_key_hex.startswith(prefix) ) ] if not candidates: result.append(None) elif len(candidates) == 1: found = candidates[0] result.append(found) if found.has_location: ref_lat, ref_lon = found.latitude, found.longitude else: if ref_lat is not None and ref_lon is not None: located = [c for c in candidates if c.has_location] if located: found = min( located, key=lambda c: _distance_sq(ref_lat, ref_lon, c.latitude, c.longitude), ) else: found = candidates[0] else: found = candidates[0] result.append(found) if found.has_location: ref_lat, ref_lon = found.latitude, found.longitude return result def is_contact_known(self, pub_key_hex: str) -> bool: """Check if a contact with this public key already exists.""" return pub_key_hex in self._contacts_by_key def add_contact(self, pub_key: bytes, contact_type: int, name: str): """Add a contact manually by public key.""" self.send_frame( build_add_update_contact( pub_key, contact_type=contact_type, name=name, path_len=0xFF, # flood - no known path to new contact ) ) # Add to local list immediately contact = Contact( public_key=pub_key, name=name, type=contact_type, path_length=-1, is_active=True, ) self._add_local_contact(contact) # Also refresh from device to get firmware-assigned fields GLib.timeout_add(500, lambda: self._frame_handler.send_get_contacts() or False) def import_contact(self, contact_frame: bytes): """Import a contact from a meshcore:// URI payload.""" self.send_frame(build_import_contact(contact_frame)) # Try to add to local list immediately by parsing the advert frame from meshy.application import MeshyApplication name, contact_type = MeshyApplication._parse_advert_name(contact_frame) if name: try: # Extract pub key from frame (after header + path) i = 0 i += 1 # header path_len_byte = contact_frame[i] i += 1 hop_count = path_len_byte & 0x3F hash_size = ((path_len_byte >> 6) & 0x03) + 1 i += hop_count * hash_size pub_key = contact_frame[i : i + 32] if len(pub_key) == 32: contact = Contact( public_key=pub_key, name=name, type=contact_type, path_length=-1, is_active=True, ) self._add_local_contact(contact) except (IndexError, ValueError): pass # Also refresh from device to get firmware-assigned fields GLib.timeout_add(500, lambda: self._frame_handler.send_get_contacts() or False) def export_self_to_clipboard(self): """Export self contact as meshcore:// URI to clipboard.""" self._pending_clipboard_export = True self.send_frame(build_export_contact(b"")) # empty key = export self def set_custom_var(self, key_value: str): """Set a custom variable on the firmware (e.g. 'gps:1').""" self.send_frame(build_set_custom_var(key_value)) def set_advert_name(self, name: str): self.send_frame(build_set_advert_name(name)) def set_advert_latlon(self, lat: float, lon: float): self.send_frame(build_set_advert_latlon(lat, lon)) def _build_telemetry_byte(self) -> int: si = self._self_info return ( (si.get("telemetry_mode_env", 0) & 0x03) << 4 | (si.get("telemetry_mode_loc", 0) & 0x03) << 2 | (si.get("telemetry_mode_base", 0) & 0x03) ) def _send_other_params(self): """Send CMD_SET_OTHER_PARAMS with current self_info values.""" from meshy.protocol import build_set_other_params si = self._self_info self.send_frame( build_set_other_params( manual_add_contacts=int(si.get("manual_add_contacts", False)), telemetry_flags=self._build_telemetry_byte(), advert_loc_policy=si.get("adv_loc_policy", 0), multi_acks=si.get("multi_acks", 0), ) ) def set_advert_loc_policy(self, policy: int): """Set advert location policy (0=don't include, 1=include).""" self._self_info["adv_loc_policy"] = policy self._send_other_params() def set_telemetry_mode(self, base: int, loc: int, env: int): """Set telemetry privacy modes (0=deny, 1=allow by flags, 2=allow all).""" self._self_info["telemetry_mode_base"] = base self._self_info["telemetry_mode_loc"] = loc self._self_info["telemetry_mode_env"] = env self._send_other_params() def set_multi_acks(self, value: int): self._self_info["multi_acks"] = value self._send_other_params() def set_radio_params( self, freq_hz: int, bw_hz: int, sf: int, cr: int, client_repeat: bool | None = None ): self.send_frame(build_set_radio_params(freq_hz, bw_hz, sf, cr, client_repeat)) def set_tx_power(self, power_dbm: int): self.send_frame(build_set_radio_tx_power(power_dbm)) def set_device_pin(self, pin: int): self.send_frame(build_set_device_pin(pin)) def remove_contact(self, contact: Contact): self.send_frame(build_remove_contact(contact.public_key)) self._storage.remove_contact(contact.public_key_hex) self._contacts = [c for c in self._contacts if c.public_key_hex != contact.public_key_hex] self._contacts_by_key.pop(contact.public_key_hex, None) # Keep in _discovered so the name is available for re-adding or discovery if not any(c.public_key_hex == contact.public_key_hex for c in self._discovered): self._frame_handler.add_or_update_discovered(contact) self._contacts_view.update_contacts(self._contacts) def reset_contact_path(self, contact: Contact): self.send_frame(build_reset_path(contact.public_key)) # Update local state immediately so the UI reflects the change contact.path_length = -1 contact.path = b"" for i, c in enumerate(self._contacts): if c.public_key_hex == contact.public_key_hex: self._contacts[i].path_length = -1 self._contacts[i].path = b"" break self._contacts_view.update_contacts(self._contacts) # Also update the content header title if this contact's chat is open if ( self._chat_view_inst and self._chat_view_inst._contact and self._chat_view_inst._contact.public_key_hex == contact.public_key_hex ): if hasattr(self, "_content_title_label") and self._content_title_label: self._content_title_label.set_label( _("{name} ({path})").format(name=contact.name, path=contact.path_label) ) else: self._content_page.set_title( _("{name} ({path})").format(name=contact.name, path=contact.path_label) ) def update_contact_flags(self, contact: Contact): """Update contact flags on the device (telemetry permissions, favorite, etc.).""" self.send_frame( build_add_update_contact( pub_key=contact.public_key, contact_type=contact.type, flags=contact.flags, path_len=(((contact.path_hash_size - 1) << 6) | (contact.path_length & 0x3F)) if contact.path_length >= 0 else 0xFF, path=contact.path, name=contact.name, lat=contact.latitude, lon=contact.longitude, ) ) def set_contact_path( self, contact: Contact, path_len: int, path: bytes = b"", name: str | None = None ): """Set a custom path and/or name for a contact. path_len=0xFF for flood.""" self.send_frame( build_add_update_contact( pub_key=contact.public_key, contact_type=contact.type, flags=contact.flags, path_len=path_len, path=path, name=name or contact.name, lat=contact.latitude, lon=contact.longitude, ) ) # Optimistic update: reflect the change in local state immediately if path_len == 0xFF: new_path_length = -1 new_path = b"" new_hash_size = 1 else: new_path_length = path_len & 0x3F new_hash_size = ((path_len >> 6) & 0x03) + 1 new_path = path contact.path_length = new_path_length contact.path = new_path contact.path_hash_size = new_hash_size for i, c in enumerate(self._contacts): if c.public_key_hex == contact.public_key_hex: self._contacts[i].path_length = new_path_length self._contacts[i].path = new_path self._contacts[i].path_hash_size = new_hash_size break self._contacts_view.update_contacts(self._contacts) if ( self._chat_view_inst and self._chat_view_inst._contact and self._chat_view_inst._contact.public_key_hex == contact.public_key_hex ): if hasattr(self, "_content_title_label") and self._content_title_label: self._content_title_label.set_label( _("{name} ({path})").format(name=contact.name, path=contact.path_label) ) else: self._content_page.set_title( _("{name} ({path})").format(name=contact.name, path=contact.path_label) ) def set_channel(self, index: int, name: str, psk: bytes): self.send_frame(build_set_channel(index, name, psk)) # Re-fetch channel info so the UI updates GLib.timeout_add(500, lambda: self.send_frame(build_get_channel(index)) or False) def add_channel_from_qr(self, name: str, secret: bytes): """Add a channel scanned from a QR code, using the first empty slot.""" for idx in range(self._device_info.max_channels): ch = next((c for c in self._channels if c.index == idx), None) if ch is None or ch.is_empty: self.set_channel(idx, name, secret) self.show_toast(_('Channel "{}" added').format(name)) # Refresh channels after a delay GLib.timeout_add(700, self._fetch_all_channels) return self.show_toast(_("No empty channel slots available")) def _fetch_channels_sequential(self, index): self._conn_ctrl.fetch_channels_sequential(index) def _send_sync_next_message(self): self._conn_ctrl.send_sync_next_message() def _finish_msg_sync(self): self._conn_ctrl.finish_msg_sync() def _fetch_all_channels(self): self._conn_ctrl.fetch_channels_sequential(0) return False def remove_channel(self, channel: Channel): # Clear channel on device by setting empty name and zero PSK self.send_frame(build_set_channel(channel.index, "", bytes(16))) self._storage.remove_channel(channel.index) self._channels = [ch for ch in self._channels if ch.index != channel.index] self._channels_view.update_channels(self._channels) self._frame_handler.rebuild_key_store() def set_channel_notification_level(self, channel: Channel, level): channel.notification_level = level self._storage.set_channel_notification_level(channel.index, level) def reboot_device(self): self.send_frame(build_reboot()) def factory_reset_device(self): from meshy.protocol import build_factory_reset self.send_frame(build_factory_reset()) def refresh_contacts(self): self._frame_handler.send_get_contacts() def refresh_battery(self): self.send_frame(build_get_batt_and_storage()) def refresh_stats(self): self.send_frame(build_get_stats(STATS_TYPE_CORE)) GLib.timeout_add(200, lambda: self.send_frame(build_get_stats(STATS_TYPE_RADIO)) or False) GLib.timeout_add(400, lambda: self.send_frame(build_get_stats(STATS_TYPE_PACKETS)) or False) def set_path_hash_mode(self, mode: int): """Set the device's path hash mode: 0=1-byte, 1=2-byte, 2=3-byte.""" self.send_frame(build_set_path_hash_mode(mode)) self._device_info.path_hash_mode = mode self._device_view.update_device_info(self._device_info) # ─── Public API for views ───────────────────────────────── @property def storage(self) -> Storage | None: return self._storage @property def self_info(self) -> dict: return self._self_info @property def split_view(self): return self._split_view @property def content_page(self): return self._content_page @property def toast_overlay(self): return self._toast_overlay @property def ble_transport(self): return self._ble @property def rx_log(self) -> list: return self._rx_log @property def cli_prefix_counter(self) -> int: return self._cli_prefix_counter @cli_prefix_counter.setter def cli_prefix_counter(self, value: int): self._cli_prefix_counter = value def show_toast(self, message: str, timeout: int = 3): toast = Adw.Toast(title=message, timeout=timeout) self._toast_overlay.add_toast(toast) def send_frame(self, frame: bytes): self._transport.send_data(frame) def refresh_chat_header(self): self._refresh_chat_header() def show_contact_detail(self, contact: Contact): self._contacts_view._show_contact_detail(contact) def get_regions(self) -> list[str]: if self._storage: return self._storage.get_regions() return [] def set_cli_response_callback(self, callback): self._cli_response_callback = callback def set_rx_log_callback(self, callback): self._rx_log_callback = callback def set_advert_listener(self, callback): self._advert_listener = callback def set_status_callback(self, callback): self._status_callback = callback def set_neighbors_callback(self, callback): self._neighbors_callback = callback if callback is None: self._cancel_pending_binary("neighbors") def set_acl_callback(self, callback): self._acl_callback = callback if callback is None: self._cancel_pending_binary("acl") def _cancel_pending_binary(self, req_type: str): stale = [k for k, v in self._pending_binary_requests.items() if v["type"] == req_type] for k in stale: del self._pending_binary_requests[k] def set_advert_path_callback(self, callback): self._advert_path_callback = callback def set_path_discovery_callback(self, callback): self._path_discovery_callback = callback def remove_tcp_companion(self, address: str): self._remove_tcp_companion(address) def get_messages_for_contact(self, contact, limit=30, offset=0): return self._storage.get_messages(contact.public_key_hex, limit, offset) def get_channel_messages(self, channel_index, limit=30, offset=0): return self._storage.get_channel_messages(channel_index, limit, offset) @property def contacts(self) -> list[Contact]: return self._contacts def update_contacts_from_storage(self): """Reload contact flags and names from storage and refresh the view.""" for i, c in enumerate(self._contacts): stored = self._storage.get_contact(c.public_key_hex) if stored: self._contacts[i].flags = stored.flags self._contacts[i].name = stored.name self._contacts_view.update_contacts(self._contacts) @property def channels(self) -> list[Channel]: return self._channels @property def device_info(self) -> DeviceInfo: return self._device_info @property def is_connected(self) -> bool: return self._transport.is_connected # ─── Backup & Restore ────────────────────────────────────── def get_auto_add_config(self) -> dict: return self._settings_view.get_auto_add_config() def export_backup(self) -> dict: return self._backup.export_backup(self) def import_backup(self, backup: dict, restore_messages: bool = True): return self._backup.import_backup(self, backup, restore_messages) meshy/uv.lock000066400000000000000000001134741521052255700135250ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.11" [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "distlib" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/b2/d6fc3f2347f43dada79e5ff118493e8109c98400a0e29a1d5264a3aa479b/distlib-0.4.1.tar.gz", hash = "sha256:c3804d0d2d4b5fcd44036eb860cb6660485fcdf5c2aba53dc324d805837ea65b", size = 610526, upload-time = "2026-06-02T11:17:40.691Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/18/3497c4fa83a76dcb154923fd2075522e8dd6995ecee4093c00ae18160046/distlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:9c2c552c68cbadc619f2d0ed3a69e27c351a3f4c9baa9ffb7df9e9cdc3d19a97", size = 469216, upload-time = "2026-06-02T11:17:38.779Z" }, ] [[package]] name = "filelock" version = "3.29.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, ] [[package]] name = "identify" version = "2.6.19" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "meshy" version = "0.0.0" source = { virtual = "." } [package.dev-dependencies] dev = [ { name = "pre-commit" }, { name = "pycryptodome" }, { name = "pyserial" }, { name = "pyzbar" }, { name = "ruff" }, { name = "segno" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ { name = "pre-commit", specifier = ">=4.6.0" }, { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pyserial", specifier = ">=3.5" }, { name = "pyzbar", specifier = ">=0.1.9" }, { name = "ruff", specifier = ">=0.15.15" }, { name = "segno", specifier = ">=1.6.6" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "platformdirs" version = "4.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] name = "pre-commit" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] [[package]] name = "pycryptodome" version = "3.23.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, ] [[package]] name = "pyserial" version = "3.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, ] [[package]] name = "python-discovery" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyzbar" version = "0.1.9" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/24/81ebe6a1c00760471a3028a23cbe0b94e5fa2926e5ba47adc895920887bc/pyzbar-0.1.9-py2.py3-none-any.whl", hash = "sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d", size = 32560, upload-time = "2022-03-15T14:53:40.637Z" }, { url = "https://files.pythonhosted.org/packages/8e/87/7b596730179ddf17857eea33ba820354dd4e1cf941e57f51ffccce26c409/pyzbar-0.1.9-py2.py3-none-win32.whl", hash = "sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518", size = 810633, upload-time = "2022-03-15T14:53:43.446Z" }, { url = "https://files.pythonhosted.org/packages/0a/e2/1c6a8e94197612dbdfc51eab8dfb674168829885fac2c4f50ac8366c25ca/pyzbar-0.1.9-py2.py3-none-win_amd64.whl", hash = "sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c", size = 817363, upload-time = "2022-03-15T14:53:46.691Z" }, ] [[package]] name = "ruff" version = "0.15.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, ] [[package]] name = "segno" version = "1.6.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/2e/b396f750c53f570055bf5a9fc1ace09bed2dff013c73b7afec5702a581ba/segno-1.6.6.tar.gz", hash = "sha256:e60933afc4b52137d323a4434c8340e0ce1e58cec71439e46680d4db188f11b3", size = 1628586, upload-time = "2025-03-12T22:12:53.324Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d6/02/12c73fd423eb9577b97fc1924966b929eff7074ae6b2e15dd3d30cb9e4ae/segno-1.6.6-py3-none-any.whl", hash = "sha256:28c7d081ed0cf935e0411293a465efd4d500704072cdb039778a2ab8736190c7", size = 76503, upload-time = "2025-03-12T22:12:48.106Z" }, ] [[package]] name = "virtualenv" version = "21.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, ]