pax_global_header00006660000000000000000000000064145246246660014531gustar00rootroot0000000000000052 comment=7722b2d8ffe8a36de04029f46a4b33480fa3ab9b passes-0.9/000077500000000000000000000000001452462466600126775ustar00rootroot00000000000000passes-0.9/.gitignore000066400000000000000000000000711452462466600146650ustar00rootroot00000000000000*.json~ .flatpak-builder /subprojects/blueprint-compiler passes-0.9/COPYING000066400000000000000000001045141452462466600137370ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 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. Copyright (C) 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: Copyright (C) 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 . passes-0.9/README.md000066400000000000000000000014431452462466600141600ustar00rootroot00000000000000![Application icon](data/icons/hicolor/scalable/apps/me.sanchezrodriguez.passes.svg) # Passes *Passes* is a digital pass manager made with libadwaita for the GNOME desktop. ![Screenshot](/data/screenshots/passes.png) ## Features - Adaptive user interface. - Support for ".espass" and ".pkpass" files. - Ability to update ".pkpass" files. ## Build You can build *Passes* using GNOME Builder: import the project and press the Play button. ## Install The recommended way of installing *Passes* is via Flatpak: Download on Flathub
## License *Passes* is available under the terms of the [GPL 3.0](/COPYING) license. passes-0.9/build-aux/000077500000000000000000000000001452462466600145715ustar00rootroot00000000000000passes-0.9/build-aux/meson/000077500000000000000000000000001452462466600157125ustar00rootroot00000000000000passes-0.9/build-aux/meson/postinstall.py000077500000000000000000000012121452462466600206370ustar00rootroot00000000000000#!/usr/bin/env python3 from os import environ, path from subprocess import call prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') datadir = path.join(prefix, 'share') destdir = environ.get('DESTDIR', '') # Package managers set this so we don't need to run if not destdir: print('Updating icon cache...') call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) print('Updating desktop database...') call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) print('Compiling GSettings schemas...') call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')]) passes-0.9/data/000077500000000000000000000000001452462466600136105ustar00rootroot00000000000000passes-0.9/data/icons/000077500000000000000000000000001452462466600147235ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/000077500000000000000000000000001452462466600163625ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/scalable/000077500000000000000000000000001452462466600201305ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/scalable/actions/000077500000000000000000000000001452462466600215705ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/scalable/actions/info-symbolic.svg000066400000000000000000000004361452462466600250660ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/scalable/actions/qr-code-symbolic.svg000066400000000000000000000014541452462466600254660ustar00rootroot00000000000000 passes-0.9/data/icons/hicolor/scalable/apps/000077500000000000000000000000001452462466600210735ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/scalable/apps/me.sanchezrodriguez.passes.svg000066400000000000000000000250231452462466600271010ustar00rootroot00000000000000 passes-0.9/data/icons/hicolor/symbolic/000077500000000000000000000000001452462466600202035ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/symbolic/apps/000077500000000000000000000000001452462466600211465ustar00rootroot00000000000000passes-0.9/data/icons/hicolor/symbolic/apps/me.sanchezrodriguez.passes-symbolic.svg000066400000000000000000000727701452462466600310060ustar00rootroot00000000000000 Adwaita Icon Template image/svg+xml GNOME Design Team Adwaita Icon Template passes-0.9/data/icons/meson.build000066400000000000000000000013711452462466600170670ustar00rootroot00000000000000application_id = 'me.sanchezrodriguez.passes' scalable_dir = join_paths('hicolor', 'scalable', 'apps') install_data( join_paths(scalable_dir, ('@0@.svg').format(application_id)), install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) ) symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') install_data( join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) ) action_dir = join_paths('hicolor', 'scalable', 'actions') action_icons = [join_paths(action_dir, 'info-symbolic.svg'), join_paths(action_dir, 'qr-code-symbolic.svg')] install_data( action_icons, install_dir: join_paths(get_option('datadir'), 'icons', action_dir) ) passes-0.9/data/me.sanchezrodriguez.passes.desktop.in000066400000000000000000000004551452462466600230770ustar00rootroot00000000000000[Desktop Entry] Name=Passes Comment=A digital pass manager Exec=passes Icon=me.sanchezrodriguez.passes Terminal=false Type=Application Categories=GTK;Utility; StartupNotify=true # Translators: Do NOT translate or transliterate this text (these are enum types)! X-Purism-FormFactor=Workstation;Mobile; passes-0.9/data/me.sanchezrodriguez.passes.gschema.xml000066400000000000000000000013261452462466600232250ustar00rootroot00000000000000 'relevant_date' Default sort order The default order for passes. passes-0.9/data/me.sanchezrodriguez.passes.metainfo.xml.in000066400000000000000000000074361452462466600240350ustar00rootroot00000000000000 me.sanchezrodriguez.passes.desktop Passes Manage your digital passes CC0-1.0 GPL-3.0-or-later Pablo Sánchez Rodríguez

Passes is a handy app that helps you manage all your digital passes effortlessly. With Passes, you can conveniently store your boarding passes, coupons, loyalty cards, event tickets, and more, all in PKPass or esPass format.

Moreover, the app seamlessly adjusts to different screen sizes, allowing you to access your passes on various devices, whether it's a desktop computer or a mobile phone.

Stop wasting time searching through your email or printing out your digital passes. Download Passes now and keep all your passes in one convenient location.

application/vnd.apple.pkpass application/vnd.espass-espass+zip
  • Added the ability to sort passes based on different criteria.
  • Visual refinements to match the state of the art of GNOME apps.
https://raw.githubusercontent.com/pablo-s/passes/main/data/screenshots/passes.png none none none none none none none none none none none none none none none none none none none none none none none none none none none keyboard pointing touch 332 https://github.com/pablo-s/passes https://github.com/pablo-s/passes/issues me.sanchezrodriguez.passes.desktop
passes-0.9/data/meson.build000066400000000000000000000026421452462466600157560ustar00rootroot00000000000000desktop_file = i18n.merge_file( input: 'me.sanchezrodriguez.passes.desktop.in', output: 'me.sanchezrodriguez.passes.desktop', type: 'desktop', po_dir: '../po', install: true, install_dir: join_paths(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: 'me.sanchezrodriguez.passes.metainfo.xml.in', output: 'me.sanchezrodriguez.passes.metainfo.xml', po_dir: '../po', install: true, install_dir: join_paths(get_option('datadir'), 'metainfo') ) appstream_util = find_program('appstream-util', required: false) if appstream_util.found() test('Validate appstream file', appstream_util, args: ['validate', '--nonet', appstream_file] ) endif install_data('me.sanchezrodriguez.passes.gschema.xml', install_dir: join_paths(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 subdir('icons') mimedir = join_paths(get_option('prefix'), get_option('datadir'), 'mime/packages') mime_sources = [ 'mime/me.sanchezrodriguez.passes.espass.xml', ] install_data(mime_sources, install_dir: mimedir) passes-0.9/data/mime/000077500000000000000000000000001452462466600145375ustar00rootroot00000000000000passes-0.9/data/mime/me.sanchezrodriguez.passes.espass.xml000066400000000000000000000004061452462466600240410ustar00rootroot00000000000000 esPass pass passes-0.9/data/screenshots/000077500000000000000000000000001452462466600161505ustar00rootroot00000000000000passes-0.9/data/screenshots/passes.png000066400000000000000000001773311452462466600201700ustar00rootroot00000000000000PNG  IHDRMeXIfII* (1 2iHHGIMP 2.10.362023:11:13 08:25:57t;iCCPICC profilex}=H@_S-"vPqP" EjVL.& IZpc⬫ ~8)HK -b=8ǻ{wP/3nD\dW+BeIR=||!5g1'ǘa3y8̊J|N X)nbKGDC pHYs  tIME 9e_ IDATxy|egrrQ‚r] CW9Ud'$3LwB#n@DDDDDDDTpm""""""""5*Xw"""""""ZVVJDDDDDDDvjEP%nDDDDDDDDnFUCDDDDDDDTԘJ`=j:DDDDDDDDj :򚵱NDDDDDDDޤ^{n6""""""""k n:BQ#jC*Z?wDDDDDDDD6U* 'QDDDDDDDDUUNUU#h{IFvHAh%TS[r%KnereX#Tr;WLUYsH7Ъhpʕ@Ut;-DDDDDDDD5d籣rW[802@ دP툈jB(ەU")&)/6 \F1"""""""Fray{!UE-W[[^(˝("""""""*3lUE{OU(WP@Jpr ]Iv핳|l@UpS %L %8񘈈ȝIN<2:['Tfrg:J)Xl(7EDDDDDDD̕Փsp*CgC)rvxP6n*W_UeTUS 'ᨗe(GC'_+Q%:Xgu`*CR""""""""r'x*o@U-TUS9J iSDDDDDDDH *NgHKH(e+R8xWONDDDDDDDT]LX]e)r&qe(e+xXG۔DDDDDDDD5|o/ئUi8`yH*~۔š|DDDDDDDD$9x(e +QUz{Te p)""""""";ه9x7LϜXT$x,Y_ظ<ֶ&3WXo/RX.vx`ܕ Ju(ek(S2CSzLbbtsJ)eRYUDDDDDDDDd%NbFd^fkPb%uTv˩ g J7gC){Ie7q'^^^C%Ib(EDDDDDDDt$I oXg*ssۻ0]e3 LUD%l4XQ`)xW*1 J$ &&==}>/XOdc%WRVjG%LlPu(JY1޸qcooo'J'IشiSoܹxTV e+7xI #eosge73\[$Ij`61EDDu ""*U?? q|睲kd*IЭʔ1UIK7,PpePe^cJ9O&7nN?!}: 9۔do^s%EU8(zfLѺh"dff{=|L7n4h%jb{UיLJ*j%$`=A%Ad /dB@hh( ໼=iii={6.\Pƍػw/ڴi aÆ8q"A~ʃG_ȟEGٳo&T"7///d2K]mW//RAkea$$$ĤIj{_ރ=lϯ)oN#jUJQ޽;Q\\~ SN{%"""$-- ?<.\0̚5 8|0֯_CHHΜ9 & ##N#""7Ç1k֬jw=7 \v /^O<۷c0L.JP(b *Z'N*;v5DeR=ڴiYf! ԩSشi}WaOJ_eT娘+˹j;rA~zj"77k֬ARRnݺ@bh4ÇcڵHMM7"""obZ BAA:t耿hذa%""";wW,YbD .3<3gK?\8y$:w,)**G}K>ɶLB`ӦM;w&LRݻK̿n:l߾7oDDD&LnݺY_~ľ}O`„ ^G {N>/pdzٳgc߾}ɓ',YN:9ՖX4j7oFÆ ˯ၽ{d2aذax;BCC믣Ce^`08\3g`٘?>Ao4iҤ}(L#_T׈ FÇѸqc("!!_|;6̙37n!CгgOxzz:7o6n܈-[{8z(~mHTDDDDuV@||Ó'O>zj7>v oooDEEsKٳǪ̼yyfE8{l8SƒȆ^zav.]+WJB׮]qEL6 U͛7c CNNʟOv^_~rhǣqN姟~իVZYФIdggcʕ7nZ-ի?o6 Cm1ϝ;5•+W0{l̞=))) ܹs+ڦ e}@R{{Ŕ)S ^AXbT*-[Nޏ3eJE~sN#22f7obƍh֬@Rax_CV jY+W/ݻ |\.==GƑ#GsU{}~Æ 6mڔ-}Qej*|爉 < \4ha{矎ԫW ,Yp@1|Vjߕm$kL0wTk 6L~P(/#ϟ? 6G^^$IB^^UV1l0tcǎE˖-KMM?~THp[""""|@PPrUwO%I۷qm/_`zʴf(emڴiW\6=kjPQFwӰBбcGԯ__׬Y3@FF^{54h:"""0rH/Ε+Wg۷˗/G֭t /U]6lZp;DDDldee zM2sL,Z |~gu{A۶mytǙ2jXmtpkۯ_ɓ'aÆ1c5kCL_xzzV5-ͮz܍ {6eggcHKK;M\zgƂ  QzV瘢:ƌ3'Rpit:DGGSN$_!!!8q"֬YѣGCEEDD@PҥK DӦMG%"""w^ڵk^oU6nh51._(ݻW}Vpw.c.\`/gb}c9hKDDСCU> vr)t:t ;wy^9_mUpU[\{k3gJ-X ,@&MpU̙3RuNJ_Tկ_e >}˗1|t ͛7w^zxGi&L<}ŭ[PTT7xDDDDf_p8)**³>7n`˖- Uys͛QFHOOǥKԩSxмysVвe2bbٳ'yRÜZhhy^VZaPՈ+y͛a4I-ddd`ʕ;v{-l|܍Zvxehs̑{G-XsAJJJv'19ر#Ǝ J;w"22z ڵ ׯ x饗еkWॗ^B||< ֯_s $-nЍ}ǭ[0e1sL\z-[O֛WQϞ=K+yuyW^ĵk0o4mv < T*mۆVZY]^|EcƍtRwWŕm)kM6aٲeVCl2lڴRGWH( 6Ux.((|S޽W}luA8/8 \:ʗ#w&""rU,쑜ǞPDD\I&Xr%f͚cѢE6˵iͫPvy0Ube^Ou+G""""r իcl۶ ΝCvv6`۷/qm#^Ε}||PXX˫(;vWC Q]á|DT[hZUj*vg9P>KQE)|||*J*~D'j5wJ^߫ RL&z= F# JZ F)"""""""{DT˭""""""""j`)"""""""" Z0""""""""j`)"""""""" Z0""""""""j`)"""""""" Z0""""""""j`)"""""""" Z0""""""""j`:IxFNI(TDT]́P]LQbyg~,2DDUdP("wLQP22L&L&F^ @RATBT=b0EDDDDDn2lE$h4BCT*BD5;TPPNgdE%I}G1""""""?3KQ BBBDDJjj@!''FQ=Nᔂo?+PJEL& 4`(ED54hoood2ɽ=-)"""""rK%EZ"jVTڌCȭ(󃯯/ E{L1$000 =V 0 rPڌ5^  "a2ܪ] ȭ-e2R89jPT0Lnk-Qa08___ 9r -I$bo)"rr)^3f 233=z4{ {߾};w.DDDDD5D^ET 9~*JlAmr7k׮?7n@h޼9?DDDDDnܛ|W qR5 ~gǏǤIuIIIpL&no6BBBУG<3xW6oތ˗SNѣG9r$vizذa'Oij>+V@TgŇ~Xy޽{cС4iznݺᣏ>z5 #FiA]bƌZ_MSLZ:`ԩhڴ)?D`m92s`W_֭[1~xtЁ`"""""itr V\8j O1o:t^^^Gnn.֬Y$ܺu h0i$;wK,A˗/ƍcǎU۷/;=V\.]{ IfyyyXx1Ծ,_p郿V"##1h emڴ[[7nO>$*|VZŋ222uV5jnݺk׮ؼy3 Cm2H <FZZ=)Quv˗cPTx衇p%̞=N,Z[lApp05ksYChܸ1z Q/.p ycǎ!00Pv9}4 SO9,i?k IDATqƘ;wn6)))ضm|.w]F-[ޫUt邽{… k"**JǑٳgC )wRSS֭[DDuTff&~G4k ׿Re,^ h߾=]"441T*>c|~ _8;v 񈍍Ŋ+pqŵkא!CKDDDDD5[ZZ ((a͛x']jݻ{n+g: 3g?<nO=> ӧOƍ㭷*U8~kjSv镵j*wzz͜9&"=RRR "z!}\rsr[T? .ĸq /@$4hΝCQQ;VQ πʞ &&?f>,[Ν+T( 2|ZFѪdR*7~xڵ ,@ӦMmNd'NDRRn݊|rOVZDDu3*..z=A'cma^{ /^믿ʘ8|/Z x7zj1Ӷ<0i$\vM^h|$ .Dpp0g}bP^È##} }砪<|[lDDDDDT{M0aaaꫯj*xxx]v˃U^zᩧ·~kbJ4rHӓO>D}],]0 h߾=yիW/k$&&"11>>>hݺ5-[y_4e}4iÆ CBB;.]ET woeq^aevc}[V?uV,^𹢢"?ÇQ\\VZaĉdggcҥ8y$7n1c࣏>£>8g}(gϞj8<֭[''!!+VrrrޠDDDuAFF͉(d2h4-ZA#"___T*(Jj%i^@.Žxhq,nxe%Kǭȑ#={6 iӦ_)""kLQ]noiۿ?gϞ<DDDDDDDDw1bFFC=BDDDDDDDt*vQ];v """""""*=Z0""""""""j`)"""""""" ZUZ-z=F#i"_*4 x0 cL& HQa4a4 RɃBDDDDDn-R$ո O&Qe4QPP ""jpy,[ gϞE-K/sUR&??˗/lj'p DDD? Q_㧟~˗QFa~)n݊DFF⥗^C=dmZcƌA޽k ;w"55>>>ɓT]m3-os9Zm$Iֲ9FZKMM7/ .Lnn.ƍ;w.֭[QF!88}v$%%__qaعsO?;#kעcǎxWi}K,Azz2N[n_Ƿ~S?uuԷm"zLzJ? bkBq'/A'{Q-zMc_~%zꅱcq9|;wKˬ[m۶ŬY׏?#8p|8~8ك~ۇQFSN)S`׮]8{,ׯoնxǭcԩ}aÆxee6;S򴉈܃[HNNVjiZNF&ɪDTwp<""{oǎ0`ղbϞ=./zZzFCfͬ;#Z.8ܾ}5rζٙ:&"rHpGE F::c }7oƫO>vY={ 55CuǏǖ-[`YWe8p@ܞ+WO>x嗑lpδ:S܋4TEL&z||̿R%>>^T#"""""r%77jyAA;C\]K.֭ЧO 6 ۷o/UL,\/2sERR֯_ 6O>$7nᅬ &C=qD=z7nDn0c \p:fgl"r/nLYNp.IF#V 񡔙(a0FǗLb|||TCV|5k YYYVz=N|Æ a͘7oZlp,X:uʕ+ܹ]AA#::o_"::6l>9T(S"22pδٙ:SO1e4q:vt:Chd)""""*-۹s|cǎő#G4 kYmvΔ!"S')IG(=S7Ú5kpMlذ;wĘ1c\^駟Fbb"kܼyvBbb"F o&RRRpBHV V IЪU+cƌߑ~ 6l@nsRR.]'NHJJᅬt:UWgL}]&"]8|ܲb;sMy^u>֭[Ș'+9hDaa!쎨uo>WJ%_GBBB.c%@PPPyL&FENĉ㏑ ڶm[%e<+V %%͚5äIеkWw&;mqӦMhԨ__HJJB~~>6m#F8 pBxxxȓ᫯ѣG___tмysloEDTפ* J nF |}}{``>-nasW}0emd0PTTTk)ooojCgd">>ԺÇcѢEhժLE8vva̘1' v^}R޽W\Aaa!իN:aԨQVWǏcx7ԨפIpu/_ƍ}q ̟?̙3xb)"""wL`JU4wgnCU2;w.t:F0bxW"|gv%Kpe9͛7ZFFFΞ= OO2떜%K`r(U[gqӦM8uf͚%*w `8|0u&[n  RyM""""""R?wEΝѠAt .֭[gU~_p ݶm\?111 Chh(:tQFݼy ,qBZ_Nte oooh4(J7A(w` :֭d:u .]IDDDDDDT >|84rZ1c`޽(((׫Wcǎt:0zhrש.뇿/5p1̞=/_GY'h֭[!I֮]#FϯRIDDDDDDTY ܘhN+uTTT,DFFG6m IR@ B@@lnWPP۷oe˖宷^… q}aر5f111ǻヒ "st;}g;v 1%"""""",A(q-kpyZ Ff~O?Lff& zOOO";;j 3g"66V볲~zy/z͋/?#jE}-!!!ꫯ+{V9:^f={Ė-['`ڴiPT>DDDDDDD` p5^y$⇓+Y2UW2u|||J5_ͰA5k ƿo,Xj]hh(;{<2d`ʕvݶm[K,EШQY_~fDQҥK|r(J߳4h ddd ::үIDDDDDD * $ Y&쿤X.???={/^Ba79r$^}Ul߾j'BCC֭[3_yq6lRSS`,ZHLjR}j͡N§~iӦ+L/w/%QmPۯJDuWU\&S>n|ˀ@oP¿ ~Zj(X~= zxx`ĉXn]^ÇǏ?k׮U^/|||W?ƌ3J Q䛭eo5軌ȝT%d8}<)uR1 R9Oȑ#1cмysdddoEAAz)GEEsضmbbb1}t3۷'.]ÇO>V1}tL>Vĉkt}8kbΜ9]T;^3@TNxGD5UR 7w{Q;a0U ZuA>J<]5u# x~z|W~:#F9/CjرcߏT*a„ ݻw oiӦ4hPoΝѪU+yx?l{ n)QyVP(0|RɀܖpVVGۙ6[QXlg~n)ޫ>6ܺu`y~پ} (EZSvGmƠvXuyL4J)q!hQb<\mrY} j  !!!Nɱ9GX]a($ Fz4N^5"%ӈBSDT(Т TD@@RAo`FF|['(0QPP-ZEDn!%%2A@.Žxhq,nxe%K=% n曰$z IQO o:GxgK rQU3{HN"9 l?S̃DDJVB=. #m<1M򜏖c5+b0U %(_\q Yo}_ѣ}"|=P౎>cġ|MRrQMeJ{jZNVbRD)3kѧe^ ̃@Dn!''ǭ` F Ez M՘98FTI󏁰 5f?bUdp+&X"J*99w%V/""xGr1^^^r) )""Ld`4IP)xi;F%@.k0Ix3""r!":J{[J'볜sa0URJ)hK{Lixw~%R=Φ됙P,'5Ot~5G’Z"r{Kv1BP@E( ""ZT%%-ZJBjj*> dr\)#Ȁid!#Owѣ<$$-[?N:,c6rHL2j]ѫW/+\zC_-[____tSNEӦM1p@ܾ}9Fxg"66V^׻wo :&MroGm6m?DTLUQ1'1P9 ")) ;Zli^ǶmбcG$''qrts7nt邰0۷qQhZxxx ::|wh޼9.]|Z7oď?pǪ>& ޽;AٳgwƊﴴ44l?`ڵh߾=[nEΝ ''("22> ~gԩ;4( 4jݺu?:F$ >Ja?,)z""wӯ_?>?,EVbccw^lذo6`۶m$a׮]r0em~p9_oV^ӧCʕ+Xr%ЪU+(Oloδ*1 e]/ N:_~h߾=2F;w|8>C`*22 Nq<9r$8 پ};qؽ{7y}DFFb.f^3"WarHP@@5vg!S}m{B$-B4 Q@k#Ue㨝T3Gڵk-KIImr]tY +WŋxѤI|ǸzwT$''u.o}jDDUBTT^l9jڴ)ѡCj͛#55:Ϊ[q9 ARhFZg4Q*^Zƈ#Tu1996VThԨ222Cʕ+Gdd$;C:t5k޼9g߾}<#:={S6luUpB.\@ƍ>}zK+ׂJvAݓGff&CZZV}DGGi&_E$''s5koBZDFFҠA,մ*^l6Lll/(Et +U>hDVӧ$::`n.DEEjgyincۉ_~B/N{:ՖPw3-2r( LA8=*U'$HMt7hԊozl8qF8=jU1: *槬r%++ JWfݾ[nygϞW_e۶m<ڵk[C9y_+t\~+J2 !DMSufx0 8W[ʫgϞ$&&}vCxx8VX)~ϟt: ZFV3p@mۆ&00C cٹs'Ǐgeh׮߲: \fsظq^GQ|_M6%%%o^Ott /`Zqtҥ755pͯ&RJMx}vovѧc0pzE}ip)[C"^Q.-SzWU)ŗQ[;_5{_C333yP:++xp:8l6/솨5Q{SzZ-՞B\^677zqC vq\8NL&Uz*eڳÌl$(@ŷ{| E+sO4a5,3Lm6װb)Dt`4 j`;155}WrBIRRFR~EQ0}?^߻ bwbv{Ksy婳5ET*4 u~c]h4T( J !Dw`wz1( iHj):k؜T]k] f7'ݥ~#r. |C !BQԻ|ZfS5UUT1nihk֔O!꣜W@TE԰+OsSJxH-d^bǝ]B!DVoڦh|:zPվS !^yk R) azO`NB!D=WS?8 Z6K3>!Y B!>u@:*(nxm Ly_˃RBANFB *tרpoSyإB!u.0e0%jMyR*}5JY_TBԃ F`qkk;Mv~Vܼ{w1a:)˝ ;?wHRB!B>F#f볩5|Ziu`4eG\CQ!tCQ+ظ)Z b4%R&k[ٞd}+Du Ppm 4`-ty1=Gp.Ͼ&ho԰PiT LadD{ QZ,.dYByT V W{S)-/_5t:Ԕ Q36a* }_.YzPRBTBQŻkr9]cyyd3>C)vԣ(دfFZJ0 ! kg{B6s>Iz^-L G !BupԛP;$ %5+D&-Ksᬠ'_+סV#;Ujmq3{Mo<=|[NV|ݻ#BQ/>y=9@%)!6k "T$DR]Ng|r-!ՋRQ[9r֓61x`mRqr64 !T&|qBqml?egq;YЪz7 @{vVZc{8ĠUP m[smFy0mlhh4ZFz_ O<{ZaÆ|rz)!5'5BJkk1ji4Zq{ %5yti@zN9] A*6ظ:!W |v:'DK{-rpovZ ?q-m i'M iSf*ߘEixuy6 Eoos@Q{Y,VX /@DD 6cƍ~ˍ3 &кuk0f̘Av툈iӦ4mڔѣGo߾2MCDDDٳg8q"-Z 66|ƍs!9pBZO !Щ!!RM^cf6pP}#DMKwS+425B7y 2.niȆOƻܦc6c_?5.WҮ(py榒璃^K%''vy}Ov:X,M1;uTj޼9V5kZ~'hܸ1Nnn.Wf߾}L&Ν;f'$0%BT@Fqarcq&F8׏ezqfijSUY|m" Nhhh(SPP<СC@LL <.:B X昛51BQ_5jԈ@6mѣkUގ9Bnn.O?oIK-!D'}L !7utBQ4 SLaΜ9رEZZyl6l2XnP+,,<yh]_e!B!Dz!xIMM%"")SЮ]뚯~i0|ː!C_Wx` !j P2}U2^|UA}Us5433sD|rzn7. Ӊdb"y~Z:Cpp0ZJ+Ѥ^ p\!ntIIIJ]Eh4,]lش=]+4B!B!ׅB!B!u!)!B!Bq]H`J!B!B\u|seοyG !B!B\gu.0ճgOL&ǎ#--N:@f !B!Bu.0sSNrB!B!jM}-/ddddдiS&OL^|q 2N֭[q\32o<9B xgرc0`111^h}2WO>ѣGywر#L4 G}DLL &ӱcGfϞ-|!B!B\w9sh޽;'Odƌ 6HN>͛oRk֬O?EӲerWf߾}|b`ٹs'лwo !B!V5222IHH`h4~fϞ͒%K|>cZ-| 9}e̙#_`jϞ=Fl6;wd߿>}ȧ^!B!B1{h4E!Clhh(Zm_tJ J]>2i&$$ˡCpٳ!CЩS'~QF4iD>B!B!e)M(^/s=Z][v.^4òeصkIII[n>B!B!jP2}U2^|UA}Us5433s#%5˼n˅d21q"!Dt`4 j'bjj*04o\>0B:!)) Xk(ƾ@^߻ bwbv{KsyQ!B!B!׃B!B!u!)!B!Bq]H`J!B!B\B!B!ׅB!B!u!)!B!Bq]hd!B!;N߾}KL4i>,}w̝;o~RRwu'Ndڴi~(BXX={_&88ط޹s;v,_5-Z(5FN:/ҤI￟gy3gw1gvzEy-Fo?|*Nc۶meHZh>J׮]-k1mmܸrL6M>BrBQ/Msw6W |]oZfz3 LKɑl=`Nɇ+q=42ulC.K% Тשw~G *ѺKw=%D]]1 ..KU9z[0"B4gEo oR/A1|px||9t۷M[hD:wq#F`ƍ,[ YEQxYr%V'W^*絼2f{=xK̂ bAH-WyMZ`Rmȗ7fR *L|Mى]st<MA;nAQ!5aZl7Xp!g 55t5jD޽ٳ'}oڵk=z4N"11G}D>Fȑ#9{,;w,Ǐ!C0eʔ+ܸqc{ƍG.]:t(PTjу>}o[h#;0a׿SekiB~B!Jש5b`B-. itg n 9CN0wzRY[yh=CY=iZ/!ndi\Yy*_eyN0s0ҽÎC*/$֬YѣAAA7onݚ>.]pٳݻw7`5jo^rr2'N'..yq9Js~7n&&iӦѸqcx E_Wロ+W/spp0!<BrsSk!5/ԨQ7.}-Dj *JsC\7mq 9nVb1: *V)o!F D}-&_BeU9:GGUHF6bT)}q$&&?7:u7o߾nݚ>V\ɓKMgϞ=;_j:47Wq_O;_BeU9:GOL Jf',UN_\??0?4oS1EQxxWp\4iҤNNԩS/ /fԨQnݚkbYnwbb_e˖ 2?ӪU+oVN>͊+2dH|T%K.5^}JՈ~B~嵒n7. ]ko~BԼ0߰ȥZfkQ&$8@9&ߴ+ ҟ &t!EθAT&_BeU9:Gv#f?ziƒXjNdȑdffDRRS_tz z+˗/ٳ~*kFxx8:W<ӨT*{jُ^.]*_uO Ly;ހ餰, f q:U !6R8]o2N^L:(ЯӥZXY=a)pd-q÷i 5B=WskOw8z J?ĉ[<~6o̒%K}c޽xظqctx <O^}UyJ3T6 <<'|+޷Nb|WL2\^~e*+-M.J9 OmapբN&8"ۑ!jT*#SiB\759I)DirsOX}M;<ّ˝C#}lv7{+ ZPR.n7U<ġZ/!/늸ILLO4g}o0`rYjV"((n{P֮]K۶m-ۭ[7ذacǎK'..dzdvEΝK;dٲe|g}Uke 0n8VZůzEvڵ[FѡCfϞMƍ}/&McצMP Q))o=﫪˨ꋯッۮPiA4<~ H]N@z)hwJQRnn.ukd21qQկ'Dz ^a6wi7 k ƒ5䘜×L YB:!)) Xk(ƾ@^߻ bwbv{KsLOjj;(U,8u? }EBSB@SB?u-0U*^[`I8ppcv_'ߔB!B!zsm*ڬþЅB!B!:r8\\.r/B!B!čט*йQ9u B!B!7MnNU?e^pYy_~SO=ѣ:u*jr̬YxرQFqA3ϔlj'|۫6FM䧢}Py >#Gw^ΛoYbެYxG}X/tB!B]PDPP:Z BQVXA^^zQFӇpRNMMeƌ<(n,X_~رcL<ٷnBΝ;W25Au,N~'|FݻK,c4K @p(]!B!H`Zt) xXn&odb~3oĉq8|wx<>C>BBBj۶m>,[T~Qڵn&z͖-[J,3rH_cǢ(]!B!hdlsr(̊id["-a2h _]oXʢM6ԩsѶm[ҫJ۶my衇JԩSVl63vثfi f更nݺj"?+Ip`*u6lׯ?q:h4>Gzdzxb:u@ff&[neѢE\B!B!j}y3֝dP68)}Z_rV|@pp0YYYiGɓw}e;x`VX;믿ܸmeذaDEE1sLf̘Qf S>VZŪU*,fwG8NKϞ=;v,K,ĉjՊKrm|U !B!5ASX'ҭ8nZZT9ۃr^50 @zz:F|ݎbQFi;wfРA̚5sڣG&%%ů+&*5jTr̚5EVk|mjXk׎͛7 1zh+}Y֬Y J7UIW!B! *GŁAQ1[C&!&Ut Yf7&&$HVpQCf08<9CMͯ)v$ #:s =>( ?#O=1[_g[gժU,]ԾO O|'̚5XVz!|AzBTJPP1hi BVakPjw̤I0Gs/ ',ժմ2 f?F3{r:N/fq<ߟ/(zh~G&NXjztڕ 2tP^~em[P o(iBQWIzd00w\>S>cΟ?O&M޽;?ZmM#""9s&=~Omzٓ6mSAur 4y믿駟.1/$$dIWhޞIX=eia!h.FL!2QRINM)8]RICsA*^-fW{;lZaA*6=AODj^Ffz^_.'fmn:^ui#F`̙.1N89sx;~X 4aIS>!5pK;kTks]6ԪFv CC֫ẔL[%AZ5Λ6mljl2nV_QРAOjvX,<… >}oj;׿O> >>߼^x08vӟXp!*"J'B\/_^Πn,؜Ϣ27h v^_Yrp{ Ġ߅]|qoq^&5o04 BىB\MLwܭaĄK>wޣYZbРA~֯_ϰaݻTn+ڎNKutL&r@1U .7ؓΤ^1DuU^?`tiv!CBԣYW::> J q.oᣇSxp~;M4… ̞= DQ/H`2?`nw 53Q v{pJ !bBkqU }0u^|k*^5=i.9PBë8N;XCB9>;{Yv@t{8U@iP[tM̙3yGѺukϟOf|䐘ȢEJvIIIaÆDGGc_Jjj*FΝ;(j̜9t2d?Z!nD@^B&|;L w%hi`ol=+;U!j>-i{4 𻁮LM)K͌ rs9ܴ lQ/}tĐfl7VtN%0Ut֍ }].ZR_ƛ4i´iL+""BQȷ`D?a{s4mLeڪR̈́1 m4oh(V!Dtc@]UU9\lNp_k?-D!D%5*iҼVN5>@%z"PBAV"!@`vٱBQ,kՕ_Ӽ|9,~wߜ MrnXKKJ@`6>lD>Ks.yx[bu;~6]9V L ! rEc9ݕ+-r6;`|n!ӔB?eq,i`#;aq®M~r|Vm^! ;l<:( djե>r B[Rcrl|xMɱ:q2RK=V :DNgТa`?-ZЧOT*,];S'\IՕg^nh׮|u2Ćil;t9;7M#-G/8dg Qf}SWF~hkBF: BQI`B{Nt:57pa,k$1clfժUӮ];z=`0T[eܸqOQge[<84 ܔ_Ӻ~+tBSB!/ LU}soU|φ-F8\($$$TkB!*\?}ż0f 'Ͷߜg~))D5Zsʠ mk`J r[Ǣ?W'B_*GQKNM݅Q!¨r:AZB L6'zﻠٳg9<ݻw:u*.\`۶m8IOOR|7w}deei&ƏӨT*bcc0`@˲gL&r|rOLL -Yf\pC~hҤ /e˖;v^zѺukΝ;),,$<<c0J[!s䂝d]hT GR6y;±9=lZBTVҹHg!ko#:D_'DU+Y9_vBzMS=2vFrEtahH3 2GLteq:L۶mYQFٳg9}UysΑ5q IDAT=܃(8ew|r>ƏOnXr%ɜ?-ZSm6ڵc?p0LpJLLd̘1/}vZjUjo!;lo. GU]Ʌ<68VZj^<\p'<'iV.ӧE]`򥦔B὏]P?P6Rtt4dddnrzAjjod Ҽys B^^^aF`~?@vv6Wr 6$??Tp8_u[!*|$DixH Nژ fPbB>b⡏3x J QV71ܜvwqToBbTj*_ @n ʆ PTDEEh( 4kb0 #00vڅbvӽ{wJgʕ~}LuF`` DGG|r5jDxxQ'Νc޽0bĈ2jٺu+{AҡCJ_ZZZ-1 rE뾟Ֆ+r+r*ӅHyXaq)5wqeggNBZH8TLyy_U_FUl=wP_|\|} vysJ`vp80ʹ /F#ZJ D2?ĉo}SBgr\.N'&~Z:Cpp0Z]ko_04o\>0B:!)) Xk(ƾ@^߻ bwbv{Ksy呦|{ܑ#GHHH"B!BW)p@5ӧ}6EGGӷo_93B!BQ|`QF#-}7Lmh4~n۷}7f|B!B!č^4SJFx}EnއFARՋB!BQԛ(jZ-ozQZ-jW!wxTU$3 @@BBA `cYWMVZAQ\ş E wAzM I3dR'(y3s=sϜ3sB!Bw}t ?qu{*G\W~Ŏt:RײKB!B!/A ~ JAcSq;qݾօ|j45Z-*wG F !B!>ׁ)EQx<@w\}AAAu"(U6޼yReZJ!B!5{T*7Յ<B!B!z>*߲-z.*!B!B fu90U6 !B!~>v~wGDDЫW/?a4}V^Ͳe8u5"99{ _tƍ~JRRRc :w5kVa}tt4III̢E8~8$$$0ydFUiTs y|WINNm3p@ƍ̙3ˀo~ChhTZ! UC9w+SR_!Bqm37|Czj/^[df͚Edd$3f̠]v|o]ZZ_ϟOϞ=}S@VmWu.W ֬Yիy饗ؿ?#G*D="B!BR=wfߟKrqbccK_\\j%>>(gYn 55oVZߟ֭[W\T*M66]׮]=z4.ȑ#y饗>|xM%!t:Ҹ\.jo>--lݺ'|ӧKEB!B[]NN:t@_{ŊуӧOsip!B< >B26UVѵkW;U婧BR ޱcGq:$%%1dZjU>js 8ro?f#11ѷlڴiܹ+V0uTƧ"B!BR8pB>3F#7|3,Y7 4ӧOg1p@ǻヒ(ZhΝ̘1Woiƌ0o<;͝9slVXAAAAw++223g˷,..ӧo3uTnv4 K.tڦ\7n\cy۷x/_NTTC(D!)!B!lʕ\Pڶm˼yJ[#5i҄+V믓?Δ)Sn|۷W{R!0մiS.>3vE.]|YnM4cǎk?~<+WСCeSN%11K`BBBܹ3ӧOtڦ/.rK /0w\V\SN̞=0vTH!TS>*/FUf;wR_~\~} ZB h޼y?Ort:1L4B0}>كhDѠV}p3L"##0ʹlR*^8}4!k( ~@!eݗL2w2YV~Sߥ1B!B!u!)!B!Bq]41rZ!B! [ LHե7 ͓B!BQ_57S.RB@JTB!B!oL`l0vvq:\.nwP)JBVhPTT*EHpJ!B!JLJ9l6_aYVGi5*jm%t:ZB!BQ/Uqȅn6թa #q΅kUlB!B! UC8IoPraZy:*jS B!B!~.:0U~s?i|PuZv@B!B!D}`?vy֎өvQTRcB!BQoHGR+ LY.{eO!B!FU[L s|B=pކy6-"3Qiش䷮ŏg,[]ǁ1DStyx퓋8Jk9{텘-.~31`u]"Ζc)^y+_rL%.96_o٠a4 ",TS%lo4=@LKoMo]7LlQ@ɹ,;RlqWHSGp>FNF&sV`mY䛜tJ [Pb#5x **oݒ%KHKKc֬Y=gRSS+,8p +BLL r ӦM#<kS7Eò~oҭD5!cdd}i67q"7֝ʰHan6(QپE0yEN" j=g;}ELĐ lm.*b"ɗyu}:SLaӦM 0|ѢE=r:f1yd9{,~!=s%&&c?~9s?) DJJ ۷gǎ~A8s ۷oo߾Uj9}Yڴi /@xx8999\ȶlmoًZs ]W"Ņ;g/xr|#t4aߕ|G`-ȶG32+/˦qAtoʒU٤e] >-ׇ@Uy/Ww}׏ 55z3gm6`Ѵ롘^-ZYh߾};v~ؗnŊӻwo4iB߾}y70,^gff2k,MFt:ټy33gDѰ{ i C TM:~A)56z}m۶N0`BH`J=܃opB|A|i>s~@7X1e֭[?΁dcȑy~fڵkaaamۖ>}iӦ iF Bl2ƍ^Vf֭<Ӝ:u4. NVVO>XNʛoffFףhZD[d 'O\ e>e؅VT&-c|7RA(-` )AǏgJ[Jt)|ӬTh+1#5%7"IZ5U&~PJ~Z&5%7%^&%%:&7 ְhE+GrĖ&:&XK؊HەcSEEkDMTu}w}ǰa4h7nᐖmBqȧ_=xx7jfƍ[_\\Lnn.ڵtΝ;xHOOd_J:u_ٟjt>))) 8[o_~-Ms]wtR8R999lٲ?[vyxhӦM4ڵ㦛nga :ثÝwɺuXd >h@VYt py<233Yh#F;ϕ?FFFFêSSϫ21[\ˌU@v N-bbz3aP3kJ•\ϩ+ǭ?ȸd瞡;obǖ9to`-nr=dd΀J`wxJ)rJ J3ocwx8x ֩-|P-K[\nc 5]#yƍ &11łK.[N G!i1UK*4 j /G-&#_9թEྎc; DG6ϿtRNviҤoY.]HNNfΜ9̟? &TyWn݊^iӦ?:t`ƍK{h(cǎeٲe<ӬYEUZ6ٯ4hV͜9sQ{9$$I&1ao_|̙3?>III@RRƍ^7ިƌÌ38rINNs{Ǿ .жmۀpue}^M GXH-a!jz %¨ iE]SlqU{7Sbu+6/bX5q ԛB.a|_U%XW:6WӚL1l?llq&cft0 &&\>?cyEN E(dҁ W>\q# p`Bfz.䔎;պ[;Z%_#j^jkoZ5k֬ BH`W1P EhJ!ڠ-x&y )(q]jJ?@˖-+?z(*M-8s fbW@_ƌSa3_PPٳgO !/JQ1[8=`O`tu;3O?noh͒%K7n\~ٳg/Wfvo7Saa!s[n~DDD0bVX|/^̋/Hbbb@V&((xy4hVMƴiضm}&2dUP*[ZMq{qIOJSպE+/UX.V}ElWT!P3烌<ɩ~g8Z9Kn~^_]6v)h;`bD,i t^{9keɈvJJJ1jIP m|I 8'gϞgi5啐رcyp:$&Lu<|ל={tRRRضm[0}^)Be4hR!~85)K=u+88dp5kFϞ=y衇jUnٳygh޼9ƍIJjjonV>Sz |w)UW^k׎ƍWy o.]BӦM^y5kƛoyU5c v։|5kְrJoO+W_~z}T*͛7t 0}^)B߮r/$׀Ridb>*/FUf;wR_~\~} Ծőwۍp`6 X!كhDѠVL"##0͕5)7ӧOc0+`.Y})3y)3Oe2VmPGZLՂN-K3=Z6:tnv6/RBT4nκڡO!B!Ttjw AR͈X4*BD2ʠs`\npg7sӃGSB!B!(̾3mDѪK[MUEQ4j&pwHv,_dcuHdJ!B!$0Z BtPQWQ)$DjQ) ҡO!Za@#o!)i! 6nnJۜ~|BQgLhk=G~ByyR4"t( o=&^6nylNd9NS7p8έiNό8-ֳD͓Tٞ%9~<1:N˲fNd90{(sw V1sh/RGkx<b jvwՙ|Vn*lfN A+/Zbkn,\qcF<$2׍{s 7h mk1;\H$j9rC /| \,lbʭF^] cLJF:SVp<{U5?,t/| .\y]BI=jeb^>~RյRg(*,r8Ɏ6Lam7ӣfL.9<ܞz!Jf.cvv`snv)XB!ꂼb7m_ ^*‚^6ӷ i?ZG k>I: -n\夕A+z M#5tKoP_#2c,Bt*j|-Q]hեA3o]!7N9kJ r`?&Oe+=zbF#1%֡:).͂6?u'֨flP+ x0&jfbSI}6Sbi䗦Դw|\i;:՞uon1R^vSF}X}B!HZL)f !1F*?=wb7Bc"^7V`;Njiy)@N"<t*B\njLSY˦n# ǬeLZbwN3o5#5V=ogqw:S*b+ |*^Aas-:7q䢽5FP*nU+yt$r.IC$= 㷟r"M1Z_wñUvKӒ_"p~.~{Ek Q3TzBAg =EV72喎et*bj~Py]wކ4pfQe"+BHSr{`Y+U+x<ܞAQ fjAaS -ʸ/ц[AZJt_ckuiJ`Bz\ Vj-tl;m<@Ze:o`֭IqZR[hc7jܶk TJR15w=>²s2L8}eH5E.gq< z4PPRcv%)|ą*rU_J=,67hwFY"uqxi*bPdqmPcqxPe.B4dB/ӥY!m+v"6(C^Ft#*TÏ_"_N˘HbjXp 0>{O~슃{Ka*<]Kܨ/w.My=sZngH )=<ȿW0s,TDһby}dbP`NA/MMɖVs+V|Vkdz{k[E|E:Q.I:yG/^ۧ-Q os-&U_+P_#o%uNךaV*Le h\pf|Sipʰb_}+OMy 2]yyrw15%~]uU Lh]ždGYsBFQwDEEI!!ꅂzu>rW\(tَBLVߴ6qbWSqgy4>ٞAObf I^ |Aq\ܹaÆY#hdĈdeeq_n7[laذa4iJZ&66QEQhѢH%B,iyN^+A 9;3(k/azk$֨f\P ӝZNOl nWy}&3>ʩGt]VUvQ;#ZOH mky{Ʉ jԊBNr.Jn\n(` Oh!}@Z-[a4m؅jZt)Zرcݛf͚_7l]w xx}l2nvbbbZt:V+_|wy'aaa۷| l&,, Ɨ_~СCiԨ&M"""'OsN~;wCU'{dڴiJbJKKc݌7EQp:h4,KҺuJ q#+((y<^].N=JI!DdFFZ{b}Pgle˖Raӧ1 ]E`0 ,]f򔙼󔙧̲*JA5~U}hh(fsc9c4`0SzKn^/))a8\.* ɞ={0͔P\\YIIIVޮ]ZoDDl޼-ZZkUu.UB!B!/KSPdd$_ J5:wÆ l6QeѸqcF<;; 60`(((]QJvӫ U]@FsqQv <2^B!B˒!FC=XnҶlOll,qqqdffGQQgϞJB;trDV*O#%%gv}ws:+Wp8J[@L v;6-[2x` UKUB!B 0N:X~=^O֭ҥ Pڵn]V^'99Fɖ-[سgZ;ҲeKYlzƍM6w^6n܈(DFFh^QYv-DDDR!]~~>vMϞ=tKhhhB!Bˑo\r=B4L2 ~.hxҕO!B!B\ *0( z^H(!B!BK>0 (F*4עhC!Bٓwyoٳ'6l[>p@^{5Çyg;߿?&M6IOOgϞٓSNUguC饗^XQg5EARh\¸sZOϷ^0+&J%A)!B!UV:0qDEѣ~7 $ 5kPCJJ IIIGrr2Æ 7q(7RZV兑*]Sz #UhZjB!uGQQ/2}aܹ޹:4^k׮eر9sT}ZUV9R^! v+jJ-m hTƜk b6Żt~KB!uÇ)**bҤIUIp9N8Arr2 㤧jPzǮ5k|yuV{6Ćvn+[ѸhjT*%%B!Dݒ@LL >߹sg@iT*k֬!((^zѴiSz-RRRxڇWjj* )/Nׁ)o\._׾:*go޼A)2i-%B!/t-w\j_(==m2rH:tuXf @@ingݺu(<`ڴi< !nrHޠw)hT6?<ͷB!◕#G|~Gl6tؑ >cN'III 2VZӜ>}<:tK.!|VS*ʯ㩓->/B!ɓ'Yjo~СݛK( ,_(JXXӧOfԩ~h4_zxƷj*_먝;w2c ֯_ĉk܇w)Dڵk'/Nj0cL N}ˁ,\!B۰a6lݛ^xsrJ:ubل0uTYt) , $$Ν;3}tX5Yv-/ IDATwNhh()))L81@1&N()!D\jJSvGU%eӨlNˏϽ999[QԥT.:4o޼ޟvq\8NL&|(H!DdFF|w̬t@>+f3-[ #N>`گ( PgG2䝧O*B!.4RB!l;vcj:%hm+F:*oy[ݺǼt4+9|B+H0OrJDV(*v 3ABlARB!u"B!J[:.B$qiVZ7գV.p(pϐ(Z5գR]i}[:P5Ifb.;+v֩8xs^1~./B!nŔBI*z8ZKb#I :,J[K囜d8p<+Mtʰrng@0& ņ}EOɣ$ҨrW6/b~S|L[H1Ef};bVm19B!8TS>*/FUf;wR_~\~} xBjмyzۍtb2C&h>h4hPըT__333 l6ӲeK0Bz `0 pyt_~.3yLySfYyO~GƘB!B!ׅtB!B֯_ϼy8q"YfM(DƏϤI|-{V\ɉ'0$&&r]wq]wI !D%$0%B!h9믿NFFYYY}̜9y嗱X,L:՗gϞ=<<8=`B*H`J!B\C2~x~ 냂0  ƌdV^ݻYlWڦM)\!B! Zڵ+{?BQ3\!B!j)''BBBZѩS')!%i1%A16HzCUٟf5YܾtܤcX~総藤w1M`yfH`BFZBu*W̊EܵmV?pw:WH_nNbє*ӝf-ۏ.rwR((R*kd@=-c\(p(yĻ?.89`&'/9*޽IVwS=`nŠ&\ꊒGnj\v}I61j "5$H4 D`2{zTu>Sϩs9o5}&,xt9OhQdxϥr_l~FmL?cۥIĹLJYځR/??{`lKis7G_|6J$b)~뵃QHaUh|ᦛnn)$={5\W^yeDDڵ+"fs9GcA0s}qbܟr7|s~qGqg Sħ='_iH9-gOLUcۦ4ILS3Y|щc/cv?~j)ڿxseAMęşUHR}Gn6&gr>#Yq7KNoLu|$GfY1@/ʲo\/L@D̔gMNgu _xɣ4>ŧ7~8hSY׿߹btp+_X^܁۹|c4{ŶG ޞ&o1zxbh:lwjF' g\R1td&FƗNىK$1==}}}i.d79 O*{./;? cWmO,?F jNEMfEUSp`MFD I닱AM6-m[TLVe4M?:qС4M ~ukKV3<3āb```>dJW>`] _$I HDx߿^e. $IY-XV_%ISSSw0!],&&&RZVe)`]+ $bqr\Tزeʲk=LRZrb*I(ˑiEe nk[U{Ktѭm/é,/4r<DU{U=yoUe}` X{©TZFU߯3u4ʄS@s&#+Dyjk5[\fkvNYb \ȅ` \ȅ` \z=[\:vg ԬkML L 0+jm>n;NNL L 0+:5jo):N*ZTL L L ]cgYmNZh?` )r! )r! fzVf[=>_nb \ȅ` \ȅ` \zyFyu޷S)r! )r! )raV>`͵;+\f딼fk}ͦt;SB0@.SB0@.S¬|@:덊)r! )r! )raV>`%Iuvgujvڳԭ,{jR1@.SB0@.SB0@.tF,uyשXk*ȅ` \ȅ` \ȅY5lq]Siwvvڳt SB0@.SB0@.S¬|@k^Yt SB0@.SB0@.S¬|@kwN:ڳu^BL L L D2L47Ih}@#)$*Bq}Cno(b.:O04IIZ|,|R=dpcň$4d2I ?pE|r2~DZ$-*j$"$I"$(DR싽'ͿxŧN,i '' _iI_DR$If;@)`iiQHBD_B';,+Gd6ڈ$$H QQ(~Gtn)SchYt#,":4tHHl %1΍A6r_"IDATH%Hw;fK3~iiQc$$frDi$J0!ܠMݘDD:#M6=R$I 8B|eZ{jj~YZ\P",(h t^RrI( ~ `{⣚c5[&"4*uIEXEPjtT =M0@o^'Iib1N23w|\ì^ .Yux~+;zwb5]r@ez[2  LSk2T$e@{0SYvFeuW/k$I2Оr<3Q?s:wT'ɲ~[jYEDLMNN~/I&6LNN~/"Vg.YL[D kšUTj_S/+Gwi_%%Iwqǧ#b:/q:]Rlez^<_{KϽOyJuR^j5~i*ݾvv>Ne1ڗFD_Dx\r%W e,R_I`#O>YCa߿?5!%IR.߿5\w1;T *tsڐ_Ԭ~.bYZ/bqT8hzˤڰ:J~8UZUnRJQ%tܪ+jYWz]uKWem{{>ݦ6)׹_T<ݮ٘SU-hb UlFUMN\j}Ggk*W47M/5yARkrN8_oz=< U۔k^Sn㸅U@޲6khzYˣrVJu\KZ|VB+T@XNVVJuZ*sTm [j<JY_`4_P6-'v4jMR mpͶX/QKVYʺVVCmvf(z]s[ r"Wk-©V^:մܮq -scۮ$ljwdmJ HUO-UYV/8i7Dɀz@ (5U~XP*buf'+&i=U@k%8T[/'dM;ֹۭN?Y" gV#juKY'`Z)ksR+ q:]m.xƗ6ƙ}Ͷ[ξ[TKVI Sf+<4YX*5xes'BJ}G*/5+ Vuz(f%"FDwuK]HN9. ^u>;v\$]~|Eeuv[zdv!OңfY;">I{l=WM7FB,`e dA![!!ʽb!. :dzGPКAz=;+`lG-ۨ'XIENDB`passes-0.9/me.sanchezrodriguez.passes.json000066400000000000000000000017521452462466600210620ustar00rootroot00000000000000{ "app-id" : "me.sanchezrodriguez.passes", "runtime" : "org.gnome.Platform", "runtime-version" : "45", "sdk" : "org.gnome.Sdk", "command" : "passes", "finish-args" : [ "--share=network", "--share=ipc", "--socket=fallback-x11", "--device=dri", "--socket=wayland" ], "cleanup" : [ "/include", "/lib/pkgconfig", "/man", "/share/doc", "/share/gtk-doc", "/share/man", "/share/pkgconfig", "*.la", "*.a" ], "cleanup-commands" : [ "find /app -type d -name blueprintcompiler | xargs rm -fr" ], "modules" : [ "modules/blueprint.json", "modules/zint.json", { "name" : "passes", "builddir" : true, "buildsystem" : "meson", "sources" : [ { "type" : "dir", "path" : "." } ] } ] } passes-0.9/meson.build000066400000000000000000000004171452462466600150430ustar00rootroot00000000000000project('passes', 'c', version: '0.9', meson_version: '>= 0.50.0', default_options: [ 'warning_level=2', ], ) i18n = import('i18n') subdir('data') subdir('src') subdir('po') meson.add_install_script('build-aux/meson/postinstall.py') passes-0.9/modules/000077500000000000000000000000001452462466600143475ustar00rootroot00000000000000passes-0.9/modules/blueprint.json000066400000000000000000000004231452462466600172450ustar00rootroot00000000000000{ "name": "blueprint-compiler", "buildsystem": "meson", "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler", "tag": "v0.10.0" } ], "cleanup": [ "*" ] }passes-0.9/modules/zint.json000066400000000000000000000004751452462466600162340ustar00rootroot00000000000000{ "name": "zint", "buildsystem": "cmake", "sources": [ { "type": "git", "tag" : "2.12.0", "commit" : "9cfc2a85b03da0a7879422e2bd085f3772fcd914", "url": "https://github.com/zint/zint.git" } ], "cleanup": [ "/bin/zint" ] } passes-0.9/passes.doap000066400000000000000000000022171452462466600150440ustar00rootroot00000000000000 Passes A digital pass manager GTK 4 Libadwaita Python C Pablo Sánchez Rodríguez pablo-s passes-0.9/po/000077500000000000000000000000001452462466600133155ustar00rootroot00000000000000passes-0.9/po/LINGUAS000066400000000000000000000000171452462466600143400ustar00rootroot00000000000000es eu fr it nl passes-0.9/po/POTFILES000066400000000000000000000014111452462466600144620ustar00rootroot00000000000000data/me.sanchezrodriguez.passes.desktop.in data/me.sanchezrodriguez.passes.metainfo.xml.in data/me.sanchezrodriguez.passes.gschema.xml src/main.py src/model/digital_pass_factory.py src/model/digital_pass.py src/model/digital_pass_updater.py src/model/persistence.py src/view/barcode_widget.py src/view/pass_list/pass_list.py src/view/pass_list/pass_row_header.py src/view/pass_list/pass_row_header.py src/view/pass_viewer/additional_information_pane.py src/view/window.py src/view/barcode_dialog.blp src/view/help_overlay.blp src/view/pass_list/pass_icon.blp src/view/pass_list/pass_list.blp src/view/pass_list/pass_row.blp src/view/pass_list/pass_row_header.blp src/view/pass_viewer/additional_information_pane.blp src/view/pass_viewer/pass_field_row.blp src/view/window.blppasses-0.9/po/README.md000066400000000000000000000005241452462466600145750ustar00rootroot00000000000000 | File | Translated | Fuzzy | Untranslated | Progress | |:-----|-----------:|------:|-------------:|---------:| | es.po | 50 | 0 | 0 | 100.00% | | eu.po | 23 | 6 | 21 | 46.00% | | fr.po | 43 | 2 | 5 | 86.00% | | it.po | 39 | 4 | 7 | 78.00% | | nl.po | 23 | 6 | 21 | 46.00% | passes-0.9/po/es.po000066400000000000000000000154371452462466600142760ustar00rootroot00000000000000# Spanish translation for passes. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passes package. # See file COPYING or go to for full license details. # Pablo Sánchez Rodríguez <>, 2022-2023. # msgid "" msgstr "" "Project-Id-Version: passes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\n" "PO-Revision-Date: 2023-11-10 08:38+0100\n" "Last-Translator: Pablo Sánchez Rodríguez <>\n" "Language-Team: Spanish - Spain <>\n" "Language: es_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: Gtranslator 45.3\n" #: data/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "Pases" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "Un gestor de pases digitales" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 msgid "Manage your digital passes" msgstr "Gestiona tus pases digitales" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" "Pases es una práctica aplicación que te ayuda a gestionar todos tus pases " "digitales sin esfuerzo. Con Pases, puedes almacenar cómodamente tus tarjetas " "de embarque, cupones, tarjetas de fidelización, entradas para eventos y " "mucho más, todo en formato PKPass o esPass." #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" "Además, la aplicación se adapta perfectamente a los diferentes tamaños de " "pantalla, lo que te permite acceder a tus pases en varios dispositivos, ya " "sea un ordenador de sobremesa o un teléfono móvil." #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" "Deja de perder el tiempo buscando en tu correo electrónico o imprimiendo tus " "pases digitales. Descarga Pases ahora y conserva todos tus pases en una " "única y práctica ubicación." #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "Orden predeterminado" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "El orden por defecto para los pases." #: src/main.py:139 msgid "Supported passes" msgstr "Pases compatibles" #: src/main.py:144 msgid "All files" msgstr "Todos los archivos" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "Pase actualizado" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "El fichero no es un pase" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "Formato no compatible aún" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "Codificación de archivo desconocida" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "Lunes" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "Martes" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "Miércoles" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "Jueves" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "Viernes" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "Sábado" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "Domingo" #: src/model/digital_pass.py:267 msgid "Today" msgstr "Hoy" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "Mañana" #: src/model/digital_pass_updater.py:115 msgid "Pass already updated" msgstr "Pase ya actualizado" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "Pase no actualizable" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "Error de actualización del pase: {} {}" #: src/model/persistence.py:88 msgid "File already imported" msgstr "Archivo ya importado" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "Formato de código de barras no soportado" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "No tienes pases" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "Usa el botón “+” para importar un pase" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "Sin información adicional" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "General" #: src/view/help_overlay.blp:18 msgctxt "shortcut window" msgid "Show shortcuts" msgstr "Mostrar atajos del teclado" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "Salir" #: src/view/help_overlay.blp:31 msgctxt "shortcut window" msgid "Passes" msgstr "Pases" #: src/view/help_overlay.blp:35 msgctxt "shortcut window" msgid "Import a pass" msgstr "Importar un pase" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "Actualizar el pase seleccionado" #: src/view/window.blp:55 msgid "Import a pass" msgstr "Importar un pase" #: src/view/window.blp:64 msgid "Menu" msgstr "Menú" #: src/view/window.blp:113 msgid "Show additional information" msgstr "Mostrar información adicional" #: src/view/window.blp:121 msgid "Update pass" msgstr "Actualizar pase" #: src/view/window.blp:131 msgid "Additional information" msgstr "Información adicional" #: src/view/window.blp:145 msgid "Back" msgstr "Atrás" #: src/view/window.blp:166 msgid "Sort" msgstr "Ordenación" #: src/view/window.blp:172 msgid "A-Z" msgstr "A-Z" #: src/view/window.blp:179 msgid "Creator" msgstr "Creador" #: src/view/window.blp:186 msgid "Expiration date" msgstr "Fecha de expiración" #: src/view/window.blp:193 msgid "Keyboard shortcuts" msgstr "Atajos del teclado" #: src/view/window.blp:194 msgid "About Passes" msgstr "Acerca de Pases" #: src/view/window.blp:200 msgid "Delete" msgstr "Eliminar" #~ msgid "Show barcode" #~ msgstr "Mostrar código de barras" #, fuzzy #~ msgid "Pass list:" #~ msgstr "Pases" #, fuzzy #~ msgid "Addition information pane:" #~ msgstr "Sin información adicional" #, fuzzy #~ msgid "Fixed shorcuts." #~ msgstr "Atajos de teclado" #~ msgid "Anytime" #~ msgstr "Cualquier momento" #~ msgctxt "shortcut window" #~ msgid "Open file" #~ msgstr "Abrir archivo" #~ msgid "Unknown date" #~ msgstr "Fecha desconocida" passes-0.9/po/eu.po000066400000000000000000000133451452462466600142740ustar00rootroot00000000000000# Basque translation for passes. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passes package. # See file COPYING or go to for full license details. # Sergio Varela , 2022. # msgid "" msgstr "" "Project-Id-Version: passes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\n" "PO-Revision-Date: 2022-10-12 16:04+0200\n" "Last-Translator: Sergio Varela <>\n" "Language-Team: Basque - Spain <>\n" "Language: eu_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: Gtranslator 40.0\n" #: data/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "Emanaldiak" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "Emanaldi digitalen kudeatzaile bat" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 msgid "Manage your digital passes" msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "" #: src/main.py:139 #, fuzzy msgid "Supported passes" msgstr "Emanaldi bat inportatu" #: src/main.py:144 msgid "All files" msgstr "" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "Fitxategi hau ez da emanaldi bat" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "Formatu hau ez da oraindik onartzen" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "Artxiboaren kodifikazioa ezezaguna da" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "Astelehena" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "Asteartea" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "Asteazkena" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "Osteguna" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "Ostirala" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "Larunbata" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "Igandea" #: src/model/digital_pass.py:267 msgid "Today" msgstr "Gaur" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "Bihar" #: src/model/digital_pass_updater.py:115 #, fuzzy msgid "Pass already updated" msgstr "Fitxategia inportatuta" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "" #: src/model/persistence.py:88 msgid "File already imported" msgstr "Fitxategia inportatuta" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "Barra-kodearen formatua ez da onartzen" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "Ez duzu emanaldirik" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "Erabili “+” botoia emanaldi bat inportatzeko" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "" #: src/view/help_overlay.blp:18 #, fuzzy msgctxt "shortcut window" msgid "Show shortcuts" msgstr "Teklatuko lasterbideak" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "" #: src/view/help_overlay.blp:31 #, fuzzy msgctxt "shortcut window" msgid "Passes" msgstr "Emanaldiak" #: src/view/help_overlay.blp:35 #, fuzzy msgctxt "shortcut window" msgid "Import a pass" msgstr "Emanaldi bat inportatu" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "" #: src/view/window.blp:55 msgid "Import a pass" msgstr "Emanaldi bat inportatu" #: src/view/window.blp:64 msgid "Menu" msgstr "Menua" #: src/view/window.blp:113 msgid "Show additional information" msgstr "" #: src/view/window.blp:121 msgid "Update pass" msgstr "" #: src/view/window.blp:131 msgid "Additional information" msgstr "" #: src/view/window.blp:145 msgid "Back" msgstr "Atzera" #: src/view/window.blp:166 msgid "Sort" msgstr "" #: src/view/window.blp:172 msgid "A-Z" msgstr "" #: src/view/window.blp:179 msgid "Creator" msgstr "" #: src/view/window.blp:186 msgid "Expiration date" msgstr "" #: src/view/window.blp:193 #, fuzzy msgid "Keyboard shortcuts" msgstr "Teklatuko lasterbideak" #: src/view/window.blp:194 msgid "About Passes" msgstr "Emanaldiak-ri buruz" #: src/view/window.blp:200 msgid "Delete" msgstr "Ezabatu" #~ msgid "Show barcode" #~ msgstr "Erakutsi barra-kodea" #, fuzzy #~ msgid "Pass list:" #~ msgstr "Emanaldiak" #, fuzzy #~ msgid "Fixed shorcuts." #~ msgstr "Teklatuko lasterbideak" #~ msgid "Anytime" #~ msgstr "Edozein unetan" #~ msgid "Unknown date" #~ msgstr "Data ezezaguna" passes-0.9/po/fr.po000066400000000000000000000152561452462466600142750ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # Irénée THIRION , 2023. # msgid "" msgstr "" "Project-Id-Version: passes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\n" "PO-Revision-Date: 2023-10-19 22:39+0200\n" "Last-Translator: Irénée THIRION \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: Gtranslator 45.2\n" #: data/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "Passes" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "Un gestionnaire de passes numériques" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 msgid "Manage your digital passes" msgstr "Gérez vos passes numériques" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" "Passes est une application pratique qui vous aide à gérer tous vos passes " "numériques sans effort. Avec Passes, vous pouvez stocker facilement vos " "cartes d’embarquement, vos coupons, vos cartes de fidélité, vos billets " "d’événements et bien plus encore, le tout au format PKPass ou esPass." #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" "De plus l’application s’adapte facilement à toutes les tailles d’écrans, " "vous permettant d’accéder à vos passes sur divers appareils, ordinateurs ou " "portables." #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" "Ne perdez plus de temps à fouiller vos courriels ou à imprimer vos passes. " "Téléchargez Passes et conservez tous vos passes dans un endroit pratique." #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "" #: src/main.py:139 msgid "Supported passes" msgstr "Passes pris en charge" #: src/main.py:144 msgid "All files" msgstr "Tous les fichiers" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "Passe mis à jour" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "Le fichier n’est pas un passe" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "Format pas encore pris en charge" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "Encodage du fichier inconnu" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "Lundi" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "Mardi" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "Mercredi" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "Jeudi" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "Vendredi" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "Samedi" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "Dimanche" #: src/model/digital_pass.py:267 msgid "Today" msgstr "Aujourd’hui" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "Demain" #: src/model/digital_pass_updater.py:115 msgid "Pass already updated" msgstr "Passe déjà importé" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "Ce passe ne peut être mis à jour" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "Erreur de mise à jour du passe : {} {}" #: src/model/persistence.py:88 msgid "File already imported" msgstr "Fichier déjà importé" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "Format de code-barres non pris en charge" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "Vous n’avez aucun passe" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "Utilisez le bouton « + » pour importer un passe" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "Pas d’informations supplémentaires" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "Général" #: src/view/help_overlay.blp:18 msgctxt "shortcut window" msgid "Show shortcuts" msgstr "Afficher les raccourcis" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "Quitter" #: src/view/help_overlay.blp:31 msgctxt "shortcut window" msgid "Passes" msgstr "Passes" #: src/view/help_overlay.blp:35 msgctxt "shortcut window" msgid "Import a pass" msgstr "Importer un passe" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "Mettre à jour le passe sélectionné" #: src/view/window.blp:55 msgid "Import a pass" msgstr "Importer un passe" #: src/view/window.blp:64 msgid "Menu" msgstr "Menu" #: src/view/window.blp:113 msgid "Show additional information" msgstr "Afficher les informations supplémentaires" #: src/view/window.blp:121 msgid "Update pass" msgstr "Mettre à jour le passe" #: src/view/window.blp:131 #, fuzzy msgid "Additional information" msgstr "Pas d’informations supplémentaires" #: src/view/window.blp:145 msgid "Back" msgstr "Retour" #: src/view/window.blp:166 msgid "Sort" msgstr "" #: src/view/window.blp:172 msgid "A-Z" msgstr "" #: src/view/window.blp:179 msgid "Creator" msgstr "" #: src/view/window.blp:186 #, fuzzy msgid "Expiration date" msgstr "Sans date d’expiration" #: src/view/window.blp:193 msgid "Keyboard shortcuts" msgstr "Raccourcis clavier" #: src/view/window.blp:194 msgid "About Passes" msgstr "À propos de Passes" #: src/view/window.blp:200 msgid "Delete" msgstr "Supprimer" #~ msgid "Show barcode" #~ msgstr "Afficher le code-barres" #, fuzzy #~ msgid "Pass list:" #~ msgstr "Passes" #, fuzzy #~ msgid "Addition information pane:" #~ msgstr "Afficher les informations supplémentaires" #, fuzzy #~ msgid "Fixed shorcuts." #~ msgstr "Raccourcis clavier" #~ msgid "Anytime" #~ msgstr "N’importe quand" passes-0.9/po/it.po000066400000000000000000000331251452462466600142750ustar00rootroot00000000000000# ITALIAN TRANSLATION OF PASSES. # Copyright (C) 2022 PASSES'S COPYRIGHT HOLDER # This file is distributed under the same license as the passes package. # Michael Moroni , 2022. # Albano Battistella , 2023. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\n" "PO-Revision-Date: 2023-06-19 07:23+0100\n" "Last-Translator: Albano Battistella \n" "Language-Team: Italian \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/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "Biglietti" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "Un gestore di biglietti digitali" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 #, fuzzy msgid "Manage your digital passes" msgstr "Importa e usa i tuoi biglietti digitali" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 #, fuzzy msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" "Passes è uno strumento che ti permette di importare e utilizzare i tuoi " "biglietti digitali, come carte d'imbarco, coupon, carte fedeltà, biglietti " "per eventi, ecc." #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "" #: src/main.py:139 msgid "Supported passes" msgstr "Biglietti supportati" #: src/main.py:144 msgid "All files" msgstr "Tutti i file" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "Biglietto aggiornato" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "Il file non è un biglietto" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "Formato non ancora supportato" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "Sistema di codifica del file sconosciuto" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "Lunedì" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "Martedì" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "Mercoledì" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "Giovedì" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "Venerdì" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "Sabato" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "Domenica" #: src/model/digital_pass.py:267 msgid "Today" msgstr "Oggi" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "Domani" #: src/model/digital_pass_updater.py:115 msgid "Pass already updated" msgstr "Biglietto già aggiornato" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "Biglietto non aggiornabile" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "Errore di aggiornamento del biglietto: {} {}" #: src/model/persistence.py:88 msgid "File already imported" msgstr "File già importato" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "Formato del codice non supportato" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "Non hai biglietti" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "Usa il tasto “+” per importare un biglietto" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "Nessuna informazione aggiuntiva" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "Generale" #: src/view/help_overlay.blp:18 msgctxt "shortcut window" msgid "Show shortcuts" msgstr "Mostra scorciatoie" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "Esci" #: src/view/help_overlay.blp:31 msgctxt "shortcut window" msgid "Passes" msgstr "Biglietti" #: src/view/help_overlay.blp:35 msgctxt "shortcut window" msgid "Import a pass" msgstr "Importa un biglietto" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "Aggiorna biglietto selezionato" #: src/view/window.blp:55 msgid "Import a pass" msgstr "Importa un biglietto" #: src/view/window.blp:64 msgid "Menu" msgstr "Menù" #: src/view/window.blp:113 msgid "Show additional information" msgstr "Mostra informazioni aggiuntive" #: src/view/window.blp:121 msgid "Update pass" msgstr "Aggiorna biglietto" #: src/view/window.blp:131 #, fuzzy msgid "Additional information" msgstr "Nessuna informazione aggiuntiva" #: src/view/window.blp:145 msgid "Back" msgstr "Indietro" #: src/view/window.blp:166 msgid "Sort" msgstr "" #: src/view/window.blp:172 msgid "A-Z" msgstr "" #: src/view/window.blp:179 msgid "Creator" msgstr "" #: src/view/window.blp:186 #, fuzzy msgid "Expiration date" msgstr "Senza data di scadenza" #: src/view/window.blp:193 msgid "Keyboard shortcuts" msgstr "Scorciatoie da tastiera" #: src/view/window.blp:194 msgid "About Passes" msgstr "Informazioni" #: src/view/window.blp:200 msgid "Delete" msgstr "Elimina" #~ msgid "Show barcode" #~ msgstr "Mostra codice" #, fuzzy #~ msgid "Pass list:" #~ msgstr "Biglietti" #, fuzzy #~ msgid "Addition information pane:" #~ msgstr "Nessuna informazione aggiuntiva" #, fuzzy #~ msgid "Translations" #~ msgstr "Traduzione olandese." #, fuzzy #~ msgid "Spanish translation has been improved." #~ msgstr "Traduzione in spagnolo" #~ msgid "Current features:" #~ msgstr "Funzionalità correnti:" #~ msgid "Adaptive user interface." #~ msgstr "Interfaccia utente adattiva." #~ msgid "Import espass and pkpass files." #~ msgstr "Importa file espass e pkpass." #~ msgid "Display Aztec, CODE128, PDF417 and QR codes." #~ msgstr "Display Aztec, CODE128, PDF417 e QR code." #~ msgid "What is new?" #~ msgstr "Cosa c'è di nuovo?" #~ msgid "Initial support for esPass format." #~ msgstr "Supporto iniziale per il formato esPass." #~ msgid "Italian translation." #~ msgstr "Traduzione italiana." #~ msgid "Basque translation." #~ msgstr "Traduzione in basco." #~ msgid "Support for CODE128 codes." #~ msgstr "Supporto per codici CODE128." #~ msgid "What has been changed/improved?" #~ msgstr "Cosa è stato cambiato/migliorato?" #~ msgid "Pass information is now displayed inside a card/pass widget." #~ msgstr "" #~ "Le informazioni sul biglietto ora vengono visualizzate all'interno di un " #~ "widget tessera/pass." #~ msgid "Updated version of application dependencies." #~ msgstr "Versione aggiornata delle dipendenze dell'applicazione." #~ msgid "In pass list, pass icon is now rounded." #~ msgstr "Nell'elenco dei biglietti, l'icona è ora arrotondata." #~ msgid "Default size of barcode dialogue." #~ msgstr "" #~ "Dimensione predefinita della finestra di dialogo del codice a barre." #~ msgid "Improved URL recognition inside back fields" #~ msgstr "" #~ "Migliorato il riconoscimento dell'URL all'interno dei campi posteriori" #~ msgid "Other minor improvements." #~ msgstr "Altri miglioramenti minori." #~ msgid "What has been fixed?" #~ msgstr "Cosa è stato risolto?" #~ msgid "Fixed how encoded strings are passed to the barcode library." #~ msgstr "" #~ "Corretto il modo in cui le stringhe codificate vengono passate alla " #~ "libreria dei codici a barre." #~ msgid "Avoid adding links to back fields if they already have them." #~ msgstr "" #~ "Evita di aggiungere collegamenti ai campi posteriori se li hanno già." #~ msgid "Fixed shorcuts." #~ msgstr "Corretto le scorciatoie." #~ msgid "Fixed drawing of passes without logo." #~ msgstr "Corretto l'estrazione dei biglietti senza logo." #~ msgid "Other small fixes." #~ msgstr "Altre piccole correzioni." #~ msgid "" #~ "In pass list, pass description is now grayed out if the pass has expired." #~ msgstr "" #~ "Nell'elenco dei biglietti, la descrizione ora è disattivata se il " #~ "biglietto è scaduto." #~ msgid "" #~ "After application startup, the pass list will be shown (instead of " #~ "automatically navigating to the first pass in the list)." #~ msgstr "" #~ "Dopo l'avvio dell'applicazione, verrà mostrato l'elenco dei biglietti " #~ "(invece di navigazione automatica al primo biglietto nell'elenco)." #~ msgid "" #~ "Removed unnecessary files and dependencies from the Flatpak container." #~ msgstr "File e dipendenze non necessarie rimosse dal container Flatpak." #~ msgid "Code changes toward supporting other pass formats in the future." #~ msgstr "" #~ "Il codice cambia per supportare altri formati di biglietti in futuro." #~ msgid "" #~ "Pass fields can display links for URLs, (some) telephone numbers and e-" #~ "mails." #~ msgstr "" #~ "I campi del biglietto possono visualizzare collegamenti per URL, (alcuni) " #~ "numeri di telefono ed e-mail." #~ msgid "Support for drawing PDF417 codes." #~ msgstr "Supporto per il disegno di codici PDF417." #~ msgid "Pass list is now sorted by relevant date instead of style." #~ msgstr "" #~ "L'elenco dei biglietti è ora ordinato per data rilevante invece che per " #~ "stile." #~ msgid "Pass fields were updated to be non-activatable." #~ msgstr "" #~ "I campi dei biglietti sono stati aggiornati per essere non attivabili." #~ msgid "" #~ "Barcodes will not be drawn at a fixed size, but they will adapt to the " #~ "available space." #~ msgstr "" #~ "I codici a barre non verranno disegnati con una dimensione fissa, ma si " #~ "adatteranno al spazio disponibile." #~ msgid "Tooltips were added to the buttons that did not have them yet." #~ msgstr "" #~ "Le descrizioni comandi sono state aggiunte ai pulsanti che ancora non le " #~ "avevano." #~ msgid "New features:" #~ msgstr "Nuove funzionalità:" #~ msgid "Added support for Aztec codes." #~ msgstr "Aggiunto il supporto per i codici aztechi." #~ msgid "Passes will now be recognized as mobile compatible by Phosh." #~ msgstr "" #~ "I biglietti saranno ora riconosciuti come compatibili con dispositivi " #~ "mobili da Phosh." #~ msgid "Improvements:" #~ msgstr "Miglioramenti:" #~ msgid "Replaced python3-qrcode with zint." #~ msgstr "Sostituito python3-qrcode con zint." #~ msgid "Decreased memory consumption of instances of PKPass." #~ msgstr "Diminuzione del consumo di memoria delle istanze di PKPass." #~ msgid "In pass list, increased spacing between cards and descriptions." #~ msgstr "" #~ "Nella lista dei biglietti, maggiore spaziatura tra schede e descrizioni." #~ msgid "Bug fixes:" #~ msgstr "Correzioni di bug:" #~ msgid "Pass field rows can now be created even if their value is not text." #~ msgstr "" #~ "Le righe del campo del biglietto ora possono essere create anche se il " #~ "loro valore non è testo." #~ msgid "Included website and description in About dialogue." #~ msgstr "" #~ "Sito Web e descrizione inclusi nella finestra di dialogo delle " #~ "informazioni." #~ msgid "The selected pass will not be highlighted if the leaflet is folded." #~ msgstr "" #~ "Il bilgietto selezionato non sarà evidenziato se il foglio illustrativo è " #~ "piegato." #~ msgid "Corrected margins and spaces between widgets." #~ msgstr "Margini e spazi corretti tra i widget." #~ msgid "" #~ "Moved pass description in the right pane from title bar to pass view." #~ msgstr "" #~ "Descrizione del biglietto spostata nel riquadro destro dalla barra del " #~ "titolo alla vista biglietto." #~ msgid "Pass field rows can now wrap text if it is too long." #~ msgstr "" #~ "Le righe del campo del biglietto ora possono mandare a capo il testo se è " #~ "troppo lungo." #~ msgid "Pass view now shows a bigger logo inside a colored row." #~ msgstr "" #~ "La vista del biglietto ora mostra un logo più grande all'interno di una " #~ "riga colorata." #~ msgid "Capitalized application name in main menu." #~ msgstr "Nome dell'applicazione in maiuscolo nel menu principale." #~ msgid "" #~ "Fixed a bug when displaying standard fields that do not have a label." #~ msgstr "" #~ "Risolto un bug durante la visualizzazione di campi standard che non hanno " #~ "un'etichetta." #~ msgid "Fixed import of passes with images that are not PNG." #~ msgstr "" #~ "Corretto l'importazione dei biglietti con immagini che non sono PNG." #~ msgid "Fixed import of passes with text files encoded in UTF-16." #~ msgstr "" #~ "Corretta l'importazione dei biglietti con file di testo codificati in " #~ "UTF-16." #~ msgid "Fixed QR code icon color in dark mode." #~ msgstr "" #~ "Risolto il problema con il colore dell'icona del codice QR in modalità " #~ "scura." #~ msgid "Fixed the background of the barcode dialogue in dark mode." #~ msgstr "Risolto lo sfondo del dialogo del codice a barre in modalità scura." #~ msgid "Fix 'Add' icon." #~ msgstr "Corretto l'icona 'Aggiungi'." #~ msgid "Hide Settings entry from primary menu." #~ msgstr "Nascondi la voce impostazioni dal menu principale." #~ msgid "Initial release" #~ msgstr "Versione iniziale" #~ msgid "Anytime" #~ msgstr "Qualsiasi momento" #~ msgid "Unknown date" #~ msgstr "Data sconosciuta" passes-0.9/po/meson.build000066400000000000000000000000471452462466600154600ustar00rootroot00000000000000i18n.gettext('passes', preset: 'glib') passes-0.9/po/nl.po000066400000000000000000000131251452462466600142700ustar00rootroot00000000000000# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Heimen Stoffels , 2022. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\n" "PO-Revision-Date: 2022-06-12 14:19+0200\n" "Last-Translator: Heimen Stoffels \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: Lokalize 22.04.1\n" #: data/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "Kaarten" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "Een digitale kaartenbeheerder" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 msgid "Manage your digital passes" msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "" #: src/main.py:139 #, fuzzy msgid "Supported passes" msgstr "Kaart importeren" #: src/main.py:144 msgid "All files" msgstr "" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "Dit bestand is geen kaart" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "Dit formaat wordt nog niet ondersteund" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "Onbekende tekenset in bestand" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "maandag" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "dinsdag" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "woensdag" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "donderdag" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "vrijdag" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "zaterdag" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "zondag" #: src/model/digital_pass.py:267 msgid "Today" msgstr "Vandaag" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "Morgen" #: src/model/digital_pass_updater.py:115 #, fuzzy msgid "Pass already updated" msgstr "Dit bestand is al geïmporteerd" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "" #: src/model/persistence.py:88 msgid "File already imported" msgstr "Dit bestand is al geïmporteerd" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "Dit barcodeformaat wordt niet ondersteund" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "U heeft nog geen kaarten toegevoegd" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "Klik op ‘+’ om een kaart te importeren" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "" #: src/view/help_overlay.blp:18 #, fuzzy msgctxt "shortcut window" msgid "Show shortcuts" msgstr "Sneltoetsen" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "" #: src/view/help_overlay.blp:31 #, fuzzy msgctxt "shortcut window" msgid "Passes" msgstr "Kaarten" #: src/view/help_overlay.blp:35 #, fuzzy msgctxt "shortcut window" msgid "Import a pass" msgstr "Kaart importeren" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "" #: src/view/window.blp:55 msgid "Import a pass" msgstr "Kaart importeren" #: src/view/window.blp:64 msgid "Menu" msgstr "Menu" #: src/view/window.blp:113 msgid "Show additional information" msgstr "" #: src/view/window.blp:121 msgid "Update pass" msgstr "" #: src/view/window.blp:131 msgid "Additional information" msgstr "" #: src/view/window.blp:145 msgid "Back" msgstr "Terug" #: src/view/window.blp:166 msgid "Sort" msgstr "" #: src/view/window.blp:172 msgid "A-Z" msgstr "" #: src/view/window.blp:179 msgid "Creator" msgstr "" #: src/view/window.blp:186 msgid "Expiration date" msgstr "" #: src/view/window.blp:193 #, fuzzy msgid "Keyboard shortcuts" msgstr "Sneltoetsen" #: src/view/window.blp:194 msgid "About Passes" msgstr "Over Kaarten" #: src/view/window.blp:200 msgid "Delete" msgstr "Verwijderen" #~ msgid "Show barcode" #~ msgstr "Barcode tonen" #, fuzzy #~ msgid "Pass list:" #~ msgstr "Kaarten" #, fuzzy #~ msgid "Fixed shorcuts." #~ msgstr "Sneltoetsen" #~ msgid "Anytime" #~ msgstr "Geen specifieke datum" #~ msgid "Unknown date" #~ msgstr "Onbekende datum" passes-0.9/po/passes.pot000066400000000000000000000113411452462466600153370ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passes package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: passes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-10-31 08:26+0100\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/me.sanchezrodriguez.passes.desktop.in:3 #: data/me.sanchezrodriguez.passes.metainfo.xml.in:4 src/main.py:107 #: src/view/window.blp:39 msgid "Passes" msgstr "" #: data/me.sanchezrodriguez.passes.desktop.in:4 msgid "A digital pass manager" msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:5 msgid "Manage your digital passes" msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:10 msgid "" "Passes is a handy app that helps you manage all your digital passes " "effortlessly. With Passes, you can conveniently store your boarding passes, " "coupons, loyalty cards, event tickets, and more, all in PKPass or esPass " "format." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:11 msgid "" "Moreover, the app seamlessly adjusts to different screen sizes, allowing you " "to access your passes on various devices, whether it's a desktop computer or " "a mobile phone." msgstr "" #: data/me.sanchezrodriguez.passes.metainfo.xml.in:12 msgid "" "Stop wasting time searching through your email or printing out your digital " "passes. Download Passes now and keep all your passes in one convenient " "location." msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:16 msgid "Default sort order" msgstr "" #: data/me.sanchezrodriguez.passes.gschema.xml:17 msgid "The default order for passes." msgstr "" #: src/main.py:139 msgid "Supported passes" msgstr "" #: src/main.py:144 msgid "All files" msgstr "" #. Notify user #: src/main.py:194 msgid "Pass updated" msgstr "" #: src/model/digital_pass_factory.py:169 msgid "File is not a pass" msgstr "" #: src/model/digital_pass_factory.py:175 msgid "Format not supported yet" msgstr "" #: src/model/digital_pass_factory.py:181 msgid "Unknown file encoding" msgstr "" #: src/model/digital_pass.py:232 msgid "Monday" msgstr "" #: src/model/digital_pass.py:232 msgid "Tuesday" msgstr "" #: src/model/digital_pass.py:232 msgid "Wednesday" msgstr "" #: src/model/digital_pass.py:233 msgid "Thursday" msgstr "" #: src/model/digital_pass.py:233 msgid "Friday" msgstr "" #: src/model/digital_pass.py:233 msgid "Saturday" msgstr "" #: src/model/digital_pass.py:233 msgid "Sunday" msgstr "" #: src/model/digital_pass.py:267 msgid "Today" msgstr "" #: src/model/digital_pass.py:270 msgid "Tomorrow" msgstr "" #: src/model/digital_pass_updater.py:115 msgid "Pass already updated" msgstr "" #: src/model/digital_pass_updater.py:121 msgid "Pass not updatable" msgstr "" #: src/model/digital_pass_updater.py:127 msgid "Pass update error: {} {}" msgstr "" #: src/model/persistence.py:88 msgid "File already imported" msgstr "" #: src/view/barcode_widget.py:123 msgid "Barcode format not supported" msgstr "" #: src/view/pass_list/pass_list.py:46 msgid "You have no passes" msgstr "" #: src/view/pass_list/pass_list.py:47 msgid "Use the “+” button to import a pass" msgstr "" #: src/view/pass_viewer/additional_information_pane.py:36 msgid "No additional information" msgstr "" #: src/view/help_overlay.blp:14 msgctxt "shortcut window" msgid "General" msgstr "" #: src/view/help_overlay.blp:18 msgctxt "shortcut window" msgid "Show shortcuts" msgstr "" #: src/view/help_overlay.blp:24 msgctxt "shortcut window" msgid "Quit" msgstr "" #: src/view/help_overlay.blp:31 msgctxt "shortcut window" msgid "Passes" msgstr "" #: src/view/help_overlay.blp:35 msgctxt "shortcut window" msgid "Import a pass" msgstr "" #: src/view/help_overlay.blp:41 msgctxt "shortcut window" msgid "Update selected pass" msgstr "" #: src/view/window.blp:55 msgid "Import a pass" msgstr "" #: src/view/window.blp:64 msgid "Menu" msgstr "" #: src/view/window.blp:113 msgid "Show additional information" msgstr "" #: src/view/window.blp:121 msgid "Update pass" msgstr "" #: src/view/window.blp:131 msgid "Additional information" msgstr "" #: src/view/window.blp:145 msgid "Back" msgstr "" #: src/view/window.blp:166 msgid "Sort" msgstr "" #: src/view/window.blp:172 msgid "A-Z" msgstr "" #: src/view/window.blp:179 msgid "Creator" msgstr "" #: src/view/window.blp:186 msgid "Expiration date" msgstr "" #: src/view/window.blp:193 msgid "Keyboard shortcuts" msgstr "" #: src/view/window.blp:194 msgid "About Passes" msgstr "" #: src/view/window.blp:200 msgid "Delete" msgstr "" passes-0.9/po/scripts/000077500000000000000000000000001452462466600150045ustar00rootroot00000000000000passes-0.9/po/scripts/update_translation_status_table.sh000077500000000000000000000033561452462466600240240ustar00rootroot00000000000000#!/bin/sh # # update_translation_status_table.sh # # Copyright 2022 Pablo Sánchez Rodríguez # # 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 . readonly project_root=$(dirname $(dirname $(dirname $(realpath $0)))) cd ${project_root}/po # Empty file truncate -s 0 README.md # Add warning echo "" >> README.md # Add table header echo "| File | Translated | Fuzzy | Untranslated | Progress |" >> README.md echo "|:-----|-----------:|------:|-------------:|---------:|" >> README.md for po_file in *.po do po_stats="$(pocount ${po_file})" [[ ${po_stats} =~ Translated:[[:space:]]+([[:digit:]]+) ]] translated=${BASH_REMATCH[1]} [[ ${po_stats} =~ Untranslated:[[:space:]]+([[:digit:]]+) ]] untranslated=${BASH_REMATCH[1]} [[ ${po_stats} =~ Fuzzy:[[:space:]]+([[:digit:]]+) ]] fuzzy=${BASH_REMATCH[1]} [[ ${po_stats} =~ Total:[[:space:]]+([[:digit:]]+) ]] total=${BASH_REMATCH[1]} progress=$(echo "scale=2; 100 * ${translated} / ${total}" | bc) # Append a new row echo "| ${po_file} | ${translated} | ${fuzzy} | ${untranslated} " \ "| ${progress}% |" >> README.md done passes-0.9/po/scripts/update_translations.sh000077500000000000000000000023661452462466600214350ustar00rootroot00000000000000#!/bin/sh # # generate_reference.sh # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . readonly project_root=$(dirname $(dirname $(dirname $(realpath $0)))) readonly meson_build=translation-build function cleanup { if [[ -d ${meson_build} ]] then rm -r ${project_root}/${meson_build} fi } trap cleanup EXIT cd ${project_root} # Extract all translatable strings from files listed in POTFILES and generate a # template (pot) file meson setup ${meson_build} meson compile -C ${meson_build} passes-pot # Update all translation files (po) listed in LINGUAS files meson compile -C translation-build passes-update-popasses-0.9/src/000077500000000000000000000000001452462466600134665ustar00rootroot00000000000000passes-0.9/src/__init__.py000066400000000000000000000000001452462466600155650ustar00rootroot00000000000000passes-0.9/src/main.py000066400000000000000000000177361452462466600150020ustar00rootroot00000000000000# main.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import sys import gi gi.require_version('Gdk', '4.0') gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import GLib, Gdk, Gio, Gtk, Adw from .digital_pass import DigitalPass from .digital_pass_factory import FileIsNotAPass, FormatNotSupportedYet, PassFactory from .digital_pass_list_store import DigitalPassListStore from .digital_pass_updater import PassUpdater from .persistence import FileAlreadyImported, PersistenceManager from .settings import Settings from .window import PassesWindow class Application(Adw.Application): # Application ID ID = 'me.sanchezrodriguez.passes' def __init__(self): super().__init__(application_id=Application.ID, flags=Gio.ApplicationFlags.FLAGS_NONE) self.__file_chooser = None self.__persistence = PersistenceManager() self.__settings = Settings(Application.ID) self.__pass_list = DigitalPassListStore(self.__settings) pass_files = self.__persistence.load_pass_files() for pass_file in pass_files: digital_pass = PassFactory.create(pass_file) self.__pass_list.insert(digital_pass) def do_activate(self): window = self.props.active_window if not window: window = PassesWindow(application=self, pass_list_model=self.__pass_list) self.create_action('about', self.on_about_action) self.create_action('delete', self.on_delete_action) self.create_action('import', self.on_import_action, ['o']) self.create_action('quit', self.on_quit_action, ['q']) self.create_action('update', self.on_update_action, ['u']) pass_list_is_empty = self.__pass_list.is_empty() window.force_fold(pass_list_is_empty) if not pass_list_is_empty: window.select_pass_at_index(0) window.present() def do_startup(self): Adw.Application.do_startup(self) def import_pass(self, pass_file): try: digital_pass = PassFactory.create(pass_file) if digital_pass in self.__pass_list: self.window().show_toast("Pass already imported") return stored_file = self.__persistence\ .save_pass_file(pass_file, digital_pass.unique_identifier()) digital_pass.set_path(stored_file.get_path()) self.__pass_list.insert(digital_pass) if self.window(): if not self.__pass_list.is_empty(): self.window().force_fold(False) found, index = self.__pass_list.find(digital_pass) if found: self.window().select_pass_at_index(index) except Exception as exception: self.window().show_toast(str(exception)) def on_about_action(self, widget, __): about = Adw.AboutWindow() about.set_application_icon('me.sanchezrodriguez.passes') about.set_application_name(_('Passes')) about.set_copyright('Copyright © 2022-2023 Pablo Sánchez Rodríguez') about.set_license_type(Gtk.License.GPL_3_0) about.set_developer_name('Pablo Sánchez Rodríguez') about.set_issue_url('https://github.com/pablo-s/passes/issues') about.set_version('0.9') about.set_website('https://github.com/pablo-s/passes') about.set_transient_for(self.window()) about.show() def on_delete_action(self, widget, _): if not self.window(): return selected_pass = self.window().selected_pass() selected_pass_index = self.window().selected_pass_index() self.__persistence.delete_pass_file(selected_pass) self.__pass_list.remove(selected_pass_index) if self.__pass_list.is_empty(): self.window().force_fold(True) self.window().navigate_back() return index_to_select = min(self.__pass_list.length() - 1, selected_pass_index) self.window().select_pass_at_index(index_to_select) def on_import_action(self, widget, __): if not self.__file_chooser: self.__file_chooser = Gtk.FileDialog.new() supported_types_filter = Gtk.FileFilter() supported_types_filter.set_name(_('Supported passes')) for mime_type in DigitalPass.supported_mime_types(): supported_types_filter.add_mime_type(mime_type) all_files_filter = Gtk.FileFilter() all_files_filter.set_name(_('All files')) all_files_filter.add_pattern('*') filter_list = Gio.ListStore.new(Gtk.FileFilter) filter_list.append(supported_types_filter) filter_list.append(all_files_filter) self.__file_chooser.set_filters(filter_list) self.__file_chooser.set_modal(True) self.__file_chooser.open(parent = self.window(), callback = self._on_file_chosen) def on_preferences_action(self, widget, _): print('app.preferences action activated') def on_quit_action(self, widget, _): self.window().close() def on_update_action(self, widget, __): """ Update currently selected pass """ selected_pass = self.window().selected_pass() if not selected_pass: return try: # Download and save the latest version of the pass file latest_pass_data = PassUpdater.update(selected_pass) stored_file = self.__persistence\ .save_pass_data(latest_pass_data, selected_pass.unique_identifier() + '.tmp') # Create a new pass from the saved file digital_pass = PassFactory.create(stored_file) # Replace the old pass with the new one self.__pass_list.insert(digital_pass) selected_pass_index = self.window().selected_pass_index() self.__pass_list.remove(selected_pass_index) # Replace the old pass file with the new one self.__persistence.replace_pass_file(selected_pass, replacement=digital_pass) # Select the new pass in the pass list found, updated_pass_index = self.__pass_list.find(digital_pass) self.window().select_pass_at_index(updated_pass_index) # Notify user self.window().show_toast(_('Pass updated')) except Exception as exception: self.window().show_toast(str(exception)) def create_action(self, name, callback, shortcuts=None): """ Add an Action and connect to a callback """ action = Gio.SimpleAction.new(name, None) action.connect("activate", callback) self.add_action(action) if shortcuts: self.set_accels_for_action(f'app.{name}', shortcuts) def _on_file_chosen(self, file_chooser, result): try: pass_file = file_chooser.open_finish(result) if not pass_file: return self.import_pass(pass_file) except Exception as exception: self.window().show_toast(exception.message) def window(self): return self.props.active_window def main(version): app = Application() return app.run(sys.argv) passes-0.9/src/meson.build000066400000000000000000000044231452462466600156330ustar00rootroot00000000000000pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) plugindir = join_paths(get_option('prefix'), get_option('libdir'), meson.project_name()) moduledir = join_paths(pkgdatadir, 'passes') gnome = import('gnome') blueprint_files = files( 'view/barcode_dialog.blp', 'view/help_overlay.blp', 'view/pass_list/pass_icon.blp', 'view/pass_list/pass_list.blp', 'view/pass_list/pass_row.blp', 'view/pass_list/pass_row_header.blp', 'view/pass_viewer/additional_information_pane.blp', 'view/pass_viewer/pass_field_row.blp', 'view/window.blp') blueprint_compiler = find_program('blueprint-compiler') ui_files = [] foreach blueprint_file : blueprint_files path_as_string = '@0@'.format(blueprint_file) filename = path_as_string.split('/')[-1] ui_file = custom_target(path_as_string.underscorify(), input: blueprint_file, output: filename.replace('.blp', '.ui'), command: [blueprint_compiler, 'compile', '--output', '@OUTPUT@', '@INPUT@']) ui_files += ui_file endforeach gnome.compile_resources('passes', 'passes.gresource.xml', dependencies: ui_files, gresource_bundle: true, install: true, install_dir: pkgdatadir, ) subdir('model') python = import('python') conf = configuration_data() conf.set('PYTHON', python.find_installation('python3').path()) conf.set('VERSION', meson.project_version()) conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) conf.set('pkgdatadir', pkgdatadir) configure_file( input: 'passes.in', output: 'passes', configuration: conf, install: true, install_dir: get_option('bindir') ) passes_sources = [ '__init__.py', 'view/barcode_dialog.py', 'view/barcode_widget.py', 'view/pass_list/pass_icon.py', 'view/pass_list/pass_list.py', 'view/pass_list/pass_row_header.py', 'view/pass_list/pass_row.py', 'view/pass_viewer/pass_widget.py', 'view/pass_viewer/additional_information_pane.py', 'view/pass_viewer/pass_field_row.py', 'view/window.py', 'main.py', 'model/digital_pass_factory.py', 'model/digital_pass_list_store.py', 'model/digital_pass_updater.py', 'model/digital_pass.py', 'model/espass.py', 'model/persistence.py', 'model/pkpass.py', 'model/settings.py', ] install_data(passes_sources, install_dir: moduledir) passes-0.9/src/model/000077500000000000000000000000001452462466600145665ustar00rootroot00000000000000passes-0.9/src/model/barcode_content_encoder.c000066400000000000000000000050021452462466600215570ustar00rootroot00000000000000#include #include #include const char FOREGROUND = '1'; const char BACKGROUND = '2'; char * last_result = NULL; enum BarcodeType { AZTEC = 0, CODE128, PDF417, QRCODE }; char * encode_2d_symbol(struct zint_symbol* symbol, unsigned char * data, unsigned data_length); char * encode_barcode(unsigned char * data, unsigned data_length, unsigned symbology, unsigned * out_width, unsigned * out_height); char * encode_2d_symbol(struct zint_symbol* symbol, unsigned char * data, unsigned data_length) { symbol->input_mode = DATA_MODE; // DATA_MODE | UNICODE_MODE symbol->output_options |= OUT_BUFFER_INTERMEDIATE; ZBarcode_Encode_and_Buffer(symbol, data, data_length, 0); unsigned amount_of_modules = (symbol->height * symbol->width) + 1; unsigned module_size = symbol->bitmap_width / symbol->width; char* modules = malloc(amount_of_modules * sizeof(char)); unsigned bitmap_index = 0; unsigned modules_index = 0; for (int row = 0; row < symbol->height; row++) { for (int column = 0; column < symbol->width; column++) { char module = symbol->bitmap[bitmap_index] == FOREGROUND? FOREGROUND : BACKGROUND; modules[modules_index] = module; bitmap_index += module_size; modules_index++; } bitmap_index += symbol->width * module_size; } modules[amount_of_modules - 1] = '\0'; return modules; } char * encode_barcode(unsigned char * data, unsigned data_length, unsigned symbology, unsigned * out_width, unsigned * out_height) { struct zint_symbol* symbol; symbol = ZBarcode_Create(); switch (symbology) { case AZTEC: symbol->symbology = BARCODE_AZTEC; break; case CODE128: symbol->symbology = BARCODE_CODE128; break; case PDF417: symbol->symbology = BARCODE_PDF417; break; case QRCODE: symbol->symbology = BARCODE_QRCODE; symbol->option_1 = 1; // Error Correction Level L=1 M=2 Q=3 H=4 break; } last_result = encode_2d_symbol(symbol, data, data_length); *out_width = symbol->width; *out_height = symbol->height; ZBarcode_Delete(symbol); return last_result; } void free_last_result() { if (last_result == NULL) { return; } free(last_result); last_result = NULL; } passes-0.9/src/model/barcode_content_encoder.py.in000066400000000000000000000053751452462466600224070ustar00rootroot00000000000000# barcode_content_encoder.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import ctypes import math from enum import IntEnum class BarcodeType(IntEnum): AZTEC = 0 CODE128 = 1 PDF417 = 2 QRCODE = 3 class BarcodeContentEncoder(): native_implementation = ctypes.CDLL('@plugindir@/libbarcode-content-encoder.so') # arguments to encode_a_barcode native_implementation.encode_barcode.argtypes = [ctypes.c_char_p, ctypes.c_uint, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint)] # return type after encoding a barcode native_implementation.encode_barcode.restype = ctypes.c_char_p @classmethod def encode_barcode(this_class, text, barcode_type, encoding): if not encoding: encoding = 'iso-8859-1' encoded_text = text.encode(encoding) code_width = ctypes.c_uint() code_height = ctypes.c_uint() module_list = this_class.native_implementation\ .encode_barcode(encoded_text, len(encoded_text), barcode_type, ctypes.byref(code_width), ctypes.byref(code_height))\ .decode() this_class.native_implementation.free_last_result() return module_list, code_width.value, code_height.value @classmethod def encode_aztec_code(this_class, text, encoding): return this_class.encode_barcode(text, BarcodeType.AZTEC, encoding) @classmethod def encode_code128_code(this_class, text, encoding): return this_class.encode_barcode(text, BarcodeType.CODE128, encoding) @classmethod def encode_pdf417_code(this_class, text, encoding): return this_class.encode_barcode(text, BarcodeType.PDF417, encoding) @classmethod def encode_qr_code(this_class, text, encoding): return this_class.encode_barcode(text, BarcodeType.QRCODE, encoding) passes-0.9/src/model/digital_pass.py000066400000000000000000000303321452462466600176040ustar00rootroot00000000000000# digital_pass.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import locale import re from gi.repository import Gdk, GdkPixbuf, GLib, GObject class DigitalPass(GObject.GObject): __gtype_name__ = 'DigitalPass' def __init__(self): super().__init__() self.__path = None def additional_information(self): raise NotImplementedError() def background_color(self): raise NotImplementedError() def barcodes(self): raise NotImplementedError() def creator(self): raise NotImplementedError() def description(self): raise NotImplementedError() def expiration_date(self): raise NotImplementedError() def file_extension(self): raise NotImplementedError() def format(self): raise NotImplementedError() def get_path(self): return self.__path def has_expired(self): expiration_date = self.expiration_date() return (expiration_date and Date.now() > expiration_date) \ or self.voided() def icon(self): raise NotImplementedError() def is_updatable(self): raise NotImplementedError() def mime_type(): raise NotImplementedError() def relevant_date(): raise NotImplementedError() def set_path(self, new_path: str): self.__path = new_path def unique_identifier(self): raise NotImplementedError() def voided(self): raise NotImplementedError() @classmethod def supported_mime_types(cls): return [pass_type.mime_type() for pass_type in cls.__subclasses__()] @classmethod def supported_file_extensions(cls): return [pass_type.file_extension() for pass_type in cls.__subclasses__()] class Barcode: def __init__(self, barcode_dictionary): self.__format = barcode_dictionary['format'] if not self.__format: # Format is a required field raise Exception() self.__message = barcode_dictionary['message'] if not self.__message: # Message is a required field raise Exception() self.__message_encoding = None if 'messageEncoding' in barcode_dictionary.keys(): self.__message_encoding = barcode_dictionary['messageEncoding'] self.__alt_text = None if 'altText' in barcode_dictionary.keys(): self.__alt_text = barcode_dictionary['altText'] def alternative_text(self): return self.__alt_text def format(self): return self.__format def message(self): return self.__message def message_encoding(self): return self.__message_encoding class Color: def __init__(self, r, g, b, a = 255): self.__r = int(r) self.__g = int(g) self.__b = int(b) self.__a = int(a) def red(self): return self.__r def green(self): return self.__g def blue(self): return self.__b def as_gdk_rgba(self): rgba = Gdk.RGBA() rgba.red = self.__r / 255 rgba.green = self.__g / 255 rgba.blue = self.__b / 255 rgba.alpha = self.__a / 255 return rgba def as_tuple(self): return (self.__r, self.__g, self.__b) @classmethod def from_css(this_class, css_string): if css_string.startswith('rgb'): result = re.search('rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)', css_string) if not result or len(result.groups()) != 3: raise BadColor() r = result.group(1) g = result.group(2) b = result.group(3) elif css_string.startswith('#'): result = re.search('\#(\S{2})(\S{2})(\S{2})(\S{2})', css_string) if not result or len(result.groups()) != 4: raise BadColor() r = int(result.group(2), 16) g = int(result.group(3), 16) b = int(result.group(4), 16) else: raise BadColor() return Color(r, g, b) def invert(self): self.__r = 255 - self.__r self.__g = 255 - self.__g self.__b = 255 - self.__b @classmethod def named(cls, color_name): if color_name == 'black': return Color(0, 0, 0, 255) elif color_name == 'white': return Color(255, 255, 255, 255) else: raise BadColor() class Currency: SYMBOLS = {'CNY': '¥‎', 'EUR': '€', 'GBP': '£', 'INR': '₹', 'JPY': '¥', 'KRW': '₩', 'RUB': '₽‎', 'USD': '$'} @classmethod def format(cls, amount, international_code): symbol_to_show = cls.get_symbol_from_code(international_code) output = locale.currency(amount, symbol=symbol_to_show, grouping=True) if symbol_to_show: localeconv = locale.localeconv() symbol_to_replace = localeconv['currency_symbol'] if symbol_to_replace != symbol_to_show: output = output.replace(symbol_to_replace, symbol_to_show) return output @classmethod def get_symbol_from_code(cls, code): localeconv = locale.localeconv() local_currency_code = localeconv['int_curr_symbol'] if code == local_currency_code: return localeconv['currency_symbol'] if code in cls.SYMBOLS: return cls.SYMBOLS[code] return None class Date: days_of_the_week = (_('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday'), _('Sunday')) MAX = GLib.DateTime.new_utc(9999, 12, 31, 23, 59, 59) MIN = GLib.DateTime.new_utc(1, 1, 1, 0, 0, 0) def __init__(self, date): self.__date = date def __eq__(self, other): return self.compare(other) == 0 def __gt__(self, other): return self.compare(other) > 0 def __lt__(self, other): return self.compare(other) < 0 def __str__(self): return self.__date.to_local().format('%c') def as_relative_pretty_string(self): now = GLib.DateTime.new_now_utc() today = GLib.Date.new_dmy(now.get_day_of_month(), now.get_month(), now.get_year()) this = GLib.Date.new_dmy(self.__date.get_day_of_month(), self.__date.get_month(), self.__date.get_year()) difference_in_days = GLib.Date.days_between(today, this) if difference_in_days == 0: return _('Today') if difference_in_days == 1: return _('Tomorrow') if 0 < difference_in_days < 7: return Date.days_of_the_week[self.__date.get_day_of_week() - 1] return self.__date.to_local().format('%x') def compare(self, other): return self.__date.compare(other.__date) @classmethod def compare_dates(cls, date1, date2): if not date1 and not date2: return 0 if date1 and not date2: return -1 if not date1 and date2: return 1 return date1.compare(date2) @classmethod def from_iso_string(cls, string): """ Creates a Date corresponding to the given ISO 8601 formatted string """ # Include hours and seconds if they have not been specified. # Why? DateTime.new_from_iso8601 does not accept times without them. matches = re.finditer('(T|t)([0-9]{2}\:?)+(\+|\-|Z)?', string) for match in matches: missing_info = None colon_count = match.group(0).count(':') if colon_count == 0: # Minutes and seconds are missing missing_info = ':00:00' elif colon_count == 1: # Seconds are missing missing_info = ':00' if missing_info: insertion_index = match.end() - 1 string = string[:insertion_index] + missing_info + string[insertion_index:] # We are only interested in the first match break date = GLib.DateTime.new_from_iso8601(string) return Date(date) @classmethod def now(cls): date = GLib.DateTime.new_now_local() return Date(date) class Image: def __init__(self, image_data): self.__data = image_data def as_pixbuf(self): loader = GdkPixbuf.PixbufLoader() loader.write(self.__data) loader.close() return loader.get_pixbuf() def as_texture(self): return Gdk.Texture.new_from_bytes(GLib.Bytes(self.__data)) class PassDataExtractor: """ A PassDataExtractor contains a reference to a dictionary and a set of helper functions to easy the process of creating a pass. """ def __init__(self, dictionary): self._dictionary = dictionary def _cast_to_boolean(self, value): """ Protected method that creates a boolean from a string. """ return True if value.lower().startswith('true') else False def get(self, key, type_constructor=None): """ Return an element from the dictionary using the provided key. If a constructor is specified, it will be used to create the instance that will be returned. """ try: value = None if key in self._dictionary: value = self._dictionary[key] else: # The key does not exist... nothing to do return None if not type_constructor and type(value) == dict: return PassDataExtractor(value) if type_constructor: if type(type_constructor) == bool: value = self._cast_to_boolean(value) else: value = type_constructor(value) return value except: return None def get_list(self, key, item_constructor=None, extra_arguments=None): """ Return a list of elements from the dictionary using the provided key. If a constructor is specified, it will be used to create each of the items that will be appended to the list. """ data_list = self.get(key) if not data_list: return [] if not extra_arguments: extra_arguments = () elif type(extra_arguments) != tuple: extra_arguments = (extra_arguments,) result = list() for item_data in data_list: if item_constructor: try: arguments = (item_data,) + extra_arguments instance = item_constructor(*arguments) except: continue else: instance = item_data result.append(instance) return result def keys(self): """ Return the set of keys """ return self._dictionary.keys() class TimeInterval: def __init__(self, start_time, end_time): self.__start_time = start_time self.__end_time = end_time def __contains__(self, time): if (self.__start_time and time < self.__start_time) or \ (self.__end_time and time > self.__end_time): return False return True @classmethod def from_iso_strings(cls, start_time, end_time): start_time = Date.from_iso_string(start_time) if start_time else Date.MIN end_time = Date.from_iso_string(end_time) if end_time else Date.MAX return TimeInterval(start_time, end_time) def end_time(self): return self.__end_time class BadColor(Exception): pass passes-0.9/src/model/digital_pass_factory.py000066400000000000000000000125031452462466600213330ustar00rootroot00000000000000# digital_pass_factory.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import json import re import zipfile from gi.repository import Gdk, GObject, Gtk from .espass import EsPass, EsPassAdapter from .pkpass import PKPass, PKPassAdapter def decode_string(string): encodings = ['utf-8', 'utf-16'] decoded_string = '' for encoding in encodings: try: decoded_string = string.decode(encoding) except UnicodeDecodeError: pass if not decoded_string: raise UnknownEncoding() return decoded_string class PassFactory: """ Create a digital pass """ @classmethod def create(cls, pass_file): try: path = pass_file.get_path() archive = zipfile.ZipFile(path, 'r') if 'main.json' in archive.namelist(): digital_pass = cls.__create_espass(archive) elif 'pass.json' in archive.namelist(): digital_pass = cls.__create_pkpass(archive) else: raise FileIsNotAPass() digital_pass.set_path(path) return digital_pass except zipfile.BadZipFile as exception: raise FileIsNotAPass() @classmethod def __create_espass(cls, archive): """ Create an EsPass object from a compressed file """ pass_data = dict() pass_images = dict() for file_name in archive.namelist(): if file_name.endswith('.png'): image = archive.read(file_name) pass_images[file_name] = image if file_name.endswith('main.json'): json_content = archive.read(file_name) pass_data = json.loads(json_content) espass = EsPass(pass_data, pass_images) return EsPassAdapter(espass) @classmethod def __create_pkpass(cls, archive): """ Create a PKPass object from a compressed file """ manifest_text = archive.read('manifest.json') manifest = json.loads(manifest_text) pass_data = dict() pass_translations = dict() pass_images = dict() for file_name in sorted(manifest.keys()): if file_name.endswith('.png'): image = archive.read(file_name) # For every type of image (background, footer, icon, logo, strip # and thumbnail), only load the image with lowest resolution image_type = re.split('\.|@', file_name)[0] if image_type in pass_images.keys(): continue pass_images[image_type] = image if file_name.endswith('pass.strings'): language = file_name.split('.')[0] file_content = archive.read(file_name) translation_dict = cls.__create_translation_dict(file_content) pass_translations[language] = translation_dict if file_name.endswith('pass.json'): json_content = archive.read(file_name) pass_data = json.loads(json_content) language_to_import = None if pass_translations: user_language = Gtk.get_default_language().to_string() for language in pass_translations: if language in user_language: language_to_import = language break if language_to_import is None: # TODO: Open a dialogue and ask the user what language to import pass pass_translation = None if language_to_import: pass_translation = pass_translations[language_to_import] pkpass = PKPass(pass_data, pass_translation, pass_images) return PKPassAdapter(pkpass) @classmethod def __create_translation_dict(cls, translation_file_content): content = decode_string(translation_file_content) entries = content.split('\n') translation_dict = dict() for entry in entries: result = re.search('"(.*)" = "(.*)"', entry) if not result or len(result.groups()) != 2: continue translation_key = result.group(1) translation_value = result.group(2) translation_dict[translation_key] = translation_value return translation_dict class FileIsNotAPass(Exception): def __init__(self): message = _('File is not a pass') super().__init__(message) class FormatNotSupportedYet(Exception): def __init__(self): message = _('Format not supported yet') super().__init__(message) class UnknownEncoding(Exception): def __init__(self): message = _('Unknown file encoding') super().__init__(message) passes-0.9/src/model/digital_pass_list_store.py000066400000000000000000000110351452462466600220520ustar00rootroot00000000000000# digital_pass_list_store.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import re from enum import StrEnum from gi.repository import Gio, GObject from .digital_pass import Date, DigitalPass class DigitalPassListStore(GObject.GObject): __gtype_name__ = 'DigitalPassListStore' def __init__(self, settings): super().__init__() self.__settings = settings self.__list_store = Gio.ListStore.new(DigitalPass) self.__sorting_criteria = settings.get_sorting_criteria() def __contains__(self, digital_pass): return self.find(digital_pass)[0] def find(self, digital_pass): # The implementation of this method should use # Gio.ListStore.find_with_equal_func() instead of get_item(). However, # the method is broken and the fix has not been merged yet. # https://gitlab.gnome.org/GNOME/pygobject/-/merge_requests/218 for position in range(self.length()): item = self.__list_store.get_item(position) if item.unique_identifier() == digital_pass.unique_identifier(): return True, position return False, 0 def get_model(self): return self.__list_store def insert(self, digital_pass): self.__list_store\ .insert_sorted(digital_pass, self.__sorting_criteria.sorting_function()) def is_empty(self): return self.length() == 0 def length(self): return len(self.__list_store) def remove(self, index): self.__list_store.remove(index) def sort_by(self, sorting_criteria): self.__sorting_criteria = sorting_criteria self.__list_store.sort(sorting_criteria.sorting_function()) self.__settings.set_sorting_criteria(sorting_criteria) def sorting_criteria(self): return self.__sorting_criteria class SortPassesBy: @classmethod def creator(cls, pass1, pass2): """ Sort passes by creator. """ return pass1.creator().lower() > pass2.creator().lower() @classmethod def description(cls, pass1, pass2): """ Sort passes by description. """ return pass1.description().lower() > pass2.description().lower() @classmethod def expiration_date(cls, d1, d2): """ Sort passes by expiration date. In the event that two passes have the same expiration date then they will be sorted by description. """ dates_comparison = Date.compare_dates(d1.expiration_date(), d2.expiration_date()) d1_is_later_than_d2 = dates_comparison > 0 dates_are_equal = dates_comparison == 0 return d1_is_later_than_d2 or \ (dates_are_equal and d1.description() > d2.description()) @classmethod def relevant_date(cls, pass1, pass2): """ Sort passes by relevant date. In the event that two passes have the same expiration date then they will be sorted by description. """ dates_comparison = Date.compare_dates(pass1.relevant_date(), pass2.relevant_date()) d1_is_later_than_d2 = dates_comparison > 0 dates_are_equal = dates_comparison == 0 return d1_is_later_than_d2 or \ (dates_are_equal and pass1.description() > pass2.description()) class SortingCriteria(StrEnum): CREATOR = 'creator' DESCRIPTION = 'description' EXPIRATION_DATE = 'expiration_date' RELEVANT_DATE = 'relevant_date' __SORTING_FUNCTIONS = { CREATOR: SortPassesBy.creator, DESCRIPTION: SortPassesBy.description, EXPIRATION_DATE: SortPassesBy.expiration_date, RELEVANT_DATE: SortPassesBy.relevant_date } @classmethod def from_string(cls, criteria): return cls[criteria.upper()] def sorting_function(self): return self.__SORTING_FUNCTIONS[self.value] passes-0.9/src/model/digital_pass_updater.py000066400000000000000000000105411452462466600213300ustar00rootroot00000000000000# digital_pass_updater.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import http.client class PassKitWebService: @staticmethod def get_latest_version(web_service_url, pass_type_identifier, serial_number, authentication_token): endpoint = '{}/v1/passes/{}/{}'.format(web_service_url, pass_type_identifier, serial_number) authorization = 'ApplePass {}'.format(authentication_token) headers = {'Authorization' : authorization} protocol, host_and_path = endpoint.split('//', 1) host, path = host_and_path.split('/', 1) connection = http.client.HTTPSConnection(host) connection.request("GET", '/{}'.format(path), headers=headers) return connection @staticmethod def get_from_new_location(new_location): protocol, host_and_path = new_location.split('//', 1) host, path = host_and_path.split('/', 1) connection = http.client.HTTPSConnection(host) connection.request("GET", '/{}'.format(path)) return connection class PassUpdater: @classmethod def update(this_class, a_pass): """ Download and return the latest version of a digital pass """ if not a_pass.is_updatable() or a_pass.format() != 'pkpass': raise PassNotUpdatable() return this_class._update_pkpass(a_pass.adaptee()) @classmethod def _update_pkpass(this_class, pkpass): """ Download and return the latest version of a PKPass """ web_service_url = pkpass.web_service_url() pass_type_identifier = pkpass.pass_type_identifier() serial_number = pkpass.serial_number() authentication_token = pkpass.authentication_token() first_iteration = True last_response_status = -1 new_location = None while first_iteration or last_response_status in [301, 302]: first_iteration = False if new_location: connection = PassKitWebService\ .get_from_new_location(new_location) else: connection = PassKitWebService\ .get_latest_version(web_service_url, pass_type_identifier, serial_number, authentication_token) response = connection.getresponse() last_response_status = response.status if response.status == 200: """ 200 OK """ pkpass_data = response.read() connection.close() return pkpass_data elif response.status in [204, 304]: """ 204 No Content / 304 Not Modified """ connection.close() raise PassAlreadyUpdated() elif response.status in [301, 302]: """ 301 Moved Permanently / 302 Found """ # Web Service URL has been updated new_location = response.getheader('Location') connection.close() continue raise PassUpdateError(response.status, response.reason) class PassAlreadyUpdated(Exception): def __init__(self): message = _('Pass already updated') super().__init__(message) class PassNotUpdatable(Exception): def __init__(self): message = _('Pass not updatable') super().__init__(message) class PassUpdateError(Exception): def __init__(self, error_code, reason): message = _('Pass update error: {} {}').format(error_code, reason) super().__init__(message) passes-0.9/src/model/espass.py000066400000000000000000000107471452462466600164470ustar00rootroot00000000000000# espass.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from .digital_pass import Barcode, Color, Date, DigitalPass, Image, \ PassDataExtractor, Date, TimeInterval class EsPass(): """ A representation of an esPass pass """ types = ['BOARDING', 'COUPON', 'EVENT', 'LOYALTY', 'VOUCHER'] def __init__(self, pass_data, pass_images): self.__data = PassDataExtractor(pass_data) self.__images = pass_images self.__type = self.__data.get('type') self.__front_fields = [] self.__hidden_fields = [] fields = self.__data\ .get_list('fields', EsPassField) for field in fields: if field.is_hidden(): self.__hidden_fields.append(field) else: self.__front_fields.append(field) self.__validity_time_intervals = [] timespan_dicts = self.__data\ .get_list('validTimespans') for dict in timespan_dicts: timespan = TimeInterval.from_iso_strings(dict['from'], dict['to']) self.__validity_time_intervals.append(timespan) # Container def icon(self): return Image(self.__images['icon.png']) # Mandatory fields def type(self): return self.__type def description(self): return self.__data.get('description') def id(self): return self.__data.get('id') # Time info def valid_timespans(self): return self.__validity_time_intervals # Metadata def creator(self): return self.__data.get('creator') or _('Unknown') # Fields def front_fields(self): return self.__front_fields def hidden_fields(self): return self.__hidden_fields # Color def accent_color(self): return self.__data.get('accentColor', Color.from_css) # Barcode def barcode(self): return self.__data.get('barCode', Barcode) class EsPassAdapter(DigitalPass): def __init__(self, pkpass): super().__init__() self.__adaptee = pkpass def adaptee(self): return self.__adaptee def additional_information(self): return self.__adaptee.hidden_fields() def background_color(self): return self.__adaptee.accent_color() def barcodes(self): return [self.__adaptee.barcode()] def creator(self): return self.__adaptee.creator() def description(self): return self.__adaptee.description() def expiration_date(self): now = Date.now() latest_expiration_date = None for interval in self.__adaptee.valid_timespans(): latest_expiration_date = interval.end_time() if now in interval: break return latest_expiration_date def file_extension(): return '.espass' def format(self): return 'espass' def icon(self): return self.__adaptee.icon() def is_updatable(self): return False def mime_type(): return 'application/vnd.espass-espass+zip' def relevant_date(self): return None def unique_identifier(self): return '.'.join([self.__adaptee.id(), self.format()]) def voided(self): return False class EsPassField: """ An EsPass Field """ def __init__(self, espass_field_dictionary): self.__hide = False if 'hide' in espass_field_dictionary.keys(): self.__hide = espass_field_dictionary['hide'] self.__label = None if 'label' in espass_field_dictionary.keys(): self.__label = espass_field_dictionary['label'] self.__value = espass_field_dictionary['value'] def is_hidden(self): return self.__hide def label(self): return self.__label def value(self): return self.__value passes-0.9/src/model/meson.build000066400000000000000000000007161452462466600167340ustar00rootroot00000000000000compiler = meson.get_compiler('c') libzint = compiler.find_library('libzint', dirs: '/app/lib') shared_library('barcode-content-encoder', ['barcode_content_encoder.c'], install: true, install_dir: plugindir, dependencies: [libzint]) conf = configuration_data() conf.set('plugindir', plugindir) configure_file( input: 'barcode_content_encoder.py.in', output: 'barcode_content_encoder.py', configuration: conf, install: true, install_dir: moduledir ) passes-0.9/src/model/persistence.py000066400000000000000000000057601452462466600174740ustar00rootroot00000000000000# persistence.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import os, tempfile from gi.repository import Gio, GLib from .digital_pass import DigitalPass class PersistenceManager: """ """ def __init__(self): self.__data_dir = GLib.get_user_data_dir() self.__supported_file_extensions = DigitalPass.supported_file_extensions() def load_pass_files(self): file_names = os.listdir(self.__data_dir) pass_files = list() for file_name in file_names: basename, extension = os.path.splitext(file_name) if extension not in self.__supported_file_extensions: continue pass_file_path = os.path.join(self.__data_dir, file_name) pass_file = Gio.File.new_for_path(pass_file_path) pass_files.append(pass_file) return pass_files def delete_pass_file(self, a_pass): target_path = a_pass.get_path() target_file = Gio.File.new_for_path(target_path) target_file.delete() def replace_pass_file(self, pass_to_replace, replacement): source_path = replacement.get_path() destination_path = pass_to_replace.get_path() os.remove(destination_path) os.rename(source_path, destination_path) replacement.set_path(destination_path) def save_pass_data(self, pass_data, file_name): with tempfile.NamedTemporaryFile() as temp_pass_file: temp_pass_file.write(pass_data) pass_file_path = os.path.join(tempfile.gettempdir(), str(temp_pass_file.name)) pass_file = Gio.File.new_for_path(pass_file_path) return self.save_pass_file(pass_file, file_name) def save_pass_file(self, pass_file, file_name): destination_file_path = os.path.join(self.__data_dir, file_name) destination_file = Gio.File.new_for_path(destination_file_path) if Gio.File.query_exists(destination_file): raise FileAlreadyImported() pass_file.copy(destination=destination_file, flags=Gio.FileCopyFlags.NONE, cancellable=None, progress_callback=None, progress_callback_data=None) return destination_file class FileAlreadyImported(Exception): def __init__(self): message = _('File already imported') super().__init__(message) passes-0.9/src/model/pkpass.py000066400000000000000000000240571452462466600164510ustar00rootroot00000000000000# pkpass.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Gdk, Gtk from .digital_pass import Barcode, Color, Currency, Date, DigitalPass, Image, PassDataExtractor class PKPass: """ A representation of a PassKit pass """ styles = ['boardingPass', 'coupon', 'eventTicket', 'generic', 'storeCard'] __slots__ = ('__description', '__format_version', '__organization_name', '__pass_type_identifier', '__serial_number', '__team_identifier', '__expiration_date', '__voided', '__locations', '__maximum_distance', '__relevant_date', '__style', '__auxiliary_fields', '__back_fields', '__header_fields', '__primary_fields', '__secondary_fields', '__transit_type', '__barcode', '__barcodes', '__background', '__background_color', '__foreground_color', '__grouping_identifier', '__icon', '__label_color', '__logo', '__logo_text', '__strip', '__authentication_token', '__web_service_url') def __init__(self, pass_data, translation, images): data = PassDataExtractor(pass_data) self.__description = data.get('description') self.__format_version = data.get('formatVersion') self.__organization_name = data.get('organizationName') self.__pass_type_identifier = data.get('passTypeIdentifier') self.__serial_number = data.get('serialNumber') self.__team_identifier = data.get('teamIdentifier') self.__expiration_date = data.get('expirationDate', Date.from_iso_string) self.__voided = data.get('voided', bool) self.__locations = data.get('locations') self.__maximum_distance = data.get('maxDistance') self.__relevant_date = data.get('relevantDate', Date.from_iso_string) self.__style = None for style in PKPass.styles: if style in data.keys(): self.__style = style break self.__auxiliary_fields = data.get(self.__style)\ .get_list('auxiliaryFields', StandardField, translation) self.__back_fields = data.get(self.__style)\ .get_list('backFields', StandardField, translation) self.__header_fields = data.get(self.__style)\ .get_list('headerFields', StandardField, translation) self.__primary_fields = data.get(self.__style)\ .get_list('primaryFields', StandardField, translation) self.__secondary_fields = data.get(self.__style)\ .get_list('secondaryFields', StandardField, translation) self.__transit_type = data.get(self.__style).get('transitType') self.__barcode = data.get('barcode', Barcode) self.__barcodes = data.get_list('barcodes', Barcode) self.__background = Image(images['background']) \ if 'background' in images else None self.__background_color = data.get('backgroundColor', Color.from_css) self.__foreground_color = data.get('foregroundColor', Color.from_css) self.__grouping_identifier = data.get('groupingIdentifier') \ if self.style() in ['boardingPass', 'eventTicket'] else None self.__icon = Image(images['icon']) if 'icon' in images else None self.__label_color = data.get('labelColor', Color.from_css) self.__logo = Image(images['logo']) if 'logo' in images else None self.__logo_text = data.get('logoText') self.__strip = Image(images['strip']) if 'strip' in images else None self.__authentication_token = data.get('authenticationToken') self.__web_service_url = data.get('webServiceURL') # Standard # mandatory def description(self): return self.__description # mandatory def format_version(self): return self.__format_version # mandatory def organization_name(self): return self.__organization_name # mandatory def pass_type_identifier(self): return self.__pass_type_identifier # mandatory def serial_number(self): return self.__serial_number # mandatory def team_identifier(self): return self.__team_identifier # Expiration def expiration_date(self): return self.__expiration_date def voided(self): return self.__voided # Relevance def locations(self): return self.__locations def maximum_distance(self): return self.__maximum_distance def relevant_date(self): return self.__relevant_date # Style def style(self): return self.__style # Fields def auxiliary_fields(self): return self.__auxiliary_fields def back_fields(self): return self.__back_fields def header_fields(self): return self.__header_fields def primary_fields(self): return self.__primary_fields def secondary_fields(self): return self.__secondary_fields def transit_type(self): return self.__transit_type # Visual appearance def barcode(self): return self.__barcode def barcodes(self): return self.__barcodes def background(self): return self.__background def background_color(self): return self.__background_color def foreground_color(self): return self.__foreground_color def grouping_identifier(self): return self.__grouping_identifier def icon(self): return self.__icon def label_color(self): return self.__label_color def logo(self): return self.__logo def logo_text(self): return self.__logo_text def strip(self): return self.__strip # Web Service def authentication_token(self): return self.__authentication_token def web_service_url(self): return self.__web_service_url class PKPassAdapter(DigitalPass): def __init__(self, pkpass): super().__init__() self.__adaptee = pkpass def adaptee(self): return self.__adaptee def additional_information(self): return self.__adaptee.back_fields() def background_color(self): return self.__adaptee.background_color() def barcodes(self): barcodes = self.__adaptee.barcodes() if not barcodes: barcodes = [self.__adaptee.barcode()] return barcodes def creator(self): return self.__adaptee.organization_name() def description(self): pass_style = self.__adaptee.style() fields = self.__adaptee.primary_fields() if pass_style == 'boardingPass' and len(fields) == 2: return '%s → %s' % (fields[0].label(), fields[1].label()) return self.__adaptee.description() def expiration_date(self): return self.__adaptee.expiration_date() def file_extension(): return '.pkpass' def format(self): return 'pkpass' def icon(self): return self.__adaptee.icon() def is_updatable(self): return self.__adaptee.web_service_url() and not self.has_expired() def mime_type(): return 'application/vnd.apple.pkpass' def relevant_date(self): return self.__adaptee.relevant_date() def unique_identifier(self): return '.'.join([self.__adaptee.pass_type_identifier(), self.__adaptee.serial_number(), self.format()]) def voided(self): return self.__adaptee.voided() class StandardField: """ A PKPass Standard Field """ def __init__(self, pkpass_field_dictionary, translation_dictionary = None): self.__key = pkpass_field_dictionary['key'] try: # Pass field values contain information, provided as a string, that # may have to be parsed and formatted. This is the case of numbers, # currencies and dates. value = pkpass_field_dictionary['value'] if 'dateStyle' in pkpass_field_dictionary: value = Date.from_iso_string(value) elif 'currencyCode' in pkpass_field_dictionary: value = Currency.format(value, pkpass_field_dictionary['currencyCode']) elif translation_dictionary and value in translation_dictionary.keys(): # The value is neither a date nor a currency value = translation_dictionary[value] except Exception: # If any error occur during the processing of the provided value, # this software will show the original text (as is). Because of # this, all exceptions produced in this block will be ignored. pass finally: self.__value = value.strip() if not self.__key or not self.__value: # Keys and values are required fields. raise Exception() self.__label = None if 'label' in pkpass_field_dictionary.keys(): self.__label = pkpass_field_dictionary['label'] if translation_dictionary and self.__label in translation_dictionary.keys(): self.__label = translation_dictionary[self.__label] self.__label = self.__label.upper() self.__text_alignment = None if 'textAlignment' in pkpass_field_dictionary.keys(): self.__text_alignment = pkpass_field_dictionary['textAlignment'] def key(self): return self.__key def label(self): return self.__label def value(self): return self.__value def text_alignment(self): return self.__text_alignment passes-0.9/src/model/settings.py000066400000000000000000000023771452462466600170110ustar00rootroot00000000000000# settings.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import os, tempfile from gi.repository import Gio from .digital_pass_list_store import SortingCriteria class Settings: """ """ def __init__(self, application_id): self.__gsettings = Gio.Settings.new(application_id) def get_sorting_criteria(self): value = self.__gsettings\ .get_value('default-sorting-criteria')\ .get_string() return SortingCriteria.from_string(value) def set_sorting_criteria(self, sorting_criteria): self.__gsettings.set_string('default-sorting-criteria', sorting_criteria) passes-0.9/src/passes.gresource.xml000066400000000000000000000007161452462466600175070ustar00rootroot00000000000000 additional_information_pane.ui barcode_dialog.ui help_overlay.ui pass_field_row.ui pass_icon.ui pass_list.ui pass_row_header.ui pass_row.ui style.css window.ui passes-0.9/src/passes.in000077500000000000000000000023641452462466600153240ustar00rootroot00000000000000#!@PYTHON@ # passes.in # # Copyright 2022 Pablo Sánchez Rodríguez # # 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 . import os import sys import signal import locale import builtins VERSION = '@VERSION@' pkgdatadir = '@pkgdatadir@' localedir = '@localedir@' sys.path.insert(1, pkgdatadir) signal.signal(signal.SIGINT, signal.SIG_DFL) locale.bindtextdomain('passes', localedir) locale.textdomain('passes') builtins._ = locale.gettext if __name__ == '__main__': import gi from gi.repository import Gio resource = Gio.Resource.load(os.path.join(pkgdatadir, 'passes.gresource')) resource._register() from passes import main sys.exit(main.main(VERSION)) passes-0.9/src/style.css000066400000000000000000000016701452462466600153440ustar00rootroot00000000000000.barcode-button { background-color: rgba(255, 255, 255, 1.0); padding: 0; box-shadow: 0 0 0 1px rgba(0,0,0,0.03), 0 1px 3px 1px rgba(0,0,0,0.07), 0 2px 6px 2px rgba(0,0,0,0.03); } .barcode-button:hover { background-color: rgba(245, 245, 245, 0.9); } .barcode-button:active { background-color: rgba(215, 215, 215, 0.9); } /* Use light background color in barcode dialogue. */ .barcode-dialog { color: rgba(0, 0, 0, 0.8); /* @window_fg_color of Adwaita's light variant */ background-color: white; } /* Do not highlight the selected menu row when the leaflet is folded. */ navigation-view row:selected:not(:hover):not(:active) { background-color: transparent; } /* This is a workaround for the following issue: "Placeholder causes additional divider on boxed list" (https://gitlab.gnome.org/GNOME/gtk/-/issues/5309). */ .list-box-with-placeholder > row:nth-last-child(2) { border-bottom: 0; } .rounded { border-radius: 6px; } passes-0.9/src/view/000077500000000000000000000000001452462466600144405ustar00rootroot00000000000000passes-0.9/src/view/barcode_dialog.blp000066400000000000000000000013161452462466600200560ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $BarcodeDialog : Adw.Window { default-width: 360; default-height: 529; styles ["barcode-dialog"] ShortcutController { Shortcut { trigger: "Escape"; action: "action(window.close)"; } } Gtk.Box { orientation: vertical; Gtk.HeaderBar { title-widget: Adw.WindowTitle {title: "";}; styles ["flat"] } $BarcodeWidget barcode { vexpand: true; } Gtk.Label alternative_text { margin-start: 6; margin-end: 6; margin-bottom: 6; styles ["heading"] } } } passes-0.9/src/view/barcode_dialog.py000066400000000000000000000024161452462466600177330ustar00rootroot00000000000000# barcode_dialog.py # # Copyright 2022 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Adw, Gtk @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/barcode_dialog.ui') class BarcodeDialog(Adw.Window): __gtype_name__ = 'BarcodeDialog' barcode = Gtk.Template.Child() alternative_text = Gtk.Template.Child() def __init__(self): super().__init__() def set_barcode(self, code): self.barcode\ .encode(code.format(), code.message(), code.message_encoding()) alternative_text = code.alternative_text() if alternative_text: self.alternative_text.set_text(alternative_text) passes-0.9/src/view/barcode_widget.py000066400000000000000000000102061452462466600177530ustar00rootroot00000000000000# barcode_widget.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Adw, Gdk, Graphene, Gsk, Gtk from .barcode_content_encoder import BarcodeContentEncoder from .digital_pass import Color class BarcodeWidget(Gtk.Widget): __gtype_name__ = 'BarcodeWidget' FOREGROUND = '1'; BACKGROUND = '2'; def __init__(self): super().__init__() self.__data = [] self.__data_width = 0 self.__data_height = 0 # Set background color to white self.__background_color = Color.named('white').as_gdk_rgba() # Set foreground color to black self.__foreground_color = Color.named('black').as_gdk_rgba() # Amount of barcode dots/modules that should fit in every margin (either # horizontal or vertical) self.__margin_size = 4 def aspect_ratio(self): return (self.__data_width + self.__margin_size) / (self.__data_height + self.__margin_size) def do_snapshot(self, snapshot): canvas_width = self.get_allocated_width() canvas_height = self.get_allocated_height() barcode_width = self.__data_width + 2 * self.__margin_size barcode_height = self.__data_height + 2 * self.__margin_size h_scaling = canvas_width / barcode_width v_scaling = canvas_height / barcode_height scaling_factor = int(min(h_scaling, v_scaling)) translation = Graphene.Point() translation.x = (canvas_width - barcode_width * scaling_factor) / 2 translation.y = (canvas_height - barcode_height * scaling_factor) / 2 snapshot.translate(translation) snapshot.scale(scaling_factor, scaling_factor) # Draw the barcode for i in range(self.__data_height): for j in range(self.__data_width): is_foreground = \ self.__data[i * self.__data_width + j] == BarcodeWidget.FOREGROUND if not is_foreground: continue rectangle = Graphene.Rect() rectangle.init(j + self.__margin_size, i + self.__margin_size, 1, 1) snapshot.append_color(self.__foreground_color, rectangle) def encode(self, format, message, encoding): encoding_function = None if format in ['AZTEC', 'PKBarcodeFormatAztec']: encoding_function = BarcodeContentEncoder.encode_aztec_code self.__margin_size = 2 elif format in ['CODE_128', 'PKBarcodeFormatCode128']: encoding_function = BarcodeContentEncoder.encode_code128_code self.__margin_size = 7 elif format in ['PDF_417', 'PKBarcodeFormatPDF417']: encoding_function = BarcodeContentEncoder.encode_pdf417_code self.__margin_size = 2 elif format in ['PKBarcodeFormatQR', 'QR_CODE']: encoding_function = BarcodeContentEncoder.encode_qr_code else: raise BarcodeFormatNotSupported() module_list, width, height = encoding_function(message, encoding) self.__data = module_list self.__data_width = width self.__data_height = height def minimum_height(self): return self.__data_height + 2 * self.__margin_size def minimum_width(self): return self.__data_width + 2 * self.__margin_size class BarcodeFormatNotSupported(Exception): def __init__(self): message = _('Barcode format not supported') super().__init__(message) passes-0.9/src/view/help_overlay.blp000066400000000000000000000017741452462466600176410ustar00rootroot00000000000000using Gtk 4.0; ShortcutsWindow help_overlay { modal: true; ShortcutsSection { section-name: "shortcuts"; max-height: 10; ShortcutsGroup { title: C_("shortcut window", "General"); ShortcutsShortcut { title: C_("shortcut window", "Show shortcuts"); action-name: "win.show-help-overlay"; } ShortcutsShortcut { title: C_("shortcut window", "Quit"); action-name: "app.quit"; } } ShortcutsGroup { title: C_("shortcut window", "Passes"); ShortcutsShortcut { title: C_("shortcut window", "Import a pass"); action-name: "app.import"; } ShortcutsShortcut { title: C_("shortcut window", "Update selected pass"); action-name: "app.update"; } } } } passes-0.9/src/view/pass_list/000077500000000000000000000000001452462466600164415ustar00rootroot00000000000000passes-0.9/src/view/pass_list/pass_icon.blp000066400000000000000000000001341452462466600211140ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassIcon : Gtk.Box { styles ["card", "rounded"] }passes-0.9/src/view/pass_list/pass_icon.py000066400000000000000000000100541452462466600207710ustar00rootroot00000000000000# pass_icon.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Gdk, Graphene, Gsk, Gtk from .digital_pass import Color @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/pass_icon.ui') class PassIcon(Gtk.Box): __gtype_name__ = 'PassIcon' BORDER_RADIUS = 6 BORDER_WIDTH = 0 ICON_SIZE = 45 def __init__(self): super().__init__() self.__background_color = None self.__image = None self.props.height_request = PassIcon.ICON_SIZE + PassIcon.BORDER_WIDTH self.props.width_request = PassIcon.ICON_SIZE + PassIcon.BORDER_WIDTH def __draw_background(self, snapshot): if not self.__background_color: return rectangle = Graphene.Rect() rectangle.init(0, 0, PassIcon.ICON_SIZE + PassIcon.BORDER_WIDTH, PassIcon.ICON_SIZE + PassIcon.BORDER_WIDTH) rounded_rectangle = Gsk.RoundedRect() rounded_rectangle.init_from_rect(rectangle, PassIcon.BORDER_RADIUS) snapshot.push_rounded_clip(rounded_rectangle) snapshot.append_color(self.__background_color, rectangle) snapshot.pop() def __draw_icon(self, snapshot): if not self.__image: return rectangle = Graphene.Rect() rectangle.init(PassIcon.BORDER_WIDTH, PassIcon.BORDER_WIDTH, PassIcon.ICON_SIZE - PassIcon.BORDER_WIDTH, PassIcon.ICON_SIZE - PassIcon.BORDER_WIDTH) rounded_rectangle = Gsk.RoundedRect() rounded_rectangle\ .init_from_rect(rectangle, PassIcon.BORDER_RADIUS - PassIcon.BORDER_WIDTH) texture = self.__image.as_texture() texture_height = texture.get_height() texture_width = texture.get_width() scale = (PassIcon.ICON_SIZE) / max(texture_width, texture_height) padding_top = (PassIcon.ICON_SIZE - (texture_height * scale)) / 2 padding_left = (PassIcon.ICON_SIZE - (texture_width * scale)) / 2 rect_texture = Graphene.Rect() rect_texture.init(padding_left + PassIcon.BORDER_WIDTH, padding_top + PassIcon.BORDER_WIDTH, texture_width * scale - PassIcon.BORDER_WIDTH, texture_height * scale - PassIcon.BORDER_WIDTH) snapshot.push_rounded_clip(rounded_rectangle) snapshot.append_texture(texture, rect_texture) snapshot.pop() def __guess_background_color(self): if not self.__image: return pixel_buffer = self.__image.as_pixbuf() data = pixel_buffer.read_pixel_bytes().get_data() # This method assumes that the background color of an image is the color # of the first pixel of the image if it is not transparent. background_color = None if not pixel_buffer.get_has_alpha() or data[3] > 0: background_color = data[0:3] return Color(*background_color) if background_color else None def do_snapshot(self, snapshot): self.__draw_background(snapshot) self.__draw_icon(snapshot) def set_background_color(self, color): bg_color = self.__guess_background_color() if not bg_color: bg_color = color self.__background_color = bg_color.as_gdk_rgba() self.queue_draw() def set_image(self, image): self.__image = image self.queue_draw() passes-0.9/src/view/pass_list/pass_list.blp000066400000000000000000000002431452462466600211400ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassList : Gtk.ListBox { styles ["navigation-sidebar"] margin-bottom: 6; margin-end: 6; margin-start: 6; } passes-0.9/src/view/pass_list/pass_list.py000066400000000000000000000066571452462466600210320ustar00rootroot00000000000000# pass_list.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Adw, GObject, Gtk from .digital_pass import DigitalPass from .digital_pass_list_store import SortingCriteria from .pass_row import PassRow @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/pass_list.ui') class PassList(Gtk.ListBox): __gtype_name__ = 'PassList' __gsignals__ = { 'pass-activated' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (DigitalPass,)), 'pass-selected' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (DigitalPass,)) } def __init__(self): super().__init__() self.__list_model = None self.__selected_row = None self.__sorting_criteria = None self.set_header_func(self._on_update_headers) # Create a placeholder widget to be displayed when the list is empty placeholder = Adw.StatusPage.new() placeholder.set_icon_name('me.sanchezrodriguez.passes') placeholder.set_title(_('You have no passes')) placeholder.set_description(_('Use the “+” button to import a pass')) self.set_placeholder(placeholder) self.connect('row-activated', self._on_row_activated) def _on_row_activated(self, pass_list, pass_row): self.__selected_row = pass_row self.emit('pass-activated', pass_row.data()) def _on_update_headers(self, row, row_above): row.update_header_text_for(self.__sorting_criteria) if row_above: row_above.update_header_text_for(self.__sorting_criteria) if not row_above or row.header_text() != row_above.header_text(): row.show_header() else: row.hide_header() def bind_model(self, pass_list_model): self.__sorting_criteria = pass_list_model.sorting_criteria() self.__list_model = pass_list_model super().bind_model(pass_list_model.get_model(), PassRow) def row_at_index(self, index): row_at_index = self.get_row_at_index(index) if not row_at_index: row_at_index = self.get_row_at_index(0) return row_at_index if row_at_index else None def select_pass_at_index(self, index): row_at_index = self.row_at_index(index) if row_at_index: self.select_row(row_at_index) self.emit('pass-selected', row_at_index.data()) def selected_pass(self): selected_pass = None if self.__selected_row: selected_pass = self.__selected_row.data() return selected_pass def selected_pass_index(self): index = None if self.__selected_row: index = self.__selected_row.get_index() return index def sort_by(self, sorting_criteria): self.__sorting_criteria = sorting_criteria self.__list_model.sort_by(sorting_criteria) passes-0.9/src/view/pass_list/pass_row.blp000066400000000000000000000013471452462466600210020ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassRow : Gtk.ListBoxRow { can-focus: true; margin-top: 6; selectable: true; Gtk.Box box { can-focus: false; margin-bottom: 6; margin-top: 6; spacing: 12; $PassIcon icon {} Gtk.Box { orientation: vertical; spacing: 3; valign: center; Gtk.Label title { ellipsize: end; hexpand: true; use-markup: true; xalign: 0; } Gtk.Label subtitle { styles ["subtitle"] hexpand: true; xalign: 0; } } } } passes-0.9/src/view/pass_list/pass_row.py000066400000000000000000000060471452462466600206570ustar00rootroot00000000000000# pkpass_row.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Gdk, GLib, Gtk from .digital_pass_list_store import SortingCriteria from .pass_icon import PassIcon from .pass_row_header import PassRowHeader @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/pass_row.ui') class PassRow(Gtk.ListBoxRow): __gtype_name__ = 'PassRow' icon = Gtk.Template.Child() title = Gtk.Template.Child() subtitle = Gtk.Template.Child() def __init__(self, a_pass): super().__init__() self.__pass = a_pass self.__header_text = '' self.__sorting_criteria = None if a_pass.icon(): self.icon.set_image(a_pass.icon()) if a_pass.background_color(): self.icon.set_background_color(a_pass.background_color()) description = GLib.markup_escape_text(a_pass.description()) if self.__pass.has_expired(): description = '%s' % description self.title.set_label(description) self.subtitle.set_label(a_pass.creator()) def data(self): return self.__pass def header_text(self): return self.__header_text def hide_header(self): self.set_header(None) def show_header(self): if not self.__header_text: return header = PassRowHeader(self.__header_text) self.set_header(header) def style(self): return self.__pass.style() def update_header_text_for(self, sorting_criteria): if self.__sorting_criteria == sorting_criteria: return self.__header_type = sorting_criteria if sorting_criteria == SortingCriteria.CREATOR: self.__header_text = self.__pass.creator() elif sorting_criteria == SortingCriteria.DESCRIPTION: self.__header_text = self.__pass.description()[0].upper() elif sorting_criteria == SortingCriteria.EXPIRATION_DATE: row_date = self.__pass.expiration_date() self.__header_text = row_date.as_relative_pretty_string() \ if row_date else _('Without expiration date') elif sorting_criteria == SortingCriteria.RELEVANT_DATE: row_date = self.__pass.relevant_date() self.__header_text = row_date.as_relative_pretty_string() \ if row_date else _('Without relevant date') else: self.__header_text = '' passes-0.9/src/view/pass_list/pass_row_header.blp000066400000000000000000000003271452462466600223070ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassRowHeader : Gtk.Label { can-focus: false; halign: start; margin-bottom: 0; margin-end: 6; margin-start: 6; margin-top: 12; styles ["heading"] } passes-0.9/src/view/pass_list/pass_row_header.py000066400000000000000000000017251452462466600221650ustar00rootroot00000000000000# pkpass_row_header.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Gtk @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/pass_row_header.ui') class PassRowHeader(Gtk.Label): __gtype_name__ = 'PassRowHeader' def __init__(self, text): super().__init__() self.set_text(str(text)) passes-0.9/src/view/pass_viewer/000077500000000000000000000000001452462466600167675ustar00rootroot00000000000000passes-0.9/src/view/pass_viewer/additional_information_pane.blp000066400000000000000000000010541452462466600252060ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $AdditionalInformationPane : Gtk.Box { Gtk.ScrolledWindow scrolled_window { hscrollbar-policy: never; Gtk.Viewport { scroll-to-focus: true; Gtk.ListBox fields { styles ["list-box-with-placeholder"] hexpand: true; margin-bottom: 12; margin-top: 12; margin-start: 12; margin-end: 12; selection-mode: none; } } } } passes-0.9/src/view/pass_viewer/additional_information_pane.py000066400000000000000000000044271452462466600250700ustar00rootroot00000000000000# pkpass_back_view.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Adw, Gtk from .pass_field_row import PassFieldRow @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/additional_information_pane.ui') class AdditionalInformationPane(Gtk.Box): __gtype_name__ = 'AdditionalInformationPane' fields = Gtk.Template.Child() def __init__(self): super().__init__() # Create a placeholder widget to be displayed when the pane is empty placeholder = Adw.StatusPage.new() placeholder.set_icon_name('info-symbolic') placeholder.set_title(_('No additional information')) placeholder.add_css_class('compact') self.fields.set_placeholder(placeholder) def clean(self): row = self.fields.get_row_at_index(0) while row: self.fields.remove(row) row = self.fields.get_row_at_index(0) def content(self, a_pass): self.clean() fields = a_pass.additional_information() if len(fields) == 0: alignment = Gtk.Align.FILL if self.fields.has_css_class('boxed-list'): self.fields.remove_css_class('boxed-list') else: alignment = Gtk.Align.START if not self.fields.has_css_class('boxed-list'): self.fields.add_css_class('boxed-list') self.fields.set_valign(alignment) for field in fields: label = field.label() value = field.value() passFieldRow = PassFieldRow() passFieldRow.set_label(label) passFieldRow.set_value(value) self.fields.append(passFieldRow) passes-0.9/src/view/pass_viewer/pass_field_row.blp000066400000000000000000000007771452462466600225010ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassFieldRow : Gtk.ListBoxRow { activatable: false; Gtk.Box { margin-bottom: 6; margin-end: 6; margin-start: 6; margin-top: 6; orientation: vertical; spacing: 6; Gtk.Label label { styles ["caption-heading"] halign: start; } Gtk.Label value { halign: start; wrap: true; wrap-mode: word_char; } } } passes-0.9/src/view/pass_viewer/pass_field_row.py000066400000000000000000000041061452462466600223420ustar00rootroot00000000000000# pkpass_field_row.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . import re from gi.repository import GLib, Gtk @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/pass_field_row.ui') class PassFieldRow(Gtk.ListBoxRow): __gtype_name__ = 'PassFieldRow' label = Gtk.Template.Child() value = Gtk.Template.Child() def __init__(self): super().__init__() self.value.set_use_markup(True) def set_label(self, label): if label and label.strip(): self.label.set_text(label) self.label.show() else: self.label.hide() def set_value(self, value): value = str(value) value_has_links = re.search('', value) if value_has_links: value = re.sub('&', '&', value) else: value = GLib.markup_escape_text(value) # Create a link for URLs value = re.sub('(?:(https?://)|(www))(\S+)', '\\1\\2\\3', value) # Create a link for telephone numbers value = re.sub('(\+\d+[\(\)\-\d\s\.]+\d)', '\\1', value) # Create a link for e-mails value = re.sub('(\S+\@[\w\-]+\.\w+)', '\\1', value) self.value.set_label(value) passes-0.9/src/view/pass_viewer/pass_widget.py000066400000000000000000000527721452462466600216670ustar00rootroot00000000000000# pass_widget.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Adw, Gdk, GObject, Graphene, Gsk, Gtk, Pango from .barcode_widget import BarcodeWidget from .digital_pass import Color PASS_WIDTH = 320 PASS_HEIGHT = 420 PASS_MARGIN = 12 BACKGROUND_BLUR_RADIUS = 30 class PassFont: label = Pango.FontDescription.new() label.set_size(9 * Pango.SCALE) label.set_weight(600) value = Pango.FontDescription.new() value.set_size(11 * Pango.SCALE) big_value = Pango.FontDescription.new() big_value.set_size(17 * Pango.SCALE) biggest_value = Pango.FontDescription.new() biggest_value.set_size(24 * Pango.SCALE) class FieldLayout: def __init__(self, pango_context, field, label_font = PassFont.label, value_font = PassFont.value, alignment = Pango.Alignment.LEFT): self.__label = Pango.Layout(pango_context) self.__label.set_alignment(alignment) self.__label.set_font_description(label_font) self.__label.set_text(field.label() if field.label() else '') self.__value = Pango.Layout(pango_context) self.__value.set_alignment(alignment) self.__value.set_font_description(value_font) self.__value.set_text(str(field.value())) self.__value.set_wrap(Pango.WrapMode.WORD_CHAR) width = max(self.__label.get_pixel_size().width, self.__value.get_pixel_size().width) width = min(width, PASS_WIDTH - 2 * PASS_MARGIN) * Pango.SCALE self.__value.set_width(width) self.__label.set_width(width) def append(self, snapshot, label_color, value_color): label_height = self.__label.get_pixel_size().height value_height = self.__value.get_pixel_size().height snapshot.save() snapshot.append_layout(self.__label, label_color) point = Graphene.Point() point.y = label_height snapshot.translate(point) snapshot.append_layout(self.__value, value_color) snapshot.restore() def get_height(self): return self.__label.get_pixel_size().height + self.__value.get_pixel_size().height def get_width(self): return self.__label.get_width() / Pango.SCALE def set_alignment(self, alignment): self.__label.set_alignment(alignment) self.__value.set_alignment(alignment) def set_width(self, width): self.__label.set_width(width * Pango.SCALE) self.__value.set_width(width * Pango.SCALE) class PassPlotter: def __init__(self, a_pass, pass_widget): self._pass_widget = pass_widget self._pango_context = pass_widget.get_pango_context() def _create_fields_layouts(self, fields): rows = [] spacing_per_row = [] current_row = [] accumulated_width = 0 max_row_width = PASS_WIDTH - 2 * PASS_MARGIN for field in fields: field_layout = FieldLayout(self._pango_context, field) field_width = field_layout.get_width() if (accumulated_width + field_width) + len(current_row) * PASS_MARGIN < max_row_width: current_row.append(field_layout) accumulated_width += field_width continue spacing = (max_row_width - accumulated_width) / (len(current_row)-1) if len(current_row) > 1 else 0 spacing_per_row.append(spacing) rows.append(current_row) accumulated_width = field_width current_row = [] current_row.append(field_layout) if current_row: spacing = (max_row_width - accumulated_width) / (len(current_row)-1) if len(current_row) > 1 else 0 spacing_per_row.append(spacing) rows.append(current_row) return rows, spacing_per_row def _plot_background(self): rectangle = Graphene.Rect() rectangle.init(0, 0, PASS_WIDTH, PASS_HEIGHT) self._snapshot.append_color(self._bg_color, rectangle) def _plot_fields_layouts(self, fields): self._snapshot.save() point = Graphene.Point() point.x = PASS_MARGIN point.y = 0 self._snapshot.translate(point) row_height = 0 rows, spacing_per_row = self._create_fields_layouts(fields) for row in rows: row_height = 0 spacing = spacing_per_row.pop(0) amount_of_fields = len(row) self._snapshot.save() for index, field_layout in enumerate(row): # Decide the alignment of the label and value according to the # location of the field in the row. if index == 0: field_layout.set_alignment(Pango.Alignment.LEFT) elif index == amount_of_fields - 1: field_layout.set_alignment(Pango.Alignment.RIGHT) else: field_layout.set_alignment(Pango.Alignment.CENTER) # Plot the standard field field_layout.append(self._snapshot, self._label_color, self._fg_color) layout_height = field_layout.get_height() if layout_height > row_height: row_height = layout_height # Add a horizontal space between fields point.x = field_layout.get_width() + spacing point.y = 0 self._snapshot.translate(point) self._snapshot.restore() # Add a vertical space between rows point.x = 0 point.y = row_height + 6 self._snapshot.translate(point) self._snapshot.restore() # Perform a translation so that the next drawing starts below this one point.x = 0 point.y = row_height * len(rows) + PASS_MARGIN self._snapshot.translate(point) @classmethod def new(clss, a_pass, pass_widget): if a_pass.format() == 'pkpass': return PkPassPlotter.new(a_pass, pass_widget) return EsPassPlotter(a_pass, pass_widget) def plot(self, snapshot): raise NotImplementedError() class EsPassPlotter(PassPlotter): def __init__(self, a_pass, pass_widget): super().__init__(a_pass, pass_widget) espass = a_pass.adaptee() # Accent color accent_color = espass.accent_color() self._accent_color = accent_color.as_gdk_rgba() \ if accent_color else Gdk.RGBA() # Background color self._bg_color = Color.named('white').as_gdk_rgba() # Foreground color self._fg_color = Color.named('black').as_gdk_rgba() # Label color self._label_color = self._fg_color.copy() # Logo self._logo_texture = None if espass.icon(): self._logo_texture = espass.icon().as_texture() # Fields self._fields = espass.front_fields() def _plot_background(self): rectangle = Graphene.Rect() rectangle.init(0, 0, PASS_WIDTH, PASS_HEIGHT) self._snapshot.append_color(self._bg_color, rectangle) def _plot_fields(self): self._plot_fields_layouts(self._fields) def _plot_header(self): header_height = 32 rectangle = Graphene.Rect() rectangle.init(0, 0, PASS_WIDTH, header_height + 2 * PASS_MARGIN) self._snapshot.append_color(self._accent_color, rectangle) # Draw the logo if it exists if self._logo_texture: logo_scale = header_height / self._logo_texture.get_height() logo_width = self._logo_texture.get_width() * logo_scale rectangle = Graphene.Rect() rectangle.init(PASS_MARGIN, PASS_MARGIN, logo_width, header_height) self._snapshot.append_texture(self._logo_texture, rectangle) # Perform a translation so that the next drawing starts below this one point = Graphene.Point() point.y = header_height + 3 * PASS_MARGIN self._snapshot.translate(point) def plot(self, snapshot): self._snapshot = snapshot self._snapshot.save() self._plot_background() self._plot_header() self._plot_fields() self._snapshot.restore() class PkPassPlotter(PassPlotter): PRIMARY_FIELD_LABEL_FONT = PassFont.label PRIMARY_FIELD_VALUE_FONT = PassFont.biggest_value def __init__(self, a_pass, pass_widget): super().__init__(a_pass, pass_widget) # At this point we know we are going to plot a PKPass pkpass = a_pass.adaptee() # Background color bg_color = pkpass.background_color() self._bg_color = bg_color.as_gdk_rgba() \ if bg_color else Color.named('white').as_gdk_rgba() # Foreground color fg_color = pkpass.foreground_color() self._fg_color = fg_color.as_gdk_rgba() \ if fg_color else Color.named('black').as_gdk_rgba() # Label color label_color = pkpass.label_color() self._label_color = label_color.as_gdk_rgba() \ if label_color else Color.named('black').as_gdk_rgba() # Images self._background_texture = None self._logo_texture = None self._strip_texture = None if pkpass.background(): self._background_texture = pkpass.background().as_texture() if pkpass.logo(): self._logo_texture = pkpass.logo().as_texture() if pkpass.strip(): self._strip_texture = pkpass.strip().as_texture() # Fields self._header_fields = pkpass.header_fields() self._primary_fields = pkpass.primary_fields() self._secondary_fields = pkpass.secondary_fields() self._auxiliary_fields = pkpass.auxiliary_fields() @classmethod def new(clss, a_pass, pass_widget): pkpass = a_pass.adaptee() style = pkpass.style() if style == 'boardingPass': return BoardingPassPlotter(a_pass, pass_widget) elif style in ['coupon', 'storeCard']: return CouponPlotter(a_pass, pass_widget) elif style == 'eventTicket': return EventTicketPlotter(a_pass, pass_widget) elif style == 'generic': return GenericPlotter(a_pass, pass_widget) def plot(self, snapshot): self._snapshot = snapshot self._snapshot.save() self._plot_background() self._plot_header() self._plot_primary_fields() self._plot_secondary_and_axiliary_fields() self._snapshot.restore() def _plot_header(self): header_height = 32 # Draw the logo if it exists if self._logo_texture: logo_scale = header_height / self._logo_texture.get_height() logo_width = self._logo_texture.get_width() * logo_scale rectangle = Graphene.Rect() rectangle.init(PASS_MARGIN, PASS_MARGIN, logo_width, header_height) self._snapshot.append_texture(self._logo_texture, rectangle) point = Graphene.Point() point.y = PASS_MARGIN right_margin = (PASS_WIDTH - PASS_MARGIN) self._snapshot.save() self._snapshot.translate(point) for field in self._header_fields: field_layout = FieldLayout(self._pango_context, field, alignment = Pango.Alignment.RIGHT) field_original_width = field_layout.get_width() field_layout.set_width(right_margin) field_layout.append(self._snapshot, self._label_color, self._fg_color) right_margin -= field_original_width + PASS_MARGIN self._snapshot.restore() # Perform a translation so that the next drawing starts below this one point.x = 0 point.y = header_height + 3 * PASS_MARGIN self._snapshot.translate(point) def _plot_primary_fields(self): raise NotImplementedError def _plot_secondary_and_axiliary_fields(self): raise NotImplementedError def _plot_footer(self): raise NotImplementedError class PkPassWithStripPlotter(PkPassPlotter): """ PkPassWithStripPlotter is a PkPassPlotter for PKPasses that may contain a strip image. """ STRIP_IMAGE_MAX_HEIGHT = 123 def __init__(self, pkpass, pkpass_widget): super().__init__(pkpass, pkpass_widget) def _plot_primary_fields(self): # Draw the strip strip_height = 0 draw_strip = self._strip_texture and not self._background_texture if draw_strip: strip_scale = PASS_WIDTH / self._strip_texture.get_width() strip_height = self._strip_texture.get_height() * strip_scale rectangle = Graphene.Rect() rectangle.init(0, -PASS_MARGIN, PASS_WIDTH, strip_height) strip_height = min(self.STRIP_IMAGE_MAX_HEIGHT, strip_height) strip_area = Graphene.Rect() strip_area.init(0, -PASS_MARGIN, PASS_WIDTH, strip_height) self._snapshot.push_clip(strip_area) self._snapshot.append_texture(self._strip_texture, rectangle) self._snapshot.pop() # Draw the primary fields point = Graphene.Point() field_layout_height = 0 if self._primary_fields: field_layout = FieldLayout(self._pango_context, self._primary_fields[0], value_font = self.PRIMARY_FIELD_VALUE_FONT) field_layout_height = field_layout.get_height() self._snapshot.save() point.x = PASS_MARGIN point.y = 0 self._snapshot.translate(point) label_color = self._fg_color if draw_strip else self._label_color field_layout.append(self._snapshot, label_color, self._fg_color) self._snapshot.restore() # Perform a translation so that the next drawing starts below this one point.x = 0 point.y = strip_height if strip_height > field_layout_height \ else field_layout_height + 2 * PASS_MARGIN self._snapshot.translate(point) class BoardingPassPlotter(PkPassPlotter): def __init__(self, pkpass, pkpass_widget): super().__init__(pkpass, pkpass_widget) def _plot_primary_fields(self): # Origin origin_field = FieldLayout(self._pango_context, self._primary_fields[0], value_font = PassFont.biggest_value, alignment = Pango.Alignment.LEFT) # Destination destination_field = FieldLayout(self._pango_context, self._primary_fields[1], value_font = PassFont.biggest_value, alignment = Pango.Alignment.RIGHT) destination_field.set_width(PASS_WIDTH - 2 * PASS_MARGIN) self._snapshot.save() point = Graphene.Point() point.x = PASS_MARGIN point.y = 0 self._snapshot.translate(point) origin_field.append(self._snapshot, self._label_color, self._fg_color) destination_field.append(self._snapshot, self._label_color, self._fg_color) self._snapshot.restore() # Perform a translation so that the next drawing starts below this one point.x = 0 point.y = max(origin_field.get_height(), destination_field.get_height()) + 2 * PASS_MARGIN self._snapshot.translate(point) def _plot_secondary_and_axiliary_fields(self): self._plot_fields_layouts(self._auxiliary_fields) self._plot_fields_layouts(self._secondary_fields) class CouponPlotter(PkPassWithStripPlotter): STRIP_IMAGE_MAX_HEIGHT = 144 def __init__(self, pkpass, pkpass_widget): super().__init__(pkpass, pkpass_widget) def _plot_secondary_and_axiliary_fields(self): self._plot_fields_layouts(self._secondary_fields + \ self._auxiliary_fields) class EventTicketPlotter(PkPassWithStripPlotter): PRIMARY_FIELD_VALUE_FONT = PassFont.big_value STRIP_IMAGE_MAX_HEIGHT = 98 def __init__(self, pkpass, pkpass_widget): super().__init__(pkpass, pkpass_widget) def _plot_background(self): if not self._strip_texture and self._background_texture: rectangle = Graphene.Rect() rectangle.init(-BACKGROUND_BLUR_RADIUS, -BACKGROUND_BLUR_RADIUS, PASS_WIDTH + 2 * BACKGROUND_BLUR_RADIUS, PASS_HEIGHT + 2 * BACKGROUND_BLUR_RADIUS) self._snapshot.push_blur(BACKGROUND_BLUR_RADIUS) self._snapshot.append_texture(self._background_texture, rectangle) self._snapshot.pop() else: super()._plot_background() def _plot_secondary_and_axiliary_fields(self): self._plot_fields_layouts(self._secondary_fields + \ self._auxiliary_fields) class GenericPlotter(PkPassPlotter): def __init__(self, pkpass, pkpass_widget): super().__init__(pkpass, pkpass_widget) def _plot_primary_fields(self): if not self._primary_fields: return field_layout = FieldLayout(self._pango_context, self._primary_fields[0], value_font = PassFont.big_value) self._snapshot.save() point = Graphene.Point() point.x = PASS_MARGIN point.y = 0 self._snapshot.translate(point) field_layout.append(self._snapshot, self._label_color, self._fg_color) self._snapshot.restore() # Perform a translation so that the next drawing starts below this one point.x = 0 point.y = field_layout.get_height() + 2 * PASS_MARGIN self._snapshot.translate(point) def _plot_secondary_and_axiliary_fields(self): self._plot_fields_layouts(self._auxiliary_fields) self._plot_fields_layouts(self._secondary_fields) class PassWidget(Gtk.Fixed): __gtype_name__ = 'PassWidget' def __init__(self): super().__init__() self.__pass_plotter = None self.__barcode_button = None self.__children = [] self.props.width_request = PASS_WIDTH self.props.height_request = PASS_HEIGHT self.props.hexpand = False self.props.vexpand = True self.props.halign = Gtk.Align.CENTER self.props.valign = Gtk.Align.CENTER self.props.focusable = True self.add_css_class('card') def __on_barcode_clicked(self, args): self.emit('barcode_clicked') @GObject.Signal def barcode_clicked(self): pass def do_snapshot(self, snapshot): if not self.__pass_plotter: return self.__pass_plotter.plot(snapshot) if self.__barcode_button: self.snapshot_child(self.__barcode_button, snapshot) def content(self, a_pass): if self.__barcode_button: self.remove(self.__barcode_button) self.__barcode_button = None self.__pass_plotter = PassPlotter.new(a_pass, self) self.create_barcode_button(a_pass) # After changing the plotter, we have to redraw the widget self.queue_draw() def create_barcode_button(self, a_pass): barcode = a_pass.barcodes()[0] if not barcode: return self.__barcode_button = Gtk.Button() self.__barcode_button.connect('clicked', self.__on_barcode_clicked) self.__barcode_button.add_css_class('barcode-button') barcode_widget = BarcodeWidget() barcode_widget.encode(barcode.format(), barcode.message(), barcode.message_encoding()) aspect_ratio = barcode_widget.aspect_ratio() # Square codes if aspect_ratio == 1: max_times = 140 // barcode_widget.minimum_height() barcode_button_width = max_times * barcode_widget.minimum_height() barcode_button_height = max_times * barcode_widget.minimum_height() # Horizontal codes elif aspect_ratio > 1: max_times = (PASS_WIDTH - 2*PASS_MARGIN) // barcode_widget.minimum_width() if max_times * barcode_widget.minimum_height() > 140: max_times = 140 // barcode_widget.minimum_height() barcode_button_width = max_times * barcode_widget.minimum_width() barcode_button_height = max_times * barcode_widget.minimum_height() # Vertical codes else: barcode_button_width = 177 barcode_button_height = 177 self.__barcode_button.props.width_request = barcode_button_width self.__barcode_button.props.height_request = barcode_button_height self.__barcode_button.set_child(barcode_widget) self.put(self.__barcode_button, PASS_WIDTH/2 - barcode_button_width/2, PASS_HEIGHT - PASS_MARGIN - barcode_button_height) passes-0.9/src/view/window.blp000066400000000000000000000133311452462466600164470ustar00rootroot00000000000000using Gtk 4.0; using Adw 1; template $PassesWindow : Adw.ApplicationWindow { default-width: 1110; default-height: 600; // Minimum size of the window width-request: 360; height-request: 294; Adw.Breakpoint { condition("max-width: 676sp") setters { main_split_view.collapsed: true; } } Adw.Breakpoint { condition("min-width: 1008sp") setters { info_button.visible: false; inner_split_view.collapsed: false; } } content: Adw.ToastOverlay toast_overlay { Adw.NavigationSplitView main_split_view { sidebar: Adw.NavigationPage { title: _("Passes"); Adw.ToolbarView { //top-bar-style: raised; width-request: 332; [top] Adw.HeaderBar main_header_bar { [start] Gtk.Button { action-name: "app.import"; can-focus: false; icon-name: "list-add-symbolic"; tooltip-text: _("Import a pass"); visible: true; } [end] Gtk.MenuButton { icon-name: "open-menu-symbolic"; menu-model: primary_menu; tooltip-text: _("Menu"); } } content: Gtk.ScrolledWindow { hscrollbar-policy: never; Gtk.Viewport { scroll-to-focus: true; $PassList pass_list {} } }; } }; content: Adw.NavigationPage { width-request: 294; Adw.OverlaySplitView inner_split_view { sidebar-width-fraction: 0; sidebar-position: end; min-sidebar-width: 300; max-sidebar-width: 1000; collapsed: true; content: Adw.NavigationPage { Adw.ToolbarView { //top-bar-style: raised; [top] Adw.HeaderBar { [end] Gtk.MenuButton { icon-name: "view-more-symbolic"; menu-model: secondary_menu; } [end] Gtk.Button info_button { icon-name: "info-symbolic"; tooltip-text: _("Show additional information"); } [end] Gtk.Button update_button { action-name: "app.update"; icon-name: "view-refresh-symbolic"; tooltip-text: _("Update pass"); } } content: $PassWidget pass_widget{}; } }; sidebar: Adw.NavigationPage info_panel { title: _("Additional information"); width-request: 332; Adw.ToolbarView { [top] Adw.HeaderBar { show-back-button: false; [start] Gtk.Button back_button { icon-name: "go-previous-symbolic"; tooltip-text: _("Back"); valign: center; visible: bind inner_split_view.collapsed; } } $AdditionalInformationPane pass_additional_info {} } }; } }; } }; } menu primary_menu { section { submenu { label: _("Sort"); item { action: "win.sort"; target: "description"; label: _("A-Z"); } item { action: "win.sort"; target: "creator"; label: _("Creator"); } item { action: "win.sort"; target: "expiration_date"; label: _("Expiration date"); } item { action: "win.sort"; target: "relevant_date"; label: _("Relevant date"); } } } section { item (_("Keyboard shortcuts"), "win.show-help-overlay") item (_("About Passes"), "app.about") } } menu secondary_menu { item (_("Delete"), "app.delete") } passes-0.9/src/view/window.py000066400000000000000000000117661452462466600163340ustar00rootroot00000000000000# window.py # # Copyright 2022-2023 Pablo Sánchez Rodríguez # # 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 . from gi.repository import Gio, GLib, GObject, Gtk, Adw from .additional_information_pane import AdditionalInformationPane from .barcode_dialog import BarcodeDialog from .digital_pass_list_store import SortingCriteria from .pass_list import PassList from .pass_widget import PassWidget @Gtk.Template(resource_path='/me/sanchezrodriguez/passes/window.ui') class PassesWindow(Adw.ApplicationWindow): __gtype_name__ = 'PassesWindow' main_split_view = Gtk.Template.Child() inner_split_view = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() # Left panel pass_list = Gtk.Template.Child() # Main panel update_button = Gtk.Template.Child() info_button = Gtk.Template.Child() pass_widget = Gtk.Template.Child() # Right panel back_button = Gtk.Template.Child() pass_additional_info = Gtk.Template.Child() def __init__(self, pass_list_model, **kwargs): super().__init__(**kwargs) # Set help overlay help_overlay = Gtk.Builder\ .new_from_resource('/me/sanchezrodriguez/passes/help_overlay.ui')\ .get_object('help_overlay') self.set_help_overlay(help_overlay) self.get_application()\ .set_accels_for_action('win.show-help-overlay', ['question']) # Bind pass list and model self.pass_list.bind_model(pass_list_model) # Create action for pass sorting menu action = Gio.SimpleAction\ .new_stateful("sort", GLib.VariantType.new('s'), GLib.Variant.new_string(pass_list_model.sorting_criteria())) action.connect("activate", self._on_sort_action) self.add_action(action) # Connect callbacks self.back_button.connect('clicked', self._on_back_button_clicked) self.pass_list.connect('pass-activated', self._on_pass_activated) self.pass_list.connect('pass-selected', self._on_pass_selected) self.pass_widget.connect('barcode-clicked', self._on_barcode_clicked) self.info_button.connect('clicked', self._on_info_button_clicked) def _on_back_button_clicked(self, button): self.inner_split_view.set_show_sidebar(False); def _on_barcode_clicked(self, button): try: selected_pass = self.selected_pass() barcode = selected_pass.barcodes()[0] if barcode: dialog = BarcodeDialog() dialog.set_modal(True) dialog.set_transient_for(self) dialog.set_barcode(barcode) dialog.show() except Exception as error: self.show_toast(str(error)) def _on_info_button_clicked(self, button): self.inner_split_view.set_show_sidebar(True); def _on_pass_activated(self, pass_list, digital_pass): self.update_button.set_sensitive(digital_pass.is_updatable()) self.pass_widget.content(digital_pass) self.pass_additional_info.content(digital_pass) if self.inner_split_view.get_collapsed(): self.inner_split_view.set_show_sidebar(False); self.main_split_view.set_show_content(True) def _on_pass_selected(self, pass_list, digital_pass): self.update_button.set_sensitive(digital_pass.is_updatable()) self.pass_widget.content(digital_pass) self.pass_additional_info.content(digital_pass) if self.inner_split_view.get_collapsed(): self.inner_split_view.set_show_sidebar(False); def _on_sort_action(self, action, target: GLib.Variant): sorting_criteria = SortingCriteria.from_string(target.get_string()) self.pass_list.sort_by(sorting_criteria) action.set_state(target) def force_fold(self, force): self.main_split_view.set_collapsed(force) def is_folded(self): return self.main_split_view.get_collapsed() def navigate_back(self): self.main_split_view.set_show_content(False) def select_pass_at_index(self, index): self.pass_list.select_pass_at_index(index) def selected_pass(self): return self.pass_list.selected_pass() def selected_pass_index(self): return self.pass_list.selected_pass_index() def show_toast(self, message): toast = Adw.Toast.new(message) self.toast_overlay.add_toast(toast)