mirage-0.7.2/000077500000000000000000000000001407747233600130015ustar00rootroot00000000000000mirage-0.7.2/.flake8000066400000000000000000000013311407747233600141520ustar00rootroot00000000000000# vim: ft=dosini # https://flake8.pycqa.org/en/latest/user/configuration.html [flake8] # E131: continuation line unaligned for hanging indent # E301: when method is after a commented line + one blank line # E302: expected 2 blank lines, found 1 when using @dataclass # E303: more than one blank line between methods # W504: when line breaks occur after a binary operator # A003: when class attribute name is the same as a builtin # E402: when a module import isn't at the start of the file ignore = E131, E221, E241, E251, E301, E302, E303, W504, A003, E402 max-complexity = 99 inline-quotes = " format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s mirage-0.7.2/.github/000077500000000000000000000000001407747233600143415ustar00rootroot00000000000000mirage-0.7.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001407747233600165245ustar00rootroot00000000000000mirage-0.7.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013761407747233600212250ustar00rootroot00000000000000--- name: Bug report about: 'Report an unexpected behavior ' title: '' labels: bug assignees: '' --- ### Description Describe your issue in details, provide error logs or screenshots if possible. ### Your environment - OS or distribution (e.g. Arch Linux, macOS 10.15, Windows 7...) - Architecture (e.g. x86 64bit) - For Linux users: your desktop environment or window manager (e.g. GNOME 3.34 Wayland, i3 4.17, etc) - How did you install Mirage? (e.g. manual build, distribution repository, AppImage, Flatpak...) - For manual installations: your Qt version - For manual installations: your Python version ### Steps to reproduce 1. Do this... 2. Do that... ### Expected behavior Tell us what should happen ### Actual behavior Tell us what happens instead mirage-0.7.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000002031407747233600222440ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for the application or project title: '' labels: enhancement assignees: '' --- mirage-0.7.2/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000001721407747233600207150ustar00rootroot00000000000000--- name: Question about: Ask a question about the application or project title: '' labels: question assignees: '' --- mirage-0.7.2/.gitignore000066400000000000000000000005671407747233600150010ustar00rootroot00000000000000__pycache__ .mypy_cache *.egg-info *.pyc *.qmlc *.jsc *~ tmp-* build dist .qmake.stash Makefile mirage mirage.pro.user *.AppImage tags packaging/flatpak/flatpak-env packaging/flatpak/requirements.txt packaging/flatpak/flatpak-env-requirements.txt packaging/flatpak/flatpak-pip.json packaging/flatpak/flatpak-pip-generator .flatpak-builder/ flatpak-build/ docs/TODO.md mirage-0.7.2/.gitmodules000066400000000000000000000010641407747233600151570ustar00rootroot00000000000000[submodule "submodules/qsyncable"] path = submodules/qsyncable url = https://github.com/benlau/qsyncable [submodule "submodules/RadialBarDemo"] path = submodules/RadialBarDemo url = https://github.com/mirukana/RadialBarDemo [submodule "submodules/hsluv-c"] path = submodules/hsluv-c url = https://github.com/hsluv/hsluv-c [submodule "submodules/gel"] path = submodules/gel url = https://github.com/Cutehacks/gel [submodule "submodules/SortFilterProxyModel"] path = submodules/SortFilterProxyModel url = https://github.com/oKcerG/SortFilterProxyModel mirage-0.7.2/.isort.cfg000066400000000000000000000002011407747233600146710ustar00rootroot00000000000000# https://pycqa.github.io/isort/docs/configuration/options/ [settings] multi_line_output = 5 include_trailing_comma = True mirage-0.7.2/.mypy.ini000066400000000000000000000004001407747233600145500ustar00rootroot00000000000000# https://mypy.readthedocs.io/en/stable/config_file.html [mypy] cache_dir = ~/.cache/mypy ignore_missing_imports = True follow_imports = silent warn_redundant_casts = True warn_unused_ignores = True warn_unreachable = True mirage-0.7.2/COPYING000066400000000000000000001045151407747233600140420ustar00rootroot00000000000000 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 . mirage-0.7.2/COPYING.LESSER000066400000000000000000000167441407747233600150440ustar00rootroot00000000000000 GNU LESSER 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. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. mirage-0.7.2/README.md000066400000000000000000000075371407747233600142740ustar00rootroot00000000000000# Mirage [![Latest release](https://img.shields.io/github/v/release/mirukana/mirage)](https://github.com/mirukana/mirage/releases) [![Built with matrix-nio](https://img.shields.io/badge/built%20with-matrix--nio-brightgreen)](https://github.com/poljar/matrix-nio) [![#mirage-client:matrix.org](https://img.shields.io/matrix/mirage-client:matrix.org?color=blueviolet)](https://matrix.to/#/#mirage-client:matrix.org) [Features](#currently-implemented-features) ⬥ [Installation](docs/INSTALL.md) ⬥ [Configuration](docs/CONFIG.md) ⬥ [Theming](docs/THEMING.md) ⬥ [Contributing](docs/CONTRIBUTING.md) ⬥ [Screenshots](#screenshots) A fancy, customizable, keyboard-operable [Matrix](https://matrix.org/) chat client for encrypted and decentralized communication. Written in Qt/QML and Python, **currently in alpha**. ![Chat screenshot](docs/screenshots/01-chat.png?raw=true) ## Currently Implemented Features ### General - **Fluid, responsive interface** that adapts to any window size - Toggleable **compact mode** - Customizable **keyboard shortcuts** for everything, including switching rooms, navigating messages, sending/opening files... - Versatile **theming system**, properties can refer to each other and have any valid ECMAScript 7 expression as values - Comes by default with **dark** and **transparent themes** - Desktop **notifications**, sounds and window alerts - Support for HTTP and SOCKS5 proxies including TOR ### Accounts - Built-in public homeservers list - **Multiple accounts** in one client - **SSO** and password authentication - Set your display name and profile picture - Import/export **E2E** key files - Inspect, rename, manually verify and sign out one or multiple **sessions** - Sessions for accounts within the same client automatically verify each others - Set your account's **presence** to online, unavailable, invisible or offline - Set custom **status messages** - Automatically set your status to unavailable after a period of inactivity - Advanced **push rules** editor ### Rooms - Create, join, leave and forget rooms - Send, accept and refuse invites - Edit the room's name, topic, invite requirement, guest access and enable E2E - Kick, ban and set the power level of users - Pin rooms to the top of the list - Unread message and highlight counters - Sending **read receipts** to mark rooms as read - Seeing who has read a message and when - Inspect and manually **verify** other users's **E2E sessions** - See other users's **presence, status message and last seen time** - **Typing notifications** ### Messages - Send and receive **E2E encrypted messages** - Send and receive emote messages (e.g. `/me reads attentively`) - Receive notice (bot) messages - Send **markdown** formatted messages - Additional syntax for **coloring text**, e.g. `(Some text...)` - [SVG/CSS color names](https://www.december.com/html/spec/colorsvg.html), `#RGB`, `#RRGGBB` and `#AARRGGBB` hex codes can be used - Send and receive normal or **E2E encrypted files** - Client-side Matrix & HTTP URL **image previews**, including animated GIF - Upload images by pasting or drag-and-drop - Full-size image viewer - User ID, display names, room ID and room aliases **mentions** - **Autocompletion** for usernames and user ID - Individual and mass **message removal** - Sending **rich replies** ## Documentation - [Installation](docs/INSTALL.md) - [Configuration](docs/CONFIG.md) - [Theming](docs/THEMING.md) - [Contributing](docs/CONTRIBUTING.md) ## Screenshots ![Sign-in](docs/screenshots/02-sign-in.png) ![Account settings](docs/screenshots/03-account-settings.png) ![Room creation](docs/screenshots/04-create-room.png) ![Chat](docs/screenshots/01-chat.png?raw=true) ![Main pane in small window](docs/screenshots/05-main-pane-small.png) ![Chat in small window](docs/screenshots/06-chat-small.png) ![Room pane in small window](docs/screenshots/07-room-pane-small.png) mirage-0.7.2/autoreload.py000077500000000000000000000037201407747233600155170ustar00rootroot00000000000000#!/usr/bin/env python3 """Usage: ./autoreload.py [MIRAGE_ARGUMENTS]... Automatically rebuild and restart the application when source files change. CONFIG+=dev will be passed to qmake, see mirage.pro. The application will be launched with `-name dev`, which sets the first part of the WM_CLASS as returned by xprop on Linux. Any other arguments will be passed to the app, see `mirage --help`. Use `pip3 install --user -U requirements-dev.txt` before running this.""" import os import subprocess import sys from contextlib import suppress from pathlib import Path from shutil import get_terminal_size as term_size from watchgod import DefaultWatcher, run_process ROOT = Path(__file__).parent class Watcher(DefaultWatcher): def accept_change(self, entry: os.DirEntry) -> bool: path = Path(entry.path) for bad in ("src/config", "src/themes"): if path.is_relative_to(ROOT / bad): return False for good in ("src", "submodules"): if path.is_relative_to(ROOT / good): return True return False def should_watch_dir(self, entry: os.DirEntry) -> bool: return super().should_watch_dir(entry) and self.accept_change(entry) def should_watch_file(self, entry: os.DirEntry) -> bool: return super().should_watch_file(entry) and self.accept_change(entry) def cmd(*parts) -> subprocess.CompletedProcess: return subprocess.run(parts, cwd=ROOT, check=True) def run_app(args=sys.argv[1:]) -> None: print("\n\x1b[36m", "─" * term_size().columns, "\x1b[0m\n", sep="") with suppress(KeyboardInterrupt): cmd("qmake", "mirage.pro", "CONFIG+=dev") cmd("make") cmd("./mirage", "-name", "dev", *args) if __name__ == "__main__": if len(sys.argv) > 2 and sys.argv[1] in ("-h", "--help"): print(__doc__) else: (ROOT / "Makefile").exists() and cmd("make", "clean") run_process(ROOT, run_app, callback=print, watcher_cls=Watcher) mirage-0.7.2/docs/000077500000000000000000000000001407747233600137315ustar00rootroot00000000000000mirage-0.7.2/docs/CHANGELOG.md000066400000000000000000001467301407747233600155550ustar00rootroot00000000000000# Changelog All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - [0.7.2 (2021-07-26)](#072-2021-07-26) - [0.7.1 (2021-03-04)](#071-2021-03-04) - [0.7.0 (2021-02-28)](#070-2021-02-28) - [0.6.4 (2020-09-16)](#064-2020-09-16) - [0.6.3 (2020-09-16)](#063-2020-09-16) - [0.6.2 (2020-08-28)](#062-2020-08-28) - [0.6.1 (2020-08-21)](#061-2020-08-21) - [0.6.0 (2020-07-17)](#060-2020-07-17) - [0.5.2 (2020-06-26)](#052-2020-06-26) - [0.5.1 (2020-06-05)](#051-2020-06-05) - [0.5.0 (2020-05-22)](#050-2020-05-22) - [0.4.3 (2020-04-03)](#043-2020-04-03) - [0.4.2 (2020-03-27)](#042-2020-03-27) - [0.4.1 (2020-03-23)](#041-2020-03-23) - [0.4.0 (2020-03-21)](#040-2020-03-21) ## 0.7.2 (2021-07-26) ### Added - Navigation keybinds: - `Keys.quit` keybind to exit the application, unbound by default - `Keys.earlier_page` and `Keys.later_page` keybinds to navigate the pages/chats history, Ctrl+H/Left and Ctrl+L/Right by default - Mouse button 4/5 can now be used to navigate the history - `General.wrap_history` setting, affects the behavior of history navigation - `Keys.Rooms.Direct` section, allowing keybinds to jump to specific rooms - `Keys.Rooms.oldest_unread`/`latest_unread` to jump to the room with the oldest or newest unread message, by default Ctrl(+Shift)+U - `Keys.Rooms.oldest_highlight`/`latest_highlight`, same as above but only considers rooms where you've been mentioned/replied to/etc, by default Ctrl(+Shift)+H - Ignoring users: - Ignore option in the context menu for room members - Ignore option when rejecting invites - Editable ignored users list in account settings - Invites and messages from ignored users are silently discarded. Their display name, avatar and presence are removed. They will also always be placed at the bottom of the room member list. - Status messages history in the left pane account context menu, and auto-suggestion for the status field. The number of saved entries can be controlled with `Presence.saved_status`. - "Add another account" entry in the top left settings menu - Copiable room ID field in the room settings pane - Back button in account settings and server browser when the window is too narrow to show the side panes - Escape key handling in the account settings, server browser and add chat pages - Support for rendering HTML `
` lines (markdown `---`) in messages ### Changed - Keybinds: - `Keys.messages.clear_all` default keybind is now Ctrl+Shift+L - `Keys.Account.AtIndex` keybinds will consistently move to the corresponding account settings, instead of skipping to the first room if the account is expanded in the left pane - `Keys.messages.open_links_files`(`_externally`): ignore matrix.to user and room mention links - Presence: - Allow using the invisible mode on servers not supporting presence, which will still prevent sending typing notifications and read marker updates - Restore any previously set status message when reconnecting after being offline, unless another one has been set from a different client - Render set status message striked out while invisible/offline to indicate that it isn't being broadcasted - Error popup: - Multiple unexpected errors will be combined into a single popup, instead of opening a new one for every error - Report button now links to Github issues - More details shown for matrix errors - Messages: - Require a space after the `#` for markdown titles - Render matrix.to URL in messages as shorter hyperlinks - Left pane rooms: - Last message display: shorten long "In reply to..." prefixes for the message to be shown as "↩ : " - Show the inviter for invites where the room has an explicit name/alias set - Leave, decline invite and forget room options are merged into an unified popup - Raise default `General.tooltips_delay` from 0.5 to 0.7s - Updated headers UI navigation arrow icons - Misc UI text changes for shortness and consistency ### Fixed - Fix error popup appearing when invalid room events appear in syncs - Fix parsing of URL in messages containing some special characters - Left pane rooms last message display: fix `> quote` right after another quote not getting colored - Fix the forgetting rooms feature - Fix rendering status messages containing HTML-unsafe characters - Fix chat bottom bar for invited/left rooms glitching at certain sizes or not properly updating when the room's state changes - Prevent theme `animationDuration` property from affecting the speed of loading spinners, progress bars, server ping indicators and image rotation button cooldown - Hopefully fix account presence stuck flickering between two states ## 0.7.1 (2021-03-04) ### Fixed - Fix loading custom themes when compiled in release mode - Fix homeserver list errors with aiohttp 3.6.x and less, used in Flatpak - Various corrections to default config file comments ## 0.7.0 (2021-02-28) ### Added - **Push rules and notifications support**: - Add native desktop notifications support - Add sound effect playback support - Add button and keybinds to mute all notifications in the running client - Add notification context menu options to rooms in the left pane - Add a push rule editor to account settings: - Control for any rule whether matching messages are marked as unread, highlighted, trigger a desktop notification, sound, window alert, or any combination of those actions - Create custom rules targeting a particular room, message sender, messages containing certain words or messages matching advanced conditions - **New configuration system** replacing the previous `settings.json`, see the [documentation](https://github.com/mirukana/mirage/blob/master/docs/CONFIG.md) for more info - Rooms in the left pane can now be pinned to the top of the list, using the added context menu option or config file setting - Support drag-and-dropping text and files to upload in chats - Add tooltips to the message read counters, listing who has read a message and when. Tooltips can also be shown for the keyboard-focused message using a keybind. - The chat header now indicates when messages are selected in the timeline, and offers copy/redact/clear selection buttons - Add a visible indicator when downloading files - Add command-line arguments parsing and a `--start-in-tray` option, see `mirage --help` - Support for HTTP and SOCKS5 proxies, can be set in config file or using the `http_proxy` environment variable - Hovering on stability percentages in the sign-in page's homeserver list now shows more detailed tooltips about the server's recent downtimes ### Changed - Config files and theme (with the exception of `accounts.json`) are now automatically reloaded when changed on disk - The top-left settings button now opens a menu giving access to the settings folder, theme folder and developer console - Clicking on the "Mirage x.y.z" text in the top left no longer opens the github page - Merge the "Encryption" and "Sessions" account settings tabs into a new "Security" tab - Developer console improvements: - Improve default colors and provide clearer separation of different commands's outputs - Support multi-line input, use shift+return to insert a newline - The output text can now be selected and copied - Improve room page loading speed - In the left pane, lock the position of the room corresponding to the currently visible chat page if any. This fixes annoyances like clicking on a room with unread messages only to see it immediatly fly down the list, potentially outside of scrolling view. - When replying to a message, pressing the reply keybind again while focusing on that message will now cancel the reply - Make user ID in account settings a copiable read-only text field - Hide useless context menu entries for read-only text fields (undo/paste/etc) - Make the scroll to top/bottom keybinds work faster for long timelines and be more accurate - Better explain why not all selected messages can be removed in the message removal confirmation popup - When clicking on an account in the top left account bar or using the previous/next account keybinds, focus the account settings instead of the account's first room - While keyboard-focusing an image message, hide its sender and time bubbles as if it was hovered by mouse - Color key words in invite/leave/forget/error popups instead of using italic - Apply theme radius on context menus - Theming: - Reduce default `fontSize.big` from `22` to `20` - Change the default style of room unread indicators - Add new properties to the `mainPane.accountBar.account.unreadIndicator` and `mainPane.listView.room.unreadIndicator` sections, see [620b5815](https://github.com/mirukana/mirage/commit/620b58151d7d9d15e242402da34ef55a05549ca5#diff-fdb828d814eca61316a31204666263a410da6dd9c1cf0099dbf54da9e82e33e1) ### Fixed - Fix build failing on Python 3.9 due to incompatible `blist` dependency - Fix event context menu "Reply" option targetting the wrong message - Fix read counter on image events lacking color and having extra padding - Prevent opening multiple instances of the same context menu by right clicking or using keybinds - Fix current page not being highlighted in the left pane when Mirage starts and the initial page to load is an account settings page - Fix list delegates (especially left pane rooms) occasionally appearing as invisible items - Fix incorrect user ID text hue in account settings - Close the reply bar when switching to another room while composing a reply - Fix "Copy link address" entry in message context menu not being visible - Fix some characters being rendered incorrectly in redacted messages reasons (e.g. `` was shown as `<test>`) - Fix cancel button in the "Join Room" page not returning to previous page - Fix Matrix server errors lacking a `M_CODE` triggering an account logout - Fix "Go to previous/next unread/highlighted room" keybinds ignoring rooms with a local unread counter ([!] markers) - Fix copying multi-line mouse-selected rich text, newlines were not preserved - Prevent warnings spam when the XScreenSaver protocol is available but not supported, e.g. when running in XWayland - Fix message timeline occasionally breaking and mixing messages from multiple rooms when switching room - Show an error when loading a JSON config file fails instead of silently failing and using a default configuration, which can potentially overwrite user files - Fix "focus previous/next message" keybinds sometimes skipping messages and focusing the middle of the screen while the timeline scrolling was at the bottom - Fix read marker updates sometimes getting stuck and never clearing the unread message counts for a room - Fix scroll keybinds not working when kinetic scrolling is disabled - Fix chat right pane having an invisible 10px edge when hidden/collapsed, interfering with any button in the way - Fix the "expand right pane" button failing to bring back the pane and turning the chat room header invisible - Fix "open debug console for this message" keybind erroring - Prevent horizontal dragging of flickable column layout pages - Fix scrolling keybinds not working to scroll popups - Revert 0.6.2's message combining fix, which caused message bubble movements to randomly stop in the middle of their animations and be left at odd positions or overlap with other bubbles ### Known Issues - Flatpak does not seem to support direct ALSA access, sound notifications might not work within it - Desktop notifications within Flatpak might not work out of the box when Xorg is launched without a display manager, try running `export $(dbus-launch --autolaunch "$(cat /var/lib/dbus/machine-id)")` before launching Mirage in a terminal. ## 0.6.4 (2020-09-16) ### Fixed - Fix checkboxes in the room settings not having their default values updated after switching room - Fix various minor features broken on Qt 5.12 and the AppImage since v0.5.2 ## 0.6.3 (2020-09-16) ### Added - Add a **system tray icon**. A left click will bring up the Mirage window, middle will quit the application and right will show a menu with these options. - Add a `closeMimizesToTray` setting to the config file, defaults to `false`. Controls whether closing the Mirage window will leave it running in the system tray, or fully quit the application. - Add a discrete **read marker indicator** to messages, shows how many people have this event as their last seen one in the room. A way to see who read the message and when will be added in the future. - Themes: add `chat.message.localEcho` and `chat.message.readCounter` color properties - Add a `zoom` setting, defaults to `1.0` - Add a `lexicalRoomSorting` setting, to sort rooms by their name instead of recent activity. A restart is needed to apply changes to this setting. ### Changed - Restrict Mirage to a single instance per config folder, trying to launch a new window will instead focus the existing one. The `MIRAGE_CONFIG_DIR` and `MIRAGE_DATA_DIR` environment variables can be set to run different "profiles" in parallel. - Reduce the visible lag when opening a chat page, switching rooms should be a lot smoother - When using the `focusPreviousMessage` and `focusNextMessage` keybinds, if no message is focused and the timeline has been scrolled up, focus the message in the center of the view instead of returning to the bottom of the timeline and focusing the last one. - Don't re-center the room list on clicks by default. This prevents the list from jumping around every time a room is selected. The previous behavior can be restored with the new `centerRoomListOnClick` setting. - Show a better terminal error message than "Component is not ready" when the window creation fails, giving details on what went wrong in the code - If an account's access token is invalid (e.g. our session was signed out externally), say so with a popup and cleanly remove it from the UI, instead of spamming the user with errors. - Rename message context menu option "Debug this event" to just "Debug" - Unify up/down and (shift+)Tab navigation for the account Sessions page - Changes to the UI scale/zoom via keybinds are now persisted across restarts - Themes: `uiScale` is now bound to `window.settings.zoom`. This change is necessary to keep the zoom keybinds working. ### Fixed - Midnight theme: fix missing `}` from change to the `chat.message.styleSheet` property introduced in 0.6.2, see [2a0f6ae](https://github.com/mirukana/mirage/commit/2a0f6aead17d05fd35e8a944e5434781e9c08d50). - Fix multiple consecutive one-line events (/me emotes, "x joined", etc) not combining properly - Fix theme finder ignoring the `MIRAGE_DATA_DIR` environment variable - Fix theme background image not updating when reloading theme/settings - Fix up/down keys not working when the text cursor is in a word starting with `@`, but the word doesn't match any usernames to complete. - Fix context menu copy options for messages containing URL thumbnails - Fix context menu copy option for single non-message events - Fix GIF URL thumbnails not being animated in the timeline - Fix image viewer sizes shown as "0x0" for loading images and GIFs - Fix incorrect sync filter usage introduced in 0.6.1, which caused problems like redaction events never arriving - Fix redacted media messages keeping their thumbnails - Fix terminal warnings when uploading to a non-encrypted room - Fix some cases of undetected power level changes, e.g. a muted user (level -1) going back to the default (level 0). - Don't show popup for `400 M_UNRECOGNIZED` errors that can occur when trying to fetch an offline user's presence - Focus the filter field again when exiting a room member profile page ## 0.6.2 (2020-08-28) ### Changed - When replying to a message, you can now press enter without entering any text to send it directly (useful to "forward" a message). - Sending a file while replying to a message will create a pseudo-reply, consisting of an "In reply to" text message with no body, followed by the actual file event. This is a workaround to the reply restrictions imposed by the Matrix spec. - **Composer aliases cannot contain whitespace anymore.** This includes spaces, hard tabs or newline characters. If an alias from your config still has whitespace, only the first word will be taken into account (ignoring any leading or trailing space). - Faster server browser loading, now gathers all needed data with a single request instead of one for each server - Auto-focus the "Join" button on invited room pages ((Shift+)Tab can be used to navigate between buttons) - Auto-focus the "Forget" button on left room pages - Themes: modify `chat.message.styleSheet` to add some spacing between HTML list items, see [48663ae](https://github.com/mirukana/mirage/commit/48663ae8465e90646855435b47b89c01395ae4d9) ### Fixed - Fix @username autocompletion closing if there's more than one character after the @ - Consider the partial text from IME (input method editors) and touch screen autocompleting keyboards for username autocompletion - Reset IME state upon autocompleting a username - Fix clicking on autocompletion list user not making the username a mention - Fix UI freezing when mentioning user lacking a display name - Fix mentioning users with blank display name (e.g. only spaces), mention them by their user ID - Fix text fields/areas unable to be focused on touch screen - Fix random chance of profile retrieval requests failing if one of the logged in account doesn't federate with other servers (e.g. localhost synapse) - Fix composer text saved to disk for the wrong account if that text begins by an account alias - Servers can potentially return an outdated member list for rooms on initial sync, which is one of the possible cause of "Members not synced" error for encrypted rooms. When loading the full room list, discard members from the initial sync list that are absent from the full list. For those not using the AppImage or Flatpak, this fix requires **matrix-nio 0.15.1** or later to take effect. - When erasing an account alias inside the composer, send a "x isn't typing anymore" notification corresponding to that account - Fix potential 403 error on chat pages for invited rooms. - Start loading room history immediately when the room join state changes, e.g. when clicked "Join" for an invited room page. ## 0.6.1 (2020-08-21) ### Added - **SSO authentication** support - **Homeserver browser**: - To add a new account, you will be asked first to pick one of the listed public server (list data from [anchel.nl](https://publiclist.anchel.nl/)) or to manually enter a server address - Typing in the server address field will also filter the public server list, Up/Down or (Shift+)Tab and Enter can be used to navigate it by keyboard - If the address doesn't have a `scheme://`, auto-detect whether the server supports HTTPS or only HTTP - Use the .well-known API if possible to resolve domains to the actual homeserver's URL, e.g. `matrix.org` resolves to `https://matrix-client.matrix.org` - The server address field will remember the last homeserver that was connected to - **Room members autocompletion**: - Type `@` followed by one or more characters in the composer, or one or more characters and hit (Shift+)Tab to trigger username/user ID autocompletion - Only autocompleted names will be turned into mentions, unlike before where any word in a sent message that happened to be someone's name would mention them - **Full image viewer** for matrix image messages and URL previews: - Click on a thumbnail in the timeline to open the image viewer - Middle click on a thumbnail (or use the option in the context menu) to open the image externally - Left click on the image (mouse only): expand to window size if the image's origin size is smaller than the window, else expand to original size - Tap on the image (touch screen/pen only): reveal the info and button bars when auto-hidden (bars will auto-hide only when they overlap with a big enough displayed image) - Any mouse movement: reveal auto-hidden bars - Double click on the image: toggle full screen - Middle click anywhere: open externally - Right click anywhere: close the viewer, back to chat - Drag when displayed image is bigger than window to pan - Wheel to pan up/down, hold shift or alt to pan left/right - Ctrl+wheel to control zoom - Buttons to control rotation, scale mode, full screen, GIF play/pause and GIF speed - New keyboard shortcuts are available for all these actions, see `keys.imageViewer` in the config file (will be automatically updated when you start Mirage 0.6.1) - Add `media.openExternallyOnClick` setting to swap the new click and middle click on thumbnails behavior - Add `openMessagesLinksOrFilesExternally` keybind, by default Ctrl+Shift+O - Add `copyFilesLocalPath` keybind, by default Ctrl+Shift+C - Room and member filter fields now support (Shift+)Tab navigation, in addition to Up/Down - Add a colored left border to the currently highlighted item in list views (e.g. room list, members list, etc) to improve visibility - Themes: - Add `controls.listView.highlightBorder` and `controls.listView.highlightBorderThickness` properties (can be set to `0`) - Add the `chat.userAutoCompletion` section ### Changed - Messages context menu: - Use a cleaner icon for the "Copy text" entry - Replace the confusing broken "Copy media address" entry with: - Copy media address: visible for non-encrypted media, always copies the HTTP URL - Copy local path: always visible for already downloaded media, even if they were downloaded before mirage was started - The `openMessagesLinks` keybind (default Ctrl+O) is renamed to `openMessagesLinksOrFiles` and can now also open media message files - Using the `openMessagesLinksOrFiles` keybind on a reply will now ignore the matrix.to links contained in the "In reply to XYZ" header - Pressing Ctrl+C to copy selected/highlighted non-encrypted media messages will copy their HTTP URL instead of the filename - Retry downloading image thumbnails if they fail with a 404 or 500+ server error (uploads sometimes take a few seconds to become available on the server) - Non-encrypted media messages are now always downloaded on click and opened with a desktop application (or the image viewer), instead of being opened in a browser - Compress thumbnails and clipboard images in a separate process, to avoid blocking every other backend operation while the compression is running - Reduce the level of optimization applied to clipboard images, the previous setting was too slow for large PNG (10MB+) - Increase applied scrolling velocity when using the `scrollPageUp`/`scrollPageDown` keybinds, now similar to how it was before Mirage 0.6.0 - Don't catch SIGQUIT (Ctrl+\ in terminal) and SIGTERM signals, exit immediately - Slightly increase the top/bottom padding to the multi-account bar in the left pane - Dependencies: minimum nio version bumped to 0.15.0 ### Removed - Themes: remove unused `controls.listView.smallPaneHighlight` property ### Fixed - Don't show account avatar tooltips when the context menu is open - Don't automatically focus member power level control when grayed out - Fix uploading files for servers not telling us their maximum allowed file size - Fix message context menu "Copy text": when an event was highlighted with the keyboard, right clicking a message and clicking "Copy text" would always copy the message that was highlighted instead of the one the user aimed for. - Fix pressing menu key in chat opening both the composer's and the timeline's context menus - Fix random chance of failure when fetching thumbnails or user profiles and an account other than the current one is offline - Catch potential 403 errors when fetching presence for offline room members - Fix room right pane stealing focus from opened popup when resizing the window from narrow/mobile mode to normal mode - Never try to send typing notifications in rooms where we don't have permission to talk - Fix clipboard upload preview popup not updating when the copied image changes - Fix some pages not respecting `enableKineticScrolling: false` setting - Fix truncated "Loading previous messag..." text in timeline - Fix possible race condition corrupting user config files on write - Fix missing member events from initial syncs, also fixes some cases of the "Members not synced" error occurring in encrypted rooms where members have recently joined or left. - Fetch missing member display name when displaying last messages in room pane for rooms that haven't had their members list fully loaded yet - Use uploaded sync filter IDs in sync requests instead of passing a long JSON object in the URL every time, which caused problems on some servers with a short URL length limit (e.g. halogen.city) - Fix autolinking user IDs that include `;` or `<` characters - Ignore enter keypresses in pages or popups when the accept button is grayed out - Fix never-ending spinner in left pane when logging in to an account that was already connected ## 0.6.0 (2020-07-17) ### Added - **Room member profiles**: - Can be accessed by clicking on a user in the room's right pane, or focusing the filter field and navigating with up/down/enter/escape - Includes large avatar, display name, user ID, **presence** info, **power level control** and **E2E sessions list** - **E2E Verification**: - Sessions for room members can now be (manually) verified from their profile - Sessions for different accounts within the same client will automatically verify each others based on session keys - Verifying a session will automatically verify it for all connected accounts, as long as the session keys are identical - **Presence**: - Added presence (online, unavailable, invisible, offline) and status message control to the accounts context menu in the room list - Added `togglePresence{Unavailable,Invisible,Offline}` keybinds bound by default to `Ctrl+Alt+{A/U,I,O}` - Added `openPresenceMenu` keybind to open the current account's context menu, `Alt+P` by default - The room member list is now sorted by power level, then presence, then name - The room member list will display presence orbs and last seen time for members if the server supports it. Last seen times for offline members are also automatically retrieved as needed. - Set logged in accounts offline when closing Mirage - Linux/X11 specific: Add auto-away feature configurable by the `beUnavailableAfterSecondsIdle` setting (default 600 for 10mn), can be disabled by setting it to `-1`. **This requires the libX11 and libXScrnSaver/libXss developpment headers installed, see INSTALL.md for more info**. The dependencies and support for this feature can be disabled at compile-time. - **Session sign out**: you can now sign out your other sessions from the account settings. This currently only supports password authentification. - **Pasting images** via Ctrl+V or composer context menu, shows a preview of the image before uploading - Added basic keyboard navigation for account settings session list: - Up/down: highlight previous/next session - Enter/Return/Menu: open highlighted session menu - Space: check or uncheck highlighted sessions - Escape: uncheck all sessions - Alt+R/F5: refresh list - Alt+S/Delete: sign out checked sessions, or all sessions if none checked - Add a verified devices indicator to encrypted room headers - Add experimental support for rendering of inline images and custom emotes in messages - Add `kineticScrollingMaxSpeed` and `kineticScrollingDeceleration` settings - When highlighting accounts, rooms or members in lists (focus filter field and use up/down), the highlighted item's context menu can now be accessed with the keyboard Menu key - Support for Menu key when keyboard-navigating messages in the timeline - Add context menus to text field and areas - Add a button to quickly expand the room pane when collapsed and focus the filter field - Clicking on the current tab button for the room pane now fully hides it, this can also be toggled with the new `toggleHideRoomPane` keybind (default Ctrl+Alt+R) - Themes: - Add the `controls.presence` section - Add `mainPane.listView.offlineOpacity` property - Add CSS styling for `table` and `td` in the `chat.message.styleSheet` property ### Changed - When panes are smaller than their default width due to user resizing or window size constraints, focusing certain elements will auto-expand them until the focus is lost: filter fields, member profile and room settings - Reduced the default kinetic scrolling speed, which was hardcoded to an aggressive `4000` before. This can be restored with the `kineticScrollingMaxSpeed` setting. - Improve key verification popup texts and make the session details copiable - Power levels/room permission change events will now show a line of text or table containing the details of what exactly changed - Messages containing tables will no longer be width-limited - Using the `sendFileFromPathInClipboard` keybind (default Alt+Shift+S) now shows a preview of the file if it's an image and asks for confirmation - Image messages now show spinners when loading the thumbnail - Clicking on a GIF message will now open it externally like other images instead of pausing it. A dedicated play/pause button is now displayed in the corner. - Themes: - Update the `colors.positiveBackground`, `colors.middleBackground` and `colors.negativeBackground` properties to be brighter and have full opacity - Increase the opacity for the `menu.background` color (context menu), the previous value made it very hard to read in certain situations ### Removed - Themes: removed the `image` section and its `maxPauseIndicatorSize` property, no longer used since the GIF changes ### Fixed - Fix parsing user/room ID and room aliases containing dashes in messages - Fix responding to own messages sending an incorrect event ID to other clients - Fix plain text body of replies sent from Mirage - Fix high CPU usage due to the "Loading messages..." animation still being rendered when invisible - When logging in to an already connected account, redirect to the account settings page instead of overwriting it and losing the previous session - Fix signing out of an account leaving all its room in the room list - Fix all keybinds becoming disabled until next restart if a popup or menu is destroyed instead of being properly closed - Fix pressing left/right arrow to deselect text in fields and areas when the cursor is positioned at the beginning/end - Fix missing text for events involving display names that contain `< >` characters and other dangerous characters interpreted by HTML - Fix sending a typing notice indicating we stopped typing when the composer is cleared (e.g. when erasing all text or sending a message) - Fix hovering image messages not setting the pointing hand cursor - Opening a context menu and clicking at the exact spot where it was opened without having moved the cursor will now close the menu instead of doing nothing - Highlight the correct room list item when adding a new account, going to account settings or ctrl+tabbing to the "add new chat" page - Fix right room pane being shown as overlay sometimes in small window mode - Fix avatar membership icon (crown/star) position when the room pane is small - Correctly handle SIGINT (ctrl+c in terminal), SIGTERM, SIGHUP and SIGQUIT to exit Mirage - Fix opacity of topic area in room settings when disabled due to lack of permission - Fix GIF only having a cropped portion of their content rendered - Hide the "recursive layout" warnings spam in terminal that appeared in Qt 5.14 ## 0.5.2 (2020-06-26) ### Added - **Sessions/device list**: you can now inspect, rename, manually verify and blacklist your devices from the account settings page. The interface is still work in progress, keyboard navigation and signing out sessions will be added in a next version. - Re-add client-side unread/highlight room indicators. If your account has push notifications disabled, which precise cross-client counters depend on, the local indicators will be used as fallback. - Support the `MIRAGE_CACHE_DIR` environment variable to override where files and thumbnails are downloaded - Themes: - `colors.positiveText` property - `mainPane.listView.room.unreadName` property - In the `controls` section: - `scrollBar` section - `button.focusedBorder` and `button.focusedBorderWidth` properties - `tab.focusedBorder` and `tab.focusedBorderWidth` properties - `textArea.borderWidth`, `textArea.border`, `textArea.focusedBorder` and `textArea.errorBorder` properties ### Changed - Overhauled account settings to match the design of other tabbed pages. The horizontal layout design has been removed due to complicated code and being impossible to extend without breaking it. - The display name field in account settings is now colored, preview your new display name's color as you type - For rooms without image avatars set, the room settings's avatar color now responds to the name field as you type - Overhauled scrollbars: - Now match the Mirage theme and much better visibility - No more right margin for the timeline's bar - Minimum height to prevent the bar from becoming impossible to grab - Use brighter text for room names of rooms that have unread messages - Buttons, tabs, text fields and areas now have animated bottom borders to represent keyboard focus instead of being highlighted like when hovered - Text fields and areas can now have rounded corners, following the theme - Tabbed pages (Sign In, Add Chat, etc) can now be swiped left and right - Popups can now be scrolled when their content is bigger than the window's height - Replace most generic checkmark icons for apply buttons in popups - Pressing escape in forms will consistently trigger corresponding cancel buttons ### Fixed - Fix `Connections` deprecation warning on Qt 5.15 - Skip invisible entries when navigating context menus with up/down arrows - Fix tab focus for unhandled error and invite to room popups - Fix guest access event saying that guest access has been allowed when it has actually been forbidden - Deselect any selected message before clearing a room's events, not doing so made the gone messages impossible to deselect. - Properly center some previously offset popups ## 0.5.1 (2020-06-05) ### Added - **Saving room settings**: room name, topic, guest access, invite requirement, guest access and encryption can now be changed and saved from the room's settings pane - `markRoomReadMsecDelay` setting to configure how long in milliseconds Mirage will wait before marking a focused room as read, defaults to `200` - `alertOnMentionForMsec` setting separate from `alertOnMessageForMsec`, defaulting to `-1`: will trigger a non-expiring window highlight on messages received that mention your user (the behavior differs depending on desktop environment or window manager) ### Changed - **Unread message/highlight counters**: - The counters are now implemented in a cross-client, persistent way, and respect configured push rules for your account - Read receipts will be sent to the server to mark rooms as read - The `alertOnMessageForMsec` setting now defaults to `0`, disabling window highlights for messages not mentioning you - While an E2E key import operation is running, prevent accidentally closing the popup by clicking outside of it - For manual installations, `make install` will now copy files to `/usr/local` instead of `/usr` by default. This can be changed by setting `PREFIX` when running `qmake`, e.g. `qmake PREFIX=/usr`. After pulling the latest version, make sure to clean up old installation and build files before regenerating the Makefile and installing: `sudo make uninstall; make clean; qmake && make && sudo make install` - Improve the error messages shown when trying to start a direct chat with or invite a non-existing user - In room settings or creation, use a text area for the topic instead of a field limited to a single line ### Removed - Removed delay when multiple rooms are removed/hidden from the list. This should provide a smoother experience when filtering rooms or collapsing accounts, and prevent the account duplication bug. If you encounter issues with these operations like the room list becoming invisible, make sure first that your Qt installation is up-to-date (latest x.y.Z version, e.g. 5.14.2). ### Fixed - The room settings pane is now scrollable - Avoid potential error if the room list data model is initialized after an initial sync has already been completed - Closing the import key popup by pressing escape will now correctly cancel any running import operation - Fix Python pickling error when trying to re-decrypt events after importing E2E keys ([#50](https://github.com/mirukana/mirage/issues/50)) - Handle Matrix 502 errors returned when trying to start a direct chat or invite a user with an incorrect or unresponsive server in their ID - Correctly hide `socket.gaierror` error popups that appear when the internet connection drops - Hide popups for pointless `ssl.SSLError: [SSL: KRB5_S_INIT] application data after close notify` exceptions that occur in the Flatpak releases due to a Python 3.7 bug - Make sure the account shown in the left pane is immediately updated after changing display name or avatar in the account settings - When signing in a new account, correctly position it after the other existing ones without needing a restart - Correctly handle room topics containing new lines, hard tabs or text inside `<>` brackets - Starting a direct chat, creating or joining a room will now correctly update the left pane room list's highlighted item - Fix `KeyError` when forgetting a room - Fix cursor shape not changing to caret when hovering text fields and areas. This fix can only apply when the `enableKineticScrolling` setting is `true`, until the project switches to Qt 5.15. ## 0.5.0 (2020-05-22) ### Added - **Unread messages and mentions**: - Rooms in the left pane will now have a counter for unread messages and times you were mentioned - `goToPreviousUnreadRoom` (default Alt+Shift+U) and `goToNextUnreadRoom` (default Alt+U) keybinds to cycle between rooms with unread messages - `goToPreviousMentionedRoom` (default Alt+Shift+M) and `goToNextMentionedRoom` (default Alt+M) keybinds to cycle between rooms with mentions, or those with unread messages if no rooms with mentions are left - Room with mentions will be sorted first, then room no mentions but unread messages, then the rest - **Accounts navigation**: - With two or more accounts, an always visible account thumbnail grid will be visible in the left pane. Clicking on an account will make the room list jump to that account. The accounts will also show a total number of unread messages and mentions for all the rooms associated with it. - `goToPreviousAccount` (default Alt+Shift+M) and `goToNextAccount` (default Alt+M) keybinds to cycle and jump between accounts in the room list. - `keys.focusAccountAtIndex` in config file, a `{"": ""}` mapping similar to `focusRoomAtIndex` which by default binds Ctrl+1-9 and Ctrl+0 to jump to account 1 to 10 in the room list - **Replies**: - The context menu for messages now has a "Reply" option - The new `replyToFocusedOrLastMessage` keybind (default Ctrl+Q) can be used to reply to the focused message if any (use the `focusPreviousMessage` and `focusNextMessage` keybinds), or to the last message in the timeline not sent by you. - Pressing escape will cancel the reply - **Kick and bans**: room members can now be kicked or banned with an optional reason, using the option in the right pane's member context menu - `openMessagesLinks` keybind (default Ctrl+O). Will open externally all the URLs present in the selected/focused message(s), or the last message that contains links if none are selected or focused. - `clearMemberFilterOnEscape` setting. If `true` (default), pressing escape while focusing the "Filter members" field will not only focus the chat again but also clear the filter. - `maxMessageCharactersPerLine` setting to control the maximum width of messages. If set to `-1`, there will be no limit. - `ownMessagesOnLeftAboveWidth` setting, replaces the themes's `eventList.ownEventsOnRightUnderWidth` properties. Can be set to `-1` to always keep your own messages on the right. - `enableKineticScrolling` setting, try setting it to `false` if you have scrolling issues on a trackpad - Support a new `enabled` key for accounts in the accounts.json config file. If set to `false`, Mirage will not login to or show the account on startup. - Support a new `order` key for accounts in the accounts.json config file The value is an integer that will determine how accounts in the left pane are sorted, lower comes first. If multiple accounts have the same `order` value, they are sorted by their user ID. - Themes: - `mainPane.minimumSize` property - `mainPane.accountBar` section - `mainPane.listView.room.unreadIndicator` section - `chat.replyBar` section ### Changed - **Performance**: - Use room members lazy-loading, accounts that have joined large numbers of rooms will now finally be able to finish their initial sync. When the currently shown UI page is a room, the full members list for it will be loaded. - Request less events for the initial sync, and exclude some types like membership events to increase initial sync speed - Retrieving profiles for events sent by users no longer present in a room will no block and delay past events loading. Missing profiles will be fetched asynchronously when the messages are currently in view in the UI. - Reduce the number of events that need to be sent between Python and QML due to changes in list models data - Consecutive syncs will now have a one second delay between them to reduce both client and server strain - Improved group display name calculations (nio 0.11+ change): for example, a room that would previously be shown as "Alice and 6 others" will now be shown as "Alice, Bob, Carol, Dave, Erin and 1 other" (up to 5 visible names). - Group rooms with more than two users and without an explicitely set avatar will no longer show their first member's profile picture as avatar - The `unfocusOrDeselectAllMessages` keybind now defaults to Ctrl+D instead of Escape, which no longer works as of Qt 5.14. `debugFocusedMessage` is changed from Ctrl+D to Ctrl+Shift+D. - Better QML logging format: messages will now be dated, and have a symbol + color (on Linux and OSX terminals) representing their category - Messages containing code blocks will no longer have their max width limited - Set `hideUnknownEvents` to `true` in the default config file - Set a more useful default minimum size for the left pane - The `collapseSidePanesUnderWindowWidth` setting now defaults to `450` instead of `400`, to account for the larger minimum pane size. - Show a more useful error message with traceback when retrieving an account's profile or the server config fails on startup - Hide `socket.gaierror` error popups - When pressing the `startPythonDebugger` (default Alt+Shift+D) keybind, use `pdb` if `remote_pdb` isn't installed - Themes: - `mainPane.bottomBar` properties: `background` is now by default `transparent`, `settingsButtonBackground` and `filterFieldBackground` are now set to `colors.strongBackground` ### Removed - **Performance**: - After the initial sync, Mirage will no longer try to continually fetch previous events for rooms where the sync haven't brought any event that is suitable to be shown as room last event subtitle in the left pane. - Mirage will no longer try to find and autolink display names in incoming events, which was a very costly operation for rooms with thousands of members. - The uvloop python module is no longer supported or recommended as an optional dependency, due to being responsible for some segfaults - The SortFilterProxyModel and RadialBarDemo git submodules are no longer used. hsluv-c is the only submodule still used currently. ### Fixed - **Performance**: - Stop rendering and keeping in RAM rooms that aren't currently visible in the left pane. This fixes the massive memory usage that occurred with hundreds of rooms and their avatar images loaded all at once. - Room elements in the left pane will no longer be reloaded every time a list movement happens (e.g. a room is bumped to the top due to a new message). This also lets the movement animation correctly play instead of being skipped. - Don't show a popup when pressing the redact message keybind if that message can't be redacted - Stricter mention parsing, fix various cases of text being autolinked when it shouldn't - Fix exception when parsing `` HTML tags without `href` attribute - Fix crash on Python 3.6 due to `asyncio.current_task` - Fix `AttributeError` when using matrix-nio v0.11+ - Fix potential crash on startup due to asyncio event loop and threading - Fix uploads getting rejected by servers due to not passing a file size - Fix extra spacing between "Add chat" and "Expand/Collapse" account buttons - Hide the Binding deprecation warnings in terminal that Qt 5.14+ spams - Fix client not waiting before retrying a failed sync due to server error - Correctly handle server 429 "Too many requests" errors when they come purely in the form of a HTTP status code without a JSON object giving any info - Fix left rooms remaining at full opacity in the left pane - Fix escape key not working to clear the "Filter rooms" field and focus the chat again - Fix event mention link detection, and stop trying to autolink event ID strings in messages as matrix.to URLs also need a room ID to make sense ## 0.4.3 (2020-04-03) ### Added - Support for `MIRAGE_CONFIG_DIR` and `MIRAGE_DATA_DIR` environment variables to change the config and user data folders - `inviteToRoom`, `leaveRoom` and `forgetRoom` keybindings (Alt+I, Alt+Esc and Alt+Shift+Esc by default) - **Redactions support**: individual or selected messages can now be redacted/removed using the option from the message context menu, or the `removeFocusedOrSelectedMessages` keybind (by default Ctrl+R or Alt+Del). - Themes: `colors.dimColoredTextSaturation` and `colors.dimColoredTextIntensity` color properties - Themes: `controls.displayName.dimSaturation` and `controls.displayName.dimLightness` color properties - Themes: `chat.message.redactedBody` color property ### Changed - `unfocusOrDeselectAllMessages` keybind: now deselect messages first if any on first press, *then* cancels the keyboard message focus if possible on second press ### Fixed - Segfault after login on KDE - Buttons not displaying correctly on Qt 5.14 - Hard tab characters in theme files not being handled by the theme parser - `focusRoomAtIndex` keybindings: default to Cmd+numbers on OSX instead of Alt/Option+numbers, which prevented typing special characters on some keyboard layouts - Needing to press escape twice to close context menus and popups - "Go back to chat" button not doing anything when the room settings pane was focused in narrow mode ## 0.4.2 (2020-03-27) ### Added - Accounts, rooms, room members and messages can now be long-tapped on touch screens to open their context menu - New touch screen and keyboard-friendly message selection system, replaces the previous slow and buggy text selection implementation: - Tap a message to select or deselect it - Press escape, or use the context menu entry "Deselect all" to deselect all messages - Tap a first message, then shift+tap another one (or use "Select until here" from the context menu) to select all messages from the first to last - With a mouse, a single message can be partially selected and copied - The keyboard can be used to navigate with Ctrl+Up/Down (or Ctrl+J/K), Ctrl+Space to (de)select, Ctrl+Shift+Space for first-to-last selection, Ctrl+C for copying the selection, and Escape to focus the composer again (twice to also deselect messages). These shortcuts can be changed in the config file. - Themes: `chat.message.focusedHighlight`, `chat.message.focusedHighlightOpacity`, `chat.message.checkedBackground` and `chat.message.thumbnailCheckedOverlayOpacity` - Scripts and instructions to build a Flatpak package The new selection system is still work in progress, dragging to select multiple messages at once on desktop is not implemented yet. ### Changed - Themes: increased default `colors.accentBackground` brightness ### Fixed - Possible error when handling a room member event that is missing previous display name or avatar info - Correctly parse `mailto:` links where the mail address ends with a digit (e.g. `mailto:foo@localhost:8050`, or where the host is a single character - Respect case when turning display names into mentions, typing a display name containing uppercase letters all in lowercase would result in a broken link - Correctly handle `0` as a value for the `alertOnMessageForMsec` setting, this will now prevent urgency hints (window/desktop highlighting or flashing on new message for most desktops, "ready" notification on Gnome) ## 0.4.1 (2020-03-23) ### Added - `hideMembershipEvents` setting, controls whether events such as "x joined the room" are shown in the timeline. - `hideProfileChangeEvents` setting, controls whether display name and avatar change events are shown in the timeline. - `hideUnknownEvents` setting, controls whether events not yet supported by Mirage (e.g. `m.reaction`) are shown in the timeline. - Compact mode to make accounts, rooms, messages and room members take only one line as well as reducing vertical spacing between them. Set by the new `compactMode` setting in config file, can also be toggled with the `keys.toggleCompactMode` keybind which defaults to Alt+Ctrl+C. - `keys.focusRoomAtIndex` in config file, a `{"": ""}` mapping which by default binds Alt+1-9 and Alt-0 to focus room 1 to 10 in the current account. - User ID, display names, room ID, room aliases and message ID are now automatically turned into [matrix.to](https://matrix.to) links and will be rendered as mentions by clients. In Mirage, user ID/names will be colored with the same color seen when they send messages. - Track the number of times your user was mentioned in rooms. The visual counter is not yet displayed, since there currently is no way to mark messages as read and make the counter go down. - Themes: `controls.avatar.compactSize` property - Themes: mention classes styling to `chat.message.styleSheet` ### Fixed - Python exceptions occurring in the asyncio loop not being printed in the terminal - Extra newline shown after code blocks in messages - Constant CPU usage due to button loading animations still being rendered while unneeded and invisible ## 0.4.0 (2020-03-21) Initial public release. mirage-0.7.2/docs/CONFIG.md000066400000000000000000000077121407747233600152270ustar00rootroot00000000000000# Configuration [Folders](#folders) ⬥ [settings.py](#settingspy) ⬥ [accounts.json](#accountsjson) ## Folders On Linux: - `$XDG_CONFIG_HOME/mirage` if the `XDG_CONFIG_HOME` environment variable is set, else `~/.config/mirage` for config files - `$XDG_DATA_HOME/mirage` if that variable is set, else `~/.local/share/mirage` for user data - `$XDG_CACHE_HOME/mirage` if that variable is set, else `~/.cache/mirage` for cache data For Flatpak installations: - `~/.var/app/io.github.mirukana.mirage/config/mirage` for config files - `~/.var/app/io.github.mirukana.mirage/data/mirage` for user data - `~/.var/app/io.github.mirukana.mirage/cache/mirage` for cache data These locations can be overriden with environment variables, see `mirage --help`. The user data folder contains saved encryption data, interface states and [themes](THEMING.md), while the cache contains downloaded files and thumbnails. ## settings.py A file written in the [PCN format](PCN.md), located in the [config folder](#folders), which is manually created by the user to configure the application's behavior. The default `settings.py`, used when no user-written file exists, documents all the possible options and can be found at: - [`src/config/settings.py`][1] in this repository - `/usr/local/share/examples/mirage/settings.py` or `/usr/share/examples/mirage/settings.py` on Linux installations - `~/.local/share/flatpak/app/io.github.mirukana.mirage/current/active/files/share/examples/mirage/settings.py` for per-user Flatpak installations - `/var/lib/flatpak/app/io.github.mirukana.mirage/current/active/files/share/examples/mirage/settings.py` for system-wide Flatpak installations Rather than copying the entire default file, it is recommended to [`include`](PCN.md#including-built-in-files) it and only add the settings you want to override. For example, a user settings file that disables kinetic scrolling, sets a different theme, and changes some keybinds could look like this: ```python3 self.include_builtin("config/settings.py") class General: theme: str = "Glass.qpl" class Scrolling: kinetic: bool = False # Has issues on trackpad class Keys: # The default ctrl+= doesn't work on some keyboard layouts reset_zoom = ["Ctrl+Backspace", "Ctrl+="] class Messages: open_links_files = ["Ctrl+Shift+O"] open_links_files_externally = ["Ctrl+O"] ``` When this file is saved while the application is running, the settings will automatically be reloaded, except for some options which require a restart. The default `settings.py` indicates which options require a restart. You can also manually trigger a reload by updating the file's last change timestamp, e.g. with the `touch` command: ```sh touch ~/.config/mirage/settings.py ``` [1]: https://github.com/mirukana/mirage/tree/master/src/config/settings.py ## accounts.json This JSON file, located in the [config folder](#folders), is managed by the interface and doesn't need to be manually edited, except for changing account positions via their `order` key. The `order` key can be any number. If multiple accounts have the same `order`, they are sorted lexically by user ID. This file should never be shared, as anyone obtaining your access tokens will be able to use your accounts. Within the application, from the Sessions tab of your account's settings, access tokens can be revoked by signing out sessions, provided you have the account's password. Example file: ```json { "@user_id:example.org": { "device_id": "ABCDEFGHIJ", "enabled": true, "homeserver": "https://example.org", "order": 0, "presence": "online", "status_msg": "", "token": "" }, "@account_2:example.org": { "device_id": "KLMNOPQRST", "enabled": true, "homeserver": "https://example.org", "order": 1, "presence": "invisible", "status_msg": "", "token": "" } } ``` mirage-0.7.2/docs/CONTRIBUTING.md000066400000000000000000000216501407747233600161660ustar00rootroot00000000000000# Contributing - [Issues](#issues) - [Pull Requests](#pull-requests) - [Procedure](#procedure) - [Commit Guidelines](#commit-guidelines) - [Coding Conventions](#coding-conventions) - [General](#general) - [Python](#python) - [QML](#qml) - [C++](#c) - [Resources](#resources) - [Packaging](#packaging) ## Issues [Issues](https://github.com/mirukana/mirage/issues) on GitHub should be used to ask questions, report problems, request new features, or discuss potential changes before creating pull requests. Before opening new issues, please search for any already open or closed issue related to your problem, in order to prevent duplicates. You can also join us on the [#mirage-client:matrix.org](https://matrix.to/#/%23mirage-client:matrix.org) room for questions and discussions. ## Pull Requests For changes outside of simple bug/typo/formatting fixes, it is strongly recommended to first discuss your ideas in a related issue or in [#mirage-client:matrix.org](https://matrix.to/#/%23mirage-client:matrix.org). New changes are merged to the [`dev` branch](https://github.com/mirukana/mirage/tree/dev) first. Once a new version of the application is released, the current `dev` is merged into the main `master` branch. By sending your changes, you agree to license them under the LGPL 3.0 or later. ### Procedure Start by forking the main repository from GitHub, then clone your fork and switch to a new branch based on `dev`, in which you will make your changes: ```sh git clone --recursive https://github.com/yourUsername/mirage cd mirage git remote add upstream https://github.com/mirukana/mirage git fetch upstream git checkout upstream/dev git branch example-branch-name git checkout example-branch-name ``` Test and commit your changes according to the [commit guidelines](#commit-guidelines), and `git push` to your fork's repo. You will then be able to make a pull request by going to the [main repo](https://github.com/mirukana/mirage). Once your pull request is merged, you can update `dev`, and delete your GitHub and local branch: ```sh git fetch upstream git checkout upstream/dev git push -d origin example-branch-name git branch -d example-branch-name ``` Make sure `dev` is up-to-date before creating new branches based on it. ## Commit Guidelines Commit messages should be made in this form: ``` Title, a short summary of the changes The body, a more detailed summary needed depending on the changes. Explain the goal of the code, how to reproduce the bug it solves (if this is a bug fix), any special reasoning behind the implementation or side-effects. ``` - Write informative titles, e.g. `TextField: fix copying selected text by Ctrl+C` instead of `fix field bug` (assuming `TextField` was the name of the component affected by the bug) - Write the title in imperative form and without a period at the end, e.g. `Fix thing` instead of `Fixed thing` or `Fixes thing.` - The title must not exceed 50 characters - A blank line is required between the first line summary and detailed body, if there is one - Lines of the body must not exceed 72 characters - Split independent changes into separate commits, don't combine fixes for different problems or add multiple systems forming a complex feature all at once - Every commit should be able to build and run the application without obvious crashes or tracebacks - Check for linter errors before committing by running `make test` in the repository's root. The test tools can be installed with `pip3 install --user -Ur requirements-dev.txt`. - For changes that aren't yet merged in a branch of the main repo, prefer amending or editing previous commits via git interactive rebase, rather than adding new "fix this" commits. This helps keeping the history clean. ## Coding Conventions ### General - Use four space indentations, no tabs - Use double quotes for strings, unless single quotes would avoid having to escape double quotes in the text - Keep lines under 80 columns, the only exception for this is long URL links that can't be broken in multiple parts - Keep lines free from any trailing whitespace - Function definitions, calls, list/dict/etc not fitting in one line follow this format (notice the trailing comma on the last element): ```python3 long_function_call( long_argument_1, long_argument_1, long_argument_3, long_argument_4, ) very_long_list_def = [ "Lorem ipsum dolor sit amet, consectetuer adipiscing elit", "Aenean massa. Cum sociis natoque penatibus", "Mus donec quam felis, ultricies nec, pellentesque", ] ``` - When creating new files, don't forget the copyright and license header you see in other files of the same language. ### Python - All functions, class attributes or top-level variables should have type hints - Separate all top-level classes and functions by two blank lines. For classes with many long methods, separate those methodes by two blank lines too. - Readability is important. Vertically-align consecutive lines of assignments, function definition arguments, dictionaries and inline comments: ```python3 # Bad: num: int = 1 # A comment args: List[str] = ["a", "b"] # Another comment def func( self, example_argument: int = 300, # Comment other: str = "Sample text", # Other comment some_floats: Tuple[float, float, float] = (4.2, 1.1, 9.8), ) -> None: pass # Good: num: int = 1 # A comment args: List[str] = ["a", "b"] # Another comment def func( self, example_arg: int = 300, # Comment other: str = "Sample text", # Other comment some_floats: Tuple[float, float] = (4.2, 9.8), ) -> None: pass ``` If this is annoying, consider getting a plugin for your editor to automate it (e.g. [EasyAlign](https://github.com/junegunn/vim-easy-align) for vim). - Use f-strings as long as the readability isn't impacted. For more complex string formatting, use the shorter `%` syntax when features special to `str.format()` aren't needed. - Otherwise, follow the [PEP-8 standard](https://www.python.org/dev/peps/pep-0008/) ### QML - Don't add trailing semicolons to lines - If an object has more than one property, always keep each property on their own line: ```qml Rectangle { x: 10; width: 100; height: width; color: "black" } // Bad! Rectangle { // Good x: 10 width: 100 height: width color: "black" } ``` - When creating new files, the `id` for the root component should always be `root` - When writing new code, refer to parent object properties explicitely, e.g. `parent.prop_name` or `someId.prop_name` instead of just `` - Don't use [States](https://doc.qt.io/qt-5/qml-qtquick-state.html), the Rectangle in the description's example could simply be written like this: ```qml Rectangle { width: 100 height: 100 color: mouseArea.containsPress ? "red" : "black" MouseArea { id: mouseArea anchors.fill: parent } } ``` - Otherwise, follow the [QML Coding Conventions](https://doc.qt.io/qt-5/qml-codingconventions.html) ### C++ - Add C++ only if it can't easily be done in QML or Python; or if doing it in Python requires adding a dependency while a similar feature is already provided by Qt, feature that just needs to be exposed with some wrapper code ([example](https://github.com/mirukana/mirage/blob/v0.6.4/src/utils.h#L31)). - Be explicit, always use `this->` to refer to methods and class attributes - Don't split modules between `.h` and `.cpp` files, this creates unnecessary code repetition and has no benefits when most methods will contain very few lines and the module is only included once before starting the GUI. ## Resources Resources include background images, icons or sounds. New resources must have a permissive license that does not require attribution. Built-in icons must be in the SVG format. The majority of icons used in the application come from [iconmonstr](https://iconmonstr.com). When possible without any noticable quality loss, reduce the size of resources and strip any metadata by using tools such as: - `svgcleaner --allow-bigger-file --indent 4 ` for SVG images - `pngquant --force --speed=1 --strip ` for PNG images - `jpegoptim --quality 80 --strip-all ` for JPEG images ## Packaging If a new package for a distribution or any other easy way of installing the application exists, [pull request](#pull-requests) for adding instructions to the [INSTALL.md](INSTALL.md) are welcome. Some suggestions when creating packages: - As the `mirage` name is sometimes already taken by other software, prefer naming your package `mirage-im` - Among the dependencies from the `submodules` directory, `hsluv-c` is the only one that is still needed for building. The other folders are kept to allow building past versions of the application, and should be ignored. mirage-0.7.2/docs/INSTALL.md000066400000000000000000000274441407747233600153740ustar00rootroot00000000000000# Installation Instructions and releases are currently only available for Linux, but compiling on Windows and macOS should be possible with the right tools. - [Packages](#packages) - [Linux](#linux) - [AppImage](#appimage) - [Flatpak](#flatpak) - [Alpine Linux / postmarketOS](#alpine-linux--postmarketOS) - [Arch Linux](#arch-linux) - [Debian](#debian) - [Gentoo](#gentoo) - [Nix](#nix) - [OpenMandriva Lx](#openmandriva-lx) - [Manual installation](#manual-installation) - [Environment variables](#environment-variables) - [Package manager dependencies](#package-manager-dependencies) - [Alpine Linux 3.9+ / apk](#alpine-linux-39--apk) - [Arch Linux / pacman & AUR](#arch-linux--pacman--aur) - [Fedora 30+ / dnf](#fedora-30--dnf) - [Gentoo / emerge](#gentoo--emerge) - [Ubuntu 19.04 / apt](#ubuntu-1904--apt) - [Ubuntu 19.10+, Debian bullseye / apt](#ubuntu-1910-debian-bullseye--apt) - [Void Linux / xbps](#void-linux--xbps) - [Installing PyOtherSide manually](#installing-pyotherside-manually) - [Installing libolm manually](#installing-libolm-manually) - [Installing or updating Mirage](#installing-or-updating-mirage) - [Common issues](#common-issues) - [cffi version mismatch](#cffi-version-mismatch) - [Type XYZ unavailable](#type-xyz-unavailable) ## Packages ### Linux For developement, or if none of the package options are satisfying, see [manual installation](#manual-installation). Packages other than the AppImage and Flatpak are not maintained by the Mirage authors, and thus might be outdated. #### AppImage For **x86 64bit glibc-based systems**, Mirage is available as an AppImage on the [release page](https://github.com/mirukana/mirage/releases). AppImages are single executable files that contain the app and all its dependencies. Mirage images are built in Ubuntu 16.04, and should therefore run on any distro released in April 2016 or later. [How to start AppImages](https://docs.appimage.org/introduction/quickstart.html#how-to-run-an-appimage) (TL;DR: `chmod +x Mirage-*.AppImage && ./Mirage-*.AppImage`) #### Flatpak Mirage is also available as a Flatpak. 1. Download the Mirage Flatpak from the [release page](https://github.com/mirukana/mirage/releases). 2. If your operating system doesn't already have built-in support for Flatpaks, follow [these instructions](https://flatpak.org/setup/) to install Flatpak support on your system. 3. To actually install and run Mirage, it should be enough to double-click the downloaded `.flatpak` file, which will open your software manager. Alternatively, you can issue the following commands in a terminal: ```sh flatpak remote-add --user --if-not-exists \ flathub https://flathub.org/repo/flathub.flatpakrepo flatpak install --user flathub org.kde.Platform//5.14 flatpak install --user /path/to/downloaded/mirage-*.flatpak flatpak run io.github.mirukana.mirage ``` If downloading the dependencies fail due to e.g. a connection error, run `flatpak repair` before retrying. If your architecture is not listed on the release page, clone the repository and see [packaging/flatpak/README.md](packaging/flatpak/README.md) to build the package on your machine. #### Alpine Linux / postmarketOS If you are on the Edge channel of Alpine Linux or postmarketOS, Mirage can be installed right from the testing repositry: ```sh apk add mirage ``` If you are unsure about what Edge is and want to read more about it, you can do so on the [Alpine Wiki](https://wiki.alpinelinux.org/wiki/Edge). #### Arch Linux AUR packages for the [latest stable release](https://aur.archlinux.org/packages/matrix-mirage/) and [git `dev` branch](https://aur.archlinux.org/packages/matrix-mirage-git/) are available. Installing the release version with a AUR helper, e.g. [yay](https://github.com/Jguer/yay): ```sh yay -S matrix-mirage ``` #### Debian Requires [Debian Testing](https://wiki.debian.org/DebianTesting). To install the package: ```sh apt update apt install matrix-mirage ``` #### Gentoo Available in the [src_prepare overlay](https://gitlab.com/src_prepare/src_prepare-overlay) - [releases](https://gitlab.com/src_prepare/src_prepare-overlay/-/tree/master/net-im/mirage) - [git dev](https://gitlab.com/src_prepare/src_prepare-overlay/-/blob/master/net-im/mirage/mirage-9999.ebuild) Installing Mirage: 1. [Add the overlay](https://gitlab.com/src_prepare/src_prepare-overlay#adding-the-overlay) 2. [Unmask](https://wiki.gentoo.org/wiki/Knowledge_Base:Unmasking_a_package) `net-im/mirage` 3. Run `emerge net-im/mirage` #### Nix Requires the unstable channel, to add it: ```sh nix-channel --add https://nixos.org/channels/nixpkgs-unstable nix-channel --update ``` To install the package: ```sh nix-env -iA nixpkgs.mirage-im ``` #### OpenMandriva Lx Requires [Unstable or Rolling][1]. To install the package: ```sh sudo dnf install matrix-mirage ``` [1]: https://openmandriva.net/wiki/en/index.php?title=OpenMandriva_Release_Plan_and_Repositories#Release_Plan ## Manual Installation **Qt 5.12+**, **Python 3.6+** (with **pip** to install packages from the [requirements.txt](requirements.txt)), **PyOtherSide 1.5+** and **libolm 3+** are required. The equivalent `-dev` or `-devel` packages are needed, if your distro splits development headers into their own packages. To enable X11-specific features on Linux, **libX11** and **libXScrnSaver** / **libXss** are needed. The requirements can be disabled by adding `CONFIG+=no-x11` to the `qmake mirage.pro` command. For the Pillow Python package, these dependencies are recommended to support all common image formats: - **libjpeg-turbo** - **zlib** - **libtiff** - **libwebp** - **openjpeg2** **libmediainfo** is also required for the pymediainfo package. ### Environment Variables To ensure Qt **5** will be used by default, compile using all CPU cores and optimize the build for your machine: ```sh export QT_SELECT=5 export MAKEFLAGS="-j$(nproc)" export CFLAGS="-march=native -O2 -pipe" ``` ### Package Manager Dependencies #### Alpine Linux 3.9+ / apk [PyOtherSide](#installing-pyotherside-manually) and [libolm](#installing-libolm-manually) must be manually installed. ```sh sudo apk add qt5-qtquickcontrols2-dev qt5-qtsvg-dev qt5-qtimageformats \ libx11-dev libxscrnsaver-dev alsa-lib-dev \ python3-dev py3-setuptools \ build-base git cmake \ libjpeg-turbo-dev zlib-dev tiff-dev libwebp-dev openjpeg-dev \ libmediainfo-dev export PATH="/usr/lib/qt5/bin:$PATH" ``` #### Arch Linux / pacman & AUR **libolm** is from the AUR, this example uses [yay](https://github.com/Jguer/yay) to install it like other packages. Alternatively, you can just use `pacman` and [install libolm manually](#installing-libolm-manually). ```sh yay -Syu qt5-base qt5-declarative qt5-quickcontrols2 qt5-svg \ qt5-graphicaleffects qt5-imageformats \ libx11 libxss alsa-lib \ python python-pip \ python-pyotherside \ libolm \ base-devel git cmake \ libjpeg-turbo zlib libtiff libwebp openjpeg2 libmediainfo ``` #### Fedora 30+ / dnf ```sh sudo dnf groupinstall 'Development Tools' sudo dnf install qt5-devel qt5-qtbase-devel qt5-qtdeclarative-devel \ qt5-qtquickcontrols2-devel qt5-qtsvg-devel \ qt5-qtgraphicaleffects qt5-qtimageformats \ python3-devel python3-pip pyotherside \ libX11-devel libXScrnSaver-devel alsa-lib-devel \ git cmake \ libolm-devel \ libjpeg-turbo-devel zlib-devel libtiff-devel libwebp-devel \ openjpeg2-devel libmediainfo-devel sudo ln -s /usr/bin/qmake-qt5 /usr/bin/qmake ``` #### Gentoo / emerge [libolm](#installing-libolm-manually) must be manually installed. You might need to prepend the `emerge` command with `USE=bindist`, if `emerge` says so. ```sh sudo emerge -av qtcore qtdeclarative qtquickcontrols2 \ qtsvg qtgraphicaleffects qtimageformats \ libX11 libXScrnSaver alsa-lib \ dev-python/pip pyotherside \ dev-vcs/git cmake \ libjpeg-turbo zlib tiff libwebp openjpeg libmediainfo ``` #### Ubuntu 19.04 / apt [libolm](#installing-libolm-manually) must be manually installed. ```sh sudo apt update sudo apt install qt5-default qt5-qmake qt5-image-formats-plugins \ qml-module-qtquick2 qml-module-qtquick-window2 \ qml-module-qtquick-layouts qml-module-qtquick-dialogs \ qml-module-qt-labs-platform \ qml-module-qtquick-shapes \ qtdeclarative5-dev \ qtquickcontrols2-5-dev \ libx11-dev libxss-dev libasound2-dev \ python3-dev python3-pip \ qml-module-io-thp-pyotherside \ build-essential git cmake \ libjpeg-turbo8-dev zlib1g-dev libtiff5-dev libwebp-dev \ libopenjp2-7-dev libmediainfo-dev ``` #### Ubuntu 19.10+, Debian bullseye / apt Follow the steps for [Ubuntu 19.04](#ubuntu-1904--apt), but instead of installing libolm manually: ```sh sudo apt install libolm-dev ``` #### Void Linux / xbps [PyOtherSide](#installing-pyotherside-manually) must be manually installed. ```sh sudo xbps-install -Su qt5-devel qt5-declarative-devel \ qt5-quickcontrols2-devel \ qt5-svg-devel qt5-graphicaleffects qt5-imageformats \ libx11-devel libXScrnSaver-devel alsa-lib-devel \ python3-devel python3-pip \ olm-devel \ base-devel git cmake \ libjpeg-turbo-devel zlib-devel tiff-devel libwebp-devel \ libopenjpeg2-devel libmediainfo-devel ``` ### Installing PyOtherSide Manually Skip this section if you already installed it from your distro's package manager. ```sh git clone https://github.com/thp/pyotherside cd pyotherside make clean qmake make sudo make install ``` ### Installing libolm Manually Skip this section if you already installed it from your distro's package manager. ```sh git clone https://gitlab.matrix.org/matrix-org/olm/ cd olm cmake . -Bbuild cmake --build build sudo make install ``` ### Installing or updating Mirage After following the above sections instructions depending on your system; clone the repository, initalize the submodules, install the python dependencies, compile and install: ```sh git clone https://github.com/mirukana/mirage cd mirage git pull git submodule update --init submodules/* pip3 install --user -Ur requirements.txt qmake mirage.pro make sudo make install ``` To compile without the X11-specific dependencies and features on Linux, run `qmake mirage.pro CONFIG+=no-x11` instead of `qmake mirage.pro`. If everything went fine, run `mirage` to start. ### Common Issues #### cffi version mismatch When installing the python dependencies, if you get a version mismatch error related to `cffi`, try: ```sh pip3 install --user --upgrade --force-reinstall cffi ``` #### Type XYZ unavailable If the application exits without showing any window and you get a terminal message like this: file:///.../src/gui/Window.qml:83:5: Type PythonRootBridge unavailable then a QML component/type failed to import due to either a missing dependency or a programming error. If the type has `Python` in its name, ensure PyOtherSide is correctly installed. You should see a similar message: Got library name: "/usr/lib/qt5/qml/io/thp/pyotherside/libpyothersideplugin.so" To ensure the correct permissions are set for the PyOtherSide plugin files: ```sh sudo chmod -R 755 /usr/lib/qt5/qml/io sudo chmod 644 /usr/lib/qt5/qml/io/thp/pyotherside/* sudo chmod 755 /usr/lib/qt5/qml/io/thp/pyotherside/*.so ``` Note that the Qt lib path might be `/usr/lib/qt/` instead of `/usr/lib/qt5/`, depending on the distro. mirage-0.7.2/docs/PCN.md000066400000000000000000000246701407747233600147040ustar00rootroot00000000000000# PCN File Format This document explains in details the PCN (Python Config Notation) format. PCN files are organized in a hierarchy of sections and properties. PCN files can also contain normal Python code, such as imports and custom functions. - [Overview](#overview) - [Sections](#sections) - [Including Built-in Files](#including-built-in-files) - [Including User Files](#including-user-files) - [Inheritance](#inheritance) - [Properties](#properties) - [Common Types](#common-types) - [Expressions](#expressions) - [Section Access](#section-access) - [Bracket Access](#bracket-access) - [GUI files](#gui-files) ## Overview ```python3 # Lines starting with a "#" are considered comments. # Comments can also be added to the end of normal lines. # Sections can contain indented properties, other sections or functions. class Example: # Properties are written as "name: type = value", examples: integer_number: int = 5 decimal_number: float = 2.5 character_string: str = "Sample text" boolean: bool = True # or False string_list: List[str] = ["foo", "bar", "baz"] # Property values can be any Python expression, e.g. math operations: other_number: int = (5 * 4) / 2 # "self" points to the current section, Example, containing other_number. above_10: bool = self.other_number > 10 # result: False class Names: # Property names with characters outside of a-z A-Z 0-9 _ need quoting: "@alice:example.org": str = "Alice" "@bob:example.org": str = "Bob" # Section content can also be accessed with the "self[name]" syntax, # which works with quoted properties like the ones above: alice_name: str = self["@alice:example.org"] # result: Alice # Child sections are also accessible from "self": child_integer: int = self.Test.integer # result: 5 class Test: # "parent" refers to the section parent of this one, here "Names". alice_name: str = parent["@alice:example.org"] # result: "Alice" integer: int = parent.parent.integer_number # Example.integer_number, which is 5 # Top-level sections can also be accessed directly by names: alice_name_2: str = Example.Names["@alice:example.org"] integer_2: int = Example.integer_number ``` ## Sections Sections are defined like Python classes, and can contain properties, other sections, or Python functions. A section's name should be written as `CamelCase`, and can only contain letters, digits and underscores. The content of a section must be indented by that section's indentation plus four spaces: ```python3 class FirstSection: content_spaces: int = 0 + 4 class SectionInsideFirst: content_spaces: int = 4 + 4 class SecondSection: content_spaces: int = 0 + 4 ``` Empty sections can be created using the `pass` keyword: ```python3 class Empty: pass ``` ### Including Built-in Files A section, including the file's root (which is treated as a section) can include files that are supplied by the application using the `self.include_builtin(path)` function. `path` is the relative path to a file in the application's source folder, for example `self.include_builtin("config/settings.py")` refers to [`src/config/settings.py`][1]. The sections and properties from the included file will be recursively merged, see [Including User Files](#including-user-files) for an example. [1]: https://github.com/mirukana/mirage/tree/master/src/config/settings.py ### Including User Files Similar to [including built-in files](#including-built-in-files), user-written local files can be included with `self.include_file(path)`, where `path` is an absolute or relative (from the current file's directory) file path. Example with two files, `a.py`: ```python3 self.include_file("b.py") class Shared: text: str = "Sample" gets_overriden: str = "A" class FromA: number: int = 1 ``` and `b.py`: ```python3 class Shared: gets_overriden: str = "B" class FromB: number: int = 2 ``` This results in a merged PCN looking like so: ```python3 class Shared: text: str = "Sample" gets_overriden: str = "B" class FromA: number: int = 1 class FromB: number: int = 2 ``` Include functions can also be used inside a section other than the root. If `a.py` had the include line inside `Shared`, the result would be: ```python3 class Shared: text: str = "Sample" gets_overriden: str = "A" class FromA: number: int = 1 class Shared: gets_overriden: str = "B" class FromB: number: int = 2 ``` ### Inheritance Like other Python classes, sections can inherit from other sections. Unlike including files, sections are not merged recursively. This file: ```python3 class Mixin: first: bool = True second: bool = False class First(Mixin): pass class Second(Mixin): third: int = 100 ``` Would be equivalent to: ```python3 class First(Mixin): first: bool = True second: bool = False class Second(Mixin): first: bool = True second: bool = False third: int = 100 ``` ## Properties Properties have a name, optional type annotation and value. Standard property names should be written in `snake_case`. In most cases, it is recommended to include type annotations, to make clear what a property's value should be: ```python3 with_type: int = 3 complex_type: Dict[str, int] = {"abc": 1, "def": 2, "ghi": 3} any_type: Any = None same_as_above = None ``` If the property's name starts with a digit or contains characters other than letters, digits or underscores, that name must be quoted: ```python3 "!alice:example.org" = "Alice" ``` Properties with these names can only be accessed by the [brackets syntax](#bracket-access). ### Common types - `int`: An integer number, e.g. `4`. - `float`: Floating point number, e.g. `4.5`. Can also be an integer. - `str`: String, a piece of text. If the text contains quotes or backslashes, escape them with a backslash. Other properties can be included by combining strings or using an f-string: ```python3 escaped: str = "C:\\Users\\Alice \"Foo\" Bar" number: int = 1 combined: str = "foo " + self.number fstring: str = f"foo {number}" ``` - `bool`: Boolean, a value that can be either `True` or `False`. - `None`: A `None` value, represents an absence of choice. - `Any`: A value that can be of any type. - `list`: List of values, e.g. `[1, 2, 3]`. The type can be written as `list` or `List[type]` to specify what type the list's item should be. - `tuple`: Similar to lists, but the length cannot be changed once created. Can be written as `tuple`, `Tuple[type, type]` to specify for example that the tuple must have two items of certain types, or `Tuple[type, ...]` for a tuple with any number of items of a certain same type: ```python3 anything: tuple = (1, 2, 3, "foo") many_ints: Tuple[int, ...] = (1, 2, 3, 4, 5) three_values: Tuple[int, str, bool] = (1, "example", False) ``` - `dict`: Mapping of keys to values. Can be written as `dict` or `Dict[key_type, value_type]`: ```python3 anything: dict = {1: 2, "foo": "bar", True: 1.5} account_order: Dict[str, int] = {"@a:example.com": 1, "@b:example.com": 2} ``` - `Optional[type]`: A value that can be either that type or `None` - `Union[type1, type2]`: A value that can be one of the type in the `Union`. The number of types can be more than two. ### Expressions A property's value can be any Python expression. Properties can also refer to other properties, no matter what section they belong to or what order they are defined in. This PCN code: ```python3 class Section1: other: int = self.text.lower() * 2 # "exampleexample" text: str = "Example" ``` Is roughly equivalent to this in standard Python: ```python3 class Section1: @property def other(self) -> str: return self.text.lower() * 2 @property def text(self) -> str: return "Example" ``` ### Section Access The current section and its properties are accessed via `self`: ```python3 class Base: number: int = 10 other: int = self.number * 2 # 20 ``` The parent section is accessed via `parent`: ```python3 class Base: number: int = 10 class Inner: number: int = parent.number * 2 ``` Child sections can be accessed by `self.SectionName`: ```python3 class Base: number: int = self.Inner.number class Inner: number: int = 10 ``` Any section (or property, or function) defined at the root/top-level of the file can be accessed by name: ```python3 class First: class InsideFirst: number: int = Second.number * 2 # 20 other: int = Second.InsideSecond.number # 50 class Second: number: int = 10 class InsideSecond: number: int = 50 ``` The root (which behaves like a section) can also be explicitely accessed with `self.root`: ```python3 number: int = 10 class First: root_num: int = self.root.number # Same as just saying "number" class Second: first_num: int = self.root.First.root_num # Same as "First.root_num" ``` ### Bracket Access Inner sections and properties can also be accessed by the `section[name]` syntax. This is the only way to access properties with non-standard names (as described in [Properties](#properties)): ```python3 class Names: "!alice:example.org": str = "alice" class Capitalized: alice: str = parent["!alice:example.org"].capitalize() # "Alice" ``` The syntax can also be used to access properties dynamically: ```python3 class Names: alice: str = "Alice" property_name: str = "alice" first_person: str = self[property_name] ``` Top-level properties can only be accessed this way using `self.root`: ```python3 "!alice:example.org": str = "Alice" class Names: alice: str = self.root["!alice:example.org"] = "Alice" ``` # GUI Files When properties for PCN files are edited from the user interface (programmatically or due to user actions), a separate file with a `.gui.json` extension is created in the same folder. These files take priority and override properties from the equivalent user files. They should not be edited by hand. When a property in the user config file is edited, any equivalent property in the GUI file is automatically dropped, to let the user's setting apply again. mirage-0.7.2/docs/THEMING.md000066400000000000000000000043051407747233600153500ustar00rootroot00000000000000# Theming A default theme from this repository can be copied to use as a base and edit, for example: ```sh cp mirage/src/themes/Midnight.qpl \ "${XDG_DATA_HOME:-$HOME/.local/share}/mirage/themes/MyTheme.qpl" ``` Or for Flatpak users: ```sh cp mirage/src/themes/Midnight.qpl \ ~/.var/app/io.github.mirukana.mirage/data/mirage/themes/MyTheme.qpl ``` The `theme` property in [`settings.py`](CONFIG.md#settingspy) would need to be set to `MyTheme.qpl` in this case. Theme files are nested-by-indentations sections of properties and values. Properties are declared as ` : `. Values can be any JavaScript (ECMAScript 7) expressions. Most of the properties are of type `color`. Their values, if not just refering to another property, can be expressed with a: - [SVG/CSS color name](https://www.december.com/html/spec/colorsvg.html) string, e.g. `"blue"` - Hexadecimal code string, e.g. `"#fff"` or `"#cc0000"` - RGBA value, using the `Qt.rgba(0-1, 0-1, 0-1, 0-1)` function - HSLA value, using the `Qt.hsla(0-1, 0-1, 0-1, 0-1)` function - HSVA value, using the `Qt.hsva(0-1, 0-1, 0-1, 0-1)` function - [HSLUV](https://www.hsluv.org/) value, using the `hsluv(0-360, 0-100, 0-100, 0-1)` function. This is the prefered method used throughout the default theme files (why? see [this](https://www.hsluv.org/comparison/#rainbow-hsluv) and [that](https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/#hsl-is-a-lemon)) If you just want to change the background picture, or use a gradient/simple color instead, search for the `ui:` section in your text editor. When an in-use theme file is saved while the application is running, it will automatically be reloaded and changes will be seen immediatly. You can manually trigger a reload by updating the file's last change timestamp, e.g. with the `touch` command: ```sh touch ~/.config/mirage/settings.py ``` **Warnings**: - The current file format forces all theme to have all properties defined, instead of being able to only specify the ones to override from the default theme. Keep this in mind when updating Mirage. - Themes will soon be moved to the PCN format, that was introduced in 0.7.0 for user config files. mirage-0.7.2/docs/screenshots/000077500000000000000000000000001407747233600162715ustar00rootroot00000000000000mirage-0.7.2/docs/screenshots/01-chat.png000066400000000000000000013113651407747233600201460ustar00rootroot00000000000000PNG  IHDRVMK&BPLTE '  % # # $  ! "* !-    * * '  1  - !4%D ' "A $8*G <,K '2 1K#5N );'G+C*6%=-4H.H)@28R(8K Wk/N0E%, #>2s (B,'e$>cvl^67}FGg4jB)m~ŘWqgN~AMhvn:sfJRME2>.?!gS[r6~pS&`5JBn ,` IDATxMkHǻ&Hf-dj!q\7K7mǘ%a}6%a*'m2OɎRI~׿=QDuv=NRTFeͦ(6fvႧp(L0x2:M~0c0}OTi74 22+[0IǣS ] WWQ)s1L5 ~vA`cyګ=</6.:j>'B=0ߪ5zqWDL4H[;FƳnJV=y2V3"bY@l6ݦjBQ)SGhg>`Sը $ĢRq &,iYΐ՟?#zGqvadL-Lc-`U6X!KC"߇8P[|ƪīS SX3o@vj̢x~gvER(\I RZ{ĪVBI$B΄_ Xuc5­Q}G`aemPEf@Q(av ͷ9ajf+SH>SbP/”^ u%:#Jvb/*J'@.Ě-BZ1N@T\E:2?ŀ`:e3?jl0F-U{LYl*y5Ov,džU^cc 84RܮנP'PI_~ IUԩc:6\MMj VQeUҪ@ɺגvDj`U*.`-[r()AF6 qMVʒ50 p諢,]b77"ϟ?l6ŗӇDbUa* V0WI CTpģ6zev8/N9UROPUYYM6KG7#V7SҫR"U\oJh#myU! =lSp7 AW1 ֗Zd+ݝbEҗ6o*u3$D\E?g%FFʯ6*1XBQv8>\UrjM^h_6F 2TQb,05`nv7w=?`\tS Q"SQj՘PSi|&#LT3y2.xL*Xjv<)Ҥղt|t}*Nbj䪀55.@le`UAk`Bry*2 *+׊2 /BZA^*NO0-d -d_VzE[Z>7;}'Xz׫I :|חaky/#*_~W#grI*rզs͋]qQyi*a]',MD!U=SUY%i)V6;Aub4 nbm]11aБ `0j9VIhAilضH0qH}Sq3G*U-}|9>^8rʤ[iKUţU~)TD,:Ԫ'hUl :vOU)+V?heYCZez ȓ4TTarݥV*ZVٖzbw!^\/zxnXϋuۍ-}Ey﨏jԐo1j#x- oݝND0 1~]"eIIGKC0 \Ea͊nƩi9nrmNtrո"ue"ΌZevazg;Q/RΖR2S9Ms*.Vz֘X@g>{eN2>A`њV|TV^#]_᪬u zrMY~meVA^W!UA'O0q5qĪ V| WW[-Oxzu[|tj׾cX^Rc>,GCN:66vN{n`cA lŦLՖmmXXh- jSr8[_c"J̣+g(9=d}6#_jUzUfcR*V+VjmN朔92 >$W͘UXK:FdŊ6)'u*OY ي :6MKp6(gEQǟ:/d=²o,JjjE~g"ѡT1BZVIUZӒ p-"mŪ)џ!>ÌISUԘ o+Bx:. p@ef7~YU]E6Te46(̓'O'eu|c88>X6hO%,Ʋȫ2tjo&kbO{$ئUPi3gZbD/c˱&$$ $D 5wc'89>>}X `E;VaTڪYjW]T ZU g7jWzXڐ rW=WyE}XhQ ^58OvVYwe& W b*ӷ$bXaԊ*c@2%hJd~2V!#zq>K1" `hU^f̨ljA`-j::^UTvU@뮚~j|CjfxQP7T)5fVSt3-@v:t`yMŐ%sfj O0 *ra dHROV2Rt G^~QZxJLML,*h Si2_úFTE7w1=skʨ`-@µlwx9O F醒bl~kiE7-o7h_b.|TjpdJ+XO*PgymQZXʼn&@>rC0U}RMחBpiZ#l7xYgm_^{ *HTF(it:i@37m0Z?dm<& իhTZ4V] o(MQ8@UՎ0^b.V_VF* 64*8Q'@8ڰZ7QErE탿x\'ᬪaBݑfoMМMz>PI9;Gx&R2<ؙ\>R?iv5:@z긱'Ԫ}G{Ǝcw'JX~Rg )Lz2ƪTuȇD.KNZӇ,MM߇"nyU~F٥=i3[>Y_.8`f6kssݻ!x[KCWCt?I_.%&@b:$Bd#cn9 Aw51+ۋ+$ W12+W^5,a&3^Or̭\ `vޞx"iH.@jg{X\5.r3Gr)Vd1Y{ i28 +:kAxޔKL«f gUkV*j >DJSP4ts+:a @"֑D݆X5qyڪ6[^ћՍ,HmTqtCOB7 Bvi4NJ.&'N_FO.8kŅ V&e3֣e+++:j3|6;0aHSXsS‹;6gvԔS⪕4X`ҫOY)ieULM*;h,QZHXgj V~?Ն&Upʷ85P$=@UjX l魆CS`\7np%XTR,^[Ro)jhmL~QNqNLJ% sǵ9}Z/ؕ2sv+ DawvzAHHgNȧ?[8rW: 4fM|R\n񏗻^iTqWըxHt_ND1kX"AzLdj;\OQ) 2L/xL&CT U+a*HZjʮ_jrnNhSC"v)V* fɪ5ixQ @j @@,:A.DxAȌٺZ=3Z%]Dj(cJ[<U=8X]F,QFK۽r\\T/lņWGgaM \_Ӆf“ؘFKn#mWCW/vG=߬d+x>12!%ʓ^kx3(1sIK}cp D/ w aPz VFFZf-C@{~8=^=?&I.ޥX]LdI0c ꚹhOYLi@Yݔ@54%!F".ԉeWB-k'FKmyD1R||JUь1WM+NJ mWw2Lcgh+5S+4rY6}>62 'OZ`[l 1L9jJkVvKk}WVoik:r*G3n uª`G("VY4' Ns[UA]8uZKޤV zߎiP]]'Àخk'o X' =,>V-H:P. wWr|bb/#X_TdjVi$[{%i¡C*ELMi4#n)L0N}W8Q^0_} Xy8/a[D/MY]Q!ծ 'Va8W@}­.NO]$Hg,%<39q`2\ЉɚǓ]|(~аpzG? *2/$8|TYSaU5+ƾ]=V'WQCv}sS\Ԇ/X_\-ծ6S-&_ݜMm~At˼S&tO[{ !*L≒YM橹u@oe+Jm.l\1Zm7TirgZ|xk*_{tʈ*, @!:[CWuC[Af^ }1-;MCZߢVhѶfA6]M0bWBȮ0$OA2.j$1]f Cavڠv4PJ B}*HC6٨rԱdݜEX~ܞ_Dju~Xq͕ۣCM8=[,*Q!B(qF ;-xWJM<RdeL:\*/WKRe#vbV U'dŶpU0." ձ@3Vbkg4ZWX;b!.RruWfNK! VIkyZ&e7:ZVm" Åv<]Ps5Kr"jh-3ӏTTjUϷgZarnɧ}ͩ`Z|Qy7a~zuk凵lv EpDX%'6V3)ZU 7㰡Z^+䳏q9m4k"[ '޿NM )8 Yz~*!x,_[U,[P27*q8zުi9oܩ_ޅvyqnw -^l״⇡j_!p֊Vk4;v!SUDoir'W;x;;L- {Q/pߗo > ;&s)!32;=7*;ZʇZ`~V\";$;k*\Di5S?XWΤs轂7ejꈌgQ XLnЅ9JM(s^ZI,⫝̸AxޔVE4g%V`0+UϬg1\v'N~w$,mΜj 8[ 7bv.eJUCSI[b`y@;V qLUFIvCjULG[YbAYAX C7{+͑>UpX#^s^A^koo*tX9 ZxɃGOaTdc}\=3ѯ|n.)I(!rVUV}7*nr`NJ,HGiEX*^!5f-[^ZFhg+gѵ۔SG(~sX_+U$c'z#您UU.˻)<<ut\:^!oLe鎱>$T;c8X7 "]% $X D6iA4ÇBdx3 )Ґ=S0 vH#fWK>8Z75jZr\|); $=f[k\!rs+n£q[$)07 r~U^ ُ0lz _EUA+VEx4 Ħ=S;Ŏ¢ZtJo$&_ZǐjܽX]}ǩ'rzLJ0-zH& CJH [T4r{YVZ#)0Umho 7T~Ҝzy>K~,`K o[8b*f6f7V&b&D`Z0Rf,mBR곩cN$s & !S߱}fEe8nUى3EHrԪCٝ'bE8Z-?UOdGHykm9ծaӖOҚd`|\-c0J݀0LQxL3Q;u0;HH|tRXTg{ib D=LNo.j@ڞ6|JV.+YmH.()'8 BӄVDSjFitj6IysY ĩ*PjO/rjUmI RVCiBUCF~F2M|*UaEZJڸPq[2O;tWűشUӢ++ V͹6DfE\:1.zث0HĖ=V?v߄xOl^E#%Bߓh^z0_}V99x_,ڰ~^-iཔZ(g4Y.מTu8 T4gՍ^Amݱ'($1y|G'ȓx)BS3};NRG\i\Kuh%g-|/B':Ug<RMVZW+ j jqx~ !NJt]|?7100fsfUfZhe`TH2ua.,pݢ7kahRinj󷍴) AeUeh{m X=MsU<ʝoU.GեVH5zk7'H9Fi *hJ"c|z˂{e&ƪ@17׊U*Iѭm2t*sN}{HNJiI7ܾ.,+]BM#.:7&F k(֜'EYμ;f^RmLW5|LѪb!$޺9,-{:~F7vF\L6 Z*U|;`Յ=D 0|Ud*bE]9;kf'GheUmAP>ʝaua| YNW`…[5qq[:ߓ6ԃbjlOϡ+a{[ylUM `Y+p;<&ЪCi0;pħg>~$LɑJǛo6^^N#^}=8nOWs956̙pتdXUVd534ݣª MLp6F|t/|ZSQq1B IB[Syq]#*&9-eڮ[K~~3Y-aHmKz4~`n2Ybƹj-f?/Ak:"Bbu.5fYj P~ s%NU Z&b2r#fvJ^wUeTV5tuU4ʈCaTmI*OXJu}lUjWZ?ns*ZX%0nG|ĥ}"yUPp5`J]cKeywV+no(t˻ғrs}~qbn8Z)+oW+?tVG.?SL58RZPAl?#d(1L$к5H6xH,[ګvؤoY/i{[M',%@#OZA=}d:YW=UvP9R9 Ҟ\LWID(mv•e4:je:pUhkW`=B:VY쪣m0%#QOgg`;IQКj-f@VT'Q zE Ix,twڀM 2U%K%3)Sn'je*n! & GNMGe@UqLXU foJa :ʼ n~HjflջX DiA78K`p-Sd%z*X PU! I{Rn˴~:!ւILޜ j{ mUU{t~(%߽{G)P]m|n?Rw؃Yi1x Gu!zs}q7_oz܄ ZꌝjV[v _3W6RDLjb!+Va2iANUhXUGiS| ]Xz]Zr$;؏420ĨL*xe2 (j!Ѥ8p^5F낰@eVY0~QA&÷{Dʼn S_;."W:ګ:BK ؋*5jVsPT`EV=#+`VP,U ؾ 5+ja݀Sd,ԕ+>CG1;ndo X`\D+"k ʗl$|T* v05@zFL}MLV;(V.Ik0PE7?cFpSPCvg1>3ib֢ `T"j7с0_\K@< r8K^u|_y} ]u"֥+rV/ֽLX۪VC[%UDT| SX YM`5W,[;NWq_V*%HoI%f ޼"Sj [%\9i #0M )m1/jEJ^]ʗHz2[s[QlFDmUzd8UP$]-\Q5"DU وv4q5Q: Ta-UP. +Zj%qن2H4*B8WQWA+׾nS`mkIK,D5EolEQq>jiaV*Yfpbҧ|Vo*Χl[ |; J<$•:Ȕ|i/ N{]DVc7F [}ӘȪիUY)a"˨N?ЕT7w# Ai#=a.+ST)n60w ׫ptǫ%/:6q[FX=9cTjZD[ufXgr- 3MAptr|u*12QH%'SDЖU{:eQ꓿)+aé& )u&X5j)U=<P?٣$!xC"~5hq:,Xb#cjPHVYY?p?lFW}WW*b`,QVÔ&խ 0!7 Y^`*5/<'O|K۳o={vp^|qC= N.(3Y~냃l%C%ix;Hiִ-d_ӔijlS@ 18INIp` Ԁ"|D1 EÁ}|o \m8_ߣWgZ~i\tIv?{mݙoǞD`:\xy,&_Ȼ"`x‹蒜^$Ɠ?,FxL,rbDŽg@.w_Ν\˅BL;gsU9c'N۪4Nq}L+YϓezHb<^)xLIƁ_^+7N;V܍: IDAT{h\V?z;= 4^[RslJtT*%)=:ްYThL]@ o˩d|*NQj0Q4v{v1#V&4[fn,Н6. l;c[L<)k_W I96:9G3iX]2QR\^y*KO#g2AmX|^?:ܣZ__' |"9\DkZS|u5cq攮77xO5Wۖk\i,7zdccC_ugg%p-,u ~坽 T`%+K4B_}_DabhuuG Cdd4UZI*>Tӏ/k>V}XM19s,*P h5?6W%eF yIhX-|'2񔰊/!a U;S)*YVVɓ X1fU!&+A@fJ&0T{LUeH vUXmaPr?sm16L[ߠ?QRC%y}gtuUcW qJƫSêҪANwЂ_2q48A ==yCL?xr4&St&UkE\yTSx4'먇ko4M=VUa%RJ2W#FU\X U >UVkw a_q*Xrxҕ ?J 6U&sf*\SE aU}F"IlX ˪Y Y]EQ\*ݸY !3C5N;q\ݯX\~9cJQ%GQf5, Yx6JY}1곜'`33˛ 26l˚P7칬~nCa5EEUMz!g**Դ*l`h3O*iiLbvhmX"Ӹ[Ҕ@8T[JUʪCqθ&\JZ)T5iu]wt$Θ&ٔUK*b9zJ:ιʨc4R1U}8Nª&VQU$=;|)̝Uꁤ`ssʟD]V^V^(AVP*-Vꪱ3˖P72Vmxukk iH#i,'޸ V7Uʾudluv)()pc kk沊jڬtBCUcWILRXNGXX(v-i>~z~nwpohmm'6p>1wu\;6Ul8I3~lI@[ +4>)UI^ōL@=N\-+Ӄ}>j=v\]d 9:``ήesM>^rЦf'8=JFUSV*;86$ q_[kdYsNĒW p `3`T=ѣe9~J;U>̪= /i:HqUfussH@:&\$`uP19޷.iՏ'2DkTZn @`%SuW-Ti:HV=*|hOB]qH MBC T F"6EfŒ#!Γ\KQYF R-X [.EI {s6~}GǏ䪨ЈDk.Un[uUũe=tDj0ԙ35WC5n8lD\5b`('e͂p521z*ֈAeM+}jz\3WCDzKU@u@*$s5qLcaC o]">TJjU֪*XZT)*  VG)tJl_ukS5jnHlnVGDL1j Em^5aM`&sծg7ruX X%t3hi=a(4Cm*[q<ñ]@h}"J9WW8+~/U4 #ͨ]:fK*Ю]nPaf_M KNor.9:\5"R%L}Wo Q%++orUVUY(UA?6r bUU/]OK̡ AՉUjOAVָO,TUŵVu[ZEKP+tYUumbհUPG&-V)sN VբjI9j_Kz*5(j{*~ ^[-fa5 jTZTaqFvT)(5`UHSP,9W"su@J)?5<F `.CUT&[vL{5ߓt6"C=09B"Tls$*뛼gX幏#>+YRKZv2eɢtVmHHK@ 2L6,ZtTnQ!Yoyj&KDr91[6鐓UوeQjzr\&̻kYm ^*=R gU"Dtв*7, ߆VF܍_NӁFrtS42]ұЪ33eƪ-(nCE\FV}JXU_Q⷗5au "OC~Zzg,s5rWdf*zjǽCUψX5X}+az X"PکrCVKR$QP0U2?@Z5_/K7XAp,[Uv:}$UX!&=5 UVH-]L}*fd}g.-1&=b4Eiņ.B!dje1W:P:%"}=$Su[MyKvAc(%-X%3Iґ=@fv\zUW}B5(VǼua۸Xڀť8PVG@U8VYd]}"!X 5F]k$ssF?sq\q*~+X jqC>բ d?*S:DPAqt}hՕkSl.j8+걟z?-W74j} pV[qu rd[OH ^j꧇w7azj{[3K 'ѨBOOY݀5MОUZ*4@٪A6R^* ^?L'7]hV?{|W}6}QHLj}J %ત{/&PjпWRl,RmڧW915h`:J'+RiTkXWD @ZN^z}6r TѪno?P*^dH:hjd`u^S,VAn#"?gQX}SKװs֪5.0Te5)OBlAL\t.jR_4_7ѣ-z >V5W FFoVUvwwET6VOXUJP 1ơŃ=WbXZJl=.%{_7\MR UܖX|U*\M#\ujul}µg7 FvؚaVGǂ/1/~ΪpFBfT,Xī:r d@UQXn{/i}q1Ө#6jw;^ &Ѻ.= 9`тmMcB2l%v66Rd#l#i`TYyÚ1zW}? Lճt"T}&+[)znêXpUk9vI7c p@Uc+$UINCh _Pݳ" x!]$ҪIf%<Ӕq4J\ */MQ2bE0GOӣI%{utZeW\" C)efzU]]xR.jVV+j}j`Um? Vk`uX]_bh Ԗ*.|? gVb2V;a ,VVnFK`@<#V{yogVW^DXU}r%9h=JC]W=F Wi0T+Q7nҚ"L}->VOZT, TٛdA.Zȏ|j_eh W&@?_Ut|L]UQZ%ۂ\X G.洒g_:Vp6b~fgS) cJzS@T W67HBQDMTe$ a( !ws_R5m+>\5C VVwǰZ]V y `0u~靻eC30˵]{xgk-#V6_ҿq{_4*@q>-V׽&Z TUE"UUu~^ edlXjG8:`[ЁUljcbU%p` *bknTuAc6Ѿ*9 R^2Qt;64CK\\ Dԛ"ĭ;vض|&,zIĺ=ez*RV,1ܴ-E$ W&r&yj5"hUj5M)Z(FQyb^鷑lLuU04݁LUfW?%fOF_MϹjZ`i~7 iSi{qUXǚѕ_b;vCguW]U^B@ 5x,S;p3B+WŪ⍫>qW?~pʮ\X޺y\?Vj^M>UH2?R}T-FVSS**8BUWubo7GlBv[#\vA|nj{6R%bu+ Wzw7׵Kp)jW7X*7ڃoŵEX'OUT4\ + DRa`֌RJ4I0v.eSwc*jXؐ d@ֺ( Pk Vɳ"]Rf`L'70M&a.1E)+EeJXWRrUTD^O%%|*o} 2_ `{9V5A+UPs w3UR:+VY})XE \,.o1/-5B5R5$ Ֆ͛k↤UȈjǭk@ ;`Sd6Z$b̗FmREUθ}to`9px}}sf3N IDAT?XXm5+0ԲX:UqUUa %R VvP?x5eyX=Uj4b#VTݹ.J<VYY&GHylͲ N\MjTDvXuL o%hԗu~_ 4g>yTYՅ 9 ξZ2V `•%*Ȉ ^K1F0 PIՓL㓇3+LDHt&墺o:CUѨFغ+BjF$@2PshJ5\^̀s|yҼAHĐy%J\$ůJ".VQZ"S)/unlb:_ꑯTmXXŐrV+o /pmY 0XC'FU?f>'.EKW}XAB &o)ZY8XE; X%][eո][o9 syupڃm"mpu'o V1ڍcO~ge<VVAzsvwV۶&@>VtjR5bs=XNL(:ZH]ڕ8Xr 6>J%Um;f&ɹ[U[7j Kl`Du2NC[S5f aPT2u@C\uE<a &*2FJ)A:A\[Y֪=֪1lfLݿ\ǒyԀ>b!xoJ{N8 Uc"VyƪZ%v^`67Ҍ 5=ЂBU+pRV_c,V:cu5Wj̤|P=ULU\r5kC'U@K_SaP[vv\Z%`{1U̵T!}n>TBkK\uX#S7yKKK ^3 /a兽V,%ZsY_ V-1V+W7Հ\OSv~!qigT&/&53F5$\ԒDP¦$bb@BȮВ"/N]v"fU7^ٻ.^fߙcҜuwH~>y4= jd !&q\ }'o4B=dc&aW V/+\/Xq?k:XL\B x U͇4!kinNUx'!|դJjwSy)Z)HyUxCt\JéPiEqnQo 46•Mp"dP-]Nes"_umI R bI*nyZk68d֪,hʦLˎ6QêYJeh[sj"שUU*.<J 81 ' yQ9҂ZEx~1QJ"۲UET\.&[?dR/sz8[QX3@*e$jvOas/f{ ^m7Z,g߾\#auG_3V_7R1DkJEjuFUrXFjY0]ߨzcx|Oau`q6!*U\5mj"_vYwW4$7ɇ5juwXZ%vUI6x{wUEU5<'CALc}D"XaF+8&UٶF垨)m4JTuɡv W"f[Miڔ9e`*X%Tc99S JfqMJUz?;G7Q樦[h^zHq:u3'$VsURWXȂT]LL`ա!gMӎC{gVV.,UrzVg>=pZjYw/`Zyb;_V߿}yhX% ث\41G zD:ڠ*R5C^ԗJk[jUGU V[<,WOpP| ̕:#BǺ,I%x4Pۥ> (?oii1ozӓ9k͜Vs%pMG΅z١[L[$ m{"U_jGKIZߓrhq1oY lBXawID(HutZ U|j_U(p]Z|ͤRnQ]WVPoK_G\qn망\X%`USAռꭢ$Uc~sKf رz?>|z[=O4wZG.VXfbժS Viw`)ŪA&=LuU6nd2y'n_Xl҃~vWk(-Vk5WiBZHV/FBuN`w͋@(Zxo>}yiU5V+^zn+<V X+F g](`ٚɌSi@1xS#jj+qSv6-0HQx sZTb(XkXX9ӏ.8VQ]c@*^Ut\-94W(VE7!Oeea`#I;N{0ŪUb(4nmF5f*l2ӽwԳZU5UNXazhk41sqe͏P/f+IYj*7^zk1hkdqkU}WboJwZHܣIYh@V-og*G)v`uye\[,W'[*խce =#:XmQp>>xp}UZXv)*`_ n^]*.@L͓ڍJU҇mD+cHk*`/H+m8``,YkxA5GsXő*$~"A5X-=;8& V#&-Є/R6TVNlovj#dI55c*cU`~|oחj)?:(W;a#.wu+*R#.if`[1d"ӜE p~8 Ab8p@E*mA֨VCR^a虷an &*^ X#y].8:X3_*Ņ ENZ5Ӳ}ƬzDnVDzgNF(Fgbxv_OOO|Oƪt/Vj*@ʧj}j: @V=ؕeְ d5\HV]YV ,=U{\7QU".@ auaq w( 0oj3 Vkfո%jn| UOqԔDg$T0o5HWK,5+\ ò*bStTiChwX]*=wFy4r2&=}0g.ymԙ5:*ϑp q%-eWTYIi6g`n vѾYE '`g<.@꒛(%̗*&vWW% ƈp@RlcNA*):jn gQzzTO:W,Q8K1UTUUfT.Yka5ɼL2*Ϊ)U[!_Z}PlZR%ëլ 0hpJ`(5VNUZJ  Z8jk[H,UJ7@qX`ᄡWVI 8Sf4+TWႽXum2",Wc։aUMSEU^ nա.!k,`+W] 764i4%Q]]DV0p$̛ZAU4>-v\MQ3 ZqCȂ̭*RY5Sv1QwHnK<<\0FdRPCl+0H$9HbH:6U/I$CV&-wY\*Хk[g:[#0f[IGX{>ܞ;q\>~;VSCP#׳ ; 1uF9[8U-8 "Z0@j>aV`@Kv+@Zi͌T[§_f!֪؅d"jȼLxOO_ʪ/Vk6ksZ@ݗA([1>z\B *{ATʶ՝r ¦PU cf!%g ;wP3p7B!p!enb!~*Ӱ4@au?ſ*P=OIW[b` l d`ޞ˫,Vϳ+QQήƵ`%FWObTpjpRd @x8%W^}@:K_jۊYj\g]jXU\-.W RUs`յZW=NV,^EV@[XuoJj8`pmwsW`d$0;=KBP k 4 *{-X5rVS_:ªzSea0 }L*P5զ 2+UV?AY&*Ê\ӨzUK6)W*1 Mq)㫭OxӪX&"Z-U%!ӱjÊ7,] a9P4 *WA3ORm `pXmLDjC5vͩ(Q ;.VQb2A) OIkZjʱZ0T U]ժ`eǬ(66MG61'TajWūZqd\;pE֢9XĦMT",T.mlW(hma juoXUutV<x0BU\U]Uw@LH`6ft @.Ux,Pmva^{X%wM;&dPVR;RC׉USOXφ| ?PbUu@Vl1d5t,39?Q6XХTUPGQPwuy'K{ԉv0A\­5J?z%}\`PBSNaW+cAS*PQMVm,tG XKK#`ބ0% GFE]+jmqYbi+~R@um.L Sae[Mm-]Uv:'}EunmT)X4Yj-pW`]δxj_}4BuSnZAy ߎ,ifJ=âJ ^Wl:۵UkD nDz ^mZ[q*?n^ȺXf-X`u#V᱐GGW1JmEq 䪦z slZ9Z sVR n\cYAjX4$ P~cVq F#)_,w;y Lrv<Ѱ*j5VXOE]z?k*q9 VޛwSeof ӇFcgע#tXjq ΅O!ϙHR\:m~v ` %Tu9:1ڶgPWL)3lyÿQсDZB6gӒ%VW+4!'Oq!Md{L8 *kH! zޑVͨcne \SLOAJ~X5Z4"BuU;3 ;7uQ7^Ck_bcɱo-(Z52Xlygu~um:]ujol:[9IxiU]ijzlޭ7cWWFa cxZmSU7@g>F]).z5#APn??N~c5\5zX IDATX_ l|h"e\Q jnH-fKصcY @N3>X-~P5nE@!+Ut=uwތOa@'?%/LMk:Vk|l1l^V:!JVt#||_U0e*Lz~Al?Mx[Z&'g,so~ZX3=5=9fz^@pC:1|(^jT퉩%$Sx[{{1KFɥ'cvޝygWE5kh~JN`u9 Vz8sK]H֥'lR.mA.h2"RT? JT6r[j&9[%1Up*Vwtm*O,)JqIs!Z+CUGx.*W5oUjXIc =*{@ģp1 jE/J1U WIVq\űs*HOZKDo9U 1$,ZwQ^oyVaձE=VaurVX xFCrp_zF„խ9j9 WSj.*)a5A$IM Ւ鱚s?'I#jzXUKԜc.ۯIX~d"*(jxTV>|TpfeW.@ MeJ@!`u}t6d_0ZQhI bf/f7O?왁EgfvΈ&@= 㓱kUllPU_; daiAau>l7;3_<4vd%ձ5%o|1= omon4ܘJzMܯkﻻ  cUVx UQz=; oMXuV,# 4XGX\#&-# Hu'U ~k|y gz4鄴*ٜ`CٴzuBiO2mևLeW/VF Y5cKeָ|+{9W%ZNU> ;3)XڍUY%9R3Tm6G-^&usZX(X)*ghRP4u(ouxVtoU \lMjwsԴC[V[kǃ-ÉDLAS;ax1b  ildJrH`&.1:!%? ݨ<z-\BKj5.[%C1F{El SIVSSYêJZIcX]K>dJRh߁zbbbxYHTRMa ԵX$4 XG8c5Kb5 yAljqkaJV*\߈4'`\Q.B0E+)껱j?[UW@_[n4W']U ^Y(i2UN4c&VmC&j0a <:*?wr:*zE[@n bѩ&Bg`<*V "@Zn/m?4;EhUwP jCS0+z4mU bz*ޝ8rD$齩T!VРj2Z^_  >nVͽ!_ 9VVՆ@`wk.^`VD}=bwkو9=UW"~5M%@{j?ϲB1 jDMVQr7$P 'Kxܔ_mS)s8<x:cU]:TIpV٪U^"U8^!r$j߉T4n%L8><CV{85U3Y X 3X PoE}OaގU.-:VZCί-wVU A*jAҨP~{9j "▊&NsU2Oj]WdzrEYY +U+(^fU *%IRЏǁY~(iYJʍ4էL c8I k6Uۂ SAaIۖq<^HUHUE*}ԱH63"$q)UԪX). 0TT:U\MNMd L\Cя <dscL2zi\M}{/VV V+YW5UZU>G?\`rOXGV,fag\֐**Y '}xiҖIgZеJjn.CjWU`^,XX-AX5~vmg~jq]`5'翀"oίsqiۏptmҕfO/eO^F.q߃EV/L#klXUju#fVϾ\-^jw"B:xsF˾X[ƟLuoz{G^ ЖI=6I#`RIgҏu:Ukp*lU䯮4G+r)|8.H*ACIUi`5auoermZ3Y\<:VMUe**.fr*wЄBcV?&?D*HUl}B;\{4jU&j+VZE]\mPy,` T @E,$CW+p c(O^Yq̮`=Js}\~lU_ç dH@GqujK+ViK5nU# uZ]+SRGб* ,ثu\F]K\.8ř[\̭kG{n=TܸHt?Y:33'ťgMAG p9WL Vo޿dZ5_Y6O\5`84  8>V#-{+߈}'spp|DbB-9 )V,. , Xo=PgK >"U?;/XBJ}c0}l\?-g Yk 4'ؗZ#X[]s S4)XbNnyASK]$)s_;#HUtU>Uo*$ħOňsXP4A*_nu N^Er"L7w I@iHV3\AbJlWUԫ(rP=T_:U$;X͵ ZC,,@_k"%zURm=$X3քb[ի }VO5$'DŽFK{C=,f oԙÝ\PQt Urm8=vڱw p *% Z]X[%V+P*;**JLxSajHg2 V5t`u]-ۭ0Vӆ?|orXT TV10|KVN U@V޺1UUfÊ~4&cTxq ٲl9baQ#Aj:0V`| 4哏5uTA A W,/+:(HJVij6J`pE*yaQ4%z|Ϟ+ͷZ_L1)WqZbU2JkCy Zٗ2kZ(US UVs(Xs2UaC ' ڎ&|O*HV PܘDmqkqLQX:⦲81H։KeZƁJ:ܰjyYcu}Ԡª^6hTZ۶Wr"V}oUʽک"bգcKSձ['tX%KܸƬ^[ՂV)VmT]>L.+yׯZ-#l~KÚʱ 7jR|aI[}b_6#I (ˑc*Nw: [E*.f*WF~B-j\m0YX5'ܱzA(rVOlE*@@o%qׂur_2VǭYG hF6zVK 4Ҥ1oʫ{KB򖕅UUIht]ҷT[˙DW ]-JK-*UŲz!'m7*GVҿ ܔUU6U>XTEEIT}#L%N6fX`n[eR*w1*ZS:9`MwV$i|a@oU{Y'N_c|jTьIC{TKMUO; :*A,lXQ kT:ר>{ՊWT1DUZU- 5@ۢcrH̲+2<V_AUzcVaoIkajeU t1+rT**B+'x;6,T-ƚejmIMDJ[|Z[Q(-" JS8h O0w&cL^kevl,(Zyן$d[51neŁWWLX- J{iZiuTz rуɁV/Z9J~Y`O%hʍ:Y-Gr꠆@{ޫa9S](Uy>cj>n[y˱/KXm Ħ VNb)KOQX'URf*t*IU‡*cu]RԈV!*{l ej..-bZ)>o=>VLauj|# eK_U QW;rJjX-@ L~#QYM9 U!YjVoщjvjFyUdmmKJ!W9dX5*WY2U*dM.QǪřJUu X&WCI4șUjXv\%vٞM$^Cȧ20r IDAT$RRUY.*jJ$f2#fW\t~&{DVvO(@T_ iZ}+9f_nVGz"'᭲pU U-^4dJ |qԪQUgbrˊ:k .r\yeb*Qa=MTUjzd1V73jzC1VErh:Q`%Xݧ$U0'ʺn(?IXgI%1\WszUk>zddT1}嗘AQâ˃V])a&.C:6hiv\LNЪZXE՗cܖSAgi+#jRU֫UTwm8o\Z{S==R;@ic8Ҭ¦fX2g/WY2U: sT:OuVޚí}@OEBj]t+ ʂ&qeaum؇+@USjǛNrU: }jb5ƨPO`kbgĪ^)jS**VVia:,ՉHf`5Vjka McE>zEݱXͭN[n5]U}ˊ֮RjRX][XXx\aź`5j=}ry0@ K1Uu*/x4+zr2:{}՛#CȦ._mH]xbV}˨`uE\ X-ʢ\6VI.FS:@BT6I|ҫ_s%CU Z80~4RUB*p V4V}jdU*q; !Qէ=q2XbƫD(TvVHY8Uj(D@]EVGJJ\c&%j:XǂիxjkIv~М1d3{ͿGѕYS=<% PsH׶m۴ىG)zTF(ЉJh|!yH<1-ɦD.陡1; lmUrNgj5L#QK8wz0X&OXKU.y?7쫖Ami,w)4DUwYqw+ʫf!UGb : XXCB+:7.ΧQK7(~-T}Ug*WõUU@ƪU*W+``׮,Wcjl<j^p k:ᡡ;:+W UwW"(7pWo"f`unn /'0Z6XQ8:?dV9:,:`O)IյwwNj#F;U*mٔ9Qoc>FiY$x.ysTk:yv)0*^pygVVwSXPj3ZE_ hcR\A}P~VT2\xU]wS?F*}TCgĿKO\Gp\͞gx^0i!PXV}"at|(ߚ ֒TE)n=EruXP \m)Sݯ-@Q#AS0)JVW VWTK9i(]m\ՙ !֊UPs.|_%, 8"0dlb%%1کZ-CjPP-cFJ*BE`f9jf:A\1<{}tJׯ{>XD5x,,?ԠX5h oeZVɃNnK|R[q=*cUmSgR9Uɟ*ZVL h)VYmXXCa:VkKmj`ev V۰Jz믌Uqh+V=Vc~,-͢ *{U'r.U_^\2[m?`uQJzp9.:`mEFGM(Oh'a5s;1X^-U0Ż`y W )R{Z="\jÈUսHaCdlZ6n#5^UF/3"H;ҭbQΝ 0UVVwZ~wZ%Zj}jڄZ]hհ̪}]ՔiTfL| Vø.+V[EPeKkz"@; Vo,@KUpUV;:X-; 8jXU1,V1;Wm'M΃B0JjK-*V9:.'gO֭rY"0 t}\Y}TdLժ;;I1@8 Nwje,6ƸT\TK@\ V5UVI`p5 RY)ms6./EjK`uM %%/eSKA(#!XFR+TV buboWg(JVWW=WYXURQXaۼ@X˫E6ښr+:k>yX5bV{jb>xuU%j\*:ꍺq~j3I޷Ut2Ӌ3S< er{tRKSO*vW}CB@FAH6֫r@%4Ac1=^fuul~Xe f'V_!\VWfb=wTRrUw:-j-;H $HMbO@Lqr;Yz .To<xKgϘO 8U:n"IvU!kj21`ZAx-LVv ^~@bnpf*XfFGAU_0S"@AIw-Tr5jw$X/e2yFXS]MbSɹ *SfmJ᪹GfXyKbu`U[[j7b:3MVjˆWF%%pUOw 0V5\M7g'FժpucGXHMIdpObyZe+NRb%RS*UzXV bZdP.. G?jLU? kU(Xʪ4*r @Sr)V백~Tew0A;d H`]%C~jqTi؊^\EiAgtT|j&x1~?VaJӱ[-U4VgDtfrjzԪ0UWɻ QgZMjWf$*U;0#|U[z#G>y2[k`;kՁPTbH8uuu J$0OjLi%aO\6]EvbrA WqU> H11Ҕ2ʽ<"Cɖۏĉӵd^]J^8tqk,ʃ_}iY1VBR`1;v|ܱ@LU /-x:C2Mp4|)^ UnL#r۫Bnd^W֥#BDT8s2^m*cw.$u75kwOt*g뙯,XFݔo6XP&UQT?)*J[pg6Xw3VG>zжmS?5cEzskkf2blz0mZr5k-XhUgH +7Sů]TL`5(پDcMmt֬h]ZӾc5lfPuO.ޱz eB+-%bU?d7> pUZ9JX\C_ ~M]B su h$$ZP*Y>/2Rքd@а6teTпuU*H#|Z%teI!:cuUG!VƌJ$ : cl#Q"kvզHMڪUMm*ln=Vt0Au EX=0Ԛ'^N/>j=V=fq|Uč _Ԫg5zU VMǾWTL4ZTBq57 jWc{3!L}.RqO U^}/3l^VOW~<+C:䛑UmȖh6@05#nq fO<ٮ y8/T>e*+CV_^5_4W_cM` 9H3D;ZCъ^}5Pb`0]uXjJkYi0XQzDX1h/:+aQcb\Ayժ\N-w?5w?ãSѥO*s5 m,fVi9Yjp[V{sU߷ZCIpE\sB\Gr楳n2yfF:TG=Rk,Wy%Yl1qƪZl}tYJs0:5q4 V9#G"&eV)%ȴxbfΣj e~y{G2r\1UF VVp㪎.&C՞\N*.;1fSsl6@v">e KDbs!:M *Uz:kWL?j҂F خ BqT/2I鞟X"GNx  XYBxXZzuh-zE+`[g>4AruZRUlY޼kbZԕPU7Vr*V!ʴk'(V[ƽaU~~/}f|777| mAvjU;Wjoӳ]su;c!Zz V{#|?FU\J5+WML]%vjeE+VrE͍PhWeVul&K$9oԔ')pA:3Gy!aƃ]&X[=!W(Nt2e*(y/|4*@&A[]uG𠏔BHU,܊_էꕎpP0yΊUzoAZ{=7T[ז$;ʁyve2u1w|eΓ3 ` oS IDAT(45 ]8"~)rp 1, Rt Z}=P*%L(8Qf+q?59L˪7[S5墨?yck8kEOv 2 _5V/=zw&@Wn=̙W~p~FWc?q/Z5/h Z]Q0mQW[xVY殽| `FLK%_M>kS`BU/6M 9jS|L*}.2Wu,**ur`j*/e6_BeYo^S %{zFQ|O%JM%})SJO\7SwUQ$3hNOY6{UYlR7bee!S3Y\;ɮ5%jXVI"`5Lޅ3_ErT mH79INdRyOJyXIX} W[mj7 ں@/Ujxj5նnCAVdDn!! ~[3e+cw;;/tB[BՊ,PU6iTxjۏy@'p61oG{Aj^kXԣ]ұUZ7v8q#v(&>Ş#⧻C+jZPU6evv\KJ]wN8\}1N4OX nZńUx8մՖުԪ Gմ j;,ZSuZ۟1^9Įi&W#JJ۷6v@ I+ɭ^b5ו5z5U2r8UIgQX1]] iOi@)qqaKJg^ZDw]OioYPǒa2pkDNas4ݱl6Uȴv ~1?lz6W.*Ó*X-EC3CɁ~d$<W*[dƺ~tG }Vǥ9jŶW-Unb`w/ȪMBu%F+jVo< :lUZۑNUۥhW1VyץfѢV{LU/6KiU aռr=XUkV[dEGTM1j/~,}ۯ6TwP(F_lp\%=*u/ c> ~q!~#W_!ICuZEj\3%$'ė".G9tPFSz|IN"U*lKƳԵ55Z+.BĘTfG|mf8dj[QBj3W+\:%N4 drU2 )o#yU .:s!\tu+ylVF'y^]!PMhͫ6Cu+Ϲnch*Fh2:?SPŪi=QRCö'ZAZU;r2b(V :+Ď'EGfڂגMXˋy綶:揿b /v $tH42VI%{)z_VI{`5Vx&: 1V ϟ~.T}lc[`@ժa&S5qlA-aYnoxBíx]կ-y挷-%u*ۘi+Fvu<,e(/Y+pPuPƍۮ(V抙ɡb-D!qJ{n= AD )jUFI\H6#s˄wnqE#XDKVe`>m2 IN>T5VjG5գha!yAJ )+J VAj ņyՀ2;*Q7 R䭅Z&iv<ʱxj)2ۃSp͊UQ\]5jKKP5P[puU؆^ @dhɲ ,EOߠcV/ '+V[`%d\ųW])L٨w,JɕlRkEk=Eg]k$[o`wd`V{{ pHm:R<ӳkcVUXpqUN_ J zuv sM1ڜ+ͥ2[B>qu$80Ҵ;TrIH޸|4CaN:C[l-,Wѱ}SJZ}. $vQX% ySF_"!&{Gٺj 'IZ$ծ EdQ-[![˹-" CxjGE 8Z2V \b27B1DuF#6%(Z>XQրZ]F-3RU{Om8'kF;-W}:bի)"< vüU?!0ۖv*+0Ulxt$E4].B:*&H͚ʫ3*4yN|p`uI[`eUYѪ)$LjZUXWSJ{OXMaxpxުsl=л2 PZ[_(v 1`U'Mu1Xsܴb:jqz\ ?`5/TqF)Ljg"FJ*cU1RW9)o(>:x{G kq5W3CB7[m{E׀' j7TmP 6Jك|zũUWÈEin9]Bְݗ #~EcGzf<{hmAե1Ν z1vXeVۊlEcyȖzhkj zϫJU/aU8"U - V:-^*)u՚Hei<`X`uV Z}$UNŪ4i߱Ձ@P0HVeJđo(;%׫H: /+;iz\ժ +Vi iG*N9uN/eS΀h[kbby)VU5Pߴ6Xw6uU:J"@MY >VV|zlXmX]܅VzB׭ʫŪ|\bHw7OYIw[quOi9m=$V9b|s)9&)+ @XfWp`z6mu- 9g,Rӧ57 %NP|Zd?kr/P{i+M8il~l5Ԃ^4ZNP,@  x!R[Aj;]X7]BX-&2^EbnA/V; aXQDOr_vWZ[UBo T=4(L Xk."`LQ *g-Z m٧zufTi fk*` k_@)[`Z>Հ;C%i VzVD^5*2VV+W{?V{?Ppp z.?fjzpp* )- t*2̘%}X̜^j΋UW:Z!Lw)*@Qs9VNr%_L VT+gBߍ Rsa>8 +OtHx'^ *V8frdrfT+MU]¦LW* PT)7~3VR_sp_JQ!{^Q :,zn!cȍ.1ɑQZ. QC@)79hY1 ^yDay= ժUyjUi!2ƴ/gi%^cp/M0Wݻ:*Hvb&X N'w]j,I"oUth|SX VI4JCjլ'˫ij5yUk^__imտ֪7+Hw7|Yee3,/j\rthlVQ *iQ;ڟx:~qmOf}gA~ZrF[9?ozP\aqU-IUpՈqt_5YtfM*M*+lq.* uq=\΂]E^,֒1zUrA,b\(td, +f hzU ^d`VU[cG4A1fOWN3ڭD*X UnE=@6iTUm0Kqbiec P*tȤt"{nXAKrWC\de`u@V{vO^c!b5Z?i`5U+9W[M[ƀ~y ;K HY"N3s/_4bcKV;?Xj+:F1ш5:]'m^*V\jUa{p)$EyE ~{e{V4?M4MJ:t!iծcUF >!Qd@2i`T_4+(SP{0@96!֙@b҄\*9 [۷Q qơU (f|p"e׍/N!o,EP4hRnVu씸FJJ UYP-9r*4!\gynҫinĵ_vt3 @;6VBZB Ԕwpc `[+P~!Zz/WYUU^R7s\]3a3*V]js<؉zGU~U]RA:QX ]T(SuH=*5;X>o_V~Jʥ*\}d~ދ8H|qo7sM8/ƒD'tmrQeyV\r푤+ògIkSWp IDAT݀8:!ZR5::4TQG/!GpZ]l(N y-)xζ8*,RƉJ1yVXS㉞$9+ 6Nپܾ5Rdt\f("5M*'P-_c*f`fPͨ~"\xP`r V>~| R1ΰx1S6nQ49p 3G #,Vj A2b#tEJFj_X%*BFT+8Q[1ү@[ ޸~>R=ҶgJVxmKk(z/TnW4W zUʡUyX>iŇߴr>v޴C> MmӟMuc/vmmSܛ{_cNr}8>~4xݨ{V2Ze˫͇qϠV㪈z*žP ގ g^UK; Uwjծj"JDQU;mf]?VK(~2,M8حxc &"L4N .P GYBuF>cY7IZF”dJY-ST>񵶴fk*:CJsb3GV94fkkSJǝY|֨q4v/p^͗iadb!$q VsjbVapuX1PT,} m5:sTE̖QVźXUTNUO a򭂰:oUXTkD,VI(Vfbur 5\ V}cUu6V㨓|}9;nVGoZ?gVNZ{8ު쿯 }݉OOfGI8iR~W]6NZÛ'Xb5HWCm *ƮhF Ss>F٫_Yt]2$j V7tH;0ujZÒ9JAe>T YYd$JAe-?Z^,FtN8Z׎b' $mQ\BfxI RdU*CI ?9oBS݈dt⨏1*iU='cEUGȜ՗e"k?׫#_sB[6f/=:=' 'T0_#;GiV]ZRL}N ]Z\5T%50Ui|Vxa{n,ӧwtGͭ"^b8+S?i?/UU&8}qҺT<ĿȬϞ;|+F^6Z|{ )ֆ럻ZGմv^ј+a5<:Pí.!ߚ.N/z֮UV5O\EBJ6V\'iaJW0vSZTV/N\F8 u\6|x"%[dfqh2厘5D=xWhXMC8KXB@Zqsx$ +sa=ƴ s^rUq e97 C6Uzq7,km#;xFvےfd":%ZЍ77Y/8`!SB 8!$X! EbM|^ 71 ?~33r/4yj,V%bg  4UPyVV''ZBpWs1iͬ遗4` ˍlz ۏԉ`f!}- aMцGV5 bZ6 X( ܔV V?;\s$V)>Lj,7i\*ˮQ-_`-@ujU!*w;.~g=0V_dXV;v^yX2q?Up?L@60V;LQ-XmӮ@o3ZͯҸwddE*Vt6;mg|<\ *vZ&b8^ 9(X@RϕqjY*fJ>֕gʅ3%n.LxعHUh6M) q'sOAh+(ؤfN@vBXo@Y *b?|}f][\>+`4nҊln ŏ `57||?C* /:A eU.Vm Tu[{M6ta|Qټ֪y8fiBk@%ՌZ5B2fJUT}NH)dgkڂH:Ie]Ka*ՠQ_T yb5a~ipǨՉ蓧?tG_1VjubiZdn)^Kͽ6}}ӹjЋ-?g .V,W9IQVZX˾{jZ ~ЗJ4֫b<SB#b8:Xt\w.ƜE+1߀\tVK0CRf؁gfiζ00KgFξ3-b+7eC髻~'#Ľ8v9qD|`!$ ΄RU7X !ՙ:}ufz[,ޘ0}!5i2?.L;h`wGtސ;[͇COZ+yq[8͊au xܷ8X-9y ÷ÖQ۳?aYkUmh3\r:cu7zh`ϯ6Ww7ov5۟L7=_LZR2z&o(+)cU UqN\Fށ,h%bcBj -Wuα \.\BU!*XW&ƖʩTp,/iN&ZN&/AQ{[j4*E W؈X`-uH `zdUV VÙ;0e$}rȇdLGVܧ:{R9u 2X D{k&H I~*U8wXϱp˙n!d*Ba_ZQ5)k\r5t-lV7b=Lff単V/1̙Z\ NZU2pjUZE[_c j5VF*RmжL:[ZrM:׮v~se$$>An~#Iw*Ɲr &x"۔qs6wȟSZr'i n>0>eNV(W a CC8bIVO᪋UJX*AC㥥XXܚMل"SRPAΣyI>y keiBw(XeF* sGBNV^rU_Eb9SFQ"n,S9aALk\TA"d&!ºQ#󸪺̓%V8x/jIٸ#>\0? yUiMR/npǰj4l}܇2Ūو zIb*X Z]5;:gzSS[wN@sxo!VS<.VՁ*Ҵ5Vp6P涧Ud kvf5##5V)') ?y'4&Ztz=B&h%\uuJ?eq̧vN&#m[bgӋ5/zSvb fђ'+/]3o<*l{!X]VjKԡia/4n[ 2ϴdCQ[z8ƪl zY.`,VJj#ӑYTj-;NOx7:_Tzrոjmx<|Z#jkVoj̻=]#աb?S8S_._FVָX=QrWn L;"X>.jX-XbڋWX-2LP[{˜mW)55@؄UYO bDTЫ#E+4ȿXc '(]JXb7سTXi4K|b5ǑDV^# W{4iZMUwëYsՏr8GVU%VGFLZ>_Ujuwj5yj5ǡz&X.P5<>:8 ?ss65rR6%;kP`*б]lѪ.{L*L#`"ȫ}U2W-.CTY%;% HIT`^J?b8nR.İʢǕ>MʗL[j@Z :pe_ $X\鱐T!}~VԆ ዏ-hgWXlgBbiDVp@ 4ոZuVoUkWUœ?UtX]MƃTT#Tj3s0hX ؚ sQR˭U䪬%=L5 GY( (^%mU}Պ-GqQvpZr `I݆F>LF(trt̰- aR/u|eZ,qFbnNS0{ЀT!v̹Z[7MݫOVYupvcUlstrӳ좌r!Ӽ*x-t֩Zj86fhԨPom^_`f$'Gǘt ƦMkU.ğ-rͅwf*l^ .x3ǀj&vPUKKVj{u0ZHVoa@e+ګZ]F2׶FusX)Cꉋ'VXd*񱲴+ma|P2'*%gvRRtHsUjIx/^kâ) I)%fc5!ϸ* (k7+By_>*Ƴ3nq>HB:VDIUU ~` 1U%j lh}*]> М~X%'MWxOXϽÅφXcļ1\IWs}`v>-}0!#k klϿU;a1p"9C.VjZ++V`vf]`IW2\sb5ɥʉ5JJa+IhESU=64VOHs/Z-Ҡ׻-yP `,| NY,@gWRgXvsb4q+'1q</tFUu^P50!'iJWzz=X[{ VX&Ո`MJtʊ| Q-;m[t&*RHXirIL*Jo`LL0JzZ\Aq4W[U֪ªt?uU:s`ip Vm;!lգG]KGLŪ^ϥIo~2z1Cj:] )UŜ>Ud sS*Ȳ `,nָoCʹ\*ssYaAKP%2΃UWy(?S`U !t2Yɉtt16(+U,&U)&G#XUYW T$ޞh?.387#lf- D{›? u}x2v9JSIr3_/~ԟOQE3ρҭUZ@w Xח)p(]=h N :aCUػ8÷ݜt7祴ti`wϠ+6gX\evW篰:GO]N_OBvKsyfBԍ7?3 ־eݽ)]vk\˂k=܎][*}x#kE=Mef)鋽[kL<Xjo-;kG~WtX Xl`7cp-N@L5U ;~ pIczCUA%zUkkkU2AK8P;a5Ua*Ճ2[;UKBU\Rp5+iZy $VUZ *`%|rtUJ`eǐa{8cgXP˙43UX`Uk/ųи`"$az1V$=22X[4hlv.:}8sk5`}٩;[ik+  /qaN` puNn[Щ <} 98&WH[^5buz>LVfh94ұ|Lv[k` IDAT O~h,/>G׍6hu>s>|ߥV< V9 ;OnD QB@vmXŋSfn3X[6鱇7'`^e۴n-ONbvtM<6>rz^xbwMxװ2՟*{A|+} +{_ ZGԖթѩCm[!C̭J%~%}j>hoUX*3V\eU!7va zn\9UCաEX Vk*`Ո 7:4j:ڢ@@U8) Yܹ*V%XYf#\Pf!9֮zVIX~;X1gO; l= +c%V c`>ε@* Z`v&DI`LE`LgD:ň]D8)pkorp{.ҊqV`\]D„ G;)???Ll!c$*6‚F,^6 0̈-N@$XcN /H\V'p&`u>=?_zD薴+&'@6Ѿ"a37+#xnjdv򿬝_hWQbMDvbDL 6YDkJ钗S *"C)-dB^Ҁ 0F,:,FX=XDbkmۚD}Y((!ԗ=9+_ݹs=g9[ },8*mgq:wLϳ^mxc(J]S%s֌E% V#pcGYp/RkB!M]R+ 8`#Ō>_V9MI@> drISwDM^y+ H ߡVJoa ]4ֺjE V$tF$*J* [ZZJ(13Gz ֫jp(WU/AVuc*sVɮ*KhojZMLU16"|琬iUvnFt+ѩdF|',.* U{ʫ;%V|X@f*'''ӝXV%jxh5fc>ZNUފjF%dFj9g\^3j`)B{I V}e_ǩhƪr:VXmr%XUɾNmX`V|jX ViLmjlggV-O|:(ЩYҳ|Hi̵oHWXu=W*xX_|r6XUo[Ϋ1(fJ[+gYhJXVוZu{U`s~X UZjBDڱz%Vuڟ!Q#r`U+ƊK$Ywդh՝#A `,=zzɪem;lگjkXU U*F{A-& z~Tq 5㪘SRRcO_cՉtN"$'0n(g"GVU WtcjWR&o;u X! քpZe$P/JvDzʟᩱAV-թo.b~? K}d)0lnfώэ#l[qb7q}7[-eaO[$WaFsLL EwZƳ>< +D?VGK9jos OeQ#0NV@mL_d |X? FڮVi`٘gwG?hH>z Xy'S&DwZ:+Q'{)7KzX_bt\QE'j?zz,3bW@rJO&jvI$'8ê׉t VjuHT}VgOT/W=JJUU}p\#m3`k|\h^,JMOxWX{舷"@TMQĉ0#$X*՜b1ժ$!TĨv&\W9:\ހ,V/*[*DWUIP`u۰"{0c592X)̇RaaqF[g = :|`uֈLy2[qqd$6~v6 gK+<͌j1ZXٻU\칽/:"~&v6KB rۥ3YШ?Ʀ;5SX`BiX _VB~ZE }<[,"Ue:fZInk+6G#SVi}6v|vUZai\lyn]?\,^OzѶÿCM~T 5[>s<fٛŘ>%uIS\`Ng.`SŰ:QXu; }~c7VujU}@`J>v7V՚鋘=0V)A^pX+jpHXl(NzOTUA^5ƐwaڍգF5j@USշ߯VU)j?U _IK]L/CƴZ˟fD$fN^h/5! h޻%TrG"VIP*U֦V_?Je r1pJ-KSB 5R~B JJEʾf,nՑ-n,g'YPM%`5Dim,|J!j aՙQ,t>VT5Xfj)wqFDbn[r2ziLuij_Kj(t< !XG!wh?TVU>&VXMW [ 'p4kxՇ),I?Ar?3V.=b}apLYxv g"|*Gv@J-`Q 7YXuk5T"GUiV]NYΐ+&O>ZSUx.%$`(Fv$Z|ӫ+݀z/n*׳P ak8z{V}ŔXj6HH}``%ZkE]c]8ȓ7,C} yl%5zXDH\]!b3* ,Z1|TthE*頎bZ.ŃnD3*88%r_?M:~eY@#]Pw {]UOn0 v(ڲ.ИuACwc=$hBч}~Z֧bɴBkGĪ4W1j5YU\ZêXjUVɲY 4XV*#fiwk7oo{k-/\ 8IVHT*fUnV}dѹٸ7}˥RA;(; GI6`UUo}Qժi" j".RU_ ug _7`kjjjiE#PVܹ{HU9+})-Y7H_܃;j!W+:;53m_pW8b(÷uj'ҵ܄NRjEvTH=_5UfVcV*+M_=dBipah WpWbkqל WGEj9F*[IRzki}z bc6ovvv7wGc/|X-ʮVD%l ۇs՗ jR)no׫eJȞtȊ`E`)WO8eX*')*ΗX5*`=g ~s) 4{6ۂ FwSY*Ђ[Y3U QT*cuEr3H1m ^X5ž7FN4qxq5êޭ6g1bHrwfEJ;1!Gy =l%x9|84~MX-KY+6e e yOl@k߮w'D#qs.VQBGDzl;;@] 񨧩V<cՎ(4h2]f󰺥 (UP~ ׼$Q69Xf5>י2X@jՃ^2ժ UX=v(uU[vPhZ`*}1[}@ ~Dt>Mf쯺{rEUU ? wUᷪON!aaTjn~wTL K+8us=+ǵO[q: #l'<" IUj^*Jen9*gO<;FXmS{lLGa#i|pV^R84 VGUҪ@]$hDXʎ&U~3iϛU`Ucr@j̴O=Z8VX_nr`U8Nb@.Vћ*&@jɢ!~u c*UyX\ C)Slڵ6.8uX]d^M1GbJTmXUJ> jaY4{ rPWaǢ\^rsG ȪE/c4VmZOx>+I+VuGeT*,INDE"΄P wd5L頍^yrB3 2E\d%1x\"U_&VHUЧRT ]uq`ʳn;b ;PO?%M5i~n-*VK*f^UUWCX\|{*`)juJo}6J/g~j|{4[E7b(1pJbWVhMjEĶX8J &?'HL,5+UdXSAkxy$%:DƱE ?y$GՄQ"t>\,OVD򉢜0I¡9XͬX`^B.?UQ7{Oŋ;;{6Vّ~-7حwE|Qe3v,R *RQMfS_jUPԞpRAUZX4^ZPru!/$ 'J.WPCYQA)MfL?`3S:Q/ N6nSh/VX@ímjCUdT!JW(b'WXJ1n^S,:UqGr(b]h ^YtlEɄ8R.yqXSl$$v6YPDMmcH8($Zt d yK9ޙ;DzhXY)iA~9nT&djZ*Bb&HPXGDa4Fe*N')k~DUi(V js]|mkb' :V>i }* X!<;aaƮЌEUV>*(U* V#ؤzU{yj47qg ʡΈ3F IDAT B¬,j3cU7d'U)Sϔg\\4G!bޘ>jªS紵M+YҖN?!( VU4cXRCzXj@au_—"gI*"WE*^,L!oǧFLjul(:|'FG&$M6Ub:ըYSWfzUZ:04v荪~)ֿ^{TLJj׸kʆejXmR uUmX-J3ھkITYPĭT]"V;i-Ajյ$6A$+HgňU!S[ &*&\upReXeϦ:ݜ<𚣵WS|y:m]L|W(~u])Bz({v(13 #r;y^ux\嫥L}F.CTLr~MFa$1%r0Xh0xy%f(nJoTE,nPigiw4`kʅiꠀ~)ݏPH_*./%KJ'ZmV#JF“`Q0OY)^暈khz],Bҝyt:` @ k#j e_-O2 71L q8R]U*3prUhl6K+=;;+ C{1Aʎu1vZVbDO mU!Xs*cS ֙x:@j6 ( d!.LJRYJ5 w pX)UV晘*[>*RFmJ1BPtA;V*hjU8jUX BRNCP45vjVe c Qz\u3ǴAVZe7GjX+q5X%9KӰ. Zxu:JxnC ŮМ2ſ T|vƓ\o^,g";jx|WJ%N;LcYXWQG7A09\miT&q4'nehHQ.hJs'~NN:ϢRP]  ~e.7b& 3VZjzz-B~a*K\' `u\#}P{>Lpژӭ V駆|Z1wjb]mBl8.( MҜ8êcݾGp}=[yg&k wkVanaVҭƖE )za߿C!JAե-譎mSnNjX D ovol]ES lP0HE nX? c>(@ޛLjUc滾|/*̈́UJ^Wz#r򪢌L>.,ŠE' f fq07XX]O0[1 1j vAeQY54[U+YbTVv0j,%d+y4k&^] j5>ͩkjUbU"WUa~i `\UA ]ـU^hiTqjjPƪ^Z` $o#jrUkj)UfJJHg:aUרj*p5tSt٭W XEQ/j{MG_Z$V޿Y܎Q@誓)) ݦzVHg(ha:TSknşx(^[% Gv)~%/mXbK/&Yv,F qw8"*XUxmuĚyr5`)0L2c.r̉_^MAnS'س 'Gr5 ;'au+A/|d^Xze|vuPa8/!h(׺_V^'O#P`X9/nPTĪbǬI1ʓ## V'N4%32]^= ]<[xZT[+jZDjl毠V(9ZaUZLۡ`SbPHY`aKQѿ~K:+z t(!棷S1Jd?wBޫVqUj5UMECǪ<ꦹUXu]͚Zeb%JsX@VMSk$p3ՏUL,l X4 nTU l6k8 ma*j^l 5w4aǥay~b|<92<uF6GٹqL4z* 1bK\o,Z6f:BH [̇ Нx4mmIXC*u"E ~[rT'WTsL\͏>k`k.V>UUYjfװoY^xb:ם~Å>A^1{4!$P{.hGUJ_V_e*^f^]Cj.V뺸* j#q7fޚێRTIN4أ tqq!CʌUǢVZESxxN*tiE XJ8<ٙ IW,j5< q6J؂UPixuFzˋ/Opb#R3S_vv7M?zPM0xxjjJ08Q3NZ'ju/ZOj7mA*W-+NWX;FU[$TyPY2GQٲ;p}JIQ=*9{0I<<_jTL_VoU$\ Q][eڛWiPIR( T']jϷcFK 7Y1XaKjK}O5Tc_[l TVk7r (eU5VZdpU` 69 d^& ^0z5?9B.O#X{\=YQuaK8Dž%8Պ ǣ^|pR=_Ԩ6V-SLcR}ƃt/TF]+4M*ju%&tR u3nרV%R~kn0*X3V&WWDС+@IJjDǃ&9EOV/>Ma c'ǾДUƝ N/O&Z=6^=J3݉ݭ9/WF2a `gnU<;wD:s!<41qB Xњ:xlHa&/qb'rC\ ;qA>CZVmQ,݋X}0ۧ.ˣ*To]*XVz UǃG Fd@6xcA5*SM(SX>Y'34lؤ)-*Uqj Wz$e> .MUЛG1aG VqUnl:Xށ?ū8$dto+w:Ic7%ԪyooY`UYq`5*dsC8n~2qXf֩?]N  +95 OUYK띫fkDZ˪,=2pM>b5ꃯ+ХSW@Y7H,((/nbO'm oӲ* KMݧCՒb}5p8F'2:oh#ɠt?[?ĩqK˚Ul8:C]aA,YdFq!VCc66ج Pb C,ޥMe*C]Z`QaYHZX ĄҰ{ιwޙ-WX$;ҧ=i&Ax>UFeJ͠d.*f̳?; }yiwvbPiޕc/-> 7TGv_\\jdrJ1U+MXeXzՉUpi4J˩F{M0 ].TT#LT[SA꒪#!^8jՎU+VjR2a,׽y[]>IBf?8RAT[,oEfOPqiUGҺL[Opj:>%ꜛ[rzm Ƴ$2k0Þ*3&fU¨ʰ:6fM HX5&xo Q^@A10 q"Rw >N0p2'Qok2kMVh\x_+\aj26VSˍzs{~=ǭOw@`/`r ;°atUj\&-aZx=jգKGU[zVbȻU{Hxcc jl2P:H-V;k[EdHV1Wof :j՜'XGS+RB?9V2_̄{TWG4' \1c\3HYY}*-X%NQ1{7g5j>DMV n"ίsʒм"% W6Qr*29^XٰP ]`oa5JRC@ֳNYkr_RZE*No'c)V٫eEJ^V6wU*g8[RR6Th.brUG[Eªϗ)V 4<,mbbC&V=FUTU*SkoUV %k2O˥U"ոz uH['r1,izj" CŵU[עFUTije*5ҔV0L9+Ԫi5_BX@8tǢAW;E#ZM‰["q5s:EX͎Wt"vAՠ:Ҫwu3Wi-ޑJUPbD3'߃V#T8%Jy1\j:Nu;r[@%۩$Vհj$UC]Jn6W? ckwctcXU ^#ڿ[3bFqu֪^ݮFgeozZ ^mj^Bif`ZY}Ͽ|_ڼwaW77_/v o^fV>td%LuSrr9V ; `Xͬ@5Go|{C+_.4, N7/j(J,'6Z/LoRB: ,4 ͒Mh iJZڲ)uAF0s9j=M$ZMt˨t,Vő|+ӱjpaЊ6=\W3VǎU_EPϭhx2CJdy҄UWIYq֪ ~Un*}E\`ezcF[Ck4q4S:G^EV>|76X WX5Uk'X3Nm5qH}-Ꮀל Ԫ PРjs0J R9.aT"Z>_0]v_#qO uro[H ^yN\ͭ( ͛]RVxys=tĭrU@N>\t?}Ke@jA8];.W/;OgsCLc`tۮv˅ѭXE:<ŭ>ԝ?\S9͚Vl+ZLR~GCV\%3z 6;h2_ zŵuŪ**քX\Jȸdki*UaC . vʕU[Ts-VOS*>Lo-^Z?S6>"& NSV{cEME1 X5lNLSeKN=!anuaRB1;A=U#V/OvױͣmwEQ~sC@Ӈ|!Sa\wR[=@V–jdڧڧ^[WDjKz@Z~pMl Xi[EM;v_P^jevJXӿ[X ) 9U%VQPf 2aK?bj6%zW N y4^*U*WWY >Y78ɂБuDSmuP NTK!ຩwQG:'gPbj8.^N54P&`Cqf vZ21yކ;eJ/OƩ(M i5`ʯG075NqG0S#<2Nݽ{Ν۷o/,߇5?޹soee5=9; 7rggfOqb`"hS9:r667߁-,]O+^/9lJTZ,Kr| Rg{XM9WʗXM{XM U. aґZ5LHɫ/},ʹ+'/4j+[ЪAl n)W{:"A*fZUr qz5ՠwW #\P%ǩqJ8N:4\kx1q(ALo@ƪlp l  "ՈMX%ހUx)ʀpjcLFUkkwV󒫋w"V WrZzt2eY<ʕ|ªWêC*TEWYiU]~:V7)#5Qq&nzY顧b2}C8 ( Ru`P/E Ui]zsgQɉId(%0i'OlXTU)vҏ7J?VŰm bj  ,@!)O&K\s0[0(T I:LTDͰWA6^p4JRZ-vÿmzJIT0jڄՌשd-jNy7wˆuØa( nXb5axkxfF#T&U[iPZZUR>`u*ьbu xֈUYǽ*33hfƁTu*VDi4SGB +3V- Ц(e5Enl< Wu}X ,WP֫>Ɏ, V{ֺP9㯀iXU VGJ!bVX0,hVU UKպ5 ;T{z 6KrA/^;2b*Mícq&WE6`7o Kc@f6VaTQO"ӥ2"<U,He6/7~,J[__aYV% `2#07qe n*RwB!Mr/,!!G@zE`~րmW`avv{ wVJD3fJPEB FDk[ ,]|@NuX>կ ^.x6?3@NMirG 4DU<_@6U5YH~=*7S$WgIm3AA?\jRU^:!i; X1CX1&kƟ/Ŀ+<+ }g\`K!wUVcOs!awgpa5Q-N5〰XKD,rb77VUJ`4f H;T )zUXnŋHReR9*ګa&3W/ @Z5*]UqbZ@o_v\V&iGj'kT_= PʪswXM*5+US5Kf=<^S~VoZbe*SuXvYVHD\Lƛ PlJjc,ªUVenU1ҁxB3LúnjjMRv \EQ-l~U[q/{#'I]|Kꨌ 7Ȣ32|*OX d:̜:WTQTZuQxqq*6JU4l.NNONJJ2—Y2VᦥS"UռH} JUwqusk#ƻ,hQkDeGLWc4j0C`ZbȢ E U*D&Y7n"m{&Or{dM[jK$U?}<#+sY𲼼rM?_0#[?MGtX/uD1^_%sum󺷫.qG] =B iwXź\?Z]}1WX_~{ U&@n7ʱrrk@sV:fU5X| VrV3`Z’vLϛLY]Ud͊3Q *U)G :T"bI5{y;E&l+0\xU.E+U, YkV UoԝoH *WJ`#V e%e95Dpq~ζsw!Y0d4NRkf?M PLe`R+VAtEw.UsW?PE _Zǰ}/(3V-V1c1LY}r {Q NJmj(VKY[:t3U*/h y5xj{~/~n-0Z&_ԡݍ艥jWG2x%FnԪHZ]olOsuooH UAښ~m+]tQUfѾS) 6mT8jjKȟ |9Y6VEs V&%WkR V5LfªC]VZ VS|TĊS rHR3Se%Eۆ׊TuL*;)}6U{ײA-^/Fp%0 MÃܫq՛IC9e^ W8Rlr-Vh}R, cZx;^UcX:b2CR@RvŽ KAup՟7^;TۯWVf7^Ya;yEҮVsV۱zok^ώ4Wj7J="#XW;PeGm:0^ ,b&:*﯐Jəe׊ռ(t w{G1d;h}'V!|{c MԬ*s҈n` ۔i}TmdM UAOHUa2J*j}7V{2\43Y\%`;PuƿF5rURv:u`uVb̳7Ut-sf ÁRg`Cn~A굮p~xzl>uLQ*<D`uZ'HUpMY+S(J+[,6B7)JY涯r3U͕ԩdBxhҫ5U^݊S7-`gy|s7UյۢSwjfX@ ~hZЫ BUup_ܽEP][*79#35N,BՕkmG4FU֘Zş'} ^VE5c D,/J[^P>xJPv)QjGUiRbsCz F gGo82^v̬ .:~3KLV>Om%f nH[bbu`>JT-Y>g4u+cizը[lu9 (Pk~Ջ\I՞? @Vk-*Z*U. eB+g ˳%) sYv/0QcW0 kRiK }v!c>P1#RUakjil5njNcf:䓔1B(MXmݱZjHB&a/UgT;'g6SXQYeDVulub5iê f«VFSwb5 \ѻo;SC*QUoc}+u>V7I0Z R W{A!-cV6`USv* D/^xL4% `K9gP8m`ibښꁠh3S"X\jT|/I^)Ԇ'YY#nzik}O 5 pMQ3&}6kK´Ef;c5j!©P 2pZaeZk3c5V;ㆉWqߗѡd+5M!#X( U1re7M1zDvVX5VMQVjHqun|օXstQ)7]w`/XVvo^*Z5S4a fYJV"5h+.ȑ%4l(4R`$6TWP.Z/k_`sfkd#W;T]îq$7PfjC5CM=9]`%Ry V\Lf#[c5kSsVx:1CKt.UgU`P@q *~8Vf 0n97C7j-V[VTͣ CcuC*UમI5U/ V~J}j"cKc p0 IDAT`>ԝ t_QPunT5w]5[aZCidM*W3jubSC'׍58?JVp* V*ً;eՉ?aEBNlZ1):Lv~!meyNL1N6\ 2;0,}:B&v`ew/vA[(%2SvXROԈFbJ*`YdsI4IM59ܛso>rxHj +VX尪*[U~^]Zej\u*/jAX,ғ5,Q򱪪H!MZbk[, =,"j31WAU EX`i6UIerw:2`?G*#}!hB'{"`S" v+|M b#scZ%GEWSFRq GݩNZ G#V?ӣUENn*@9>Vl}ee NYWnFˉR762 UnEN"UOWlfcUzy3mѯ=gX6^pz1wKϦSz5PU!bLS0X+d.X 6V}0屪)or?~ 9oXlI7eZ[0QNcUYl5Y;ȁ2`cP) Ybѷ٦*, U*[X*YAf#)hb+`0+!gkL'ۘERuVUa!LfjG+wJX%'W-NU H#&a;pt Ǟ9LmfUclJ,,u3 U!kA%֪E--=VZ.J .l)Z9}|BaZ rUQt"!)sɄQh s4H;JX%\:G+L2"n@׆z&r*te VBrT8ڸF Ud6U;S%xSDTVx+Pfj!؋k1V?bi2*{cf(dpXJX|C͐lʝ`5YƵ;3-lP3?(aU6]p;r/,l\ޏpFnVQXѲ"Z&%;{SU|*rUpV$X=L-bU\PD+S| *GX{;,U|cGWk\i5G V=** iUV8Xu}QZ& TTm?fj+4seQV5%A(,|x*(9\ܷ 9s+^JjB p}|d-WQ0fSZe,ί?{nᇵr{,Φ^Κ^6{\+xJ5m=r>g)ʅ]K۾{b^;s`B_jLjo.ut,4c`;9B`.+kb/9VjgBkVo.Q]ݕK.kgr ~ lra/l{9gQ?-<[Rwyf`Sgn'[zݗ gy,y59hFϵBx,k v hm?P` &Y/߁VWpT۷p+G-f\(/ûLOWqȉt. ~A_%I#NqLNtB$37gl@PndkJ owm0``Gy/`UQ)oU%'9_Rv)܇x B?ש tL9=̺5Όh|s $W:G}4)ѐ -d' lcdzՐOﵶBagc[rhv®5-a2L/:0>î2i sr&O0V)?K aU L',%z:*x䳉'KDޚtXe|?ZDR]K!wbdwAxejztxP]XK4ǹZhFXЊKU0rH`!*vkeU.:' BȮ8Vu&: r0 C /6&CkRڇ²) %[8Z4T\bGRRۍ\\R²}ŘC^z;M'a͝/͝{9jXk~ ՕЫ|U+j{>T9bl桜#i G|ª)1WTAB)cʁ@/ *ٜcϘ l`8YY #DIϹa2ٜZi4F2,^Y(`XJ 9, 8 5o B:=XdnFJU"ބV̢2¢eL9ֿї'amksU"qqLFuiW66ϋ״FXNgte$yXtg(LEb$mPVta6E -X-c#*DhrnQ? I$Fh#0;JOy cf;~p R%RU>ZV/1H>8KjՍr4v~"R5b SzI,"&OdۻFm'VcXea nX6ʞ,{;:~{a W;B3lvtaF圱w$zđ}ꅟ]"|V3@6URJiV֪2 񄀁V} S{U[XՙG$tJ{/G5 LHoo*,5Z<]C 78LT,ԢFIȻ39aiWm1tZדi\ٛVVj㣋 p'IҴaZ}_!nT(~6<ޅX56j|.#XY}kJ9;`U|ntJgZIyA`4R(aYqRju GɈxr"<3DL/Ts-mRW 3雵w?S m`l"Q.VMь0 Keņf7Z>jy u~\Jk =~XQqKצj`0 ZՋVKj ?>p0ntVZ5Vc! NUGM‡iaZg8w3gζ6TU,<ι|EG!q°*J.=5<7:gJu[`[/ ՓGj4!^r4\H33ZUjgYiㄏ` ՁUMg3qJ2L=TQCZ U̶ؽ|Nڷ@3CBpv Ve+fMնY&R2mߜ^_,Vj3E'ᖻSQYO(Z,Vߕx*6INP ;y`5({oRU:GV1FԷKRc0UlYV$nrZc>=jsruE<ꔳlV-^mPbN};!+(Xu>/>݃cmwoԚT=n^SE1R'uKL/OoԋW_.æ$lV_>+5b3M2N>FenՕV͞ŕT`dLEUH%mqS-+ܧ+TuyjϮ1fTQ=g@*Ejs0_VsZZdP1BSOªzj=VXm`uU*sjJwhDY.U>ooȜp"90W;X|m`5GJ' '  TQʮ́R07A,/}3뀦⩦jhnƟN*0S%I^Y!Nڧ0VxS:,P j9puy3+6OlxQrT[doŦkbyӞϏ0.G7Waո'SӮOb ^ ;٥n`K:W٥JBM^|?9){"ra(֊!{8-~Þ;TiYav/h뫝*$'-8O)C;Ps#WpmU6aV_nN/9EsA:ՆxrсQ; ̭Pཌ)lB !{LNRݺoR۽6N6NxUSVw׆p&׆T?i5Yt%}}{m'I\zm:0S9B=V?é>NtB: q=jąc(JzF#Wa4=R$ B jX?_/^뒙᳇T;3K\<.X]-auAcUXm;ݬUd+^?Hc:*op.ӧVckV+LVa+mPjw.PS\u}a+PuKMA2*dBf)]QkV#j\N-Vc\M`)jFPͨz\eʣe Tajjp,D+%wV->NY */MG!7m.rǽ}>(JJ9tb=c oک5 쨋9JjNMU^f**<Qzbl UzE񧑬E#..VVCU3J+b?;Չ^k Z-y^x[] U_ Pʪކʯ\}X4a+E+7H:`- \-zju)o-,S}bՓ쫕 ޶">EU &WڪĀUspkHB5&uVep_*+XUjGq"eR*O31u=AlbGR|yήIGn?c;(De7%≶FvdJ^yNܜz`(}8iP̪_.W3j7ճَaLL& M .N) LjLe׮^'6z>ƚ@ IDATlcT_NaKK䚥YX\\ErՊqX=`᪇N& h@l}ul˵)vf.WsΉ(|L `LU8Z$U~d*xũ VV >vHluMi0,raZX#GGՙ H$Pu53DXMxz>MgU]npTH-"S{pML}xCN FwժU^@bPuUyea5!NH^m)T3P0y(CQ""3k^ 8VQͱ%S :NYP6LQ`W_b’*22M Rct%[u?U")=1OKZ4X kN4oOĎ~|xA!D_T*y)j-2 3{Oheѫݙ cՈOlP kqĵUY]opJdʯVHd[EMG4'oo炪3zX%k}N#6L4~ }}]6|NKol][U%Rx,')C1ԣINLD->BN[uwvXVjOKj-ց*\eXX %HN1ª/UXčˌ?X` RxP"ӓ,Ls fHy\ƪV~ Eq[V;ViW."V3S^|U[C\3mų{  շPّ\J$'O֝ZאxWlXMVA/jGjgZeUJ'V>킪T/op,y|\kjOT|9,uakI`u<4rĪS2Wê8V׉UZǭ'V@6p o^kCZ~vg_ᇲv^^B8~: ^Y svP!WPeEoUZ%zL ZjXRFj8m(F͓w6-V''!_Pu76QB9F뀪WV?ުq@][F@O*/Ѯ +qjХUUpbvKVIUR=w^d0?#9CƋ:_şXIɡs38#rOL{fH=>KP"V+[ U|;pW/ӟr{iC~]}aIa} ί~V-Z;-> CRi'sQ*r_c˧+P^iVM:)E.MrhIz6\^Yyr ^[𶪿/5CVjxGWFGrMMoZV{GGv~LrAԏ/YBV}'>YSƒ>3}^q0p Ͼ0 ֻ W:wYeQJ+VdF [Fߐ~ M+XogRkƬг]iWS 讬 'ti8זQV &_ ! `W_[XpOB;zqx8OQ Uò9rlX-uXXdJ5w+]TFNQ#{XJR:٪:q? bUzYZ@bUע50踩7+$N(Vl XoA,IVlP 6V B e/&s`\H0x$8{BJy0l K39}!=½ ':uHzJƻBqLC$ %g>PgA$?sYLv/M"5}]ыp8||zl/nŐ$'pFO_sS}~l9-Dc^2-(dGݙD_O:<+3fpj;ߊOb"K& 8E$Zj5(U:h$"H 7+0.cԍקӸ1k8 AӢvc<˦:O'Y:Ymfj12$ [].w%&ljU4ttW< t^-,wZUReUX˝-VCVK=UW[⍔ Zz(TUvU?G U0fQu ab׭4̢0jGKूeYUc?;9u Mˊ /(u3^R$313խd[5A42Y lhTuUEBoj1츛d%{Z_ ԪjEfથVoj -񉨤Y(n{b"X=6wmK@ӓ]N&kCgzLY (v&'.!@i.0GdEV1BjKVXGfCV׌aY4 h/ML *bN =#$$Za.썇棄Ս)Elx'Nh4 BfvUt45ͫ[ьUUaFJZʤVmv̯Voav /4IV%Hf4A.pc6-*c7o~"UzBʯ8tÝD ц:e,p!y Xe4[TMoͬY7k֍ZŤjeRR{ww層kΝg;|Wu.+jaպo_\x<nVM5Z*3(hժDS%VU&@ooz{M{lC.zOU3+vm\\uWfolX~)t||֟ƇnZu|MD> KhW1&X|^`hBT-ۈ<`52E%AY/]XUU܌X=RcʋUtV\LauvU KMM*-iUpNϨkJԪ[R)RUNgbMPz~gv:XgQ&M V!Z-` ^2ss̊3wZCVC'8`uBF9JJԶ5V'aVV {(m:$:dG'b"X K(*{ў,X(7VD"X}G8ߐ^ue@U/E=U{bջb5UFY-X&8*ߛ\uO;QZw;`C D.l"AXeP 2bI*0Eks ht,CN^W]TU`5[L7S7ljLњ|&בf9FxL^eGDuok6=SQzpTTӋUW^nnhHXu5V'B\9G<7ϲ=jVOU @Ke;ѴzRaUpiGb50F XuCZu:X~xQ8Znjͬj@Ԫ XXmW:VQ_=xߢssnPe#C"Qo/VN|R0e1GtUz*RR?!&3wa}v͸y,AX _͛*punyn]]j| `S(߿矛˽ύU4n:Vݩ۲@*KdjmiHzIfL!VQbyb.~*;+ r TZ*JjjE [Ʇy=6歂F,>q;:dTiW9oT#UVij5zj\:*) IJK%WR +ۘ_L,KNJjߚJ_ejxjN&YIth cŅ^ g* eibfSVdj&>H`ujnჳ39 |4oz_}`c `#Իϗ!TF]ڡ߇>[vꇟ0VC8cs \'ɺ/WʅĪ9`J3J*^oުV'V.VU}wT.jz[:4@ꦒ2[# BZX-癭Ss3-- L*AZ\U\RP#Rp[vgt,0V}EEy*ۭFoӭI0>PXh*5]<OUI4Gv ݌H*׫l\Hզ3j!YI>w*EX=jU5~mZj#۴Z#LQ/<5H[VYSDaxBUXn)_E> xYFc$"k2:f1:m" Q@y V}-*ϭ$~jbaRz% F2Sc<{qF)QlrMa5?Ê3:J 64\nU IDATfUA{b)c/>{XV6Z[a@þcRq!m1CgXwr !+%Wuyt25/WFa n v_4j>oLzT X-hbL VTKX× ͗n} 򁏯Z=db=Q\ÔcJƄ``VJ%JA;zP*^xct ]̥gʻUhEDU4<-Aw~tOV#4Vpd'Vhَ髣*g7Pzsս[9zNĪ@䙓W+L xwuCzn7bƬZ `FqG9'7$u.56S$6 Z>K l^dXz {JRKf(iRro+i;)B1z%c#e.K5Û W+@ pD ΖyqgZ`gժS*-MhX9e S%JPUeu}Ҫwm6Wm,\a]R6/k[9"Htr僭  $ddNd;L)"M##g:v3ZXʮ\L NXǦKm6*JNP*HVRVM:qêqm@V/ϯ\eZ'CȺe,h&PMƻ2mGVx԰ b?4hJ5w͗@r"vɖ'Rڽ?|Htz}Ue ۗgJUǷVjMCbk<2Q l%p\laj`=5Ȯ]w =bW淪ڬ9Zc`jkU<I] a innut*?{-g-2dY3SbugS0VݧViy>̺)qruR"=GDU{|g"SqjUجdDEƪbS'zbID:EX_&ɝzm}zG,olK^**a/*`Uvpw ַ[Vכ\cV2[Y:󤒇L<%j`U@LmRdsh꯹0qy m /&*XiU^/14Fw4Q <c}ӌ&Ոc#ƪ+ԥ%dM&ph_<1ߓBk:R+SURf Պp+aDh5Xuop0[}5Xl:d]g)Lce*5*Urd~xU3VP`5ӕ 擗o^wy9Dfs/_A&xEߟ~٭PT䓣V*UZ)ZJv V 6&gTVc۷j &GV{?tp=@;TP@gՁhx,wAUS'Y|c5U`*s\X#U7X޻wʁ*@5WT+{MXI^l\l^N P-AgvuH&l*J"t ş=iG[ܼl&DƵ@A;9mzmmV5W5x*K!K3lЪ":F0QvNL5h*Fݪ[[|4 ; .5.UܭO_ YphjAؽ0Vܤi.ituu篈g{5LVhѨ{f@Ȼ뽽3i,ϤplAS{{X€)W+j?W%V3.݊گd`xϷc̲RUvJXh%.6U yeC8Z:X`Jkݢ2LmV,VoU*EUS>"uְ\n 0h2T]&AR*g8VczL08b |+`k Z#ZGjAԍU#щj?l?rqn,OՔeS5Fjם$hա v$'G9Pن]VwUVmSb맆o/?9i]43'ag]vqRQDDNZQx¥X>gaU NYV}&U5쓰zj%1©BU2/JW$ `aU4IAlL^(64\e7r.rVjUgWŏ'e+S*{$W3'S9cwaVBtĂ/{P?_U*/>V0d=1@*tW3r[Nܚkj}-07 u4 BX'zfKf9^*x֊ZW&/Zq-J ")KHjqOŻ%?Q.&OJj;Ej\{)UP-Ƌ1.T%JAOV}}j>BHU VŎ9md̈́Mj_UmC'] UՖ PK`Τ֯E O^J~ؙ'&ܰ3qZpi$h i& Ǚ,7 Aq o/^^H:sY?)Lg^/G3? &`sǓzթ)RI>ѝ p>5䇟tƫXR" Eo9~[!:0~uoVnydL`cu4wSdg2keYgNT+aZۘUs MOw:U`dZ0XhX6jRdZb2 .jLo=Wy AZRX]k~n.@3RiCZٱʛe]vCdϻX,[BTr1zZW3-L: nj>/_Q_fx:lfDmMwvmc;$*r<  -Z5KByY6OO!4%x;:886GC;㻃Oilm=ucFï zޛ&ݱrc͈o--G4?ZG?fqQ. ZsNg3ES;8A~H+23&$7j6%}QV EgtXYZD:+tGxZ<= H,_ eOUq,V-kY%\@W}s`Ur|Z :hS!YouXf@J)͸Z9nԘ|n-uXi^6 Y^:ȔU<5)pX \}jUnՎ25P^F@\s2k_Vv!n6 d U_5tܛg>U>o(`Ϥ a-V7w0/uS,VO67 \t;ۜk;Z}dIGom2[H]#_ds2+U]l<Ƞ'c|&섪<-X{CqAjhEDU 4j'*oHb+.Qd`uR+(4<ҪdFV.Jm*U/`=<{0N+w.ߝeʫxJRTIPD'Ԑ'He̤X-L b\ ktsN$:*VqוܨXMF4@[̵Op~F^jHUM>az5VeV*Ыqy\]kV[ࡁxN -H1 -*Wի$W+e1U11MزyϨU͔@Y,} խKBp&cu )e5}pб# r!xB?ͶOU8L>@CD*o?pF U* I]Up69pDNhLfsYiWz : U_'` Xer5ӀՑt:OCjժ-\^J;V1s3]W^% Vd*N ]=LZf(:4kP}oZ(wꂀɺ/Qs&o]U/47<_(bU6Y1+ NκfWQu}}rǪyf*'$ Zkݱ5 T5V"Wѱ'%蛊9H2R.qrX}sUVn \XxB!YUI,=$@b:|5DK_tpa p䅬BSNPD 5V#XZ71;ȯA.`pb5b`RxZxT0F9^K?K|Գ:e,S2Ss(V@ V p#U}c"Nwm۱zTVnPSCkZշrU13 ̺sS*.Ca51 } VɩO_F^k V?싑?|;Zo=M]nNx4>Ώ9.:>}6l\za)(h7~f]AUbuXFjnbaս3 ⊞V_-}5k,yq?Lμ`N}Ȳ:vX9LT[$gg}Sp%7Ȼk ~tTųRݢabC'יL 8o)-YPPQWOF0wx*76`n[p-k dG*J*Vij20UixF#S_ҀZdS_5h=V}{xx|pZtWAǎ9Dgl5ΪUr kgG岉 3jmmՍv"{uUrtw!a$tՉjur`-xvOʃ2?.#iLXGuۀmdbw"qĪbd 9:rӜKt'$MJ<6 7V;-׫UdcD7pk+T5r5wY̰bU [B,`OZTE`㯊\Ee;ѵvWyy9 -&jTaY0zF@o羾Z9 %@Vj\;Uv`Mf OT큫ݭ|zG7:Bc20-V9*^U E+šqb%~Jd}MRz'ZZ0<*̩#mVBcE ܻE"Hp"r! @o0-Zމ=NF[{uy&quZ I'<LYVU3=R"W_Dn$\Ca]@?Tb uoqر^{e2ۍ9K6?W甤r>V9AzLqJҪ9C LeࡢzF~40bJn*BsɳX}&Aէ\UOVEig?#r?J,\D6;۫r pPs27s&&E-uj \ZŬfEg5oC 7o66YbwPdlRsZ%WV0205Dz@x#֬2~ 5_T\(V̯qt>xPM7SnzziSPÉQrbxx]}Ԃ*zZY浘ނ4 Qg]GaQ4@ Vu^s zu[UUy6n5yHTd6aWE^8fl|PB2\.v IDAT:%X혻sXX}B&+w\͛u%#\-TZhXꊶ^:ށfYX_)Wgb՚XBOE -&TQ\mvFt,*+]"{{,GOZ\C WԱjSI]j+2ߝ+U5޸E+W“)V{MVVےQmVe*7BP{k+ZIS +>ƵI!',{iHFiF'U"wbVTlpWqֲCU9OgR2VT"G ͙%qu>TAꃸ!kz+zҢ:y"lz@ԩ` ac5ڭsZ Y{:%.r*XjpR*Jfh:@7#S&0ߵc'# 5ukPe(&V.7jnivz :*YaW9نLn]BՔ;(Ć,&6J0MU̵c1fH9B]k%f`$*p}t"*UW1̧SQ5$iЗp1E7YTvwgT5˗%jRa0P?>ͤpP;5|,Y*Z]=ZIp*&K氚4ϲصU6ƺU֖jԱUg X]__|t?V ՙ{ؙW2[=T?(V?.oau~Wlk FեUWHTt[ީcUG {lUXƪXQRZQBb.jtkPmW#U̥~!<2A>+>03GUs<\NLE*`$c#V109=u֍`tyj7nNt7@Cw3:DK" Iլ;tI_VfbU/.-B֠\e/ yvٍWW7\j*ͯJ6rm=uXBȲUC0Vp jAʨZoY6)T8CDlF_;VOkR C7􋧛!tRE\}( GlHP8#J(ఢ+, ?(uCQeL :4VjsjU_>kJô+GhX{Xy,܋Օ}C2;r3MVlOʞEu-L*mUԩO- *= h7PWcUV#å*t+-ԈUjhsݔWI@CF">XBU[b#άކ^m3`+bcuכ/d7Tmkz{3.{zzU*nj` X6O|zOyBm uX;ʲ]eTT7=DЮr\ь PQ]jT7_?pA"B_OԄGm@T7wj܉+ZUvSE`j„n$Ms7rkYi+eG>Po!//eZ\(jT)ELO1pBH79(MG\8A(F\|@Hnf,5Dmi_ֻ~7P>Zw;Us`ų3f[5/oe+;)b5uL??9V\xߺWȿ?KP ?c%rudžո`AͬPn`VH#d*_h@x=zJ,mxaXwx^&:!<jV #G]ug\V(UH:<2p^ll!'R08JbZmH $|z& n8U/npbF "5!Zx;Fy5lZ幷`9K&ߚ<?]`u+) VE'ٙaUG}ᩫ,U-Gb*zUjS5ꂕ3g )%=j6M2('CgjmEJ;cTPMcunqd`USU <|0X Xܺ: P*S@4Fy5>VU4pyA:צ~~䟇QB>UST9-'UHQǪWjuVq6]r~YmU:u,XC{iGU ÀZ`UZ-V$wBhJl6jJș*TB(+2Pԝ1 `b<U=ZRQ.+Ý:L'6Z%   C+ꢌVʑըf\ՉXMzprWQz~?"WO/_QHի+dԡqƿ/U b*Wʫ5j ؊荦=V?Qu]G{yT=Wp]K[3 VI\}V~?Vʤ꣼T͍Tqu Vq-_ŏz/TAVj*]`]IVzQeWlЇƒ{/(̯^+j5T_.}c$ѫ"WHX}뽣feel w 1T֍"̶fDSN { 8@Z*6êi"9A V6Ɍ _&~R04w*F6BZLgrY}sq>m7ju(jwQģd-|d(aKc)nPΠY>pdFRt/e8v@ +y*ժj\oU%0i0pFocAn ONݲVlpO塋U ^TⰊLv6%prPkx0S=]+I g~!7Z%*qa`4懦'&a5yjS`U].breUHSk+'Sg尺8jTcX#+U:Wq^] DDGu׏=ydtA9glI z70<'IJѝ1mWꃀDxk;hm-XVUL 8v7G?xŭO$ ͔JRwyH#?6*lw ͩՋ zE0%; hE!KjfA)Uk;% g8 U=3*.I[Lo|z2(;YWU~)*a?'VJ(VV YzNf@ تZM[_0PusJU7&RAZVLcuap5k3*G)) MU VM Z\CPm LUY2VRUrmU-RA+~_srh |ȹhW UBۋڽʑ+ʑU"dj <vP| Zx-n |K0TteHۗ9+V"gIY Ԫ805e_!neUWqAq*KVbEխSCq`>Un9/w].2XQbuN+IU~4'P UXݶ6sVe$o:J5 Q%ݐULMܑd:9igwzfь$"&031DlH na2-x.@ E"](JCܮBzP{{>?T:qq? !)<^4JQkJNJv]eT‡7?F檀Fjrz{\ikS3qϗK5kRiGR85zp5+HU\dZ.[BE aBXSDdEzV@TBkN{ZYG4=2Uo/&8XpVtiTbu̎z8e Kp8ue,*nm3Ѱ:o&@Լ`C`L@CGXNb}Mr%¯tjauayjJPMiBW}bՖNVY]-Ex@H\bw"6T4[aيuj\7Q Yjbu ke".@'C,MZZՊpF&`ս®ѾtV!ef8VG@V7~ :{ˇGz\!ndf۟:SΔ +d2ݟc:Hjk:!رŝvZK`xZxIX=b7氒BƐr pM걺Da Y72-&[n W%DElW&k7BZ[+՛b]>z}t7PZGTg%t/ﭺ:QCz,q_(zV3ax\wv`߿> IDATf(vj9* X"׎U fzJ-8CmS4K*XUZVn3%j VSퟎ 0@rcDUi]P$7X֊i5X2VbUU_5UkHUrY.Ҧ80*5P.; `u5nŜduuMW p;VW\a2طX jUdjVq xư;CrjQlQ5*u\Qڙ<ʾtjYnX}c`<.8}_L'Ns<*yZ!W_=V~/Ҍ(XFDh[ ;zUSX?qVWVk{~t/#U"ѹ#V񐾈 E޷JՑA]:(ZHմr7No5E(VSևbHewmBVA+]l²_*v]l[VX呪mag@yXTRX8*>P' :ͷ}'߾ƍo_g혩 ۆ[xD]hV=VV\ G,X!}Y\`C:Gj.jY7D/$aulCPJ U p@j/Yۏp*VUy(}ڪT76TPZK47Vl,غzշ^OtRZ]GKP k>oR%&V\/sU֧&+tWqjΞUk4bn! W7a VĢX})XJ*UfT[n˔{7\~? .O}WFyZ`Uj[).˽Xk&b*Yjt qLʙ?gLW_y3H)E)iT*bkܳ>srS d۶4f W3iUͰDvg#XͅXMdF*k=UDVhjw$6N5K8VReXJX"J3X*\7Elއu>VaʬLZQR#'qິ.UGhpNXԪ\UOU5d @KlͫtEYU:9UU*j=uXenRd^d%J&OWXO`5EĻ #6ym}fұg~՛v?Qwd;? HxKqGhskL0IsYR;㮉'xŬ4k]%2jD(V , i?̜sD=y|*㭾N0(n~kjkB/VNQ*{ - CiP{`h> 'nFé~O*=e`*U)VV͂J\ VC*XZP Mj W=Udd^V1W >HUVQ!EՆS&kRŝDCA=Xc }VS `M0Uuv,ïp\M씕U6h *"u+[;uvjen4/fu􌊿zU# j\;@U$U VVIZ רVn/Β`%::щɬR*~32x~jz蛑sjS`wEx!HxYʂUQXۢJXL&B2m@j*QaUaUpX: gV|#)UVVk)Vu>>c`jՠjql墪ދՐId/L&YԪE ~Z&6{S= F/$4P#u֫'),MRgUHu!Hnؾ>U+V*C^5V@ VYs5Giu+uW$UXŃ Vw\ݪH*F7WKnƳjӗ"(TjoxJ-n.kz( 𸛫XuVjV(nJzrlcc'/d3*34uuf5/}vo?$.3ZVm*" jVaMRn*`M/Fu(z2JZ*8UV?00-~t|||l%CZwѿܳw7 :aVŪcI0cj]] gԩxv9Jy4VTEebU* 0|]567`*Uk N}xft&j:NHIV lNʙ}t]{r Rӭj]+}CG7=j+ 䍪:ifylLak5rU^SzUbeɺx{[U2c̪'@׍~XI#VC Uにa;3[K(X7ϼ5̓OUNJ?U. t<0e_dVVl,˪:Y1γFxjTsC`F? :b*.>+,L9V~tztzb0:?>7>/aU>O=7r֪V/pa#* n4AU"{Op>:[WI"{؜T6&Amkkjk UoƘTkln@8 `+YTr-KMWH]1 X/~^r2N-[ jG Y?"y.$:VXkm\Opy:3|Ņ 9MXfj-`/ů|~Vqj?b inЫդ^͝_^`U9X")0r'[j=V3B>~+cZmiiٿrhf##}.?71cjbtz0þVF`OWͿʆ7yW>zaPs CVWq* UrQj!/X k[]k`@vĪ>kUy&j2f` u'F|(M=:zT r42xbgV/8H\,K$T* ZFyi.0XSwEH*z0Rxe3Vw2U<95?eC!*L%zUSojۍFƪjStV*_{\XhUI;bUaXN~ƩJcC? \._|mxkɥÄrz5jGiW6mX2js'\Sϕ*=VԪU*Ϸp@)V^RU'pzP(<)uS&L8(fQ4!W6xx8wrf4WO"@@3 GfʂU%Vr>\IVV~52 #NWz4UT\1>\ ફ?qU5dQ|e ҙ.' dQ,P ҷgG*@juC-K0VDG.K"VW?bz0ߝ QjԪ> {5.V5 W<]+[0X/ssÿ:<]Z@:2fKSbUIk~`VWZvLXtU,T*`Cq}h\ݙxnޛ]fKP];/;>X Ti.?Xv^X^mbE/Jj[*Pj,f3$ha|j 3Pi1:BTՇq@E\ v*6-Ҋ{)64WM/B:)-_}}KR@4Z]Vo?QntgZew`9ݰ>+ \< y^*ԪCP{EIa`hph]Wѫ@X4- Ij9/X)qb0hUR:woqJn{(VU*c_^[~t ).߾ 17V5vci;VVcF|Ք6@ fJ^#TyOM6FIb*cPC +ɪ$#*bDa'oU*Ib갖iX 8I6N2 ¦^U,HUDY 64rxMգ8փFiIʁUEU$,x Wf9~yZ13*URU#S6k:Z ʨ7R="ۆUi "s`uu\X鹔K3ߑZQ(VWc**hrM6j O;wj-{U]lux㻔 ^T4'036+՛sL@]A&VIcw?C]z식WPXo5PVs`?dqgvt O k@sjKd*cWXULUn06<ƌ<jBH$ճZiTU( Vhpa@#vvR6&i;lVTE^A1J:$t5 )aZE:&0ibS `ENV[J1鸬nReje*$VZ]M54-V͏$Vփ 5S{}^\U[X-SdҊʞĪ}Mƪc: GO>~8y)yOag.CjjPWwcV4ceqV~;VXXTrr&O_Թro~FH|~j*]wLG!qi X,K \VviyZ[$X˷Vk?{ ~kp PX V@*Ѐv 'B6Zc9@U KK8 WU?6AQC&NM*ԉ< ևSklUUr@+Ylu*hMcJ;1 XN4Wb3[y @[RlG5`٩[U_(**lZQTxkjsseUĪU?Bu׭B`>jV*Tj0@U͌Z8@8g*ml/|fhbETDUn{j. L՘O1 IDATpPU>^"Y raHl^X5*_"B>38Mϡv~\|ŏZq.w6, G=cXFez_VxG8JV!Dk]][L| &g՘? Zlako5Em^V"e[jdܪTGdUqsZ%wz85[AU3QՑU:>i&*LAR,|DTƋd*QUe&vWX*@]]M\w*7k>_*w 8ūmp*MVW!m XE Looo/}z.`jXZjUeH{7kLj~8%*~P HJVuêܱm5/ycUK/-' u_es\zs|&܁\Ml!HPxX 28=uA#UOVw`_Pvo.VPUfFqjUJ{5\ms.LdMfNNb v6N8W ^}\P 5U)R/m 7qx'Ք)Ƀr:N.5OD<-ݽr B`S(nYݥ2RZ@m T/Xׁ.:؊ NV%Uka% >uTkULn4Huri NBUl՛Uӫ++^]Z>9t~mfx$V hv)ּ U`oU=1̃kP m\ uZL|?|2-rGjT*fog0fBBjށCGjڠNN<'y@U?zcʿP7IU.ܥ= !3avf#n.K15~f2W->*R Ho/XxVin(TrɄۼ:׿^j CK\0fX@\zU&ГYG!Q,hKj4#вryԪN0Z pdDrfjsN`%+^T[S(@g^VK* ߢe1? b8DjFn/.<^~}Tqvfk3CMt^/>}dTWyƢ(2ZZhZ57^\]Tu917wgVDab⿬]H[ieiU\6-L#VJWHas1m(XN ,tt)^l*lm tʶiXI(6زXF1R7x$vv M9h.?AGDb+ zuj*>_@UuE>z >cWo4fnc~zϪVXjFtc1ؖ (>~T*,^;Ǫ 9x B{$3AXҒZŌ༼ ҭU ^ 15/$VUW*vzNX'UտǟЪ¸ (pSP_U?P ZJ~wѦ& WU;LSܡFx= :)W0UPڕX!dHХNUB>RDŽ\ݿ_Sc4%o^ʏU]-MսK1L&`5UTUU SbUUNuQ kfvi5>yHSD2)ЩVtf.&aX.f"dk jMjՏ?YAj:|UKYqSGV]Y\2XepV Vճjm v&^ꐕ_@ ʼ7{AzY-D\ЯZu^OZ`mU1mJU?EqԙyUgWbI:vj9jUDM@ Ur%K6jSVUi*g6㩹>:V{9V+7 ,/<@5G G"[q-6ڰ{jĞ/'V%knz w~XWӷvcݕVV21sGEjBi/`UU DTbЭcu"V ۂ;F<Ҵ=C7Ʀ֌٭>G\nFBitPՇ+X=]tOkҵ*\y25Jl H;F?U (KVjV[~~zVyD!>g[@V`UgR'Ź6og8ӕd'QM3te.WmZBU^jWRJ\cb\^VrB)|.C Z\bvF~a-jN%VӒ₤Zuj{`SYJY]ޙp02kgV /VK-w$[|*N"cգ9ek⟏Nx7W±X$2ˢ*[뷟Y~e1]pQ:+]‚5oej鷙QjՂSت929 >;so~Z>mʧeMº:wnuрՙ(UWX2gS s Z YUمV`{qAV*قU^UvWݒyQn~2#}­i6AL;sN3QIJ*Q5JUA{U*"uqU*66d{|!zM TWAV]F*Os*\E&q y^=FZ `e***EW JThņӥP/íRK?jvݲ@JJER66؁JaV|MB /9[ ##C#C~>L5y0"*JV/導WV{T3vأPYl~NX,--p(R{Yj>19aLjbjc镍tz'C8rڲfbK_Wޭl./?%5 PZ `}khH Ø9Z~QՒzoaÕY)X^K ay_u]ES] t{o~glaLA]➱6_\v˻ƆϗvV1Ps!,c*EXiE+wWCgKhXJ X}"·tYxV/cW[NGK&^t#ENͧRs%qXO2clq jgAU8*M2WjҠ\~2pBȪU\oJPsm2 KY"|&+\7b4-J50K;"g5LPW+NWDp1 pRIR[bQ%+0 i`ofK@u_F3~U5NXXP6j@߇J\f9I>ĪT| V 'ϗ_,/<]__DϺ3{jwpj?Aש)~ӱ?Huﻌ_?AւDZ =t=c/ `:vhO߬wZRw?s/빻Q|.m@ i8.1?!imQH"՞-D-)4`CxSYAг(uԗUь*mduK/9vqy}$ΛjO߫-- ]_? ^ HſǀOs#t=95Lǐ@BpeIԓe]T̠VVYϺVQVmYSFՌմ&XUZV=gvVAQ2-ٷ\r}WHPu vq̊Ҭkq!&_dK#0j1^>RRv71 ,5ڍuuujd`'jg؏"Y_e}"'Z+"X#Z ]Lh]K ?՞**NԶ9C#V'6F.6m1A2xJPE=zD&+g(J2CTm.&3LNH+>WH-^fDX?*jhu6:2L-=;jZ]EE ӑd2O.̘ ňxv{a{eeq^<}5VeZ|clKhbVЏkڛyjCW-acua"X5 4o}ѥϒg慰z|xZ+,/O]*Suy"6waUj*x&7:κ98}_{ޓDw381t}AϹ IDAT?_, A^q[b50^Nَ/+biK*Cb D]@/WBv'ƼEf˱(YX)+ԦQϱZUau Ϋ)U*VIJX*-BV밑0v'BThIҫY p?u* -%C^z~Xr$9ȢX-z}f@0Az;fDT/H[I/ `e/&^\K szb|y'᪄j1`jgL_+Ū'Rư`ղ>V|I\\ ˍ͵vvȴ>Fx( ݱJv:U|9}j(XKU\j"Ӎ* {#v\yS}oޓUqg/yժUW)/K!/W_~*Ug5͎+l8853HJ@w&Kn"x:{}1Ң}3uvvvcV G|DPXNʿO@;}`b^![ /2h V ΰ)cpCrVFoM\bHvسPt;+ mX?e|`˨4ѩ:_f1emJ1j[cTWeԢ̼5hY*X)+Qד^5-dY67}]~IRaSկ6/ />gW6ѹ]NEJ_@P}PW$<Y*ey/Nqv~gXsm|RZ:* T랍 fF} ժ/'/W 2lSVpM2nJMj! +TUê JS* UEKyC-ru({Tջ(^ЪGe)̟\kH sxKQQ`[mSHRdzeǸ+(AVS$ejP.괈[ Kҹt*< R7х)<Vj5`53uӓ:jNU\ jw~X^zɭo1Zz 볮AR@hh'i@W% [qL`.;a  Co)Ysp ^~ |6] tcg g ҟ?Vo`0hv%G,W+7}=uo_ތ`8^`u_V qDaUR2"@o?1 \sN#jdXs( P$VGU+(]fұo2B([Xq*3ߪ.1iH9Cg8t&'d@ۃPEvrWtJ)pC + Qhr8;uŪCRO&zc;5{}Y4 RSgS<z.VoVd.I^*X8~V^ 1HkF-aU2Vy6Ր:Ѹl1j V/VUnzzڈ5X=yRS$K-}_[^/moWKˀU}YjdZ<}:ӓVV` "Ox^#P;a We%*b,+d5[WcX.w T.g bi j: ^oj!VHp蛙̜} YDBXh߂].*;j*hTf?NHw%#ݗ_VOI|؍7T|5ӈr|ujVeak7`ˠyWޕ G9ZV{(KҾ˫VǮjGjYg(F=J&cdJ Ub=T\danЫܣO"DH_$*uFH@UU7Ԉ]jurB%{JQh.WCg4L"QtzHR@JUX ~ +v{L8Ό3I78]ή۠7^VKSL]IE2,b%VJ]"0.xi.Л2i@>?9gӼ&ƙ33}HVbu愒(6 Z].QHxוڢaiftm5 VXYPU35 ƚYDqU6M"tlZ?G`P 6*~N6r*[V~@n%%Z󍭋?m/~3Wŷ󒘺ы5zjzشC@euXT,>U_eZ(8Z|B?ѯ QU?\%_| ˂ X XV 9yƪVj;@IXj&OP#y:L۵"e%!KWwM 9+RQ|\ZՐZ͈58*GJT)OŽب:%#[%b짚k/vrp%Z]QJe:C\ZE̼.c0{/e :pC嬭ŠHanX5~il|bՋ@NC0V# =2RbRb5QIV%&Ce2V/5pM2ct)s/eSQ]US7Awo> |Hd,a5U2c.(VYaz'-'Ew*'=U'4UȐd ;qӖX5T,nj%|qU"HM:y2zgd,>l 1J*VWaR\|\]~Ū;cXPOSX\&Xf Y=+g,#R>Pկx/GՎhkjML=Ī@xJVQS P< NZoZi*rNCƈb;JlY}Xx&bf}yHrs{{sZp9XqNes}sJ'd5'~WUɃ/Pe`fW'T/7ݫ>aG#|AX%v598Qsf]+?(-[+a5&UÁj;\AV[5 | -VuREվAbOUyT ᮊԫ@jɑ#%hʈPu| ',Z Y"9lPR>otʜ/a2s Z$xd\75Nxİ#U*' Fڱ=92ZfƪSEX]T h]2uaaxV|验;YZ ItjvjW? 0^2U f@@=:fI\k"5B5Ty[VߌզƤ$jmez>^* s}g"]wT*NմeYAUa|ʿeu:؆~eIv |͂UPxg߷VQ^*{ ;E352o,V[ŐO2pE `9VW!\BѺ4#kj{&ĽFtZLUbpOmvHb3UWEJ#WjbwdO8trlVMqzVK>U4y:7lTp!@'tk1`>T7#Te\'jDPV*Ri *CUqY'ޱ"4?g)NIKF`.WKޙlw$56Wegv:4qopOe;[.\Xb)zDVlv5/s*kSL SbY4&ԘU*tҠV}׮UdªӪOr7:]ԳCDUnU Z0E.OUBYӆT#V󕲴c V{z"<ނ5U Pڧ^V?RXMF*4(U5XZ܊~oÉ?1Uɚ4SB Ob4p*q%uتༀ WYkLuL]_[s0]EkeG+6HCWTFw?DjnEА9߮)sQAfotq< Mz܎䲞X ] 6.n UЪ{c{UJ gvɖUׅTrU+i'أ`g,ZEZ"N:g qIPZu%"APBbP떕*7KۂjX5j"~&+n6;R ruüFWXӲ4 uSpʬDW^*T/ըU!jEǔłU671ZZ2^oSzjCUeUoL_nɛV¯TY"Raê:kVj+Uzd!^:j/S(n^1U ]E3 *}U+_ U|CX V߉U~V ir?SCt4$O*;S!6(~Y,开~*#Ѧ *VHT#N*1JxUPDA,7€ث`ynuq~u V1CWVfUZ]tw(V,Km U>.'+VERլ<.YG=:WOGgW{5Lvv!qg'ىG3alԛΆ2egD6k JH@L WC,uRf-Bl?q1LQ󱘶5% f>_9g̴ }=9Mb~<ZV:)e sYmoV#V8h̪j{fLQ\ER nWw2krfwEU{Պrg>C %eݩVOi 0V,b?C!Xe U2WIɎ,v{Fv:a.NAiNEJ914Հ %5*St.,VU/ -w (J'{KT}m^fd|Ji,&'~8޲XMv#}S㫪׹Շ^XuZ{zy|[aVYh4AJ<ݹj4YҖmvN,kJc$5eWoUV*iFxd\E^ V^),0ePd zzx_4L#$WȵrAp&k*^8 ꣐XqU/[,>L~]կSLxqv+U<–VT_VaFj3kY#ԪTC꞊jXVJjf]9uQEZֆ` ~hֵ X{YVFjKkRk*P$ʫU;TZ#ބSFdI8U&%򱒪 $ 2XAUu>QUNQ&:bӦp0U-o!}$4jeEjhK֚Cq5Va.Nv8 Uhw$oGp_ywȮ? Z;cźU{~ꠤ[sj /Udl%\]xf\VP/i+(ʨ7U VO*வ֟:i jJWV/UP_֪X5X91*DUaWj WU*N?kv5T Uҫ8A Uz&YXxƭDB`C}hE/ 3U+ڟinڵLZ̪Dk`$RLhI@{6&ѧҧWqÝ2\+߇unLW ()ƪX5YXRZ >WY[xb&3ՉMnBGAa.) F/P|VVV@ Vhr$z}ܥ*5`lf[&bVvyfqM] P *P.@ dI]#ćxh`XuQ#T}Tӏ4_}O5 6@4.rz0Uv^5g,b+#akW ?lՄ@@3b"?sSa}~9t*ѽ|mP) ԙNǥG"U|'G+bgKսGW T?'uSpasAF]دR5[>RĻL|LӁU.V)V6&j{+U*ͨ=ij._VWG*Jy7LT #XU\avQlGjUjUpW`*GR 4QDmvUj 0=`}/6-O/]*\ET^ENWy I֊kW]*fg+>A?Ūv VwJ.VXX;PW%:ja諱7.t8*.UP T?*Y:C,Yuڪ9mwV*V-X(&\kU*՗U*_kmߜk];K^P"qbrgzamŅgT'&gKK wC 37NqJ|Lō[՟s;KxoBە[QU*uG\j9Əc,_>s0T?ujSKa-ʪJT@/EU֘fn}pE0Y˛ScfÄ6V[\[ݘR"ws:/>8sOa>26-/B>HO=IuzoxF),lzhLqv acN8 ǭ-*yWɭn|\JR$1d`r&j;F gui҄Òit pcb!FLL?!!Ƹ=yE,o)ukތE&/[M jӱ j&;d)RG *Rw(U'D-[V=(,(bYb0 rՏ`V˔:P^U׈%R*'GYF0U RWd]bU*V fQUbY!x"b@lH!CT=ʨV,IUY6V6Mb4'kLEeXѠj? ރq"w? {!^T3P}T)֬B;)eprSӱw˘2vrs g狗 vvK ᔝN#֑tlʱcq0WjSUtfGǝ:V?_Q*OܠPAԒw+8}̄U+êh`g!"&0olөXu DžX<޾p6_5E^ªT'_ 2 D wPR'@U+</:^aX1TKʞCdcNNUG)~W=2 PqN՚r%&f%jUjUOU*"훯 v+Irr*XԪUTVMeV)`UU Xu@D*@ T'#Uq޵jj\U]"XMXM;4]GQsfTboʫUbHe~SC@YQjXPѦ[Tsj2x˒Uǐkl~dmJF^3:6B9~g_0&U~K8j'֝˩Źըe&6DXu0N߯V _LΌ.w}WU ryVg}NKtrjEZXu]چ3|urzn~ԪBUŽsKjۉ==ϗpEtX-ҩ XL f U~4'+(KSN-Ajy0Tg)$VZŇN@*J+z:D}E\m_cU.`k jk?`7i)ShKUzUIJQ ViԪcm oUZimBVRj (b50ZϮ2cUjo(U/e+<X#i3VEգMj< =e񯊊LTQA)똆IP|Ђc`0#s༏@;WQi'dPpUb՞[aX=űڱf!Vgzc`q ?1F5գ3^='kk0äa6X- X"hҊXq7gDҼ_DJjtaV %`[V|mŧ+tXչWYVa"n GMye3VtJ%Ê;eմ}[) =a84x(>h=I((@TX**dD.jUUUյ>Uij4PuvrR:HC jU VRUM0s֫< ULVJt'Vu#_y:y,}%TPߺ)h:D jY-WzqF԰ J5IrBz(Z@nSXJXn,:بVw ˪BhErBA=(LVUV@US)7bV.hoU>Ӕ?q~F6V頫2fWUmky Yq$Wu,V@jSOVL Y*SwjۧW+uljwoowp3zO>49'i 9lo5^Mnυ/to-llX /c;F6_O0~0=pmt\P fap7lVu;cjVRy V{gjRᬃZVM@Lbjիǫ؍=4ʐzЪY,xQ^a X6F6=3#ISQ`8-vcn .bOrF{uAJEBx:<\HVO-ƗD-VKJn'c"[X29Miմdhgѕs PﱛԯszzH0>!jU1D&2(UUQ{UO]ODa\fxBc:lb;f&BԋI,-.&2I{a^eH?H]cw5քgzᅉL2}|ẁhs<>O($TDht)Ո_ak,Z*6Vm#WզIU~VuU=s Pd)攕(`>'bl$K[zsUl/2׶VTj骆ja2V+GfBc6rb CKOox'/ťsSpv_W,\z@^9o4hs{p[?V-VI@WQf* h8i@ܼtܢt\!^ud`*8Jq'a47G6l^6M9Up#&=bvV ꚹ„՘k\ NXur!IG:O*lWQ ץ(Ί=W)5Am!Zorg6=/bYS`xLkjkoI `U+`~͠L:UDEqJNJ\,m~UիYoL-*Mu#o)6W npիRxƎ5juU{=2ܰ/*bquQڗa[/* \vژ7ft9?oU8G X=7~ݚ~r[v_#V={wcս40#ibK^nX-/j8Je|ϾUkVՀ \U5*Z:&*XJS+N"po|/d{5c'bS ! s8*YԒոN*O"td^uV1VUWE7B IBcU^"S\:GP*ZPm,X-1Ue-gPmӟXtn֪jobFUjS6WXkΪs+_;?8Zd+*WlYUKUଌmrJz=6U`\=k7%pe]WSCpJfV)Q"QZTe ޒn!Ɋ^+{ʁƕ bU&jzשomnGޚ XTnggoΔ.{]q V͵j'Wo5/xkb|J'Ô㡼ժëO?x;c- ާV=Jz(Lukf~YFaKZ3lHG9ln`5TS[zujUX4O%WU#,W HƖ4 "uA)L@$WRTdrJnd=JrO~Z9ԪUZVZ"T2ŽҔɂn .j-yZ"hsZX=5,4G( 'ce,vĈ}QPek?YG .XJ*@*.?SV6U9zόOS tJwWPF[5Gp{_k\%:uB0ǍuZWcvW:UVK&9UXF_jWasEK.74-EX%ƖoXhyuF~}|wScW8ՈUTnV;bZTUf%cFKoMru!كVqP̪Ti#ʪfwc%^ vU'jjiU-V_ĝK[[ 2SdڤQ'j:pgQ2.r[AjM-] .e1vlB@{{sr/9'5ՠi{~/cmL꾀Vk۾{Z7'BUa ` /m5 _&VGj>.6c˰s~5^3mFaTk#mrR9ux(H `wkm3o 5HMZ^ Α9\&.^-UKSj} [J:ݟ9o_WF"&ET՟ng-Yyjj=& > kjVX^eU+d~ 7j cT_㤊IB)TVS8#N@hWP\1h(@$ŃjIЛo Яqs[ Va @sX9 =0u* 8YbcC=\XrYF`GsfS픸&X~趚Jܓ@݈(]+ۈW=fTN\ @J Vxd"Vi 7xg7ĊU;[]L3W{6:wwApcYN(V_cUk9UeR-ѡCl˪8TMYqGZ=V[},X[e"WY-~;;;Bj*HP Q|*U:MQLb<{&H1\uX;|gbgpjeFZ4`*J0UH;ЩC Er&2bJmIjRU P@Uuk%vE]#HVP3VWQ`iM^߆s?YמۤX2Pr*JSBKr4U;fcJktI5S*qkUտ?|0P5W̥9 M7 mWMd%X: $jU΀r [\k9 \9ѫ-4o9Lj0 0N- `a*UP6JdݥrhQx}8PlV֣'285ZT`RWB zI%1pCŻ% ,j 燄c8z6"V-2)-55կ: %*u]\v}m5l8ꆬ%5;uU&"AuԍzLa"{ʾs)Q_zX{ׂ~r|Ӆg: 윬jM9EP<Á+Zj?_Pm 1"ӱPғ-X/kzj5:a &ȪKЁDժ,Y`Bo ݨV`@47HMj\a`29. 3L\Cz7a NjtK5254,f`#6X͌#U.VWĪ6qvt-t65?Q@Q Ȅ#)ѫˋ^ :6F+29FoDQx&ccXʄ( "UiaկaՄ+RU~Tquى zWeP+Qn\᯷UpDU%]e鼫"K?XLV-`9raƐ7~ m)hMj.dolqѱQ;TT<)pNU36S!7UQ;YކU ֝\oB_dj#ߴuߏNV V5V{^;VU-L1C3|SS/>P%m&.&r1߆Uz-.eJ.nwV]SY*J5`u_=@"SqXUft?R 7CUaWn܆BҩLV}d6WKD@wǮW@h +±>ZY XZ<oMzΈ6?9%LOuQ/VQJ H%TR%|`A3_e:jYd]΀Uhc\W f @Rj?&DZ[Z%I{{EjK\>A467ruusV5ZPX+k,AjVW:V*PoUXl窉ZFRZ՘Op*.UQ齎V񝲵T0VU+U%9V}T`sꩡ/'{;@ kw:1q*z*cJUOaխՂS|%9w9Hac/syl#i{.].UP[ UK՚PuV'Cg cf%kVX}NNԐV *EIXMCnq8&XJ_l'UjYRUUQ$G\$&*lpYA27HH=G$=jV~[}@?\aj*V6#>Hyf KRڪg6S=|χZc ?*vagV&,W᳎R{U}Rj&bq*?9!k–ZkW=ag fGoSM S3trk!qVU[JRqdUvUK/Bc,P7rL ;ZnB=j+8Z9a5~JjUU܊wWUESbWнܪX뾬+ cS$>=*Vq-X-EKʿ"Īl=j ~xME{}}[Y;Vj}!PZmpU\[R9+F t|)&4 4*7$*,++JXMHUШέ\ .YU*'T ֞= pD_A'rU1pR 9˙dz3pj@@,E|XڕL4E6Zh $uJPz\%GRM-8[JL`_ȶq',ATmBO@+7BU/-V+ƪ2쐫ΡFЎ:cWWC5k4mCJONRO~KEzQq:+FU껰jlbZ>4^YZXm鶎,>p4ʷsKr獼-n3U7"qh{. HsXZjQP\EuY~ Z3Ph^Ce6ظUҫ1ο XR]!"oT~Y-[N'dXjW-jpYrNF)v0V\%c*cM5Zp+bQeVJI)Tl^O? ЭYiW; )VEp̢iJۅ*j䪼8ԪNXV55Icx2Z)q纔7T2buH꘬OR[/QW [RmCX6:gWU)8 b+t?mYZ'Wl~}~5׷p`ՍX%ZCDlƜo毽3KJo>c7RK?ͣ`Z[OVfOyݬelwe+?%R᩵àV QwµF]'k 6M{&rQT (KtH2 וHzSCaQУW18R p׋7P5+I@T~תXRahWYUj%4ZTBU Oe(ׂx#!ԠP5Bx-夒)z}X+|2fO8e$P@gom:UR~'pg Kkknpbj2A63S~b+ XuEjU!d5^\ի)"0= BG(J`ؒ20 Y1p`H֨$LeF.7Οi ?P-ڥ1^!W\Y U O.-jd8aՌ)(@:w_yӹWxoy Sذ9GzJ뭬w*~XQKl>=ԋ[t6bU~eA Xcy&2 81NCa!ί6ucD#&z`e}dT=ܳ1 iz.jZ* 'b ]cUŦ*W7WJZ-Cg Ex,˴\J&P)B`ԫy͊=ZV]UA?!S|1W+8. By%,i(X V0jc #ԇ zx\P͂Ј Ťh!UED:Kh[*U rLRP 2jXY(w%+`j_miM(+pbh* ;Py,8*P9BR\v\_RTPO8 +V wNU+WߊսUѫ)`ZV RჾjUr:72w{/06//ZdJtf~\|{KG#^ۼ8>Uǚ-#*rE^y걱:X_{ V@zs'Z?݂{RN*,TV&kbo+n,5~cuUWLpToo}d IDATEuYV%W}˨(s{&;;-W8+TEfRe*LJ ~[. f$)dXvS-p++gh>R}ޤrJRa%#jQ⊳WTjAm2#n}F:OV@nJ!ǀ=*Q-UœT2T(wvq0s60<34%jc3ss*GVW.[h- vn_tɂ U+PM)wT,o%@Zy~כcս[BWj;w~u \( Էǻ'jƞiV-: HXQJfÆXJUҹttv SnPm+{()VA%XCgZ ,Ud/J@"ϊq ]IZoF+E[[zDCVI}j5Ik"G>ƓI ՁQ*@SCS8%rB@:[UЪ A\sYHY⢢8cF"CP٪ JV;Gl@jF*uۨXFO@UedpeQ5 Zڮ/ VE~͋o/ųo4ֿ=uj㧈UjN0Ė%B=0yWuVR yz/ aumn`|Z<*yъ4H揷x_8Jacb]X5㬱:[{,]%ԃpC}`yiRlթRYUjr"<*P5*+CHэV&QU5˄ԪZ:ajn^\`Vij V6Qg'ڡUP`xiMǘjUJ+j:F=V!&p`rA ain?a 2eEs22grjy#d5Ue,|a.ib#Z*bh 4cGUX.,( 3Rl!'*P]5*'>9qeUNU_U ^upk?s^`O^IJW#ebHu,9}ge m~~jÅՇGjj`5$#V¦WWVW"w ~ p`547bQ!V.+ժ Y1HZdI*TR'3ȩҡD8,r5 SVʑRjBG;'{Ԧ6FR SmPFEE$UxL-ت,y=,"qV̹jNt}Wreh[:)V *$J qP2;=.?]e*>!y2cx6fA&f"5 JaC" h]+kcb*Ex HzE"yJYMTa|說 J\zvVcUhՙW *(\UxX%Κgf*{T[VV[˰ZZƲN%`ݗ0˯'CMDeMB38]:R>yw: )~P c?j {hŋ,JxNn:8wA0‰kp1Mnb-~?}k`!kg/v>^87)])pLuSKL|ZA UsN,u'",WZ$gVYIVGaFQ*!QTDaXhe*4*ҢVJ;}64jj V# .%Jz$pʔW&n15TmINW3UAb 33Ky1"#(>Q, Ӥ$T[`U]*UVqy@YXFwA,[]|b]Tm*Uһ9jғQ*SRH=| Q| Q|&/=\X= VdZU8ھ}Ճ/wTU,d9\t 4#;/TWY vN48MGoţ:6p-U:՜]uj["W vO= T-w_LzcNO EZ}mՇZwJV6daG"bKe -G VriSޒiŊ U ։DDeibTD|sb*BG6L\P1TBVcq"IR׌$6ɶWVejΤC֜2J~5|xCl^]*֖s :/T%ZVjF'xe(2svYxxp*<ɬ :Oz RёCaU^u| -5z/acՆkb'N|VC.47VU]Xmw?v͎|C^jjEXX2ck U5ժԭ*ژ Z"+٣NKׯSTD_UY2WHrWD\sts3j$FVq $M3mA+lauԴ+bp+WV 跰J`-HU-I jUQ UǏ l\K%SLUE Zj5 eZ*>E(z) SCSUXE'62Z*v8ܥdn7IU{U-&#k)*`e 88_\]0@f~<ͳQ 8zͤRt_:`ʓDUj$AJoRT ڦ ?V^`u^Ϫt/'h>KЬ)ZabXうC*4>Ѭg՛]s`,VWz4S9ykGFζ>cUTeqƽYXv*\2˄58B-N) XCӺz_N"X5 ŅXj?ˣc'&R`ju*u^Ӂp<ͽVTڎyd!b`\Zgxϐ T=(4K6C['Xr!@ X^jӏ?~zVqku<n?>T_;8xpp6Nl;x,6R} r^DQD#T-^jTd`dGX: 1/ ׯˍn~( WjHe_KFG u] (iIbFalŪ!O `%XU<\: 6.\hVQzCɐu[ WWr*U>8>L, 2G=::IHz 3w./R-@ )!T%b%sEW]jճ*:ԋր/j4Y|XX"bu*DAq`( j@R5$3TE9K''[Qݻw%vTڨ TDԮD־MUruTQqItrs8o\)OG\v VQNazRsTO0VE V9CN+2iD'\ U`(*EX:`-Zj9RQLU O \'X߼W:#󜌢ց3svNM% `c*Wq|aUgJV_Klժ7LSS#J(}EH#N*3_x@r.T%#۸ 5T>gsjU} ^~aUWoRM5DYyÞs縔Ff"8[ UU1ıUCUxa*3VGGkSյInZvbu}gX*UI!zJ#XU+<3*qa R8Ei{X\L90H\SEͨTjeZK^n7K1'4zZV~WAOb-VKYKU_jlyPTy(U *,VqfAY>U*`ƨzusetkV3<@-ݛXZYUiD'OH}MrUzNI9)'zHJ֩"Tw⎊3N&;%&„KU3d%+A$5T>Ur njV#2Uu;UyUjʡձV-XVLo/ZUcD6hMYk-U/&2Ut 1Hv){i{̽JUISUehnlWR.?+WuV%p= _kgu9RW"we98PB`Ep[JԆ~y49om X;07UEV V'pa5`eK+ǵ8#XGcU_hH@8H\-ϩj@z UpG U W:xwbW,y1X_P*>2 JZ4"[$K~K V>/VX>XjĻx `bj<pB?PjcVM`Zp*QS`E!YiGܸ b֛T[6@U1,ejZdL&Y ){ve JiVGVBȑjy{A "WRՑUMc?W3ELX+[6 ^^&WIdUժUy_VrU(aJ^*en;Mb4)SiZiU3@MLUkh4ĚM_TA*UIT1Ti}oۂ"U:I s nN}Htu|XJ8C] W+8V cխ (V5`UU 7HBUeZ^3XMxXauxZկy⪎RՆDH[ ~i@bJx7┃WDߣەSu2V[ۈUP wxc3FHe.G"誟!v0R!Nf"B++J/l:C8_+),gëFu?sOmԹitdMC MGX?gu>z҅Fucm¥p?ڙyޏyI:ۻ<kԴ-߀O w"LQ#PlّU&փ`a5: 5XƣJbU$ªr(_^5[6l>`&,VˑrUҺ'[5T j!ƩF6(eE Prg ,V q ijH;S1i}cYY㺃mWJzDW"<ܽdN*ew[5 1uߛ4Vˠ"_ĺ4*D0jR ID:Y+VMiNJ[ wnZ* \Ir3]HvXZŬC$(O%6KqGi-$JkKZ;ew_J萢EO4qCEAYf*RuTGJQLU zuQI5dLi~IK>kL )kthך\ 2UEjU)z~SRjZ>a@մ~"j2>TGXkVwXwˀXu|*hϑKNu~}0'zuqEűMIĝX.2,}83CDB=>d 5]Wf1euNBB!o4]Z%?g%C RZBKs={+r8|c_E/ a(oCqz6$GMV'6WqV~a ˬFFRdh*iˠT?՗_>( /`X @#`-j]LE- A@, ȺÜR29*pU/یfJ=$ )<eX alQĨ+* :V Q</YWqer5֘6&\)4jypm7^Vv!V9:x j疮-2zdgث=](`%2jX,8u0 UV?`a\'@N(HRO@ᱦ?PM:c8/=WC'\ X]M>V{lS6*-bJ125q&BA,@M WZ1;=XFBl`MHSէU>pk VVVc@S ~#Dx\m82&iLXU0"*`m%ߡ 02rQao5 2;GO.^=0 Uߝ ÙsuAlkJc`{P@]Rq=$ApJA)8!nfE@0Ր iU``7;~=YbiXP}3  [puPu#Cb-ͷ:b eb5 W y`ykWG߅0dXH`5:m#6FibUi`RrSt> oRjRմV1Ph hd +uM*Ĭi97?Y'tUz}׉իaT]4X P \. x} 2VKj e0م|TaUhmJV0-Up?*ZɿkUUH*31IK!p,h28 y|Gb;UOڥ!YEUwn_+:2݄4V/#/8pU|'RZinRV}( -`n*hÚVSzFXm@j2WRef+RLM sUCLEֶ"2+$`(rҫ)jhÊ;L#WF>\ \rZX_^K(YL1:Ksz*)ּF*ZY35SU3^Q5Xm_GajfFk/!bu);JC~$V_LW]ϲsɵu9[DhNn|u\#pu# ~rVxl9qr퍫Fjƍ6?~wwoCq2͍ƧwŃ%VѨ>'\/kgDvqeQZFCtDrR(wy`94JJlZQ$CvAY`;u,;%xɆd]D(Qi{$KLwܹx~{.2=Z>>ucGNs;>BL֘@;ɄWõހsP+hOsU9=:9`}(*X_R?=l^ ϾKWD+CXudۧvN! x/g=0z3jtKa$j|[cD{MRWY ^U: T{#VEa;t#M%*l wՠǬP(+oG-VmM-EܧEFETmojU aw -U\}b)[-ːF)HT3JZV)X`.^(<4c1ЫF+sb(j԰M%\\vM"D<̜6@F& ˑ)I ˮ9jr2a@(9>věbss{#O/' nФx!XNN ™^@aM0p5b쎆F!LnȻ HhjcS!HJ̰BdԿBDz# w9p/\ݬcC)25 ;ʠ 8;8Vq5Yxa'cK3CSL*fHtX&뀂41±H D`8F{i@X>Ӡ;˰ ȋB},4>o|gRX  wfs2 ˎ M MN% & 썤p#4"b.MYפE:sWs$);x{1h끇 o7U|VA^уJb1x Ayc^t,W,еV;2^aNFW@LRv:G;ӣgUǭYӳ1Ou]3QHXV5*: m۱iz (C'XYXm?ej7bhjj)V=2NqRZ.*G, s_qXJX5okaT`q`1]ݱ^Hl]j/J3R6&Պ]#;GFv=H&Z!%XG| r*|)vs+ļYE$ =YC2I y`eCWX)(*7r߬2/+ N"3%J JpsJy`'E  dƐ5x:g%Y H>Uqni, m0{gV$C ,ju*n A< '?X WbU'Jxbj^f- IWg KO5Ճ`͵Z}$ c½F#*`@ÎEzyJ4/_43 VE)0 ȵȀCUª(KMmmb)oŤs3 NkVE\en**5 kxVo7jX2Z|/)j0k s]z8RCk"d*|)GCPNd- 9ē.W!FY|sR~G6|FI[lc^L@0lLZ6ŁGp@jcZJ?USML*\ `|5{wŎ8ET5;y;).Wչz$ -E07nV[qPoۻw"P3oIj,vVh]=n\"Y@UVkXK\sAJN z*V1߄Us^$VREK-'TVqXݦbUѰ'Vqr?*{&jEFñ*Ld|΄UJf'vIUDMp`V|v_\l<LGʗc5"PхcXJja5\ǔ~Ejl.N֛#] {!ւz jSa&oV%ZFj'V}MZX=I*H<<!, j-^(Xi>Ǫ?U5Zgca#ժs3N6bdfqcuWj?v/x͇juIg="1!+U>\#"VG!AS V'EhPdg I(/{Vt@ IvlS0c6o#XY VJ q-EZUaUU6jM VL:%\UU>\Dv>ªD"8VbdI5bukK/1VC3NШVPBeXm(SЏWjC>XMZeX%:B-f@*j xg߹cCnZuгC)ªUF ð:iTVEIŪeS G ɥe , ݪZuVWHiڊSUA>̰*gI t>Bv!meiljj^vsCICjAH[v6/3@>l"~|RN wL(Yv^RidtThg\m[vl4_ysƗӫMo{o'szX-Vg nVθή_=m)F x)2oj̙Z륌(Vo11a#9sY٭ dFAF8uf7z3Ra Ώ$WiU3R]kPݩ\D*e|01* p*YX Pní\5Uj+"vrD@6UN!MQQ@-+4؛CgV ZE8V c^2a?GqlvK`wld]F)_Ō#I QZm$h3݈U聄hq E\X^?f +DnO^ U#ʗ%,8VmXhj˃w<^CBh^>Z}ՙZB7՝]* ֟ 6UXo(CB|) M'Tjm*3jöɺJ0Ǵ<@ʴl(SU3bur^&1?d U_!\ê E{vy>I *Z>]z\ri9Y*XE<a*fSywp6}@Uw{UXu4Lq ruXU٪9T &$v@V|XXGkx9kLbլ-E"?b JՋߺpJU߁\}GʓpUd|:Ql?^bd 4YbO7s0dۯO*0.;83Fq6wrwj)O%Z 7꟏-D9VbɰT ӟdЦ zM16G wƧˆUn ' h֒^ ^I&Z(M%ўae2>~#VSGll93كŀU|ax1 ZM9VE'a5J!Z6{ny9d{ǪA"+v.ɿ!eE%[7׃Xcժrh_y`crPi4Bs~SLM`g1Tn1YZ5gP{w!K5b<|3ߵ*;9'^/bU]SX KUoZTB^. ek Vk X* NHٳ]@u挻 `nv7iwbMÑC(U둛⽶jQj)5*IU$*N]NapkEU:*Ta^UUʋժ֪*E`UO (0Z%davjLʳ#HktC _āxv8Z{B0ۇgb&Vm3 O7՞FHƫaX߂+.$*;*FE%*IY 3*{{ QIx$@X%* {ptbgfB{x/][^êx 3z΁@QB@| 0WԪQa0>3,9ܱZWrvߋ\@s!۪EhcR| }xe/lYũաVKղҶZViOnRՆGV|S uD9Vkwl$LvVC枨vzVg>xfQ0q4T/)/oEL^4ǟ֕yj(PHb@" N0!ٻ V[&[۴ê⬪~'+Vǻ)3A45AFXιgoG:9S c<ؤVUh7u9U7;IfUI)gT=MZ*: SUomՓ'.Ko^=S rwXX(ݟEd IɧlP#Nm;;$%ŪF:"a%9(:@aq-N;p,4҉צn"OQ8NR%%q)TIpØE2WXz;G|l&V8b8d]%.,]YV}CeVsQY7I/s vuQ'e 8Sv+lYLt{5k< (Pv 6*`c$ӰZSJd[JX}=KbaWU XNU .lUScPHWUBV[?߳g1}v;r*E}#GZ5Z^I de}PG`5#$VQ:Q66=ƱJ`ei^D&<8DVUS+. IDATVԏ/ayy|I*(V}f*`ZڌUCX1#qc@R}R) %]KlO @PnHEb,Yz$ΞԄHO9],flަB"5G6Cx/\MtK!cދ|$xQ!s!VphP=^Z8@V3װ'UꮍzOn~n] [BSĪ;Շ3Q!jYZmeR#f 0aܶ*ɱ޽MXl V'th2ܢiժ7XuoguqTK"h2؈:DȩGЁmijA?صa@5@:H;M1&)!4ic6't61 dգDhU݄Ү-_aL˾ U+D5 M5Cx?2,'Ҩ7Om8UU%?50iOaB+>/ޱw~|< Ih ޞT9ūnPmVQU{K0T=2StX[mղQ *;Q5FWGB+v_,O}l`X5,\~*{L(Ubdb5nyhi2`/|n\媨Wwݔ XeTEc**gD  sl+ X @UVXW@[!Tq*Wb#ױ:˹zKl5[S pF-bA{xz8kUc R,F *>uTzxuUy5wb{#iSgȽJZYR:" Yңn2]g۱5[|Z1vĦ8|EZKyD{$yD0E!1H<} UNldovy:ªOXS'#*;_yn X죭PLh+SY/jp6o={tSU>`A#sEOr s؃p%W|F-^_ȎUr(gJ/<[` >Z*E\ݵ;܏1T3 ]VL s ^>թr: gs w@(pU+xU.O!WZ*^eYUPdOJNC*UmgH8ӧB@` D+@>6.s}Nrj3;ڦߤC؄aQkq^UET6Q&V]g٪ax MmG,HXU +Ws\ĝ-tU-$/զSO"'ݰ*QuV| X%"V\zcإkODT5xrc`W޾y6wNggk`qVW]qj~?ť~|{qhqi7l\}vĮ`C2PS嫨 UBJ\*(]0T52NU! }jUU)V+ V{? ƹr~ kz.[UNiWr*NWƀz/Ξ* S\ nUdkώֲ`@xz `;;5 $5"\$"XK# 6LW_kYXu5`Uۂ*ᦅU!(mª_CoMls0BUcRѬ$ZǪzAՁUCW[X^o_o3LN%.>Yު^]ٌRn?yO??rV-;[s l^`zýoK./ ۀK`R5re\ܮu'?X޺}5+_mW@ ` Yʼn'NVk5Y%*R`sbkUE*Z[V7orz̅iL Z0ϳ El, d-/8mq-kKڛbr\O)Jsd\{"ȴV]"]eѦV]*T0!JP5ÎwUn <Ջa5lҰj*^ 1}C*PCbU!U)@ЮXUrfLbl(&b#|W1/@RSЉ \ܱ s0eU+X-VJǪ1+"OccyP ՑzOVBYiV wuVe$QjUEz}jm*VU[:Z+ǺTŚށ DHX傚ClkסCǩvqeI+~7$(&GȑGy*76čRnȉYyU^~\7._ltQ'*;yv=%,!eD> #ai4gf>i<Pׂ;wXU/)?&(^8&1FHU$,$YeO 6 Y$E8b!P3PӒn2-mhC@ ̐j1A}v('O?{N}z8h#cԪ2f`(YOZj XM*uT 3dž4NΪUo_+V$W b@j*TzuvvvhKy&+CUPyGbuZv9zPsu`e$՘jIdU1TեWa.`.&@2XfQ V UgfYߟy3sV +i_B5텰ZNns΀/#Zr"Uj> UL#*/u2zA V˹ V+R J3?k[7+Wo@/+[6_Oj^NeL Z-:L2t TxL)`yLXMBWX1cOǂ:&V0mP)iF`]ARk2@XPaUCU)9*W&M*uNj^VZVMBiV ^PêRa8ab,I* KdCp  4 >:BJ jUkcc *դVQʱ#Y A6 ROYG .Vcՙy*Y2lA+V(3d-Y(mq^P"|qJjIMj.Z5U@תg{GR. mV6JpVE XI`)qn%j>uj Z"~ZV_Qjt=-4w7j?9ajVh JhPj0V}Ue툟zXC#f7Bu9jU4l5 h.QX*l ]}Uf*X\ ?h(NE1ө5TXD6׷ss<)3^b5B -5 W XobLk)+Vۄ+4X"V9SXڬz"}&T% *hj ЮX ZAczy@BǷ`>+eVVY/)}U_3RX..j6VqbM2W.֟?||}mj6PeRS^#j⭰zȹ#H5dਚъUҞʔd.VkTZpԪxdX;Vs>?gZR= Ǒ=_Wޛ$z'$e{y[Ih53W;*SAsfS\8ojVUE)*\ҀzU?ܼ~UU׮F6QrCVE``>SSB\H-B;4jj3P>7ǧSXBj`5he׳pJZ͊m6U޴W]bmUUm (;W'192\{BU*MʎfXLJKWt[*PjmT%+S5UVˠ1Vďa51בJH?X!*XWu?kaUrW_8א{ljj3gjuiۦpՠ0WRPuDN^= U)RWPQ p[jzzLY.a^ʑtRcZ6a7G0 6? ^%`srnժD\uVoNZ5eJ "X- Pgl-rÅ\b U*1UK|fêxsw66@tKZ#Reqx bZQlJjj1P7sf Z-uG3EժkT * XKz1j5>(51GIð0O5=_ 7 BUQMX{ϨϿVYbO2L-V)լ0 (; r*aX^>{MW3pWNO5@T#>]ՅL*1l%@W_2MX8B^9S}Qʔ(R*jzcBHVjӮ..a㥬^s4#<3U(D-ʿE P+P 8t.#O'B mZ s!G8c% V_Db0.Eڤ?ӡ34=Vf5BmVV1P%[Ӷ my7N` ~*@h X#Vyt "d is(VCMoabzӍѥ3WV(ud>`Mzb>8GWqt @{*c-U5S)Vs%籚<6`V0`u*\\d.ncA|j,9=SCXN1A`[g*D lUG`*ݲ"VAX)? VR$9_Z;AV*W aU'nXejJ)JJ6c c5Q4fggAN'UbOSUdO V~:I{x,sV9^D*" nЦ;yU4kF՗ Vur*0Ha - VMNX%:Xeы! (8lϪ @9Y q5OiR6wZꔤ{66XMՓzD(Xy6߹rwt^o靭^o։{zp}﷾>}ηkC tP~zѫne]܇' җx{޻ww߅?"1ӽ}|joL;WNw{xl :?l9ˌdJK=J۩bOz&`FZ>pb:Eխo.hA`=X7)7:v:wrʼƙVS~-SDӃVr^ߟuU|ZZX "zq+gJ*OĻeMիF2W-40UCz4S$ Qr#JsEP#<vvdV7˱Ḧ3V5WhUi Tgn&fz @zZX7s@V=BËIID4.T usCG-92xܧp0? !QQ!j0 ꮵKLӴ0[sF+ibu?Z#G]X=b8XmllήgAVG p` _Qujj󿀥o$J{Xr Xm6bF\<:Y vtVTLB6?YXZҥbn `,Mw:@jBؾZ]`Z^<ν\@`jVUK`Zʦ,Vx~=9 nT}݀U ߓJ=CLEA&&bZub'btVrJ]1Wu@ﺊ,j2yΤUklrꍻ3)-J8"urêndsD$z\z"=TpHaqQϞjUj\$WU My{AU$*]UO]\*P t-xsaaOg*Ƣ"}458af)L>@Wx SEjGNUG2d mZ+ѫ5A$v|EZ2uxWÿQ9*B5-+UT|USh%ϝ5cUZO >/0UŨ@5`u̘JW}c } JLZ n jMNU00SLݤTUȆhc՞ј^BUة:Y}U?!=jXx``3C5s LűVT.k%$pjl[CU&*nsp*?{BY;1ިTTb&.Ue:B8aPQ(˴ ~0VAy{m{bufΐj5V=,-nV;KJcjT)^%l;jj!jKVWq(Xa_]ZH#*՝6cuTLo : skhX-PjwKOg/OmvqշUYۭ XU}Yv#DH!V/\Eb۩c`J#s/_Gޟ0>XOXͥ=I:V4s5-6rcc+cLUZ 1UOB4 z> {Uh5PX}k,ɴqO^qu̠j Lӈ_j\%$jaV2+ZTVõj #x]Uj.Uj׭Or|+B2UK<0Ԯ U*`3Qeb5Xq.=7 WU#aX"V@\S[3c͂>hZL45 7nsavk[{Dh}.k+8k%#kO ڴbst;zVWbS T\<i.t iebm9@7]^f,W]bKZuRu$_Φ+Bh@0qtDkd+B0LRųD6]T3P QY1RiZQ͢"ˎTJQկ0X66?9(A9YX<Mu"VYQFAGu- Jիh"MV[a Y**T@1&Qd׵-b7.'J+\ɕ#:N. 9kzh)\-C=-UiZTv[sq_.HpU絰<SC.ՂF]n%PusLz8:3ϷqUe]53V?0\sa™z7!&l$]F\^!`%G]p }j*x})*\L?"SAso *ɁC,jZLpU^wVqҗ\P֟JƲR+Z3',o hS)T 90ѩUK%.DvEkkw^Zm:*&YP:W@.Xe)A1UVx+&/Q@VVYӟ"V[! K!G+[,*QP}ʁR(ǍW+ `lU.kŦ_`5ΗPsi_>%^Bo͞[n?~ujWq%`r"k~_ƗEa RЕZP M<6U(hU+VO"i "V!Y*_>{6W%WHv/h dVFƪeuNW܃uUH}ZUc*U0il*HU,?:Ẻ+i[*ȧjVz^"fq"Uk&T+$x{^uC_^ruppffnNig \U |̻nlZ:=2!UY k{X[Ϻqi&.,WT̩U[&Xek+ u0 : \UԎbxysuG\c}&C+_Z`gS=}_PhT k}WZ"YpUaUB; aƢal TtmZ}=T TT5qۡ */B3V>sZ՛i*kUkNe;>Uj4W X1fẑŽҋi^)vP1_ՉUj(\[eU3 XI.P Ql&Dt][J\+xNuf_&f|a1:c*audFWZe+j趧WX'|ZUU aI"Z UrGVI\ۉX 0zZM~t=s*PMn+S*z"#>M(2UY8e'ݮTEb69֤VKLM%UJm.t@#IFj + VazXV`5"uؤU؆w0kjV̆RlRc֌рR_TMq! ۠$DQԬydZ-/xEm7XS*uqw8'UY au:T*Wg&f=#X%Z ;YjU,W:hZ* V(Vg9;˻;_uUUGm쮂G>nW8r3YF`jJ`>\}7pv׶pŞb\D?4Uhb*qƀ }wRIZS! 37 ؏$*û碫8s 2qTb7[?PDЉ UJrfcN+Th WI#k5J,؜Ԉj:pV`=?VVsE=jg:į5Sq*RUؔª5.dbZ xr:4঴L ]TFUZ02rru`u*]^΂{|#x ZG֚ǽQ*W*z V<EW, LD')eꕅUqBઍ6BmZn.+ d UhEML)ȊaC5[MU_PTA2S29>fV~XmQJʬwqUtX i TߪV/ϚRfC`aj(nNƂ߹V#Ra 3j,&`⹄kVGTQo POYcDrSXjqB=׺# X݅ UtBRE Vo%gcª^Vݫ6pUж 3Z#2ZAJTvInvpVZI tcAip1YO]U-EcȍUY!3i5JA_] U|l1zI .WV /)`jc5C.ʙjU2W/aՋ2Ve'6% I#Y@dUDm@T)Y5-3ʛ(v)*֊Ұª|/}孥\cJ8`:!Taj2k8 K2WIa d ?GK1`l]BXiav˴,-7W9E`R-|E=j|G *t3Z 0zu{*{ZVE=eR15VΖ<iyлDXK +cU70U˯ H^*CZSDt +CvIR[m(pi7af۳ԝЉV .U^X32{JMb_IZP S!Yܒǭ2HIX@Np`ζ 0 H-< P+Ոpte\+ߎWAU{ۗ5VG\3`uU>?xɫWNN/^˯>#8-^*CI2227g^Anڀu+6薍|~~)_ZF&=j|\bJTf^Dd*#ղ՗4d3H=3Xq(@٪9nt|09x 4`~nUk)aU2Vu|'X˖|[}d#zlxJ+iX'xU:qx.`UAf.]!v&*ΚfX嫍DL[$ jv:,+FVVVlxc+ωbuVRꢋ88JJ8C'Zfct[oV׬bɳ"*X7U8zPzP}=b(%誊꜊U~KņA Bs9/KgUp*ܷJ`MW5V ȞOoBDO2V @cNX-lI [}(8$2A*S_e)ݽ. b3p訇ҕʩI,d}qЎ)lەб88 IR k@ܱD wz IDAT:QCOGn}I̬GԵVca?+"T%Iuk@%_C+^=(9n ݆$~$Iqķ-ATD*5tI2ۺت04j4ԹV-eОWDT?Q̰Ujc0LC X*6:86,h9msb!PݬT`bc;;/jYX~ r]UBe̲US'ufs5\5k<>*t$. * t(prσˍD]gp+锌s71iXMf?![VԵ\OJL] : SV?.bmƁhVMg#UbwyUR7s dp<s^;NXH*ji:c- фc-i G@ Xd!4DbB0֎;e%)քSaB j7vx$bR״b rj(a{矉UsUz곜(@*)SY;ʺZ`2TTt*/0UoWKª(欖%(.r 1ffLY0hZvJQ3Eh->Uu'x TILh@h W;Um'XD+ a.@,CV EAWYhV(mMJtU۪3gn jU}@ R.`yIU'Xk}[%Vjlғq<[ j{H= "ZkZCՒՇr_**mʃAVEWc*,jn.?\~x%UH ѧ?/!ʊ|zJӫ'XݰmU` @) UUH|wGV}nmXEpتDB5twn +-:hpcYunC(81%@O4EJSuki")QU\QFʟ>Z[ŪV)ruUq:STM(0+U BUs<V(ʺzSX5VL/j+`5_q'MRU Y ][ݫaU9 `JVRq4VPR 4V?S94 eW5.:_^)aJX?^nXZnR`^_|# kU̝]h[ǝ3oSSk8`*r,+$ 7Ӑs< \ƺ2J*W1VcsVd%8Jmְ,M`Y/!Ƚ8GGSslٖ-V(2|)UPn`5HXŶt'da ډ,K G:Uҧ\ 7Z}5 {$nc\%<{OH2Xs@2X4/a:R3y#=f"6IVOGZuF}2l(af-XOx[DfUc'xuX%l8}ʟܩP݆Z|[6}VIWV<XӦS8d֪+KTX6A݂dlO}V=cWiLU ި*Τ5UNYX^*dت[].y|j(cV.> jwя+;9&S`uZY e<`5+n]-7W)e.-ePI(W=TT:U.Z蠚+l2zXe杞 488ʃU'6 <]=1 }^z"eG18FCbpj[UUmIm]@EB:,eWgoN2jVM'@V`ZhS5j`A[zVͺϾ=vD&S[P,f˅@H͔rbSsWT*J&v!.N 9_Z\XMZm8QR*G W3E&SJOre魎 bÔP)A/V~YYle9K|vvs eحUX=_`)O;TmŘh&q>RVՃe !SRJzCҵxʚ5JTC T:ftV Bb5V5Eb,M`qoiuTѪ*xUV )J~Rah|qUCR U *jXڜUU)Tm|&4U)@xHZX5ZVĪ?VuXԆU p&ρk[~#j''/HV7UfUd*6b?)Ds孥rLXj5ʭەl{2l>GV#@L*m `ٶjQ1V_u7 FUj=`u/:I0W4 JiGEU"5Ӭ^SX5V$J f;>WW& 4ofeeNjTB j ڽgT ue~4|Wjj$-Af:e*U*h'_YnL BJUZjVQ;9B>IdJcժvpJXŖIUGm q,@j'U LHV݈NB(nkcLU4VZ z\緁mJ3 Tm @P5TEk!XZߚ?w..υX)mݥV_?ͽmmK BˬV3毜DH=ixHaƭ0TmVS!]*mGaU\uv#6+W-Z"(? vFCS媙S*VͱnҚԀHuC9a0>j?ʂQd#*3V(VI:'RT Ƀb1OB«g*jt70[ Cd* ui,>R~SBH:FhV尚n..N h8 YD^9sӟϑ(V[ηc2,PX]!jLkhPVCx^lV&>̂.㻧wiV6ZXo`j*UXή͠ZRyYYx폧m# :Xj(B>ˍ5 ~:լ, _^.-˔WeuVR?%jEg74 *4[/)0[S w,oh( i!lB.VRA*<_# pCû\H9DFg jwlāKX2Tĵ$Wka5jezJ;SmPCUG҄`!C_kۻ^S6vu*8a\XǷy's<*dǒ\n:],/,ʷr´[ flb]Oye?,5#OkU{3{Jbuj5Z/JSV Wg.e7a M:T5j$j >YW+VS.?]Yy~O\sV,U;dVmmHUF9? {Ǯ`Ϳxy|{ޥvv1mgVċ (BṗPA],\ ETmiE6uM:!BNX&FcgPu(dBry}{ ;68@"~<۰8Vi*Ze4e|!>2M{\ձ=AJh0[\:a*U{ծXk8:Xe`5J5E57#RabR%p%bjKbut̨& RX^jʎ{APSL!=ñj47qVeV6kT4a% \'MT?btXв?kG֪Uȱt4+!HcV,Yϊ%~Kwҟ{(GI[ HNw  ͅ\֦VoX(.v>O/7J[ ODsš H֩kXVg 4'ܟg—=g=y|b(L68ͨpV+R:{z~^Nԓru\VED\$$r*9pi >L&?~96RIbR''2X/8UV鐿9>7nMw0z?5Vy[sE+U.KJ=UֺOW@r׍X6VvCTM/Q:Nͧo5R*'PqEV]bUM(kY'8W8ab$.*!m5:LZpo7o^I|pVUJ\2РZ+FXm-/2 6K0M*U*pRZC*o[}oQDtm`&.b_Z?η6vj a Z*RU+@d5,tbVi-{EQA;PKU.Đ9xruUt))d [U:V-"V8Vg5̲xIBKbUcHy3Vu?>oHp=nN$ W+V6a1 EPs'+nUy`jln< gǗSL&J:;.KEv0.2̮'',dVj2lC˿ Z0u*+2W5&`P=rITI*rXP&:CkEKs/-T JT *?(j^Q_۰Jwٰp vTZTB2 Ue!+mGNw5PUs&SG!kU;)Q\k j uدj*VR]8^rDRRSռה<;|RUrjyy),:իd+|">pp2~jL| JE`zcOM3ZO J2*tZNN$3գL>[\^*@זZ#ȭUVf.Clbu7!zUomj1=$zVZT'Z*b9F1_a \R(@H^#]Bt\&y4F_:$jQ>3=K^V&Vʢ 40, \먿}gB= WkBJd$TE`6I峙IVv)Vy 3`+J0o%z_{=xPڍ<>ٻӪr4|G ä+D6z(g9e_U֟o[F,]XeT YTtR`Gu yh^fXmh]UϠLoן\$'q[cP(VYwfv>?meWQe:Nj % 0X!,ĨxTfCKcƵ1T!MX̪3Ҥaa7d7{{v@K1R>={x_HcE?_cz uP$=>?Q(|כͯDb;=d%←w_$Fb{n@ b+Ujph\RlZ4Vۺ@XOjlL %j +KORL0 K \-h hR=OO49ZEeɬ*U."+,&B ^fQZauzV|`"?A 3-Vo3V E@_B_@KkITd${'PEj9+UpHٞ+O~ V@>.-5ׂ]d hߡUGWGqux߬_4~qL6p5z$CBW1F%W)V:C5g2ivH+>)D~TT`DUii,MEoǒ4bFkL ֘͊j5O=mhnնjF8 P:׮Bjx VXlW+j2$j5BKGY9XO@ՈU RIlz=q.V_%WBbqsJI&U: I URMTGh<2UU9Pu%;gլqd(.%r5QfzKrtpTATz?o-ϛ t׼xo:9)qG+' eA05joHkcIQ [@u۝:z(V{{nzCmh c-=6IS'_a7:[rX?X`4# 6f`#m5VJޜrg Tq*Nk0$VXeuY; 8]'i@ hU6}Օ## 3W 6>`5*NYFpĪXyxfUpjRub]kU۪7DЈ^jb5GN*\*:GJVlN0ٕOj`g?7| V3|ܢުTB;NeH @9TH܇S_aאGY^ӣChUr՛x sխGWfa;t{ vRB±;2L,d(q |hUtLIURy=zHk*uG*$X=XRJU!֏N[oRH=4 * (fh,+ =<5U Lg/ӱ5$qe}*eevHmmJP/Wj~MR=U<XAZҋJ.tjBW:z~j7U܍+̶kۇ=(kjGQN]m\e3n>'eT`͋nִ͛_C5Te o=%V} Lem;@W$Ɯ5Gɰ~IOR Ր_t51 ) <ۣX jXBacʊ*ZccT ]qUP[ |}tJ\ٯ*/V):ª,wfZָ9(0,yU V5X:/fXV,V ^`r'TPbUWviuJcsǀ*U$5a=%i0O2F+譕{+XX:Rv%4j= 9o6XcjqLe&֓Z2z-92Vjᬯ;A]vuWv{x0ZvlS!1}&f4f OMa.a#,ԃgU&W`9R׏!ꨮ Ksm>|;rBA+~J:X`RU]OepLz@{oG4uj7 T'Z,pSNlc"Wg|b~eU_dJ'W)q*U*.c:垂t6k7ge&k6cjƧNQij}2>oH!tέ6 k_@`=9GFg_ѨVFQNT؞Cor^gTիx US-WjU7ё+Ŵ8@s~ {ꃹ҇RpG͎> V0ZqusrտAV'.lg`` U/©Bgr>oUDjcsJ/)0+bE;TO_a~'X'd;։+ũAyZ*N-LMN 8bt+uWyz 0TeL*Wi T2YM2P2I64)h>/)`6*gރuK׻ ]Z_,C%y z +'<"uTgE6oRҒjHՍ~jB1=<NR?ؘſFViI\`p`&1?  {\J WM(]58&ʢ{8>< ZQXPdb6U9XF%TUhr!BfFSvE5PSޫ`|"cÉI.UdVzaզR)NN,\jBhDH`E{Wx2ZLǷVMf10ZsG׃,[>V~]jgL-Aעi) U7Ʀ"U9(Pm@"O5oeԻ*VTjjl^d+KѸ:5XQB(y>&@1hb9< P\ jV3yER;[]W"2G& hw#WKKE-V5yAĪp V2 w.tAqY^+T Ih|9MV|Z=ian,#3pq|P+փ3]n*cX&ubW;?zab[9:Kɑ *duQQ:dR2f?1R'VTqY;Wy^;dV+]bQ@'4F'G*SդSIA /- QvEF,?V7QSn>QAY(j7bճfb{47Xʪax@aZUzjٞB0rm&FsOUtm%]hԟH${9 /k=Ȝd{z&io +T%\EZTB{#In"ؗ5*ßcu!qfU6 1Ìd5c0C>_N6/Ǫɾ$E0ŇÒ_IWIoo]7ͰWA<)9w^*n&`ojbh3f9f,_ۀ7Ð~X=VI_ên>-|[UUr `\eϲMngUk:͜U x0 PV* KJR*vz\GYkjZZ4 3*VO%1K TML1lQ膟zET Qr37vFsUIBz"Een,K9zz46AXZ]׫u[XE[*oH;kX=.ѷAJ*W _"ѫjc/b*VJê d c]"u{J@)kp=CEȰV5Qx^GvOaG A*Od*\Tml˹5 Sgūqm!Wrds?AnBe=(:+сƹEmTFc{+c`ӿcq~LLKc`4nd\׊?<8C1zף Xz= i,|NQ_Ę_!f:6ǟĥ>NBjUmwFY <W6l EU%9(X͛'QZU&S(EVZs*aգ@,V{]-:]#nZVrˬڼZ(mѾ`d2iPV4*j_< TXmqi4_P媞j`Gک`ZX}{Tb޺Ngu7D^0!9OibF6od2[3_n+dVV3ʿL\jչ*Ivw,E? 5FP{Xy`y7'\7#Ebkʱ -k=ɾt mꑤϒ.kIaHHz^KycΛuxد/yAcU^A5*b_7ZW%2_-'@$cюloc>gbN butBM [{O>: l' "#:$: tRz lD\ύD׭IRCgǧJz֖ *GKZj*J.e<{ʰiVUFV@.QG*TZՆ UyfSk*ŵ?<"SU z \0 `UZS9EE[hzS^e*u0DE1LMJA21 [JiKZkӠҵq[;1Ҙa_A3)5X ¢mȓ/V_S'NX -۪UXW}ծhj˫yq2%`u7#*V[An]n booTϤu'YhIΟ9-/$1[@jZMu;q6aB"*n;ݝ2OZKXqw/뾑 aUT>LBBf\OC0b YupQI>C[ApSs=>n͢Uօib0yLJ"5Cl`*|7x?jUY>Wo_Z8[VαRXáV?s*JSՂOc IDATj o"V%aT jtjh?XM2SyX.V{ྦྷ=誊Uj,Eٌpkkv.V (QJVM*$4 7)JDM[Ii;i/HU@V`V7i>zf2j͗S7ye2T)5IU2 bu?d寨2eA]wf-rU ZE"T/ŴKKZŊR(jeyvh]{Qg?(f W?4jCYroVnVd|VK\g!SH(=lēKZL-r~sŦ ʣdHd5ŵ82Xd(nTƪEV[lqޓdwV WdOH&DYH1ī*u*;1v}]vZM`XlB'@9zbV;nQ!Īc5*Տju:\v Xʰ :4bz_TPJVq>AJ: r`EX?10m4EUV*7/c}~KSGwZVM-c_||-Kj%K_<.U7?yWȫZ~_rpN|u~dܯ?3N}xfA/ cVիS˭{*nVRU0Zp{oEZ.eʌ666V蠗VyDT `mEj[ŔJP">wϦ"~ɣ|y>A4擓ҫmSɅQ-jZeсJ7j@ǘ׸-u (i6Kem03VE0=(S'#Ťj!ڵªgrп~C=`9So4)oh+3#Oª'?̌v"<0,rVX0]VITФ)r#OEK ,R/+Щ ZVCf lHd];V φl}}<VCěY7A`凒ZcJp$|!OZG9_*ltiohf~0v*WuORUa\ef(UgtPQpjԭXe=+ (P VvUiLPŕRU?OXT0˭P^_M%TDss I '?_%DJ,gRvRwݺss[Ҩ/]\T-Vu5n-͜ru6cI^LC@!p$EDttT^(*c7;.CkSVup$ '3MϷBiaZ Ii. :{^ V֬F4g-0 {ĎcG}Vcww)n!)*Ǐ$;Vt=w`7J\ծRY6Tj.'\*}ȳtpWL30\Ez|>ߩL ,Ml7%Q D+o@ ׺Uj [{~?<у~rS_6Կ_1|@E:FnARGj`'(y!+gccåYU\bIrz]Z[V <;;Y;4K{_U4\0OU>A|59vcwիORO֟ji\El倿\h5Ru}Xժ`cHUԺJ>TdyF5T3.AyTb5*Ūj+/.^SUV9v},2wWއ;"F ;ao>WV^]wIÜZ#bg]G@T螫yJX݈űEFejYRŁV͢ZzےۦVhzRiM7X<61 .ns%׿TחXwW =swM[JH_n;-TW+: iJ@L|ޤpM,.R"67*)VzS+VI92NC Ӫ|q' SWPUw\ƶ=QJ*.e j]*][Hfj?`]B\%zhaՊ,Drc{UYcT\DV7OpvjU\PYhiU!*VFn D?4$69e[ip끄dB&sYV!soZi>vFP{,R Q4YZCD5'3ye>M^asj5e=j YRM|XF@/Ķ{ԙ)azW*_Ǖ1?Kz3Us3L=ewǗ-,[ɥ:a:0-;-kˢxi#MUZˎYZmzZm ٩NJm[dF̴,?eO [})<ś}gE%*o^+@ }n϶`s,+~"SsHJcg4T-C*bǪ^d -@-Si)WYܹ(%oR*w?DUJH<6P Jx_UUl3pouo|/@! MDJY8}ˍZ!Qq^G S'rh iP[i{o|K[ݔUf 1ʿ!n/$;vc.^/.koyV}~[J9wdzNEЕfDqHAuCOy?f7,HI!4AMEe'vk4؞TK-$*V+%/?Rܪ[`H3"V~p6Iؿf2EJWeF.Vɩb ='Vͦw}c)╗4iB@j֞,OZ-/*2ap$ Ero,GY1Z_rV+*KBy̗.xk"+Tj*eEo$z^~kX%zTE±h}Q Uٳ*}fZ=zPpcC~i6o.zL#PܸH6"!UyEpmz'ý1Z/ r<+ytTu EȲUĪEECU ׹'o_{_j|Ĝ7Tc9[!3G^i+RP:,]4U/uJ:$R*,X pMٱ\׌fI"Pre=VzjðzWWj;)cX%aqbrUkFXM^$y:c!+oժUj3V [S~`7Xuj7GVϤVOpdױ5DM}qw5Pᵹ)>ъ@9K,G9xY2Mo4fߑ_ul@V]@՗FO3UOT&^S)ՎđՁXj`IYgU;MGhC)*]^S3'g(7A"Tj/T@QwORV"V1fWk5jbU aUT\EXUCR lDjbY!jfwZM (n&Kܻ(V^rt}Y d5T9FA~D8#|kꉃ\y:jΛpjUͱ`qMuԠitÚ)0PC*V`uL'b5h԰WQ*cٷ5Pf'\ɉq7bU ~2\a{bSJ XtSĩ"x{󍘶JCd9oJz4T칚yzS +4y1ה<2Ujuj>UvWy>չT)-Yw*qu񰐪EiU~̑W! X2*"&7Z[IN'ZWVUՄ6L`b}tt!c^_ :fyc63WN!'~Vg0kaW.f9oN>*f ooNN,Օ 4M%/9GuO+L/*0^[֊|RH9vHvn_[k O/3w]CuuuuvfW$5g_HU\C5L53_Ij<ڋ[}}U{UcR`tjlX-D齰k`eQ5j5UjN@Td={ֽXzEXc/|9ZS*kkuVZ7<ЧDW*b3`E1gf/ӣhyo5PնMXc*>J%-s v}nɯ?gO El4 xzhpzQ?<]'l1}U'ƪ&5;/G#N}SΓaYөK z>xzjU99#fFtskLj&JԵz,.C*l P]Ҥj1("~UܮʪVQV6Znj>pQJT]VWXx>lkjj@_H~*l6/{kR4W 8BΠ[< Z0#(G׺~gfJw++r˧X=K[C!lm5;ꚠ&Tú|JO&TSշqAǩߙ IDAT`pu4pULWB1#iALMm-&7 z 3ܮPu2L?G+XUxn3ẌMNAMO cg&5DڋsU_-t\EG21ÜުXK8GjEX%O+ps;&TXe%j0X%6MԘp&HV/jVѳ/j51g_HfJݾ*5pfpYE"LDlc^_b5Yˠ{ۧ@*Ueco2M4 Fꧢ3=|) S-$%6!mb ԑʦ>Q|"@})huO?sR5=s~uJV6 |1 fCPԪ[r5Lߥ̋a%ۤ*R'_返/8hk1WC*UPkޔWv IjU4+Q5=D VQFhI;TW]ݣ'!RjX 0PՉխjQVvf-lpyg`nFYo&EðT.Ln:ԧ_s]fV 8iFWC1jIbyOHTĢ@2d2~_ XTeVS(gbҢE$ Ek8 ZIUO]W%Ղ'w)`(TU UV )dmU(yJ*xZ直[NerVdJYjrgVAU&\E8 buglgZҗJnS)^/jir>xتVpfO>?e'hU l z&YNJer9+ڼChQEDZAfo]RSY_^Xme*.SVū.V#X36RUml6DXJǪ a%eUUx}ƪWIYU?UIx%> UX"J4ݱj `˞?e/Q7~-sTj1j@(nF%]^Gm}1"EiGlb3AՌ(3\PEA^ "* Ox O0IT*+`F9p&TJ*_*-`AIU]gOTft*TE,.q`?~[6Z 5}eŠۊlͪUnv`h4UYEZỲz!]VBE`S;W(.ՄpI{7*maOgUD_+^kt.ȾU (U?]ߪw}ڻv!mei_5 ěۍZNY; Z[fNbEm\me ,݁v?2[(CXlӡFS|i|UYmbgPA)/syֶ$&zsc/Ĥ췟Nfm?Z6Tr*!m1UTߓe%do% MrZj-}"3 3!͎}WIJ{[$KYh[҅- qǧ[~[/5ybfN.Tsj$j,p݁yyA릦& SdN1j Trګ{[Xiz&j՜l1".8˱sU,VMUb5F3RN;ڃ}ww"Vc)q&])5+<N3 \2m*>kj94UrSid(ҥOǕUZIMJ&VJA+<.k瘟K]ہlyr,Xf6qSj*J֏hV?j%ڬ@jV!( AZaϳq)op8Cr *ZB֔i NVI TRe;Hŕ\fF"J/qz4|ilWʭjeDRSM ,ӿh:TwQ+%SjӪ?w/z߱:VU*p5G"c^v!%V*dJ @ DT52>4Zx8|{7jz1ܮ#;{oHi!1DZcS+P>R `h:Z>Z^bjF矓jb.G{c dDHi `}Ff4iO t[Iɍrjq"2o5wh<*'W :50kl-`@ZOz!䭚J|/Mf˶)Փe%jrjh@VfZͧTB:QpF,S8pUN|_cv:r32eJ`HWq,Gc y٪^,r+|Z=jli#G43{j`F)Cv^:iIS{@՘Fbr5ST LweZ+aBǭ?&z-""d%]醆W8qex&~!)9N 询ZbjRꌒ1XHVn`hU/V?[oo kV7%L  G 9Ȫ8W3Y PBX?={VQay9p媩VgҮ{q{v=cFdZ\uIҡJņ -;*X DƒKڨ!'JoɶTm~spAK.$aIՏD zy(DMV OXt]E q{k|'Ce~?9eJ]D_Uʝ,sY-eZ=zҪ{0iok%fJB'TP} 1H6|'}IXfv&kb:X?5AjD6=LPi7}#g*6KXC̎i k8ajNYs8o<*{ -JJ*CA7`of٤V7.cMMfeZj VWdŧUJ]Jk5<¢GQ!GSaՓUzxyli(NXuVU+ݏ $ye$SUVFpTʠVo"THW(BvA}b۪gL_&V\%U TG*aYZ68kmazzwfWo$cUNCly8 z.S;b{K@GcWC rr:px E,<əjf]U'z{/Naq&F:I X0ozjuvy7FB}`EOU *b"OT nrJ`DQ[2V c33CsOhi[xJ=X6<8U`S_E\uck=rБ {B_=|{{-jUo?|;.Wr>] WÕG/rTUpأlVyW~OL`u'xVn47s=.]SE_XmeW]eSgXuqNDχՑ5!ЬkU{tFMKv͵ :ٓOLĐQ";k$Q`rT F»)o$WYHe@Rh/U=$9PXUeRr@dYbZ$Ex dɓ' VۦaFwCշUV_܋=\bP[3=Xɝ`U늎#9 ̺jub_yo}A3VCOn~Q7#/b%<ԪV`dWs[ܜfqdURxKrT +U[b ճgoTOZ^#jӍJws?oN,Z70qsS^mOG<2Ec:]XBt ۿ18:^X> B7۟оu; CϢ~s;U;WSi;ж+#KzȞ)sXWIqR&+!^] a%:1mxPGE SH˲'A%JdGn:G@J0:4x;{߻O%ϒ?=Y}sO=+gC -,ֿMWRY8n^@Ke;glqM|.~헮~}NKUyuOp;{~pc|%>϶oU%FwqDC}|@-5tZs(%G;zY~™ Jbw'hPZ. -{L/[|19%ʡ^HJV1Wv`+w϶d>~~/6ƓO7@m[mj}[[[$êY`BBȽպ:'êov2rl%a'>Zj, VdZ 8=jm ~'Ҷ6jՁXE5*{CxJ嶪X}u[Go?N[ou 9=?dCCl0pfn{vO3|11.^75 BބzAe/[6\@X\d@1\z gX "āxan__wL',ZXDQ`9dCYupRwv]L<p"Kvx3Ep2޹px~9/r~Yx*"%)URс.q!|J=D tjWo%X6&H>3Qԯ~T\ 1*8pSY_$8mxm\ VնJqx"iyV V7,>JuX5uZVɪO777%z<"vŚ泗N5V_4L-a5a5K4\UVAN&d*1WD=$XmYj5VMP=3֖VEaU=7-V)h%PN#U[Q4U*UIױ junLjh^Mdp9n" w<XG"၆Ua Ϩz 6tXNۊ2'\.v8UDgC.EEX*JR_j̅{\R@UFD*0vI10f\esDb&^s|Ԣ֙#V#CҖԯ#DJoaGRoyЪbZѵt*n)~My^c m\E(N=lvgv(MRB8*ti ez +c\c@zQ jrj8QexթS_w*ߍP!OgM3`3 'Gbgi滯Uc5LVP:譚|̱"^אj=MgFw;JsE)ePش ^7a}M-;v!^ǯ=: j+ՐR~t%ԂMUceJX*rH,_lh,'}O3d 鋑? Y;(Wʰ ˆ!"b!cզc5>66M,6g0}Vxq3*Խ18:iSp a)>!S`x:# aUgC)t=3ۿ< x hXrnYŧ_-_,os<'i٧^uznϸ! C"9դB;9>J|Nq%@`P2CeKT;A%gUdpdz YajUzPNzظVU+8Ճh4 Om[Qʪ~;>O֛ .ڹ/ٴ{3P }iĪJXWXf| -W"VVͤV UQVĪ]ǪjxivaUªڊb$C êդb asC6EujjUh%:U9"Ub{ 7tyZ*o'0c:,(ԬVǓwjU(|2V j|>?uhXXoD'_^'SZt3H&jʕ]w*U^L>ZMeWo\K[AVt/(ԪnjR48kJtFwwOЀ4Uf:BnݥժFng d~[5sj8Vk)b@E˰4lKXjM;\I}ؾ/' sWSDRL";Ul(B2*:J_bĪkZdt~Y _JҍXt|I&nUϺO҉:AQR)c]jV.mt a! fMraB2cj1c `OTVW֧1^Ʌ'9AU!OjZE lwViݨխ5n]7|LV)+\3kVAn6PUKՇ VECWptke`u:sѫ SFR,,\%XmuL!'V$>,z- buU S- O;@;w4 ">'@G 3EO,z)f=@S`t m8.Xp}HBbIwK7wsD\h OIƌF5!+~)5aƃMKbRZ'aŏA;G?YWWW"+w,=p>|oiz^<Īo^{ߨk߻~i_[kP*JLx;$sn]rnbٛo\]Z?Y^Z{3] tg0Q๯ba?+ IDAT;/fz+tdǤ۩O#TUxUD -K J˗EOIEULO}HGOMU.?B7L"6(KP/*JVSZjɟ,Ӧ%U9keL{&՚x=* TҰ L%Tu4¦Wcu5TU+5"[4Suf֖(ҊZ5$gqjbӯ]|˿ٸu텕3S==g7i˵|xݵ+ɍd7 }fq%{ᝅu3t0!?/v;2w W u1\UpcR'Ո z8*Pr, U] ϋ *oԅ]qK]@\=qa7Z8cE^ |OYy9hf}X}(c< `K9|]J4ݏX<'x;sS,Xl`U(K_(Ky4;ssepŦ2gkڧXdt"j  Rn:@;y~ן,؇zgG+ r*4DV3D.U'2PIRԥ^hAϫ=ùОUTl(p*q=!mQJ zQ2X(dB:?C [PY+cԥʌUjdu{ 4XЩ.VAK-Z=}>֫ۡ`_ğdǟَ2]x6fӓ޶MC*סBkL'V X*aZv v8ٹcdz_RUq^!@̀CއS,Ͱ0jگ[j]ԉpα*zEVSN+8C@]]|؋d&3sT R;wι9Yﴥy1zrR.v{1'Rn x: Z*KZ;20Wذ%[{N [IZ꿢p= 2+*HOyc`_3Nvd()`~F0+&m DVT4YM$SkD֓5||)*Bs\!50@85LEnuetP}:Օ\[<` *0Z¿-RT<*֫Cl6PXH+=t[hpKtSx+ӧY5BᶤVJa?Հ[6ΘjOڵx k!fe?7!hcA*sV!k5.n֍vSŪFLUSgUXB={TaJ0UY1Ōt_f.ի=z,{GR%O3)^fwMl$42P9݆]tJեӼj:t'Z`El IAJF=9/Sq-TĆI*DZcqW_VGK[ j+K2aj)P*zTdgbq`EUueY#pAW@6hjH^~KXE>!jUr R5 *Jd8VcvvtA^Ps+m)* U]V5U*^ jZyyZ\jupqXt\ ́8 T)N 4+͝x=} (Sj@0 -cwa٫7[ƪYArnV]>$ |i]Zb8ѫ%ߍ,cD Mai(-L%X&Sa n4N\F .JᯨB2ÖtB1X Z=Kbۨ _a=At٨1"ؘN_΀x^Vju@fd7ch97@?%1UvZ#km.ֲ%!\(ՌꚪɱJ 7>'֡Uӭ0IÈ팫f_U:PO?|~ꁢ KyzT},P K4'kՠT \-<5jď푳m+L՟CR2/jR$03f X"h)Vfae6dk8*lQ#\j/XCTj*jc5 B F&bB&V5S5q.ͨp_\UAU)P2:誓>b;`PUܠZU\Y X,yTyTqtVZjhJr)+R[j9VCɤk9.RtI/8Y8NV') ETV'@TW SdjPId>dITeLe&UUe~eCjyJ21TUa"+gs~*edkIU[j;jXjB=mA**t`=M`~M$SLsՈU[N`n,Wd@"plmX auPY *TjFX"G zS㓫lσq[jT ⒴M@MJOd: j1_RW:;UjUU)9/hHbSgt߃eTU[Y+%:?[2{Dt _WqJZ|)YMjSVz֚Zŭj'f%o m rOU'􏰚тS5R&?g)I`v#ƪj^V#NQn\UգZ]]][]3Dq@c ~mBKS5:ͩʰ @#Z[ `i1IÙL|E3 B4WBUmQ)] *W;UkAYyP:d,=4:1J!V荘v/5Xq_ UHVLe=ΛZGQ4qӋK!`Ֆ'-jn윊ק[X{+jIK]1Fj#w!rKj2+6iKA19F8mqxʽN%su)TEVΞ=~RR$a>h̪?YqUP 2CoLx9Kn 9}"jjl36^H^R JV*OUX旳TjVOVOՀ*4?~*m6 _q] '=EUxPYzݧ<giWIq+=@jl$3Sdv{pD6m>QKBZ#j!U/ViZQj*gz ՝3 j$)*X=jk-j2灅>8X{BagzCQV`\9 ю?z6r_4lq% . ?zmm8 ć“îB8";Ņ!q)|PUVol>co6᪪/@V/__v"zcmқgۯ^_]_yrĪJtXJUʎ,(ިk!M`oUՋK!gUbuVI6^]c u:8rӉ U)$`^iBEg2+3+rpњ;8?rN5/F\┌N4)SIs"rR5"}PQ(4II.ޗQznDH",V9'VǍR 8[)@K`J`QbiU^Έn.*IWX5:%K]uWjX+Qatώ ;W  ^!NÂU+wһqx*Tgppgo',^}Y|jUʍo֊ Rje݋q٪87esRuZXЫZL\Fji \p# ̷\퀝?-fjww7O[ZO^UuL}+!WXȜJ; R3IY֚ԤDYp3V*BQc[K'CƩIT3 Iʆ&)VTG2EdhY8P^?criiŏ3a7G-<Ұ WkʼQӋI:fSM\OH **Ϥ65&{MLOt>2L$XG6yea|LBŲ'$F#bPqm4XTl^DVLzJUMn6I] 6]? bjX=_vB\;vX{ʫXF_ ur--7$`gTW:V 9mWC*8urZqx_vqjyjՑ @3 N3lm;->j*N<G)K5vVAM4X5'CY)Fnr+@he%\RV=}Թ2SuIn] ux W@[jXm{R׭E?\*3Lq}UT5TꀺMt;rj6Thߩ]7&4@z *U\rrCԮ X*Q vEb\\*N*9}a=ڪ*UK\hAK R2`ùWj=f\}EQcc9OK 2cÉc {o %.~yR"M^JRMQ1zRcq+1Bs_8637Nl;ǯ;T(0UCU#^cf1՞XUXUR4=>c(wrX/LKi'O@b>%UZ'URgmjwNo"Ve*&:0A4bTJ6*8ZJmsW9W)[6}xޚ׉o{`б2' W vNmAu %ŬVU2N^uTּjLEr@V僼d\-Qz`` XpPt&+kP(K* ( IDAT*~OjHVUj 'WB2 L|cw:soMn(W:q^ڪLAm5ʲ9y ;.Hl2yjO\0VcN6w|wb XEuvT*&ZhY\%^{$2j&H+fiez-U0I֪_uUeTmpJqvVn-!s;:p.U|~%Kݰ#CE%Iޤ$PʰOjK'Wm_6+{TӪ*%)j> B6X+jgͫ6:ڀ_[ ;Uۯ85Q\k,Ua*Hת-*5 C:ЭՃ몽 hKɪBOETMe21g_~  15V1&FZ\j~`Uc`Ё5'84nU1 ^=[@Tu*[A*@΅Pmݫ`%>gUX*+UIڭ*V-TU52qDQQiX68ø礓 Jő]MHs2( ,YR?pݢ2+օk;۫mѬmMQ6hUCU1XKI-q5T`]%q@\9*(*_Z"cXZQTƀ*7nUJTEU:Z=>qh^FE_+3~ӛ`~}UA=J22/J VWGsX\%3@`PխVP3HEN\J YAh* +}jBU*VY 'vz4 X<DUC+肪*\7 *$wJw&_[ @C詛jȱ?qwydnU,9/h.^J]=K&RU>GE|ƒuY*;R\T6ѵ4h]mGU_WEW CģZ;Q|#`bC\B\d0=!hOKX\ ShE(kkY1^T8`r_7ʣoTXPeORj궲p)t%Ma:AuʹDx*RIuU^RGM VZG"oάr]oeyݓ2@MKbhхRϴ*qU@[RT]S$֯3L֙CGWVZҫauD=D2X)o>uݼ]a@1*li!S EV, tTWͻB΢Zbjg@nc*Yr\u",ծ߸.X=UIV-:ODE UާzY79<7aZ,}%+UGt+aJ%ˎm?kP5yF$Up&N֛QTu;: T#$Vtf+i* @ރ{ŒM&~٥^5}akkEٲZ9>b|sַBfj[V4rp jbuVot{vUXeMhR"V3TG؀\qJ 8$i" i {V+%*TcY.FhrN  \t*J`I kͮ7׈VYzYЋ壏t/=yJ2$[FqRiZu^Hg؟f9//d^IKw @B.4MlCe6F~)zR5OT lJ6Vdۡ6U!@=i+;x3V4tAY2EqZ܆hlGBd$H*Y(HRVHƩlhd&ScouzsY̹bkog0"4s'!73YR=fMcuLZU֫KRrsVb Xz}*NLNʄCjaVfٓ[vY^=-V*cĢs4gJ5Y(Nĩ=VuըAAVV*T*OZ곈Jjw}5-D)l_ MY!Z9ws% ŧE Pʏz6 >}zy.խ=n".f$*p}i#!Yi>g4n&BR:FW3%0_UJXְe1rzh* Vh^$ZS*cЪQV.Ւ \?Bt۝׳7W(m/QVfr&V{y;ך*:7,8 *r_-1a}VCzSTTQbyR='@ªdI!uTS:d Z\uC/pwSU\UT iy\\N@ahkdzOk؏EUN\ c-Ov/qIM>Ptwiy 6'Tm(I[ٖ*MYipJ˾MPbƉ`crr6dVU jY PiUPMn3-w_@ӂE/W`Tj*UWRj5R-QNޟ8W:hJh*&cUVT|E `-UjN#*B&Xkwea5#`T~( Ah5N\U\XJX>*bf/~N`nCQRt-˷b *AQu;s~էq%0K]Ö́GǍDTs%|={45Ԝƾ%U ѲܫzdꇓQ *3U8\ގ//=|]R9J`XWdpjUU]2\݁*+= 까hu Δ񟝩 N1::`M ꃻFQuVo#Vrǭ{gʅX4Vo5ONl hZ7,ƪo8X])VQB*VE<4֬V?&Ԫꐡz\m Y!@[\OtZ\+TjOź8 {u7Ul-ʦGa;՛5G./Ab QRǹZ6$A1oQ655%Vy_[՘S /EUeP1)VMmʦjUur WS:7^w [~ThR>pW5@幀%` T/ETY;}7c X$fc09 TE8B1TXHZ--Z 5@("y15WVXĪ~ɸ*;j*D4 nIlUcX VJ*UԫG{Gnu F*+V7cUK5@57dZF3r%S3J-$3(P;81Z5qA$J"IԼd7? TT}" `USUC5*p4iZ5QQjT`#eusQ=Z"j ઑZ\d>O|k\VZ*^* SR5T'cjH,1[4r6j"hJ.c ܘ5 )gxrv}1.Bc;`b Aq$W9pUh WB]VUZTSϘGԣww q&W`_kApơUϥTy74~Tħ5__hLkQ#< # mR|LM*Q P٧lg۾T]mVĤ?3WcXjϊ~ȟ8<aAfŪ*pY.rӡ+\~J3|S^aAR9W-|(U`*KB[sLNXu;HHwwo 8a:S@gNU*n۹buY:^\/zZ<*~ +O ze:xx7V>æsVVX_j`r:<:]w:G(W[j!WpzĪӪUDWgLۚwS5sM|gCjPfj&()]/#ځSl8j؜/ rܥǟ9^}=;2TA|SMzR KɕJ[@VRP%zUR/jMF".ؘ㮵0#TBjoYu(*@B%AhݝM,A*@ XS[@a6>@jTꃹk4V br~?7v؁`JP\${}ܥUUXT/XL V>xER9ml3 x|zWWϐwW֏-\[[ZuAuX\+Xu}@K) WM@uߌqi8E*#Z6ZIyDQݡwE@ܖE`[}?~v$Lv`B5lTnU@̪h*p5U^US ):Au8B2NYfP5ۗi@ 7,Vj!kMջwL{B X՚UX- Y s7Q5wJy*< *V IƮDUa`cFZ^ZuX}թ&8u/[8:a}% bͥXVٹ,ėhGO;wsKP8N NÃ$jy\rpM򓃣ڦ6I[r%lzeoAP hZ!j~2o#<܀CĴm9ǍF;P?Yo:b?cHT(U"]/ *?BXAXߙ&XeTTMaJr4jNcw8/2*UyP9+J bFy/Vd;oifԍ1  VѺ* gMMKEUdV XV 8B[͗W7T\uYtG ?+V[{yS<(&{#1XMx69AKyT&P,+wllZ__ww3Wm y/o^x!8}XN6:>Zq~^{+SG^ X^Yyn#xpO*>k V6Dm y6rm(Qwqfrǿ-Blfnhk>,*`cC!mϐ5DCkA(X]U0s53BU Jpy:F2z6%(OzMѯp㫎Sb<µnLN,UX=*,NT;dh G˧Ajd"'Z&TRSU䪑'p9;TfTifŪCc7%<V8;T6V\-WRV0ԿZ-eJ[c UCv#TST\KઐC9v;l7PN^5Ȋpdӫؚ_^_^%[+M$1KfPC4Ri̝ c*ն$QppeƨOWe׊F⨈FRǏjNFe;]'6CR~lM'^?7EUAY:˂Nke&L7 +e': T'A)X :S41ZЀUX=rPtw4SY b*PD O]RVOr*YSfW "oG<ˏdj"&l \' aCK_4)ުӣ9tDѱ6 YsX͍dkVϸWU\ez\(U B!S ;Vp9uXB+o2.X`ZUV8bUkըU*ʀ@P7u"` mV! 3uzYJyn+*78 gݯ: RXOMLʄ#pI6:}O !E>ٷٿ8kVCnz'gP{ݐ-zkjZ=825bMJKxm_%FVe҈*iUpA2XTOV2#fXu}\-VDUWz>V `P+ Vq9L\Z`X0V Vb5h?>}JL)ԪRkw*&Nc I 媲W IDATA.2RakWFM؛GG̰RwX$7VjVKJ,VSũSEj`SU:`T]oY>+V"W9βFEzňނѫsTسwEƪXaҷ`d=Ljx2*J%FTJrrG c-|Δ{my#uMWF^<]jCPʗq[׈ ZX#S5WH$ۨ¥}"{$jd9Y0-Z-seJZ}OQJ.@:VaX- jXWU\UUBE˴*ؕ Vw*5v`}wRX5wV*`VB*c^Vw/Y/p@6dezŨֵa*)cm kcjcT]5GfpZ[7b=;tPvYhiU)4VwJ7 Ub,fgGk jyaZJs+߻C&-$ԏ m!*zK]ɈwDE}? s oOOO TO.{CYTkyJQL P (6!'q͟r{ه*K8yqvּp+g =_%CbUWg^c!VқժB+&%8EVe-,Vo{jM%U VKy֯Oࣟu'O< h2?xggwS0>z竏~!z5T?"cUU Yi{4e?4 z&Ϟ?rvPU/-V5ZQÅOKaO`Lj"V* Tj:E7|۫H-4+(jR5Qc5@[.dbVz 4 pVwrսֵ\k}L[vk ZhQ U TmlnQ~Hcj ˥*"Ux=5΄**Dm^s<A{6;i:Y$2TiT/P?V;X]w\0M:~oPH{1n_ۮ=T,~X?x?xXՋgQ~D=x?ز?vN.d2o|y]toӼ5jwA@ՑkS{`#JP=q+*yLjT 0xzR[En5ĐU0ށ.SU`y<ހ|ז\Kc5 jz>t/\/md~\\RHX4Hq5eKŠ,NgS/p;Y L=.H `UG851iL,>ߧP>>+ZvW3W-\G/Ԧ&Hh:Zc):C[3Fj'*;GRg\E3ˤ5Su;۩(Xϩu猡 @eX '9xժдR:|S7O5LBR G X0V_a՞UjX jI^~OP9_ \Ps֐?gͱ}t)%`zȑa~ư @uKZMe>-~cu?VF m\5ZhMZLWjuK|?jPZ3Q &~h4[UŪYc6@9ZuQť:V0Gk8Ŭx5RUF*F#dIU)]h=ʎһԪFEN˴oP>~|z2Ob*aT-j%C+s71N)-~9r}68ϯ8ꞃ{S%jVgPoݳNY4 tDžW1/wΙTHKRuQ|LJRu8Pjڿ:y c<>W':/+GSH*D{:${z?\;5-5T̵쐦%]9WB<9iCijo kk'9ONG5x)ŮUGSJ T}_!cӆ52֋-[$W=VEUQStfY?wgR[TQHm3xyjF p0 p"~1!TYZ+//dbA}T XYzLwcZDa*Ul*ost3$+)C9\`-CZG4jUxӶA樚jJ?ej}lM]Q,SI|2"?~st?{>ъE#wsť)7{!VKCᛷ#7_ ;9 zw& ޾I]^ vcx4w8j\56>nUjP[Y?`JeHVT}Y\/kĀ•hemj a*+B+yHU΀ʯZ9ZjnkQ"UAHjUj-ZOPo05|"Tx}Oͥ摀ԍNZLGuaQG6QP+iX d `2_M*uVV;{9)N| ҕbRC &="o5%@?{u7B atoU*nKB|r8N] 0%%8>bx3 ؔT <]ŎB$WPZr>XIVP$V"/ 6K4 (ef*(\9#W"%Y?2bG!*\κUHR5myTP+օխ V}XXZS5j>J&$+GDRU ՒUXU~4[90C1WVZV)qUdY;WWY;ZqTf[:~܄z7>E**'0`\%D%x[}E$ *1U=CgA RIDD BkT/1R*գb>(]B4' 4=tڇsǥ'z$5%~ t*HׇU~R5 8N|r;}кvUncd`VbUՠ_ *]}GtRP q)a5lא\(Fm<q ahۗR_ g7 (f!V8AɧƮ=Δi0Vj;ӹ܎ wk-xrK"簭Օ0Vm۲l;Tn(VwܬU›aaCVsZHYϿx: Vj%,/UR?0c=N% PBVYVֲX WUWk@j>PqNw#R2I04?"POÏuhUT"zD&ܥ`PU,ka]/(U\p2V^Uz%ldj5jKSSjeP=ha582ɇД= .g6tI>C>OWXФ5tx1ZuIJKΟCcBJ80hΐ:0:00zv#QkdɤC ,d)Ԛ"Ic2E)21 bu,,vAFGP7^GLtd>rI\? C,@vt*XZ _';BBtʊkZ6ﱀTN=yoҲ€0bo]HS@ԫ"X߰Rj(7*l"RuusV^4]5Gj54T 1ERu ՘4kĪVDbLj2Ub{!1[` [&Zݱ?`j:\jkU:Ɖ,V@UUOvvU5GJVټƥTFKӈpHJf5Mo_picMաUC#THVO߼qxZ_R^'ҹJju;uR< Xu, o 8Ǐ&{|d(d&*[q酩熂j$|6"V'Y߷\SuߥoqC@㳵P&F\\Ў漴l*pOd gI6&VpmTsU,\sVRŹ_t-~ /gW9QW~7i(K _R<5>(]\ \ʇYHalb ڕԪv[ 3r^BSVwXe  SڸxE8ziagWM\( 2HËKӭ57k:.l;mp8`%S[3Ƴ.J=1|H{PFUcu]9VMrZ(Qz_h70ZrE)ST]K@hSrﺍJ3.oSmuv3| <˂`7Cv,SivّE*Z~b@jR'sowxg~vӟLUw`E+Z}Ң KWn!me5X]^W˫ WkkZURe]e®2ҔU 3;[jvU'uq,uVU9V@E趗j6]f,Zo@=;TW}up6*c%˃Rզ~Z*6T=LzueDq V<҂Ղ_1mE牂Μj?x .GO3B*Yb۹ajr]b 囙LqZ9=?(GGϣI|3<<dFve.ט7bld%%6&RmWyʫvN?FʭV*uӔ_ #XGdiZyPODLo֨,.|ҏ:b5U_q]`, 뷆2+faLT49g.6^QZ1FQ}jpkؿ}2-?`k vMFVIJ1CAZ]?c՗55+bZ "mB:j'Wy &. P DJ* "`XQ9$k+_&:CY .+V*u{I[DZݼZnE~4Jt\׳p+9^BsBp?TH>G'UU4+i.4śإ9joBѴe?Ԉ8DI542u0Sk#hD~]sʖ'1H&+iR^^rU*U^i6+U fjk*camY* &[P\d jj?Z\bFNU+UUڍX*ޫ""Uwc pu:zPD2T[F9ΪڂkV(Q* 4)᪙1zJ?R8/ X5(ՖvHCrZ/OQNjv*ZeVAa=G̠+KrίXf v4:p8ݒ8U}]ӄժ`UnIU  VҢP@GU RZܫ\5ť_>_FB IDAT Ī55?Zn:Yq5JsQZ?J)+:R * _ kc:JZVMJb5mJ*;eI6UPgZZ`5R\mZmVJDz;^WF:GTi;,ˤj2آN,*+h2P¶($FcDQe)dRH d,XDbm7 yfڞ{µ}csV{biMɋl֦*XojP StM]¿r$kv ՜dJ<@OwC +-Υ:1j(1,r3f(KjΪD% 4h j#mH"N$fG$5TauZ_0`5Qei$)Ok2wc G*_f}8;JrWUZ4"U2'+3Ti2\UbB,VX*Vy2,VX ^'_7J9kjU4:*U<@*S/ Vo ֠ZUV|IK VT&XVa.dٿԌ*3eoھ#u?+lTgj&#:-󞜩SZ1ϒ*BɾkJ~;NT-45*MR5  jj5gD\ a5܏[{!;4OHGyAXT@5ձ1*: Tÿ@GCUh851X"pN*֫N5V +%kT]Vrk*U&-^gЦY,Cs5V%aEE+HS՟NUu zo@\zD[zj*`J*V>`ռ=*<UMQKOY|ѥoťDԽc^Bf+"8TMUk%_v 4,EX?KM{Cr TWHѪ)Zpi/*_@57Pi>=sP!VㄡbՌ;nog HUW \m?"V`MҾJVcOS:j<8}H19y~]fC*pKV aT^EWb|ӻ1j} CjLOjb^/PEF[\jy[=dRjTh<,$>`ukkY?ƀr*U\V|=qSKy9DC⊛Zf;w?(}VWGT-U0\xI$Abjju>8<ܴVJf+X7%fmNCX*C`}mӭ4v;Fu\zM\d0;LOg`(:Gƪ#bahQwL6yubTz-hݨQ%)tL%ce"3/+n4FMƪU:8ejUPK,lZ8s<ZrP;*}_V?mjJ{B@|c* ZOT3( MWTVCUsTF lU+֗ZӕZ<4dd|jΕfrT+UJӹ 2R[L.lO-Iޖ# 0dp';d}TrNT fft AFY: ^)Q='!/hCXŴTռ\*O 1Yլ2&PitWSD"SɐJbUԪRMq\=T X{[YS?ըRJ,}+S}R\FJ.VzF+WIZUQ":jSZᒟdj$&m=+dY U3Z)&^[T*ivtç ٧3i봅;u[GN{F0Vc%/∛9¦1}P_\Vg4BX䞆R :b~cjjS߅܂ VTD?wêJT>M@ a+auquQEO~VyMYRU?.OyʪNb!2TY.#T[}rV ຮ [ ??)z!|MyV 4,ũM!Mŷb0S QDd34"X"RnS dK(+K:b_H"nU2pauNdU>[{`%q "u ǸSj=MlnW z\zߺ3?5A,`S‹wmFКɞ8GRX;!U#T#e(M kHUzuT+(\ UiZ j&fPkQmԪX}*P`5:\2"P2V4^ř?/X][U1 hI>vLUc*suaw͛ Oo5]?@7~醩,J\*_;'x ȩ?kՒ/5-l@םOI$`'@0xoRh[)(k%ۥ@ ay5EV(OŸo_3*"vؐ][Q և@Vn@R s*TKǼU*A] 2 @&{;Z .^AB^Eܫ*A:j׫0YC;' tI_? T׀PR=n4j n]<,mt@@zHT=D*Z[@UꑇKYSq2~(j<0GtY,Cw\XoV<(=XGjN͖U e6~B ^X-G-8?=mTf XEzJ}>~x UQ?kW:?nUeҴ1dN'qXY*o; RVՇXZ ނL8g`iZouƴ4X+&v|J֬KKp驺bV`D8B~*]=pUj}*j}~JҴoO *)tK9=Tt%K7_H%-#b=Я؈)bЊ'B5`pMwd3SCGGCf[UQǷgD»fS:Tש>~9S~$X6*hT ?"WWp6Q;;eѺګU;|*igH\w<@zRV`5-2X}Z'Or=VêU~ K\ższVI^ܣGչ$a^RXYV Xju+nkx~u/eҫWV6UֱiPUUeYΓpBU`}wJryR6qE7;v=<7ol(UptMk*O TCLΉ:'VlM@?GbA| T{U.ZC^sQ[{ 2%v`2^3c~}z^w W9/8[|Q6AԖ_$ f5=J):5կjgj/hzRVa V+53hll"U*Vuުs]/8`狋/P z |'V)T8 jk& s4ߖUngʂ5GU.R$(E)T=R۲M[jJ20/Ut G|b1V6ʋJN,"`յ-,kWu\S5C{R*;8v2 ][#UwykNTtLMN\0 x'o.BU2fgD;kNS[27r3C↩\a6 \ͅoalJŰZV qXKղV'b`}: [YꍰZ,20 Y^"V@R 2\(XZtjV]uX, z%@M*{T;B:`n_C- ʉSY :d`,t3j#ePinNj(|$,;Uּ<:p]X'Q5)S*>M@A0g T=8rJjRDkS8ީ3jՁ3U&͒]gNz溯R:V[UeHG:uժW&)hTA?aŁ(d9KT O  IzCYJt ,fA6[i6!>\'YjU^ӏ]]UmRzƍƪ&*zg~ lfB; ]U$jniU:;^Z%`ȓ(yIcjs5@ @T9hU*@մqrJ^FY06j{S \SԈ*-[ґ[Qeq<5q?1guLKUT9*ITfUx)֩NQ'Xw2 v1pbuS,c>ZU~c.Ӛ)!?̖V115P\E)p[UQo8 SAaFV )U_~6tixXNiFPΨodb@o?̓R.Y)5ƧOLET1wT JF_W~UhbJ:;L9jjW=6+ן /n%DX+i}}bŁBUbD-VXM* G UV!0EGVi:Pc#R3JY{"c'f*UE4Ru坄dWrAAn5 S7ᶹ w*?\U]gYU1WZ\Q _8B޳Yxx; nXgPuWZ\*}V_|99 eP GlCzz=4Zj/ }: 7K Qّa?em` = Di0{ڎk lt&VZ uVE: [``+.pmg0^4pDTŤꙨUcPU: KW'SZkGr(Zԩ}Vgk ִW`H4Z-T謂Z݌@GW?wVt3Tpϕ7",VBV=֞>>Tx"U]Vw~VDͫVoNA^TIRȧT0t .|`uruBSÀZEUZBtSˢ, RHX{Z71#ET⭑l dc N!zuٰPfC 2 ZUЉO#nzH?<.R!MT@OvL_!P%D%Y:& S84d uY0kO.,3ks} Xg j4BbQ0AբscXm6duT2*LX յ]]aj3VVe_4KXz/*@X=ХU[;\4`1V?gZUzVyyXJA*i)Yjj jS׈U(fÌժlL<Ju*}(l}bU`9Lh&m4M2 (~p\Q±ύZ)QkTHMZ>kCzQcofaԲ33/$8tc`mK*U'NT{2'WUuFmUGc:|*)S s#)5x}&Cfa&/wd|{B6fPC<P4>`l6V7cuk?OIXsձ=q.&FUFVhM\}zOmVAly _bcPr(՜;iTh?JA.c; Z8>xgDzWfC~_+y ի>W~}tZ [3Z`j9Z~Ī/\!֜.mc?^]k 5곐_`tpjfj=8JT=Im**ƈ>α~܅R{eUTUî)ҩ?C)yj"~vwiIz-cEҖ)X,oҡz>l RMU3ۅXEU)C[2*?j5HNbAjj25iUҫRӪ>[u~EYUnB_U:U=H9^zT)VӹJJsnD iJͨO$Jv\e} L/ը*t;tjBpjZ(*-uYBTe"|$\.[au;JyYJS &*c;3mP եVIbfըVVOrӑbu R5*h#qEQD<+j8{%Z8+n0KӻN)Ieat Rե} jCK.`_r 7 Oo}/2<8980OOҤkwV#TߙT8*REv*U/Qtwؒu9 uU,Ua3Bs'\Eƽkꦄ'M]t5(X1XO+ʵ[d:V*a+V!34%XҿUpiC(ht5{*A^L9%1HE,(f0r.e_dLՑ~ikcj$yXm^ wqغ E? -UMR:;0RT=A&gv*L5>tR%SQ,9Ug׫!=FK־ՊQ k^rMkY81D=xή!t1 2 6HCF)E-DYdڻ-W{ϭ iΓKtW`uU c ZjU!әbuGjQ7lXb>v?j [\uz|`Vq5U3K(U*UVG6~VlXEcLoJtw eUjL12#r-t;8-2p\45T)ÕVe3*A(g4swR\%mY\t vjn)b&6]g1롊Ll_ڨʦ5ƅx=?X=9ѠsU S5E-DzJ+NOPz >ɼ>G )؛25J^Qڿ^]]ٻj5@VWjXJ(Fe(~Ua7!W o>.RF2@aJdA%+[ =NqE/[MvPՄ.a;y U4+!z6KegA0q3/4tVW eDR~>i r:]P SI.qB443d(\Z?2ߕoLYZjVMHV{aS -BܬƯWGV?*UQ*c@*XSK=!* kG>eNw(N?i `>Ef9NxG^Qi. ΄ ̷U!-NۿnVWbFZ\8}XX5{u%U}kVX%6TZ@q:6XHEVUރ]Ug~kxn뵦E 6p\1VQ @U!tzſh%Q})DU `IU*Qrzjڷ1:&˜j?si.c3Jldp$nRtH.]?鄙&s<{}JF 5,nHL)'x\7K R)P^c_S8GVP՜T3oot2c@IcA[zU`5ꤪ6-myZV$`-߸RUk Nfu{ٜXm۩hDX+[m+ z"VaAKuhVUխ}X;4=xI|0?n M5T@XE2cE1Ď0Vu٨"Y_.bKR=ŷ&BL6`v]?< ΕK12?07 І)bڟi95޲sr)JVeʀrR!C_sT)U)\*f~r(:wrcG0ql6w«9 |0) kͥōAVU ϽXwvVN6= b:TbF`)hUa<N]xZ& P:n`m*Vn@e1V_#VaZUj*W7T]SJ6R?U-T8Bꐺ7URg))R^eiUXR}f-PٍYN-|!c#(s3\]=T+wa,7DeBލV n@JSVKxMx YT ܲ A{U4-UU*W$VUӦTLՍz$!J V5V~j*YED A_e;|F3Tpև[}o&UY[2n":~(Z4ժY`Y[1+5L-U,fV8GlZVeV6vf /KWѽAKc-z\+aj3.V$=X)U[m%UX§UXn xvXM? b5 bTBo0 }mRj,U:js4iL (˴*߱}G>#Z/`Ֆk*jZxH!N,ɡ˪C 24{f՚\mcR L?N}č^>?#*K;K*ةtzBU*ߪ:PJǩN')Sq@w1#uNQ05r6i*{^qogd͜%0,"mj \fcU<֟3[:vfV_ XU?3 $Rˊ\+X 1Sx+A&J긹Pm6=VM=IW':&X8?q:9. 4e Si:ިlP)B}-2|=SZquhs ,3BY\(swd.1hB ʨbiZ0MPB]/|W,VE|wUE( P)XG:VUSݞ[[: /s 1gQhez}*UXCribEdU:z%j#[Vl@]Z hrw^kJ!^uàV@jV9N*b TIS j\JͱX#WuȬUκaX0&UTo&#3SS~d$— axQ G\sk ZX Hu3E FPjkΡ^xˈ)jWR_jCRTWwDb\U*T *VIj OѠ*ۀ?ѤTIVơB 2EQ(DY]Ñ}"Gs/\bcC`17Aki߂?XVX]z팪/YMG>QѯO?HiUjeyT|e3nbUR`ZrBԪFXVp[UjdQUYuR}3uCZ :^nXe Qnf@bKY,V[X5j5AIoOI+BUKh&iZ_U,~35]`ӕ&:*PcUjj}! q5 *Ue+HRFPU0sO 騂N$JW9X*A*S˻~AU7PkO쩿XΗus4B^zE2WݫVX-6G-c)S?uWnyjug';ʟ`~KGA[j:a,~ VQt@U.0@}2Xk#VO3] /pգ_tZ8H&@2h\eԢt5`9`U S) IDAT}֧򁥪6 V1CX5^MG?Тzi^B@f=n;N:_@]gex5ފm7K Iz:҅T3ߎ~; X `Sުa;s.} *-Ta[5^s j$*-N$Hr~ŘlOQiTe|ZYhS"UouËդ6m]ӆXz,W)R*znOVE*"VU}x/ZP@tyϢ~H=>nMEk +6b5C{d:ui*0`"`&ۦvf @| F}QF~'R6Z?I c!58rc]Ձjo`BVWuDUxJ奌j$ Uc VakX#55N6{ %S1d?Q?5I:Tb},f@n*U*TbuhjruJn)bUXMBl\!Ň U0IVE̺+6B)}uWCk(U䈭,VezJ+)Wޟ:Z߯SF3VoXu*WYb;-UuC$xtZXm98͂ȥB:X}fUg':Xi}L?EU?"!Vc}eS%ioIUy}˧-Va)~U>OڡT*l+Tհg U`ѪiJ]UR5 S\{SljMjroHnYnnx/Xq1U㗪Zu$-\lsy:1?c*U%FjB×]jDJ0USO]0 r׮ab՛jG%U\bS:J j_q$Xd uC+=b&@Un*־(oo;JIЗjE$4z\Յhn&JUŢ S{SZMM`'UPF~W Ea V'PNvF\WAνX9UVvB UUONHUSq"~ZnQj~>얢`%j3gg_cIt\"d 5V*Tdj֪jNj{i?ә3'Gלu>.va.Z'iz@R+7R5Gts.>.jKzUf×;v\s54V0^5d-  KGijkeίVX59s(e۫7n*ToWz_t~:!\-6\M<|%wUY*ɰ-X4D0U7!Kb//zs4kT/h U8'4 t˶hQCOƤһ4im#@A췛`ى>rʱ/ZuJ JWGRJKVXL3Dft-S6H4*BHsvE/ΧlqdL^ Gy{F r@HcpzA {am qÐ 1Cٌho3:Uu-YrZq,"ONU;FfiE`Kժu0܏ZhJ!JZU8+jf6E86f~|6g  ` _X]X-*MIHX@hc*S!jcjb7T;*TZbPuPvwJt2aD `^  LW>Y\ujK&@'I29`(U2[8̉ 8FEl6 wu<Uc~g%Z+ դ܌[:uTnj>[AjK* ZrV EB>3ju vvj56W6YxjF4U$I& IUyew w*{"VQTu:ZXU5<4Ǥ;x*eD2>BUG_eTY{8[ ? V#R4V+% @U]*VSO17MXT?I(M_uy"W[-᮹5Tz&6kg"D|թ91NSusU-êέlφ$Vq Z4oލFQow5;F[t4* rՑR JFYb^! V#TUW%Iyk:֥A Ui/*5ϭFt?_82Sdߨ uP)OY`(VӶUCVͪmFbuwUnLbTEU#MLSzJB%P~ʷR,IP~W?Ϳ4\/VX[U!fT*٪Jjizt -SY}Ê@wsmVbbJppDŚՒ27M&JeUVurUOV66)q+n zOw՝MjSNj;&QQo}1gx X:+wR+*q;.qiKCUcT@Z.>@-V6pS7[ϣlʱB5PKfz/>V/ +6`Os(o60L[jPk@UG U⪁js{V\3ruU=`n/G{U׿jHfƪ^z*TO.B()G~\Ohƿ{{it c`Z"Uehџ]ճEGr='TpbMD}(MZf;w֫wUCW7Wu`jda-6*VUPn *4}6dYr`~`Tܪ&vjZkskNYjD92+*/3RIQm^]obgW[&?H3y3|O۬,:y:(MIY "I#xg9iyu՞WZPS{42g?uRʖu_/t*IU(COV爮Sj/d@Pc@&\'$T4TDk Ts#U]cj0u]-SǭVlsk PZ V3fLP]3%VE>UU>ybl@n?[RZ+1P[QT]ݙX]Y^{u**Cu{{V lI H*YM my˵ے^u\͐xT3x?U&"\"e'}d"BFQԥ@j/:- X%*U7_2OL,4K=cƶQ5t*X??}t$NLg@?> C5A$L&- w.)aeXzbjiX5N1E:"ۍ?sR55z_z/jNTRT|9jJ|fdke]mD5jʚU'9 ?0BU0hEau@[.ʒf `m铰U7`5jQJnXT_چ^Qכ{vGw י1Qz<"2VqxWDJ۲N,P@iDUV_`V[kj?Ws5 @KC_Z06HU=^7o@DFG#^` 1Xb `* jCj#&> (AIUtꡈnvW ygn[Հb&ThpTG.p;T~;Q/sy?bNwgu2ӳSp%%u3SԕRNU) V+bu-P(V(V|_ʗnQ)buAK{ZfcLE?S[ܙ&@8({Z%Kk4T%|*XkT2]P 19xUJMPx Zc`|Uj&$ f.=VLV7/Տ_ CX:c5[#Xs֪ػ2 _Fz:%POhJXhu\:T;̏O% >j҆QJ# U``X!K0Upz FxW& TWUvPN};0/yKR"]9OoXU:>&WqL?9&D҇?U&Xvv4E*q8agkJKX?METU/<|-59hV\\J"Zxb>.`)U9Kb4U߭0\j-%Zr6|`SYF9]{E YumJ[5 &\KL۷3X{‰Wi`DYu.IҔȁBC y ; V눔QH6DU!VE#ˀ(\Y0L%=085ꝢqzsP;qhy:d*7n1z-'B5ҩc՘jJ oUK g` U5qBzVVWZ j]~c5,ޅ뛌#Pכ XXb6T5UV3=ם<z);YlMΤ-0a**mIqG >#yx}TlAʑUnUS{eCl3( XCOQ25O߿/C7ABt߉sUo"mr*jlT5QVUUZA*mL6S} CU>8j)T=@4*^.tp&7&=d4+ded~:n'd VijmM#OSiÃQ*$n8`TeYWW_zckUT/^0W0{.[n`;[j2R)ª_ZzrOhMUiMor,hD< $UG2fGn[W̡)?N] yLVO TT6LqUxΪWQnB]gZ:lXJR>D::ǃz9x*hY9{ZU T5ժִʎSTM4* jޗ1e_u40z g@UT S6NАQA6xO/ XQS9[S tH^V96ȜGflpjUUѹ=Ho{1;~AY/kAe7;|l\+eJ%]N_+iPHJNڋi"mE\;X CYC(.)קbuP%Juj5y;*Cn7}V\%AȀ5,Ub^ ڷIC+6 `M+ a*@%+v[jQq}T\!N"95V H~:FdlOJoڤ*ȮwסVU:5Y͠V~dlH<*AزW]4x_UYf'0/ R +Q?{2"y_Ŝh0:u |`*VY*OҔU^LCQ v`h*J95\ڱN;V'T`*OU귪د::П^z[ X:ΡVnqaozjVεI_'(-Uia!݂j(TBhX4 Ͳ0T:YVsմcXV>< c`}9KHAWauiU*LT0 s ĬWhdr9Wx/7 ' ޾:kˋUʂ,jux|kiTXjGF+B6}L5u֗"_0U_;0Y_@OXKW{W;X-sUyZQEd  pmb8az]h0K:S&J}}/19i|S {ڎ:}gs,fa]rZ!?Urۖ4% wPAbu7ҩCn.5@ªa놄dX]ofoak`}Nzƻ<WUU+)ST!W}:k]\3/mE"TVq#C`Vo]uZ{p~pUצVsnS *\> BzI֏AZRIUဨJ{Z]N=?<(wxނ.5UE\Vq Qp'hWV}e+mZ'VI^B`&m$VC@4V%rxS3Ϥ2&8a~~uOW}ߩsz5׷LN@Pe^> W?9Z?ί- rSRCx/0@|a "szQ9x\@{p>@wk-%gu5ta^eA7iUi K9f`Z!5 W tm^i._@>VT`ucTک\ {hQ'W'8eyX@Lz5mXx ,c7*#4Nu[>7f/^+V#y,V-RyjNv(30P_2M5.U9j`nV^\ŸjBtзUJT Xr֪x^eX:4n.6/>|UN"oәgN)/VQ Tzp*'_t:E:Di7u? W~?t3ux8G ]I9wΊUDޯj!X]_0V7BWP;UXMnkw1r^zUzSZ:O^.HH:=XdU+^]}@Vm'[@Q J7]:AmO$YIv5T*孺k)&c5 h| \eXjbIC M:5`ZMV@$i&Uj܎fH- _E(-Vk tW\eGBV|[~ϲR@ ƫDM Q;UO&x#U2rU8:mxTA[*NC,Vv:Yf|zHdl>bկa UIsV\-(P VqοxիUW ^ϤQ5DVpUb^!?Nq*c>X/WUWqUM2 >\lUu^-De ]GksmV}sr+D8bt[>BrMGdue).H"ቋfsu`0ce: *;WO[I4\Md7-WY:7# [zT+\-:z+~oVgz$U4YUYԮzԿ7?қ>Cay*%N?bUSuQzmz*:"{0_Ed_+U`jUvjNA?U Pג.Ʃa궭&q JCԍ@Y }WKke7(u+܌UV]VWפ?Z5Xr WhB5Vɓx`\ImE[ RwW , {'(ogG]UK0#Zޒv]9"}b>eG)[UޱUk2;X ٵu'6P53勆zQ#y)*Kȅ[u@~jijpսj$ɭj\*o=!::#lPIbC J l85eT%b*U)VMR;7T`PzI=;},VTԳZ֫@ʓgrB\YcbU*XQAe7:\]rUCQA7> =O喍[[߅UïyxJulX@d-IUCŠV–-,Vp]zMOG8KXEO?N G UkFz!MX'`ˇ;]iZQh[_FB SY*E U>yXgpR5Xv?sB,vUB s=W$TT>9{Xp8@/EsZ Vy{{T^Lj/$YwyaUpY^ ^ x; UkV!K }UuUZen2YE;VUY~q Y^;:߸U>9YѪ{T*1C,0c*jz]`n֟*#󱚣TU'K*ª̵nHb>V7Br+b;buqV XB 7 DƍkXv` BG U0ZG-hc\tM=XNVwk5& FȰj5]\'HhZLN[&߭XS<@zPR w"QUnW[ɤijS5~)*UƩU6J\e] Gtv%yJY*KU,KgUdһ95LҪ6c o]J\UdQӏy~@׍'rc?؛zpc;M_ͯhgSڤ)^0w4sWGjX[X[UT`հ|տw qϥYpU)ϲM$j1粎H%*#jynvi+ ׺1Vq~牁V7ˬi*^K5P(@r8 \hsiK,PU.un 5T UwJNx+8/F3|8ɑ<;v Wk4})aViOO0Eqh$LUpըUWs&J< Uk,veLګb-cn 4 P=!zUGy!V];7U]TT,G2Ur+^}ay{y= \ ~s UjϪRXr Ұd=ѐpNc}<*|^"WU,gwu?ԌzUV$z7Nۂ[l*%z6f[js^Zr4J[AP`v ¨PjDm4Mf;e} RWLUoH:ITm&\q 3V `J4W *L o%% #Qdmc3f#hOwKL{TQF* ;w/~1r6@r"ꎫVɴz7ZeDzYPje]3Z0I[q7O P'Ѫ2IVwa@jwrTSr)̽~.X }:P%z<8jXa뒹 : \j.Ч^{j is=N *OUA- j٫❆t٫[-]bq+0WBU5n\K]0 ʲisblֶ%SFNRQT#T &KjKZ[A e^O&8sVX%Jڱd~/$mVJ [vUqPf}z3`a|ZJ0[ɱ0 Zb@ZТ! )7IH* I]b,5|ٹtSy%^)`;N"&??yؾBڃM8-֓־#ҩB!*#4tjJNjxS+ZX4E2m0\=3UH:*)x(>ǥs0$W)`eHBjNקrZ:NM*x4?N㱅Ă6|x4>j[}{_ǍN6P6U:XÓ+4X=r-A=)ȳjt3aݕ3ՙ ~K+9Ob=CO ,V!VU2:xX} یV>Y @A*hh*>P wCY2|ZVe{ϳUVPke<7HM$ y W_>HB bA9WGj[Xu`^Eb~je˨T-9H,*{oZU>|r ֣# &Yaj S_k ը@?UjU:V:V1YAzPMPͬT\;Su WP[hu`m46\Vdp2^;%r~4V-[ t*7[`dʪ4,>) ʽNaUs%y~EUh7QLY9-)%`>QڪAHէO\LmMJ%0)V}'6whrK ƢX{Ir{ʪU!6[U=ΉbUiB8Bo\B4vQlxXE:!+qك_Jh=1U]YT? ,]s^2c*q\WhY*L^> wdj`wTe5\TM9ԫpAJg]Q}jMW7K)UUi=NΘ.Ѫf.7&X]E]K 3E{RjMY0+X}4 Xu=V^wbkI@6 U-N&'ڡT%"WkU:$" Vr vĐV/IݕFM$9r ixGGUb3{{8T`5p;?.3`0KڢVV#W?2E-(`ra]%K^mא7pW,M޵}+' 90?ĬwȡaP5Q:zUNv1pUݡC|rR}PRILUT(TDBUUjUjaz; g~A`mηTKKQumT &ذOz'"aRU}*&*sEXEjh42Ui V"VUWzQbtg?*'Pu i@PEp:ZCt:ᵕ p+^<%?;CG,W{qxC4BrUNCA+HTҪ@FMrbX!WEVDSjv5d(kH|$a^nZ%kV౪Nӡ˛7{ŗ-kE0Zʃ6 VY&emZ߂bZ;gSd}9DHU}V$:W&Q2㫔%ugsQzS *5r a{Ⱥp R)K+R6sX r,V?o?Sx6@*ȇ%K\xC_ uPWAVVb0R&GQS̽ 5jlsTY6&VHBP5\gv́t+ rE09诋#GU0NZ)@UVZ X༪֧GhERJ.X'TJ:!.ڕ1UO!:?h5hOE! M#Z?^qjEq.Bfhr`ZrRX%ZְK}zs?\ 3VGk+Н~G}TXUL`K +;d֪T*C_X9ofVKJ2OBo?,Cu=ןVQc/g'P: N.*oSˍ~@OIDCRնU3n-0=T6C,> zejbXٰ `l5#cXE.(VgS$:@EtU.F1PƭzG J_BQH(ѕZ Gm$h ,!۸8"QZ; *Z9 ?}a7Dx?bk*"^s6\ k_d u)(WyhXͧ- du**iXA%\RU6Xx;Bq+vH*Pp'Mi(YY)d!E\x7pQܳH:*RB nf.flB7ل0}$iœcLk޶2)߾CB;Д nk[z>^ xZ"YZUBCkA%YkV@Zeê 3岍+;Ag=[CY-+eSVAܻK~g<*' h|'V/_V _1`FSCjZkVjuZUdU@Ui kۖ^lk7 UOUWEU*`qun\ 5&|'!:c WJ9 7ݺ-nO!V*LJb V9pʂsy|2Mp'(2y~5dxԝǀ*sZ1jͅQ+*E2 'P(.agO,ssB͕Lta-hRB5 5DR\qyVy*GYP(EܨYJ J\z3lZ݅ZEUSAԚ+jUɶ*U #[[d,VY4jUm>{Vjվ*UU[fkJoVy&RIN4jzxTVyzpk?p8{>@5<ڏ({~!zDz*j~Hj UZ-YSTM&ӵ%Kj':*UU'+{:7<'ٟb5_ǪY&F)UrOVقՋF*{ s_^\pֽ`;B۸׺Ew!"rV_VH`U|]M+^$*HHqfw0 bJs,sԲnރ XZ]"XFQFVV҇m]m\6v_U~XT ֕E p%҂#wV܅Uͩ>4bұ.t%-C w.S@TWYbKZ խʪdU'TX҇vyTOi}nuNj[;cF=Gc`u/eDjm\ӢVS4Y02AE.PS+!Te<1`UTQU,Hʽ08VŗJĢ^svUn~"X$V؎S<=rЩe=yj*mb9x 8ǥd+V06Zp(GӞ6p%_'6 V;Pj5#TH5Vv!'T1hBV)zʦ_*\ņ +쯌Ӫ2Z \XX+*HTJ-jRV]Ee.nv77viȪ N=UZT| U|.7OOZ;yp(l|Cq˳ G:mUß귢xSX{!jZ*ܔSVʉdPņq_.ǚFs:7`@0V+5: [P X}^|Ct&LP5US=IqUzy(crV*7б) SuEf(ŚX|Tyh@@F VXw8Qo?XŬ[_%Oe2Vs dL ZO}".qZ>׹L@ᇒjU8i>35HȺn'*вZAUॾbUR7]XAv[po6mXUoxFTEߡv#ffm5[Vw\J մjX%:ju4?Ѹr$S^`e?W_2fkn Gp.|mj ܍B%*~̈́c*}M~Xd:)X&]0 q]/l'P\0VZr%"; |K%9 nUjY5+W:[ETjT,dڪU%=燓};Š͠c KxgpqZ%,V%+UC(UqxP>`.Q P?NZz#\oSc&Sվk{jX U.)ӣKl:oO~iV9k-xjkyU?-F=w1)Z[CJT՛:Ԧܼiιb2Akڔ0trR)hj:JԵ_U@/Bʷ`|jI+ *%9_ǜtbIh[Xa X{D?5bΥ!RIKKAvW(Sd?98+ˣؚ*XqX]Q^5Qu&C 5` 8"{\NzCnSSUD/V5VO J@q6[ w̳ғv 7 *UPXKԲfה2έEpO|i+¸ L,E(dP ieETw%)(-(vEuDk,s e\l7J1m)D=_ǵDJ4kb16 ??N=v*XVan< KI^U1(՟,jU*QTN`JIx{׽ܼWWooZP^jGݮFY"m wo悕C=ۤ9j )ܩK*WAb!kf~dkZJ*ǩ`ڟ]pu*Ӕ4Vs' XGPT+pu[YSsYUV0j\\= XFZiWP5z;h)Z^g<4V9E]M9"O}GDNRdx*ȵ}~ޞIc C|n>{VB*MW0Կ{X X-t P=dUJqK\[}jkH^"Z/c5KbǯHU{otVU/O=_#\uxXqB:U d^J@boHU3\kȨMEKY&O>Qx Vga~*R{<2Ḯ'qտ!UuZmx ,Z%JP6?ybkEM-Sx @j;V\oDZV\Z~f'*Uz-kU`}%18ꫫso9Cְho~d\k)z577meT˝*kkZOpE͟-LU0K(Z/AmuYS+*X\ee@]p𪮭Z= VX9bbuu@UPFˠ1HV\N.Jc˯J1UOդڝ ߌ8D ATdiU@on{`5q5BYjNq5m݂A53? *EԼƯp*kmN}p*I^Pͮ E4eV j5 9ID<Χp:Bpd)`ukcg%VXkSJvQDHL- aZs[ w PHO=k{U㞜*? ALJUE*(`ѠlQ|whj,Tr5iXANW=VRj d%"@ +VESݰpy1촇(xLI'dY %V)+U9.{mfA T}x9~F[\ _Xqj Tՙ%jVe-#!~e R5C~U"XկCnֳr'z>jbT^S vU!wUm4'5aVa'\Ux("؟/#kghxy;{[Z%ekNھQ,L=z"4\AeΉYY!+z9hȊ9(hEN+N U{/h2^M&v^_'Je 0R;zqpa4D(#E[@i6q 5CP\fwQa !R $W-Gg~kצVhNj߀%ZeYT-Nc՚Sܭ5w5),&W_RS黏nwQ֦E_b] $%rୂ wv;TP`= p`橰[#8ŵۖGyᘟ=W~?(Z1n^"WhN`j3Lߔb*0痜buj@jX;^nQ|V:C\U:x@sܿRPj/fj"cw?Ѐ|Awf˯*JKWP xƪV@YIhs͓6TK4NmbL Pe9sGDv>mi_^V %݈ k(t 'eYb-R$颓R0:Ez_H1 ]#S{1C>ߧ㵺U ^rX)U͝+UO YZڋ4W\ HJQ(T_TQD]<0G]@z.40NA&I/Y,cՉBĮEG8ҧRۭ7jhܟʹg_JqjbuIzoVgBzU `VzzTU. VĆ+*S/GGJ5u:<{)a['y2#"`ZՎ;l1aez@b*`,}%Hk[ZUVR77efKZV%AucYv6^K#m\0둹/[!X*Cێ=eiulmu`&`v$!k˴*\m)J3Ty=3V\EVK/gm^*f6b/Y'p^ =t($qvqט!6*-ƒ)@k*T>)BU3nVcn5k \*ۘ$iil hjf % 3K*]F^8F+/󿔢:Q޾j+xRwF:”T%OU1[qcvUQUb𖄿2ItI`* EP o|>5J#?m['j`U5}if#dhԄ\,tZ6EaeؖXRM_l=vmѫ/ٷjԳKvUҫT^ͨY\OwA֑MMegc:> lQ\z)/O?g4+ur_,&q̿"80y`4ib58I 7c#kG4^CَX}xFV͌Sn* LL+z$t-nUUe|xT\_XWbX lm5O\2W&.pWdsZqNU=)(XǿM:Fho~)zt߲Z$A]ڒ=q9?0^,ooɶLoVqjw+!"*RZFUb:V(V<1|$O`7 Ӥ|n~L&OrM+XRwl[Y*@P/`յp>®fEB0na)ӫ_eU=WQe1[ɹ|:'<5IB{H$kT' <#طZi. 5FcU`5Ix\M8 7eh_Q1 ˳~ߣcPW-Wd8CX]xrugd/h5Qe4 |!WKj{nUO Vnb9zV&Wmurt+ZhŖZh\ϪQHW9VyaBjuڮMr]u]{6XվCu*@+xC>=&mn+U?yI4f-f뇨ëG@d ?!kIJܖ`k @ڊ::{JX2eV|%2Xְǥ.p,#ZCD^"UFW-05HVR5UJb՞~A*`ҪUV^ i= *Z1R9yP)r^XLDt;~JTZO񎀫qu],3BySRF4S9/Z:LԬ_qKRSUOnFGY7>1V(VK\9VmEri٫rW5ʟwX+`Uk;L]:V]RW}z`]׫QoZ=4]-ժ9!6`?t#:Vۨ @\HWO'0NQa*ՐʝvbuB Z*@auըKpGm! C%bg !`:&6 L!kOtlJx;*Y+b˫ UIe5cjnADDW||."]j'58tͫ @9_Ux]&!Mz\Kh0qf}Vs8<^/BYjջ:uINPjV pcވbUv3ZHƪK E/BVXZauLҽ罱`BvziMB)ܸ2}P޽+\ŚjӕzL]\)VtVy+˲ڣi6Y7tpM -T<q?i+VXJDƲ2UZUnqѓ5(L[ci},d]ze aRb J~mHuEo\ICR"  Tm, ,sUc*YU4h5S*GXeD ۰?(_G V秸I5.WXIbn! VF ;$9i!}_^-T?xu1zy-リ~c:szЁ\%%Wأ8 E7&]gV`-`+kEzQ\Ɣ`kZ]Z 4ߕr]rTq LRT+]d!uXI`W"#l3Rխڔveq˫@ՃB6[yujSRU8.2K,wXSXU*{V,ECT7R-VթV7)p} А!W<D9sڜò X$8tn+ϩ4bэ|BkU[#`=P~`MzjVYp<r4e| 6CUj[ѪNZ:ě-o?\W]2C˷D_,[b[m$y-Xo!#%r0C޺*nD-d`8f98UP~륩)I8r>0y\JXLhNO}hAKgUO0ꏂu9 8zoWbuxfSeCj;4QN6j jmv!Nw%pŖLww%ky[0BJπ갭UNh$C43f;g 0V`/Z0fނjşV۹Eov\E.=IKXUK(?a+vc{.ϦP@+ຠv^OUqbv%uaW@a;\]M\UêԩՋ p!ܼz<4.+ĬXPY7фz5%@ ȚC)iԌ",@(}KXN(KpQ1VQiHФX[5tS{;uY~,1+#N \;>.jի~֩'6`ub+kVN>;^wA׫!M|;uc5:.e最(Z)xrhMV>sb2%Y]^j88ąնswo.kb5L(1"d֬/=+ՔwwcRP`SjbMѡX|Qm=:iU0q".Cʋ 禀A@ۯZ8BXhUf JޯB}@5XғhNmF3 LMGpBl`m%#kbE ,Pb'_`'U&r*dgIiZd%:v$W;.ҼGK1xlroZ o0Oauri|ov]d LgYjuaiW&[G.TS}ªZUoՃs۲ :Xj\b8bu#_@)*9xdН`\n~:-%)#&hDzpCkXjd*pJ[뙝'ISLOQ)XQs}V- jՉU{=MhbhRlܔ.媘D/>Jx_qVf'W'@X*{up>pcWqFjU V*W3ŜW|U\@ˋgdPԯ NIj:`4܊GcJG*nM˨ТJGzߔ7$Uo|X&8!:8PûW=52F䮉(ozy?m| O]\_W9km5' yn+Kp7YO"T]\G\]^rV V5$^=hua;@`PV |zƮ(>1 V8h#Āvjՠ wAxA"\>_`^tQw;ޣ%&Cْ,ʲ?wOy[# !^U+c+rSU(% ʼ@E API3j2 NGzXwPӅ>+U/nxy y*qݚL +E$tb Jגn&]w"f0iyϟ V3կ~A=fF$Z)&{Ę ӢV0 U&jg IDATdf-)57jVzV> lh8RU&#G0X(-dy ?J) hR ۩rB07OxƝJ~u40dٕ2!ON" Q쒷˵}TTn#굨cG3b`u,a5y\y:lbIp]*/7`ugazn݊_՛B[œ}4b8jcU `ğ>b8OTUί3 h6^<Sf?/^6twuzB'aGAsԱ2XtVCUCaSp΀խZQiKWહb_8Յ8#@ƽc{yPXlu"\e0jUHFhTV']]QTTU4:K֗.ob#iR'iim.!⻒za+RȚDz&ZҜZZlbUiwNъRmT&@^spYj_r0՝؉#`S  Vo2pp+P5Tz|<󃬄wmKڤT_vXe1&n\[nۿ  اD}j40zC\Vc7Iwo~7Xu&DXfXf,J}㐖ǒ 4 3B˘;z.Eos _Sa5;g2S %:_J5WfSO,"6p@૰ RXUɶ ";Za`A hQLUWKZ5Urb%OLB R'U%C0'V_:Qe:MF8u*ҝZ/LJW`hTnOw5kê?5;/,k2b)9~@[Dp^v\w ;7̺W[!(+kIәdX%r%ӵR/OkG[VjZCYzF4Lx*I+hlu6$";&DUVG$puHGЁ5 3W Y_ꀇ%l~mjΊ_rF$to^Xdl,V?ЗYS=L4_5@9`,BZ5*`&̬@f"W+OUUr a@gHT#}=U=zqʤ6U\_}[V폮^r N[m-_zBAEe\upXUŨoe& Z>,m2CQwj+Te?WLa+[Y+#mzZp25WpVҪ)b;:tw:A8NF3KSФ@ZClU.b@%} 6X}A> X br#UUI{QI7Y*oz#V$yEr\]feXї'.Zx V?:uVn^k/VVgNJj0Zh-VՕ0Zw~)D)i"6uy5p?JsN+\E z:@|g'93* 5ft#۴i1Se"O :YK96.Xj Q&\QEBlU0(tX/gc%1Uբ7TIֺ׹䃫^V2 hV33 kbQ5۳$WѴv4rPfb_b{CS<0MUք!W)W>iI= ˧:#FR~o=o.^dA51 T]-G7;8J1Ur7<3䌨NK 2^F--vu,@¬v.&N"Z R LkMC8`Hٚ,[M5}|p~}L 02N#UǠF5r{:?uBGV_ z5I[~ϊnXs*\5jQ$3Bj/ y^qȑ]F6c2zy:cPur5әHհImvRj7bu5p'VY,/'#@͸3mFw -T_#߀f;^jXkYɮ!%9X._?}CE{/Eˉٶl Mo+L+ͯWK"1]7auUJդWK%X Xwf_\k *Kce4{jiG"`Mztd5V ¿Xꂑ=v^~E7cҼK:+{߫6uv J.?6$dDZZ"jT8KZy='3i4 H=KQN Mm.V){bjyEcː AGEVpL"9Mi{f&-KZsxj6b7+d,\7{*XJMpAet*X`әj ǂU(U?urxTjPm P֧PjᢶJ''lo*sbuG¶zPܳ)zK%pPgnwU]bjGE R麛n&+f"_ܨZ s2r PkܗVwcw඄,@$ "y5%(S|"wS"7έK9IbVz-I$ ݨTGS˞6}h2Fpeh(zӆ?A=Oaln^Z਼K2a$bÿJjB\U8NNd=U}ݺ"Rt-L[eÆD&r),b@)VqE(n#a MY4*1$d[ 4\i=+*l0)FX-z {Vm[r/ipӰC7h*V&l[iuSyLvwbǧd\tUz#@7 `tL&c JP~H띂.2 Wqpif3d\@Ђ 4_S-mf%UlZNQE;`ulL J%*4V?O@S+Uٚq|m7 :/w)eW*&QKXest8 fEutV/JQwI *A+-&VT5.1z)XK;˗/R!Wnl_`4(!|>9mM}Pz[[Y{1uooi61g4Q\2W5l+"{Bޣt}GWa*X:=Bd Owq\+cבUU\%sQc_91Uiֹ-U Q^{\ZHeT sPV(M•=U<Ӗ V@$UAм*\I^x|qU?[bu uV\_GUIPU0>ִzY n."3XipX&X5:U0:ZT^T 0q*މi'mZ|le*~]Ffv aU:>vaN}yCTu9̼Hw8M5mm-y:M Bhu =Wa{½ex4ZVF.ƿ>SVT% k!Qy+ t]̣]Il WX:%vuWUX]NGfiqN5W5}yW3 &j*P~U2xuUd @'XyJ"ɤ.^`E9's4xXBrTY Q"Oj 8!agRX,6c\Jpui-ªիum,Vц\) }RY+] ͫniI-UY"h!N̉U'JP-~ $Vѷwlz]& <}I)`0.GUUH/=K{$Ny+}^m _ܶڙbp;DVXpnC35h}0'f\E= IDATIvURJeݳ!^6eXZ#*on-SU`#5yˮ!.ibx HS]| xj 0!I` 3ݭ0$d$-Y-LqdVVi{2y ʌ2ʡl7γ<@TF5z-(6 տV?W&cCVlPBH-Jo@Z ]julׇ7jeV+ʒM/W{UӽQ+#hw VځQrp6pZ1s$MevxVe~d(`r1X!bb-`jXp[A.$%ϋ~ʧw=-ʑTx\z2_~B H[;yu!ۃ V5Z}Ɂ<=pr+L(rZPVGN\Ya}ZRBb&ùBĒW BtT5*Sթ᭑vPʺbwxb>FlIIT{HjG*s.c$%EO-ة@y:;VMC´U#y2Qχ|$SEc{pט+kc'P`ԡZ VPizhb5M3?I~/TڄE{NJv>p56^x0js ':'LF=z\m6 @&ؚ'BƪDNCA[kcNmV 'DGbzN80,P/0PGWPxnGҼV0}o|WcE"unM+⪷$WB)-7JbjK,*$Yp˪VP/ۀzj6 г 5 >WPRF(S{{wf3풇h+RSU@ݟ O{{ӷ=S1r2u\ŵB#ڊB,,Hڊ\R?UhomLO ,gܜ; -1<,;XI*ab&pVP2`BPrw 2=;ȕڈLj*~+):jaՊp2@ Iz][*АV9zX1X-ODê%6V+.8U+.jDKSmn0e@u?V?V{}+ NXleBLu N)jlZu Lv}hNrT0xh`To*[AԯU+Se 9VjjSXs-& kՌ&YMj\&*Lyլ\ eSh =eA-%>6"jʚFĐ|xHi 0Z5BmYS"X[X=N9~CzhnQ5;ľ! &'MzKʥB;eҌuCMMupru(E^ܽ莹(zdq?i-|A,pͫ!Coœ7f[{pmcyZ ǀUYDMJgL2fY`ʴHe}J'ȽT%-Vj]՜뛞Չ(=IV(P4.nYDa8[,Im+LU V̢Y\TfnKh9 ̟8f:W`,3.@X FO}ׯj3ˌX_nUR6z D\=i T=.D糣BYX+*FXC15?jib+!) &椒gӨT=FXyfSZdK-`uyo /_Jw*74mL V}j#fݐʔI-! aIfe&X@EX UTXnw{\UŦZ5Ȕ֤\|RZێ)ɤk.DN0bYm6b58L*VbM^-T~]VrAAydԤ)QEQjpj&izk>,x?H[٫MdUʤ|R^4V;ur9T+dZ{X΁Ȫ~ןR>?~vXrKJ*'2e V:I*eg֗:FU m3 ;r`f3!`p3_W\ rBN c{[1]a'*\xZ V!V_?$B(1 }qWEQp R3ʒUժ,P].3l+cVUDf٫:} sJU;\T7@UJ\C-T)&0,9*PE5ɬFD2@V=-Z8|FU Sf+U$_HV罊=0+YJiNU*+XЗ 6wݫ#2'R>x==U[}9V3o_isj#\EYXkvx&+ٲebz[; r翰5wU-VIolƕ؉jTZp1r"N^}/~lLk Z{EեXGUw { eQ.!u ۛytTL55'7YOE`HzQN6~dz{{O\%.(2JkBG"OK|fArcW8:\*sU>lO4s_YT );j5t`Vv7Z;0C[yVqWIX+)Vb%U lBUV Rjjfx qm OJՆԕYIҳ*eJR$#S> *gOuyWgDj˲Uoi^:MU*Wܡ;SVc*:Q,XsspUW/Yr?I7 V %{U ^_Troiڟ#v2WM'^}tR UEWW?kGtt sH\CQSHGV:a)NTJwvo{`U[Wq2Z>T{AT/zUQ`([.j:uf\ XN:b+D Y5J%4j e%s&'Ң@jEel_ 8©(!jņ~딵\kҧz1_[;Ϣlk*}N]0jːU\$V?MVO\lV bmiiqhe7; 9Rq>>tL`{X}~ܝT=:8D1LƄRp:wQ|~,k#3SFΆG2kA.POu4UjUG9CޘfeM[ՍH\ەKS3UnT-Q魮,cU]__:/,UR7&)!]yg_gtX%?E{^jgcen [h~qÿP%ɗ;RjU*4[:Ri.udg<}.ru:] bul*KTr֕U'VikIêѪcgﵪB;JЊlO&VFqGGv8UKOYkWufxk|^ӳ,VKaMߩ`)at'8nRG?@hml9:յyՖT%ddRn<JzGRSR*2ia3%Er/*k] D+R%7/tZ Nc#V?GTU=RժUa*^% jЌUYfPO*M*[U2^j[XLjʰD[efUD6%j/uͭ7Q^ȳj5ip"\]UaRZTX%K#V)=@+W؈+\ U4jBOy_%M~Rge)P&V!$xjK D*Y8 1< P;̥XEêKUgY7*r+J7Byr<8Mck=7?!kUxŪQTܦ齹bWW݋UjU\yV[@5r={++V&ujSIYTG9(搰VB;6 )TujbWIOtx٩˞y|76fXUٴS(!H\J:zoոjjeMz Nm6 J5a]q}Ici& 5Ur&Vꓤ^m3V\%b)1Qcu7iz|XWGW'pKȫ3 %d,qrkZ(>FR*52xvTp[ɀ֫( V+iU⦂+~d~J)U)Z UzZ% :p5Z݃c_c(0-!<,Q`[SUN, d܇|B(Fr|z㑌U̳_pR_C*Cꔉ _ 0bju UmϵT%,UBN¥KCj"cCPUW֒NC` )՟9M4e‰, Wg[1W3Fk{u^u4.\ \j;uN05X8ڕAVT͹~Lf癰' IDAT^ {ս&6*y\Ҫa5:gZY0tr9*VitrAU,VkU)q0Z*'5~!hٵ%c撹gdV*KUR}iYx/jNXT*1u 5noD%т*Eg!q_8=wdrL`ͣJ=dǾb&f))Z@!KT#M,ύ67 R'"35?V1/TSD2USW'ݫjs]VI5,ÄKuqT/P5:񝪽7NՆnݞ{Y/kN/ VAdVՒU>V+g=yl2hI"v\KX<+8Ka 86-> Pkm5VBUVUG`4Q/VnY*qFCzwg / VIl1.՘-Y 4Ej|!^Z\W߿\}~6l};ށT\N58] կ<U HŘju9 l7eKM U=ʱ+O{9@E- :XUTU);:vP \0T[o ::jsU ,wYU)w3PW<sXĀߒ Su&z w-YʿJNݳ(.%v3X-Xճ\]*AXm3ViHJE[Wf:DD>t {:D*++eW@~:YŮ>ͲZ']-+T+U~=dg}84# Z+ua8!jrln2j_ :W_U>_J**`[ dJՐL4++h KyC?yҰVJA&r3+GrT!/rF0‚pՋՏY4V$#WJz=/v\,bS,W؁06mGf&j<ǰ9Wt8r%g&o[\=] Tj!j˞'ꖓ{UU/W=^Eυj077f1|BHu=G FZ2K AV]<{[vXp͈Eά UQϸ#S8\8(CkCn}pE`  FPOө+R(Uرț>Lf׳]HJ*UB*ju)׃T+$rVT,[auY!pmK~JY40szVK@A&^vĨ*s61т`0Xek 7k\QyG~-N+?*jn\UOjr5LV\pێXj)ՐeP;[Д'pUk:JIBvY= `X bU ֝R$H=s zu_Z+1e}-.A% U}!Qbn R3v^Y%+Pu48j݈<A^NhDZ]_=usBl6qjWTU:ժc+ 27.U8oI."ʴ:JL]?B*^{XUC/3ttc`}^ͬc5UIez&wZ.ͩgX5սf@K VMIcuG?,pksXR}|qJ?>.WLP/\O# ͛7TIң]zE P}?jh`q]&^wwlQщ` L5 T"ВjXmd<`6:@|.U+U||~.?oULx̿ܕ*YV7Zs'?~S1K{%XڀB2LJS[Ê.6أ Že>OҸd!**JTA麪VWW7 ֦$D6rBVb\Uy>\rRΌb}ffVV=@uUʷ]BFUMMF9/dRX-i6+*Xb2$aV:V5C]S2nV* +U }x+ZX?[')ȪŒ3=6Va^{m3Э7;zAڛ ?F\%Yu=ZWu{Yrw23saկ 7U9jUX 0eTf~$ W=,t0X|:sc#?m>\zu&rcYjqU1ZZ2I-8o,nOjIRjR ;b"VSro7Ha53+=Y,-jjĘRf?ե4V(/jX7a,UR*4lс wO.'+Nj*4Tݴ!VVN0OMUUh}D(\Y SE \8VU^=~ӱuwhV9CZamS'SUc*XUN' kw-@U`qH}\#qE~u\tE@՛1J꯾ˊ/;PߠHhUV/c+gGۏ?s-e"WDE*j X%WO/|Bnf\.q|nU Sy ԡ٩~MW}M `;8M6 WrZ:" 2 Wu͵\+#=fU'uקWnU|RBVv*eƄФA a˄Yw5mz]Rb*} &{EK$聯#9i$|䄜GSޖk`0qNeqRts|䌘fyf,mTM=&U$ʼqU. eBP]emU gl)+ N^T+կ2gEzq*e TT:"@RFk_K:mح];& ? 8ꑳ=|θӽQs xJOG8^s ϋEAfǞSNjU8X($MW)k`6>ͻZɤKUv*E[DL O Ύ@&-Wlu%_?Z͙,3GUE޷^UUoc **9qX;yA|*VnxKׇTǤ?cO>ЎǞCZX<@%SiJ*|NR juj0h;{ݶ, 'ȿ%1$A3!DF2 O `Xi2 *$ Eq`\vFeZ_ç9fLՉV_jUX旲\aUjjzy 69>SAVC'U*h_PuW^ lF!:dۭ5l@z+Vs}.Z]TT!|08'U{l+ɰjw>U?O>yXG#U)4?2Z\<U@q0Uj=ݞPMl]   T_nod}J Ǐ XjUWn:J59;'B0I%Nk hP)[G8>*!lcTgYw7CƊlS{;%dAV3XUyM+=PKCX(b~+qVnAVsXD3W X_hUhkmvu3T 8ÚړYDL&&>͠W1fCB4_[qhĜ\)Uh/fԪVo m Vߙ;}uq9NDŢ{TZoa iUU`_{*Ū,XI DT$926SPunzkgG3 *D3*UEv 4Wbc4 창QJ#dxے-]P8ƛҎ`sTm=* 抏Ja+Y7C'PVߞy\=ifY]P8-4)=EUFbCm;xR3BjZF]l]UaQinJ nj2ζQwcRgK ѭ)^S7ѝ]qJJ#J Vhx T{;3k' PMȫUڕ,kXY&8Q5Љf{d@~ X\կՌpKFV+kpTW )X7^].UẸ>-U&j5MRH+Vִk,[Q\"jUjlrzWIa=R+d\j'\[LDH7r"tݽer+JY v1*7Ou jj DW@ٴ A#YmVjUbT1{}˳zV-)P2=_&UD׈YY nRxoq*c,W2]R'H5sU׎P`ȗ>V&HClE=YK@b&U陼@]ɛ~%I-c+h{T+dRkneXt+'_T͘T5HpueIVR= RI]yL W![1V9*(8?8xN\ Sg[u ZXw%j*BU4kU<6({vů 4҆s2\<!sXמ"4e^LD=~bTՓ9lhT$"V4NՋvU[peXݶ\Kl5gE1?cƲ;K*]kc󧰺q/Vd`u<6VR5U˶f8=IP*(:ڼ=Ǧ[?ǵhXnzߍUpXw=P 1h#?o]: qF"rE*CUdJ`t=uBx=<>zxDj Q&NxҘkݬRu8fSXHQuxHvnnj ~cvX}^|f7r-kP$+U=3Uh dJL׉_^~3Z:-fD5Q@&nR 6%ِͻ!^9%mȷ'*(`'I2 !h鼿'kn7VQw=VV̍gu\-`Ֆ yj.j/c]U-,j͋*:;+w.Y|Zr@j{ulUC IDAT V2v=PuZj{RoV_] QJ>MsZEsWBW|MOu֜~E4?8l ڿh3qj(pSK@C]Aծ0q6):㲔j]SkgpZ B66`"{ `|U U(LSSjQ)NUbdM+J%~saѕŁ(]KkEwʀ\J_䮸=`f UE;ߺ*檐xIY+UfrUתS'JjIaoe]Ҫ%J`zw47{a:c ǪY @V_r.jOsSaP}Fmop*<1̕n8Wo!r۴I WՀ*,:x]kYVEX%W!06pU|W,rg6JVԍWz6hN>WUf:}~Ln1LLbT:7WwGDlhՉުX1RnKmn{:m^%TL*.o@5-Z+a,?F; _GFNaGټa\l_r"ɑuIDD3"6Imc/˥x)ie2,&] -i6P f袛 y?9$3Wזdu~њzNX9" f)׆:Қb:TL |rVa\ )M`:jWZ=sk*" ;&ĂJd\ެ-t7Y("(qYKK"rnAH^T7I'g}̧$Tw-H#)3 l5ڎTV5u֤:a?O4̛5ĎrX(~I8auoYfD!"S*WkkjZkɃz)qbP4%WkaSч~UbU^e4C6aM+7Uzypux8%7v c1-M'Tk*:NG$'ko7[eOʅVPTбېBPA5kjW͘%:Uz<Ef<^G8xkUVp:YFJ֩W'dP=*U*W35p-u݁,k.q5khD qG8h3%|4ѲNa46cHX >gU3QG,SyVKn j7^R)[I]uY)rPU]XKՒ3/Zm/Uv]X]f^SJJ:SPEC(. ֪<\%fud6\QF|LǪڨXe'ϠW A۱XE4kOCq%֞+uz.ƿrUBPxZ,AJz̎V6Rf4FTIFh%NHN0TqK akjin0WcK* \eG;\TiV1VONh(yWs`0YJ5͜{snl++WU^]ܴ%qdS>_יsH.a+c-WiU-@|0kEOF}W'~"9JM 󮝥3\C?;h'B6_T]]U;V9ro<a5}rzdZ*iX4w#.fSp/pA`pD+`cP+֪TZVxe(Ps'uT\DhdjՈՒUF(@\a0RHJ,WȪVl/vm>*2h3bRqwpC =Y/:Zs_M/]DX%4~ɻֿ_Ϸusa`og>dO,ê¢&ئ䓾'W2iUV۷cՏ^gBMbkVK놧UIexVUې SdK`騯2HxSKbzՏ2j\+ke8҆GGQX*T}܂"b%_4LI[7yZSőV7ծ727SXFۙX5f&4PcOA0]_ΰ?pHի7/_\2%?oJ+o/@\W>Vt|k Pe"5خEYޑa* ӛ4r Q]ҶY4MGkPT`uל`O+?ogfvvv- ,w[0Deu yvAbiHԅAդJ,?By9fylxyrbq j`5kBV$}]4ߵ ޚlMj|V9hDZZL`5+˜ !gyV>tΖBqa`r~U^SVֈi)K*hWBZsU QNBTWܘ{RcuN TO U{^@g#*FTJyU 8p*.taz }':p @[We-D-˟~WmGQ!(MAV[{D혁5 n H~ic-)o-6YF_7o[^_F摀NZv[Vn#`XNf.MjbʭӣT-U nGP,jWmx-$DZ>sw V?i*=:fc߇$VŚ%_~⫤d%.ٍ뙽*KsQDV+o%ǩ]u,t?z򧛎mI XY{ @M1_ܡIUZ&5=~Z~3?Q=9(U-9jxJ|o8Vz5d2Ve\'%`:drYMZX- =[ #[zX@북6s_v L5P 4^ODjaWAHM_1:ɜt;Ħre@߶ϓ_?"gҵ2 ib(?ޞSrժ!4>31֖^ݪ&xm/oZG _fbSXuW`UWX..^E媌XDVּ}zĎNi.!j6c5j*=F U)T=Ҏdde/i_vGV%-t*T])1vV4p=*9Kzm[ ]IUEKu(SAUi$z"X ez=Ua^jzut5&ZjJBT})%fVGTO g6\\1`vo A6eo 90@Ŭz0`kv+ w>XMZ~^sm_lUzl>@sZiI,V5z,{&ivDj,&Zޔ?r6%HB^QUZ4UTC}ê&ӪV=JyuYs%LT"Բٰ N|Zt:VO US9UUz<rUvE;]vLQI+ *s.ѬLJq/^^v/+e 64B])3:S\U POOg}HV/u"z`UH@ՑLXV[OވUUT[e4vjڴXԈնռJbzM3[oƖg,L0q׏N C:Hc5jb*Mjͭ?+=T y+U{R}6lGџEH-Twt۠; ۣN[r''p|h\]X\X gT;W50SRZEӍs.%.Pʦqj|e\=AfrkJjV} *P quVjUjN_[^JD+Ȋ@ \ŭy֯d],Q @[Osu&p1A4ۥ-]P#GQSf_C`wsW$nUE2aXuH>6CqX0|XMT դj#߈qZtrUu,R A9Zےj9TVj;jS0-ʵINU)?j锽N|&p;&}큭:X^b}۽;.tUVZu_*dzn+6:M ݭIJ;VVђ̞j[j]{~*'zٞ*680 vǪjj&)[bj پ#(J>cKtϊܧ jϖUopE_  +>MqU3zi VHSsæӓ >lfVίuHOTWOUUR_DDMua4æ2~,L4Ve,a[M䰎XT၎b*`a -RUV5 TV}No꧈ß IVs{Rj/}6"Li{gwud^e=j4@TojsX?Y?s{cyYO]PCg`5 B֭d@6 E3Cq&^KUuPl)OZj#`5Cu.q@>Qۯk`߮w8nv2O96v7:o-=~ݟAuUUbcU J\yauźKe. kT\;9q~46N#yWԢr|jeܒ WQBߘfSVXf<ҍvYXHy{$.7*V1Xr;?OV5UQfh ֊X5֋]^e)]>;>ڔBlw5:Ԫ 4US4Vˡ*ZWVNRѰ6kk$&#}m̪Z buH9\1'VcȲ9jUVԕ̸lհ V`.jqZb-׍k#Gv7W˵a@:9͉K)VfHժD ^U?k*-{{zX{痢Y@*Xe¾ՖocNWv+5 [⪩ RO&mP躱~q:ay߭#PDꐻ~B]hoٸzcPz9*ëiVUTKj[lld5 +kmNpwU27Zj+^=W @}LKb59{tk*S%\wAp:UJS[V{k_Y5"޵.3=Ezi_b(lVgfUجa8*;v7-6VsNby+GY>Ҭ@DQn.UcU | 큆XPdj~%AVdeX]8OHJ:s Ö<UߑEUR'VZ&SUeF|i˖e$⛎QzյNZeZ*[\– ƛצVR5i U5hը]uSҬo5Aj-U f!r5y#fCȣ `*kANUⶆX}$YiA{ DUU%[YIen%ea9QT!h6;Ysq`FmR)51V Y+rmWUVajbrsE WP]-̀6Sēq/WlAicR*SᒫeC_W=zpϓNŠ,{KJKz'#Tno&9!՚'HR|EO`s8 `AIUT\Ui/Lz%\ZV#[u+Tjal 9- 4e2k gkzm8؏ `g4UC EMְو]E]h'[TU|_ZjU`[>uAY"B{B+fl<ꢐEZyNsZ UBAcu;43j5Zp\/Un wu_ <5]WW,_N$Ը¼+ɀݣZj5W?#K.U^H# e_P9RՓ~bz'4ƒ`o૦لLJɪvlT=)U!ϪB2 ajQ%ZJ8'MG0@K2MZUkW>5a7%*`~Y+oֈӵP[ҳϦ; _}_kҮZSB6jz;Z4? /vB% Z G )VIɝ($Waѵ5j6U騲#%"róje&RJ)ZԂ/{XS#9IFq*\=ԭÎξ|K'v" zzd-XԫiX9\*ɃgcggYOA0URb>>nNJu'd kN (_&T4b犪UI\ T f)UqMGFT40#a\H'VPlɪT"W?i6Z(alMUc jgq hYMfr A%yU Ьt5=ar7/OSєj}>8U+= eW@-2umXޱ8+blcTX"PLjþ[AXjkϞs_C*|)OF(eQlYRR8=E S@UY*O0<r_%l3s-&UOexT ^|D9{cPW/8ʊ*jxTO2P@ SњnUT Kt>D2sl+/:̲XUV]ɹm\%S45wpX_[`{g>XSxʶQ_}!K}߿VS0TzD(5u_ZE=;D 6UG$OZPζM5zWz_i5U13GSr% 5眚vhN\G8=x.JՉPv,nUD:OF W&/qJLaJo;<*Z>Can4H9Xz>ݨ+T$|Z^:UTh<,X+<:%}=_8O5zh؈ң+8ֈf:ɓq?Ǜr5p?u.*I04 qތ o걪P-|y\oR@1>V{4&6թ+_g1._yebwƕWǚ3K+NX駲*㷟$k r8SjS6h_j+n[Οյ' ~Swa \ƋDs.\YEUX+=ZE`A(g{s^}!XuŖ\~$X_]H'KzVU-}!o ݂-ycRB\W 5č(z4VD6{ju>ؖd :8rT :bcv8=*:?4"WS2ʹUujU)h/pgPMtpKf*Vֵ{r SS͉Sь, شrYzGdhȟԚU:V2>ܠjYJ!Z-z:XڟN)“/)l䊧:DׁYz(-&Oq ZɧYïUY\~6OJ-&XMӭ`? yܰZXAne ˲Iٽ )98Xv"@W^}Ī%yngWn|3MT$X=)9C}NmpSg566vEz2EL-[>son6p%5CZXg%kUZ?J8<9V̑w}?ۈM!g_&W/(XJU wU;:a3ʃ9)XMݔ*6¥Un duTGUE٪!?hίL;74n˴ZVoJ W`AS&E?n$Gtx+Tab'UVc5V-C"Vӧ\mG,1 #VJl}B rݝBMȳae/@!V/ioڪvgg;?+LsD},g#*v2Z GUMtP5\CZds_/iPM}:b}I kR:L `jN_C:cn̟1ҡjU>]\ۏiFklK9QѫØ*]V]|Uy޵'c1W/ Vf wjfw- bA+%tkYR6ZV].x><bXA0\uU.1.˛1"V9~m!#wuEu09 dñp5#Eb.KEt#@"C*)|cijDK_™1T2HNڔdʬ+0XObҜ׿ZN>w7 Hq!N=+?!cSZ6'pzNyHa,P;7*ub>UEY<XUֲ +OWl#FY\:<.Nzs vX-`uaJpuCV逗 @* URj@{`#"&kVG!@.P*aU IDATP/)rBM ~SѪk.j{⵷J`\8}=-d~P_uOLDRWHtBUp"EUN28TAիYETOݚ\5Rm@8ޘ*& b5!VS´6m4k*TmwWRd*Xeddji{LX<.au^!LPG+>(^`M%{Wɩ'u^q@>D>Yp#sa!He`& ag #T% -4wjGr\s1UP#Sb`J(:,ה<ʴL}dTJn?>^\ѪEU UQZe+>)5-`.լJ{ YkCͪ$Z GX{[},6hJʚ)+}F.|uaw1Zv=U=b*7 pB[ LM:֐>\Xk =P:U:?~)MQaj_?I܏oZ/ІaUx$-)FfXr(6^S|%R)nӒ`/ Pm"TN)P%%qMR*T)٩9ۨʀE5"ꁳS`U9KG꜌U$V%NQ0n"Trj7@wy.a5]:M[,Z=V?q5-/Wρդ^'C*Xݰ:4TR-U=b51\-~[K;@WYX}|AvqTwJ>t@PQfwF7iUvb4 Yu= C4JR*=aR6)X+RP@R=v -U%AMJ4IXC:UԜS*  \e^ߢYAfN *jrQP Ex7wWCWfW%z.n6|87,eհ3VC z5 Xե+ɳl@2ˬuGbݜږ:޴!P# g~U⨫*4Yer.}`H[>nOjđsZ@~Q:P X-H暏qx:f4HXFc/S<+}BN D*ָ>9C)8 JU?'6EO6ewӄ Ԭ'?&X7:R ۺs*Tv&˂@!.a/UByo8c/V®T 2ۢ`5bpRpZ̬-JA{X_\$Q7ꞽ} 1(wB~sXJXu8tSkE-(4EЋ^XAZFmԑ{y:O}VȂiA>V* %L)K +#UiĢR aՆPs᪋VʶV TZ* V]+ţ Մ<5AD@+'b窦` U?nRҁ->hu[| _}֧`[b69GNЙO~V7?Y fOCPjds+|_[5 bǖYBOsU3O*o~}U[qZ'qX R{}* (+; ?a|;&NvUg"yvM|*2Հrc;(J+ƪp|g0(A)Rns%, uŸ@PA*j׽#ր\<`l6'dO/-QC@]RAEt bK*J xYQ2M:Riq>j|M~Tu94bXYB4YէSvgO?Ϟx}{+W_Hy~nW\a?m_9gn8EsaWOsowwO|?c{qX B|YT]Y}yU[MU% N$#2k^ż*;D8_m@151=vWZ((7{JѪI=l~(ys|aTUjCUΚp/ "C Ѩ41NSRR9ϥ5Pj}cK _) `5Ih0۬zS('=zUsa5rjZ_ hn*)VX?\j&|B8>UYҢH D%]z0zO5[5eU[̺FQJr%Yc IBťk/}?ߛ]J+,;}4׿˧k \ LDc?~ 8{xo7 Γ{[BXݎPm]x S>?XZX=ţBAޖ-nn>~s8UE^8|Y/g+W`zIǪ 4ƪ; =U2  3Am$h*S]i9Z`])껑SYUՄ\=Xtknh4&~a> rm*5wE*c2Y?CYRPeJʲ>k#Ni-~u56ikI9 JfXilIzvUhsV>AlrLZ<%)ijjN-U 4%D,ٹe} z{Vohfi(K^Z..ogb[X]Tvw^jG%+ĪwaaSag1yie`gxy-/|q6jBIezؠpmJӅlQHW͵Ek\!PCA9x`\AX[m Zo1VO((4yc|B_]}Qdm[y) bT?\ W Y]t&_fz+ǚPJ2=U5J k5c%BOET(n*rR,*]OyEdb*Z6XiVʡ* ^s%beOK%..ṹb\GTg`1>a V<1X_ %V׀[JLaͽEaUo  "l:׵VFXMu?#X2굎W᱀ÎJるUy ~VOp\3`@4uL 3֏NvwkPD?`гVeA7}} UGX=/JaKUe[HU*,֌FU-T UIoz_,O9+U(UAںX-CPԪ`*JR5+^rD)%\V%b52 NOP1U^j&U8Ev1O;STzt/; (RVy>^VAI6*TjLZ..p+,V@MRo{^E;z" 9щ(+9`5U?UYǛ n>!`zrKi+XS_P-%ÊkfrJ8UEա\'dpc!U.iAXr9bH"zyNJ1CS"&eHZdHՈ+ *zŞk<&DX?_HT%)}7Ϊ"P$)ݾjժ%)aZZmk\-s .|% 'Q%$0-:Y##X'7f!V[;KA;߃*UϞoU?Hbr܏_=ά~ªҰO\|y;Ջ Kc׺o@ҺKUЦ Ph۞_JRXF\`U6QYj婩;V[wgO7 KJVo/X`$PϽ>{\5uVQ{VTy8_#b;hE1pժV=n'jj+߬oջ>9YŲ|҆ڪR{buG ԁA>6"ykXM%R;Xgv3fdaTL8XuS(r][k`p&`\m^W]:p Ca+@кMQ(Wu*hs gRu!%V`46~L;4!Z +E"ڸaJsHUhf4[ꎡΗ;u)P <(j jjRR" UEg5!*Ej?U%VW''`c5޿ #VuUi*w%?lS[+QiVV"ǚ?VCj3qQU _]ŸƋY<9{W+KB-˰Uh\Vª6Jq7*槴:p7Yȇo VǴZ<ЪJwa`%T*"I6`Ԙbnqp P%\6bـLZUV]b{n(/ *'&/*ק+6+(Q 02)#LQW*ՄZqWc?xq GUef9eV>rs5oZzADX@n?@c+ժU uNǑi3i~u]8ȱ#':B./PiTKV<(*RUL1{9I+U5|+l~^bjlSVHrRPTLݡ*/X&*eT%:q?)JO'ClkEMYJK"YGV57 mTcnhYW|lؔVٹ R`VMBjRf;2,z P=>X l3-G}tS V]z84CWHAu6Ѷ#݃?N)k7NjVZ՝US`d%[KUf} RN-aHCQ5T*׮V-z*TRFN:ejUX-K:YJMiN8jBW2X#XdR(Q5ۭZsm°!=3J0TF ?A*H@H)R8}ڼuIQ/cI-mr_L9}=T~DcT$s7Bx&`sAя+IQ_8yzzUwaZcj5+-WEZV~_.A>z:u/*:@jp[*{)HDXjT܉Xe"#ު*"YERCLhN-M @퓦}y5hM#SQ ÚҫU K弗qjV(RTnbTk`Dm#j)Ó{ª_꼏^X3E֩TJw:] Nj5Xs5"FX]XsOgՒz[<սA̲Tߩf\@CYliV).ӈUP^} .j{V;8c˦Gh^*{ݷ:UKU7a؊YQ|9ې@_BYud`ťն9/ZYZjT#ּʪ5u,!靎2B|QRXO{'2VeypXEss`5EIbO}Z|>74_~\zꥪ /\x FV4`*jm Jm,8hp;iхXU옳UOE՞Jn:*w}~^UuCRBZE JA16*NV|[9P+@jb VI|`?Ƿi;b5BjAM!G 6IX%ȾVG?H.V9nUaezT@N*pNzvu1bJ]YUj` TC:Ou{N)TKnNo.1Q%Rnx"Pu=>T{17WR<; W=VY2X\JrRP mPT*I cjXaL-ǁʇZG07鸪"&kJEWm>a򭱺ξV hC]A&`5a.^4`҃ hjBaG IDATw>#O6)zLU$Òw X]t5Y/MDUg}*fѴ .J)%p*ojk~bHg\*q׫UG*#T{{#p7Q|+YAf)ՖOm`jU6UTN0Pu UN`AQ<LX[3(4_%VxER L ?hoDuDXSGd8jʣc&-?ŋU M80ԋi wkR,`JGt}pgb=V +H JT*~݉ĩJ4+MoK:h:VKz&c*j} &G9>醔ٱ<@mF}aUU7jKM~/.7.5j fa@U;Q3g_,p7 W)ye&W. o=N.n {NѪ*ܴ7EA)0r9uF&MVgde=DZuh>kYkP ,wX5ϚVcUXU_sճ~<:.esK1**6Q9 T2?%גDS< 4C&ҊZnDrp`8S'iٚ7QZs(jcmTbUJ-okuSLLUejKNOPn4< 7R:)@Rq>0tRI9eNV+7BU|HrC 5!Leh9H۰8?&wBz8fܗlIYz<ȿ|c]So3_^݉ZkΪU_ RN?=KDa)%i-JҒO.k÷(HՑ>S*K`,-PKa1jR5@qZʑPFI*x$ZDhNwXBї٪!+%jVk. dm*V[`5V.,S%=2[V Z&.oCvX]/+U:R'jc/. Y0oJG=mJzQsË?YYʎ{=zS8xYo/P-7Aj2R#h5URY *z5vҧPR{*ӷEdNYm`"U㜤lW*mtrV߁XDq&â .ڽ< ʚ8hIX o_❫\|Z'FslǪQdDBpS bP `ժq@RUWrGT z2 &sDԪ|∑lR|5b\ePR,ηJ|$JOR@aFuX?~SݷPI"_]U(8D׿Z*ruQ|3.)X[.v+}˥>k_>5; TV{p̄7Vf~pv2W#Â)oy,% !:p ŅW&DW)Vjx}A9=czQՑߵc*S7 fjxFLݱzQqj*IZjAFBM(Ţ IRO>ΝFౚũ_jȚD]g1UTV5 :Vr\$·zUܴZ U뎫ɛP'o49NTjcé`@g`X4h jgJ[MjJ˜CXRXEKJ]\\EX0V\a3@ !Z WYfz?!G`@&>|w&Ji 02okL4㯎lXUI|vۓ~A+j`rUD+=nO)GjXsեjyڤ*vAj1:UQ]*Td{Nj{Jqχ~=V{X{XM䀝YU c52tzmyxVޑj)n WŷIϸZ/#cZ4oijU/mj}7hJ&G t>&LK5;FqQ5e"zT[9kjۥ̴@(V!VjA!u:M kU*hjiU1t>.zNj-n051mP@ՠ5?m%]sWbvtMUi]Vza _s .?uTYkώ3U `@[T=Uz:hTu# W v.e*|%'(wv{c'Q8<]dM=%i^+Eb`UZھMxV7mXU$c{ Eh!NIGVÌ?lc_W1=+g`O_ bZ-WG\A!V0'%տUXB0}?O *^Oz}@+lB-Ձ thְΓt2IeU \ ׫!Xev5 Xg1e WJՌ Jj"TY;l@B?-TZ=աrlZH8>v0$PVCEH`s]SW!tY}V#M6,VB4-m9.98Y)|OV/],mk4KuOoigV'Kfz/1VT\985FQڏUh'a RܿZ?o peV S8ک#P%(8ۿ8Qdf `(>0HuM‰z lK_^Q_&A)҆Ъ8fL}jYCǘ搉)SW}jjV-2UvIz4 Ce#VwˬT{;ZLouJD:4u=]Hxrkx QG{deR(#^EPI? ^?ȏ(”SM9n߾}¦p{V?dcTϥ Xn;Q*DU}' ŇLIH{lo1$`T%;hd@>JU6`I+j; >bg@lnFj5gQ0Bl.Ij}iu˜WJn4 0 X@ndmv`u2:B/nZIVcL>Y^<*2T2'xܵr􄧗lOa? ВXUÿ_*0 lڪꟸo41c54PWNЊn[M9 |`MAG%duf̓%wu]:Eٝ *;HC@#IP,pl*](#ƶ[?u>h`ZuRl^W#[Yσ+ҭKZr8]<] ?MA{ʏ !d 6isiy^MU.[HUo/X,VWgo TWW@KW-ůpa+[#u<J4<:u[е"“iai 6Lђ540oBACbGyZ7}2}bP{TZ:2.]CGBPD :/Dڼ+p5'jٳWlw'GRQ|W"?Mӕ2j5zVUYy\ |X=Ω$l%BW.Ϸ\E6]fء&x؀l LHMD< Ӕ~7Aj`= LbR3c 0tf^j?V"}:*{IjFNh0/zR[| 7Jr@G^FBS]Byk6 b+ |T7@vr*ՉQayYn>YRːMROK tJ֔Kk,u,WdA;'$G:^79bNI*j&x+OԁdU~OƓʰBj"Ș~WcƫբLO\nVɫPnm*iU'TQյ1J;cxpyHvGtCkPǗrmQ@d Tw ScɋE"9-Bd*Қ+S.#׈ܙ}j)Y ycH7>[4_:jr42SꐫTȉa~jU 8C龝`\/% =Wqy%ICj$G'pin,U^zmXcu-bUB/  6ߴzC O)V5Z] SXEFdE*MTVj^qeuMšh/ 9B 蓐"\&+JbZ6uf$\*ϴmj{}#@{O? B(prX5ӳb V˹ 1X+֨nZ>*MU,gbFӅ`URUbiUx\UUa*š)LZ V?"VH I?d%­vi^KIj;J'ؚZUM`Ǫ6uXp j3ĵ,~4m~DSvա/y|dFbU=Nf{jMX]b^ekڪBH _U;}6 I[UA#V[mz( @hVbgZ+aV঑'V*D_$eLVv)QjѩV{*ZY e嬺5.#cXkkvQׇ{{>=mHyabta]fka)ww_u?VU&ջZq$ Vk~`utxտ_ZkќXJR]XPŎѫ^NCʚUTL`_M~Yٹw xG+i~}CE*M!AtC\XV}ucUc;>gj}CU*wֵ=?߬ԹZR7XGZUQY<|2FX@UU|…ʚ2nnӼ:RVlפ=kٟmly:sEvTA(H=]<[X!lA螳F᫰.}C\gpk a5!Q\;DŽՆ#jrfB.X ' /=~k4]ݥ'wMܣwjUyQFVsV=9Oo=U:P._/&ս>_}lś,:?nfTSO~.|xۣM4TU- ]M'B+aPy=㸚Km(^&B CK) ۡCnꁅ[PzÐ,Bvdhv128n<1`gaSaPcy_iFF8+K#~}GJwJ X.\Un4V!al~02iĹ.U %#9*$>Hxp+XվvwԜuI*2*ԅ ~cQ؆ca ٿݠLT95n~`=zPܢ$ k)ڗVP4*Ϡ3U^QUZE#Vږ(޳]8ꝼ|aF:{-{j8m=,S<.MO1x=^ԙ U6G^bR IDAT#{Zf <՛m`W"ՉJP^((םg۱ V)J3c 6kVռ[UԡvfTIKt֓_Vl8LA>'Hn԰aobu׳Uآh 0k",\-}rR+VmRP\U:~m"E*.OB:[cxޭTw`Pl?SUmo#+,f,/rNz|B.nد[t7ׅSKNI궧W  {^TӚ{h4_SStWZqWc 3b"7dUbUNś%֙ѷi(?yѱa,Ul\i-y3U1m_8;oC_|PM`[]V?~w;X,Vy6^Ue'ԪLdjw*̿Gfj;]Uo%QrbUҤt*.-AdMGLQ1› )V" 0*j!UêFvlj.]>drSc=h*]J3ҕn@֦*7C<Uf&[(W'ieV7*LT3:*9@մon}Ua J'C>XM5b{Ua;5? 7_2\ $*;a,G.N1Q,>1V.:X9B#Dv!6"Ju#$)R#$Lz'I&F(*%@@&R%B%$?d )$+)  ";q$!&LZ ..11&1(46.0,Z(:9&23'-07"/_,<+Y-9.8<<;1h9A-$\x2r1?=e%> &A@=+GJFH(9FH!4V,BK9'8"+GCNQ':]5N=b-CuuɱU92n: 'KFlǥtUY`lV˹,=e~OpXFjmq^U~?@Y8$JoF3Z"EYg,suEMZ~?Qԙ IDATxK[[j[K#+6-s6 J ur0P,Mȥ-!DD`D:y !_*$BK/>$ꝹW,;Y)L?•t[tRAp\.ŵ2L6V{&$n.\@*̓l.{c4vocZS9U2I>k?REMOOOM- _avVFWdvZgaU|jvv#~H>ߘMU,GQxvzn՝GTwTUu톯zX5X*Չl,CHX͂U~e\VU9!UA Dv"k)uYpqVkoo U_Z+?(V'mVXYaCrEVUtUV*IY`(u\f5ZZZ]Ba?]H&⪂u ӚʁvYz`zpZ7|csA&UZ6ɰ6Q,5ʬ2D*m6C?VQxxTdjuϪ%g`e+PkV&&CRj:leϟV%U"ZXE>eE90ȖJ$+hUUw(q`CTpro-V'YeZU:fDV9\$Wi_Z=Sc*T*Z渚bVŴN.-D$ (nUZK_L[avI6K00{6@:GֺgP69jT*{lB5|լ:>Vi:??tŨ fտšef괪.W3ufNr$mT*V#5;K&*2%V*XXw:1%#Ccp4ꖏ*:rR5iqJ\-H Ad50Kf|2s)E+H-?|{5X%T#joŪ9fNU+PPI!^P'X %TVqF.CVQMUXU :#Edi/kV:yӈ("*ՇUQ\TT]"Wc jӿzn]&O8iGXA,~)$]NWUIcugGI!drT5Zʫ؁\ꢰʨFB*y4NU$*R٩UJ\Um[Rb^_GiSI(*ӷhU̟V,[U9UjXq _(-4̉U~ VycuIJ&QW:unZ zB5bˢ*eY|Z=+Sr^]? N}~b2LUC*ªWF*xJlpngJ>sOUsjrXz1T38J'aT65'g颸`VT-5=VMc EbuARC WdW7% zmKakT2{dV[}ykx랡: W>BWV8VUkիO?7+JUA?h2ŤNKX}!$yłꧢailXEXEXUR^ŠU<oH3@(*.|gѠw}A_ʟcĺn|`pHEnhdw\L23/Sˆ QrwUzz qjVYU ϫĊ]: `WW)V]pqITpU5 fU^G!31}iXqbħv],ɓR]*6E%PUXUVԦhVfJ4m+V;YU&܀0~.Fp+ժjRX5++ysXZqjff=T}8/WmVп?g$9Y}UV5 9oij(t[ڗ_QJO&zy#_.Ay{Jm5<6rN]bK%~%O1vb]? W x;˪v}vĻGsf^[ DҚ$h0V{-Vs>w24)My=zB4D9b:@ҪfaUXU#'2h*`*㪺|> &"0 a˪'JR]IHVUVU;Tu6poyP=hVkX֌_A,LJy WHg6Via_uzX Ko 7 2dUa~c`:Tg}XEXjB,yA İ ߾No/xMX a/2 A'Gkjl^h"?y;Z; ԙ۰2$^|cccƸi 'Q3|?//7?۳HT5}ߺS߸u${}47>5:~'a0Stt \1+VUMi<4qG15>\e^x^#7<@|#bG:8 Na_ᔫWRa@W+ \ 1sC7]UN&Y9%2=e`PY0Uk2m]G&ra0Jt˼\߇ܣtZa'T4蒗!`*|\f5u'o,2H9Q/,o61hHt9g.^jKVUҨVVჹ]4v}.|aړ\U$#dZ=]?T5J|V+,br(}Y6:<s R5BV[u@mJ&y@i]qamSa)e"GEcj)ב4r.@s净rg޻wً5j9:UԖQU*b\2e;Q 0liT ]7WӍWn'76taѕv},!naBjzOWQMB֫gRG"Vk{f{PXJs=fLaZLY+>%N`UMU\AUB`9Y^-V%^m T*`:`BuYkfQP;g(E V^͈b]dUDqVS!1+?XEjvui*X@:`%X.@2[Llq1QըZHV*S9fqKj&u@PPU>VB1]ks QM]X=qR}tF's>Vs FGM&,1$ ъW CZPbQ&ixa GD@a4`*#%dhQYXe$E'p"9,\-Jj\_c# ۳BX5BiejzaEɝ˖$(HdE,%:?~'OVV^_/WVV\6ޘ`*t]~o@kftV~IjusJ0{R9afflwj`*V`d } UZGlR,^O(^aaP7(Pr3&w[^ffAerW5 kPk`]VV f,U/3r3~0uBFP.\"V#tC2VUcW/ -j y-Nz5 aم6d-jĪXnyinau+j`r(]TU.4w6NbD;\B!eh~߉Dbph#-jP}}+B2/ !z1mְk~Fj7sT2Wq9niArC³A+΍defvR=9TyzNaD~jpO5"V^_뗿Og LO[޻d|j: '$M$T E#8úpc}Z^i,//3+޹V ;n1he{TS:F5+.etT*g$+ՉW\%Zfъ3=]assU"GoFO_P3,,hrVRK:OsE5TvUjfXT%SՒy8"WYրM_"U\>&TU M *_U)YQ@=B\8ʖ;jj:'@ *cC9V[X VNO_Vbu(h3䪉U#^izGPȝBIiڲM|9VbK< Ĺ7G5[RO}=>-nØ\ FDf~ Q~zMXHqL[[*A[ʸVTUCal(8hjz/'cW987TGPbKH`"8~7pudp(WՁގ/<6QCmDZ?$E.?-X:[?AV @M[]E\ݜt?6kl1o}6.WѪ@ }wkӃbu&4$FjF>Y>&;vF/LZi2UjbxsVYj!Vu.Upan-qYQn8oWe[KkVꛥى G;a*R#kjв*j*tNnBeT՟*Uj SBU@tJ"Y$6mOBLOrcm׫4VQXzj*\(ysk;$T=VYt2U+o>v+ tK'@l @rX˥FpgVfx@A]"ӴE;׶+"&d5xAI4!l2 DRp 1XM`CF5HO6DB 3 C! B/~K^ݧ&/IL)l0=~+ۣWھW9}ATt-Ad*јMɚѶ7#)i IwQjkn.z3HskkKCA').NF*}/\eň^5a||qNR z2b?^l3*M/*n4mDi0` ֔Z,>I5qPo`ZH gG?`OF h-ՄkZYi9LB~%\njӰ$eհBlarĻhTj3E^JM8u*~UAǪxz%nilXlwaWO|:TUʡq6[ V5Qr!U *-LąVxijfV/k# f͟ճG>S={w{?O1Xu%PK~'a7B2{1Z']pje u2F+b7H2:o># X+K$lH`z%lLJx eP Jт Q鍾_SRlN^;w>\kJ*>MPhcNkMZ\| ֩&wb)&DžgTUINcGj)ΏZT3Pno3!j06 >\5daYrEܨ%Ğk+U]7,*@2PDU .10*=)J ֌LVΖ_QJH\QU;M߱Rƭԕq+WZDL1I`f@u2߅bL[K|W{QiߑEWêVNݼVe@j 4}u@Ʈz W9 P4܆ 0)2LgNLc55#67Q:z&f3݄ym|*T}z>tlSO~׏nU *NêejvprW QGb]*3],164*}VX i(V)92\b2U,W.U IDATĪs(:uk{*mv (a\9 rm Ŷ@#ab$X*Ga?ӇXaWfIr9\U?XUVXn il|凃h#j& "*u1"3d'ā_ciH9:sxTY=jUŪLd.}6) /y$5F*)`t\72Ub?iEAUW޳:|*4j9kE{:Z1ʑ3c[X59U*T}j9˪M&x֩!]dsXeNUiHxʡV;-M^v:.{[y*  V PXUV !~YWe^jyOjnk3s%yMXV,Waի|dZK4gR͋ +l8g}\qҘ j3*MZo`Ru 8}VVrQJ۸5d> LP}/`U k"j TF% UTr pգV}-_Ԫҷ^~X[V0d3v>Ggx$P/3*)Z\*Z$f4cVEV2YdZrNՉIR]a/c,DθZVQVv jU{V~`O((UA 6`ν~LJp:%D:_XB"XEaժTE`Řx ZM)@FZ>RjwU:p\] V gtt@H/%^jHFF[JU_Y](ARjF: ѤX%yV"X񟣺Vτ|Lb԰dl@Yx(~aT)n(X%z#cC'*jZ>)NǪE+TӋY5AOʛbEتcJ+pk͠ y. VRkgR^j*e[ 6~ڊWՠPa"W꡸%>X;cB\O`5VWr mXՐ"VQ6tW,@Z33K8*UWfU)[j5P5x_:g^lBتynƤ(L"ǷR'URoY§ddz4VʾSw@b+_ɳ.'UVVR.rgsd&ͳ |*LVXq*m6X(rD,ͩxΗ]x@yTl =K$F2 MmU伬&*5\6b%3a#>T}d#$"5`Ucov|oe&SJ,lFtO8Oj:>QWV;)-0t nNɘen3V/]=yb2$Tp7M€UV[kx#kjUDX%FjD)t@ #@4[DA\-qGl`z Kf4{yJ+jveni,15kUQ+ &OLjj3y:a5 VlHgv]Lmrhε͡˿) $U+F"J!RjˌNN0@cu4Uq+,( ',EN!_FiͪgE_ghMxgo6=䁩hݨwJNE:;+MFVSUVۗT\մ) Em̔@C-W$L>}ĵm|>L򗉱FWH[Uyɮ&\VJeQ[eIl*W"T$/{z…+V9 'P"a.*V/fjFh?juG(fpXmM{9~ Mp$bո4~~2euc<:l%X-9j<>W0k $2VSƇ7HZEH،˗ ī`АQգM SklLc"ڿ";J Sl%u{)RMfdzpUUxVOe*Սg5buղuKZUqr؊RY!Ħ]K؂je 1+؎zu^a;|!N(g*Uk^W$ZX(*9R3ZuTEUB괮fՊMYfɕeo 4/mo  T7ڰԻeUU{z铱z~V*L%_I0 <>UW\=JqJ_R)QVɶ|PVX5HEY,TZl u x9lݡ y7kc`|~8:]u[zվ sR<po {KNu{jM=X]ʽEk-P5c$TUx |D2RêBfΌmnUa`,W ʄh#t#VQ Ћ;cV&?^w9+]Gg5v*ʹ|Ue:DsSVJ\u@ՌJ57Ea3}_N;wcJ'_)2ls jP֘X0V@Ndشb5YB%uj ZU:6j]Ryư X5PŷC- b:I[YcT/};ǎ'd-_2Rf $~G[hUC'^GԱ'&Ĝ&ͤ7ٖT`={kXWq_%ѐ-Kw K P\E̪^xezJ*Eע1VSuuԵi,ԵI6gcxx5B! ;b[VRqWUHQ]}鲪323h B:޿u}FBUwVUi\ujjdjO*}cznմa+Qv"6ŲcBLUTesrHNY ZV< NT؝ߑް3nC'KdJ9zdP Wtw*}ރ=XK3UX;0YX&޼bu+%W\lN X;Ve!֞RHkPS o3Zǟys!%x%ilxU[Q%b$͝HZA 6Y] GU)fƋ52B}> `~@[c2ܝRٛ5b]X[ WZQU_ef}-(cR Pe*%V{rTAp榆s/&ZO*Udo,VϜvaV`UK& |e.V9T B7eXv#X_uU*Z,wCzZtsAUάܽEy< c) -2._) V?4#㎪͇G`!(NY*X鈦Q<#V=\5`h^kadWd.1k O*X-Bmm$T2qK*}Ǔ|ޮoC>bn* V9Oz> jٞ|SrU* dno7)X >{|D26(hA%fj%6Xbu0,UѪ/5U/' vBwߖUPsgQugxˬ~}a% GKAU= @,V#kSK_UIb Rpjچ:wU֋Q @zQij\m?*Ttdmbd7T=^ZӨyꞸ;+TO>yքeVä'nPHW Rc>Re#I+G_uԤLJ5U2n ][\`VŮ $b5WYU'v UHs(ĉU\Ld2QWgZ<_|޴OL\9M;OeKU]jzUBX~JV~`V]B'Q{bʾQbd<g\Mh"Bߊe `[ƣQszLvaa߉Ul_p\u7K//v!g ʾiU65UM_c&J] }K:ie#$@LVQ-1v$ X 1rmb-Y : |NޥTl?n!lȘZ ::R*Q3NK_'jOvXSFXvO*%+TU \J]LVvjΦJ@kڕ(_.*V?AIx|T} $a6;eݙYm\Oo s 0^z5lpjcX,jUΫ|X߶&'V1)!X-`U =j*;OSv/~r\}gIXLjɳ]USj*oӯ& Mj}QjrgZQ*jCJX]Gvʰ &M隖tV4&pT+;cƟnDeOwT8&'T4w0TL__M6=#sތ6lc:_VV:W0;** ܪ^R:>cXuУ=3Rt8Ջ|lՆ:#\.1:tZ퓼bwJu1H_ʌyUmª}bë1#=\$_:nG￿ @XÜ풰MN:?*6tY>_QJ4!Wqw8`_1:x sȶ8O§L YY_ s:e$78`}~H)@j^5pk$ZWϣd'VV: IDATcMϟzq4o@j)O*U}'nmX뛈"]R\:X=#5>MzlyŚGŻzy^RX'dK['1tKPQFfrjGHL_lFB?b6Y_KRXZuUKЍ{"A@'>}lzz 55=5EPP2YK91q~~T{4Gb@ ZwZ=foUեۻb/ҡBCY &S4jU*23eטj+yk8W(P0Lо,K&VV^=\Q`(%Ck1YE#>!2p*4zG7Zz2V{hXZ?L>u\$ղ@XG&OselAz T\m=O.7GG&hHUV:y@P*R=!nX1%7@B-T Sģh@p9tN#_8 +qZ9+fX}4{Uq \&%zUmb9⍫~J ÐDU7믶XUBjX B U~\66똌eHs_)X%g7zjvTNy U,5̨ĞO'!'37`{IW-\N*hVO ٲ%6V"sUMgi*^ժ,F2q`uuS+Z~mI:$1VsbU'p3X3Vi4(JV=ҷVLȌ}!e@Uu}hPVyx %5ؽ[Pri`E2_EXH݈t# !XN*lՏ[T۵TeaHOT*SSAUbT(h2ehU@t5&eVdE J,X d`h;4vZ;4l:HA gL!ՎL.kJ# C"uRk/0{XBO|yy~nlu[ -@_JTJqV=:ppZMrԏNV u` MtWjp wmmma>hkX'P@z!0` VXk^,(W->S t գ?>.a9_ (y5i= WS*"{GYqh)0VX^1_m斾oT,V^:o^d1rժbuUU&juKb.m*"-_ZݷWZ2öQ-ϙ<ɝ*cunO;&RHLXoW4] l31;Xn[Ҳ<"ϰ j2pK, d%d[* ѪV tV XU^ UC4:#˰QV83G{!#Mtj-efF&U٘S7FGgQgZ*䜅߇-$m0X=ISRuU^RU5Du*Ses-Udw XxVb^u*EM-HbUO$gXF˗-',Xa-Q/lsS^0돉XՏR\ߐȩF-שXٚC!Y*?pM$ˮ? UogX{/u֭81Fux WͿ^͓wA.fE0 \*j:UXbOywCriLl#=R(4;Wyƪc/ n)//ע_UwRsOD'$ *y  ƪa*:y q*+U 6h}{ſ+h^ݐ8Yp{XX}>Vj-e/S5ALž5z1[VZ0VGNPKӿe+a;Ujk},~-+իB^Y:;]q5bʝ)rUEo<)1mK 3_ڈrei*W I`F80c3sA<(?Z;vĪv"X巍Zty^2I]ӳN\n+cի'0j. ͲXn7 kQ4Q} P 񲱳ZG  Mֵx*05mv䂨Ϝw 5ݲ8S~!v/ŠLV_ |%+Uy֚Z6X0 Φyq#oX7%;Ն Jn,ZgoT=omm}+\)TvHmC3MX2CZCVyU )u MܚM\*jWR'XYedYX}j`qV+Tѯm?j"eXhD@H Uzz_X a<=| sե]>Vŕ#?V;Xzŧj[M{Y5O{RAłW(E6P^e&ОWHh~􏺑ɬUU^1c54H_Ыg( \RR}[d[~ Vcޫ5NK9<E[](t&2~޳WX@*tXz 46QF/ :<;/q~\uU& VD;t/* P5Uχ V(U[[W^F.TUe+*j p^bW)Rv:ZU>\*!SҠ`3]VidurVjUSY]Ud_"'v1oXH>uI M6q|ݎ`U6،UcZB\tKkbbcRv*$Hi6jA:[ze՛0,9-is|?kҮ/hF?<+ ;xM =ag6WgD̻* JtXMړV(``ϻ'~P͈Y^p(wtfq]UYl9gV>"'|D骵upY&ZgE⍌F_ѩYU4qPel2w_/>38Cݹ\B~>\ 6?VqZe7l'Z2ĪXvY|ټz6Ue  ՝ T-RGٝe*G&e?+#顏.Tضτ~=XW|O7t,c%:@x>U\Q=ҮZzcڲUUub@-ͬfm^_Y#p~53 [s*:zdTE&V7#t a9i"\;\mM\5v#kɂg6P"ᄂuԙfwŪ㪣+4pv7T-d tfe@J6N89dQ\u\ѰT'ﯯvR]A?JQ*S ]"U1{ y:o[i@ E+[ʪ @yҪY-.j?=7=hBԪWX[jqzF;ꮚUOQ5S꣫#ػr%4I2MV"?DU_MLhz}:H~T*U1ŪTwy+FIGǀ)j ԵJs*X5A+]~ %б|1\qَLv\Yro%܃GHai@-Y;lJUT>ƵNbVeUkt?dhAQgؼjG՚ah0& =?G:VX&b@}UKlbTj&Wհn8 t 3dDJ<^QZj\ݟ oM'LbM[Ӽ_xe5a;$Q8kV{UMXUUK5'H\ %kkqS;`~[ShJ]}`DA VTxrDa~t;;2%JoX^4 *44J&,##XqWPu+''uUBV(gZdjK6ϊL/`Nj/auΊ;~I]XX=/VZ=5toDzQz9ZX3 *1aɐC•`!ViV77ks`U#P~:Qs Z8X+'SnE\F"rFX^ K5_SCU3G;_ڪ18~*jz#ŽJWZwtTphb$ ޹Ef6}淒xZQ|Ryv\{U QKYY\PrDN3 0;3r 0n?zoQ"mʛ,i,:l65+I?6l"@;eBaM saPE#Q1V% @9%Ô?Wtb#Vq=+d Z (DPjUJU|% e=X{UVM41.\=C/nZbVbϊq^(U>ZUu|SzRo+*~a'b*QqD:zģ>V CPxxFњO6+m e+Q:>y<`e 6YE"yJ^4VBv9̋?6c!%dw4M>3jc/Ud^Fej);@[4c?UA .t~/MkQ%ekU֧ҭ⥪Hp#+@ױK2auŪ{W_D> IDATbu"UXjRU1=ju(1DVN(Ue!X=V]*6ֆUfhَ݃lgVGlV)P z 㪳SwUu--Jd]{q; lH[uS4~˚5 ˯T-TN|:~Mޘ nϓG L2Y/̭[zZ_Z FfG$*U҈na O|P^ZCq.iٿB2X5q 2{p<(K[\|HTյp}JDRs5PU[Y!k0lٲQӤ@x7_la/_V:0ycR<&T",|m=%)TKX)הƱ}4upER}*:z9߷ը]m v\srMLX9_/VGy IV\9",9@j5[%Xv UVvthfZvx a)UJzeケb81VIY0^cguF&`bRc  !A*I,("g܈(YX2,Ke"y3EؠJ a Y^Ey=o|?%&VSEO%}o0yd$v]ZtXeu jmVCV~=ԗ=WEk#VvD2adb:TEXmdOלX}LA* th бq;Ϊ* Yޔ&>F! +뾾3g sS +b"0 =W^&0-/~}jZK)&?v{d/]KѕR  Bf6r [P*1$5 բփ5o VŪ"6[]:eT2P uiX'!uFץrz^@TX{.*;MdxBWYFGX5. d" gV' .T#U'WѦӪz=U!SgSIJlZe2y(U*^9`کkՍ=qUްjֆUj\-X =踪 \aXU#X}?uT*XV݉C\]rk|;;;1;8XwGnj%PȚeFt2v95,X%nذBp*FBW+|okgɋy^UB^._HpPݵ摑/ySpmB7[ʺgdiR<ࠊ5ljRRI8:˒L|ٲOk3SI@t/aKGBUjF"O-޽X Uoԃ+nZU$R}X}16P#hXam>ժeKY®?IW08PH3jͤRZhu#CEITFZMJeY5{ j,Wٯ`k}9BiH}5TuTe9:-˫RFXO;*Ctt̕|0KaB~/}TvUeuV`eXT:k!V }Ul&Qz:u::<3\&Mr |kC|5ͻ8 gh=U  U<~K Q"U8(!@,)^#WB>F^{Ok]aUĤ yOQZӊ`F\X}U/rZdzID߂2OF`U?`/{_|]yeM=nUQ`xEzb 2>GDo|"Ҫ7 ds@V؈U JY]?3U([1[{]* T YPu^dl[˴aUzU`j#!Mä'C#j5@%ac*4T޾t}*XqjHyUj[t+bauiU411 J|,p☨6R6-an1̛.JH>V~`eCd Jy:՗lXZgXefuCVU*VVejj X1VnanEȖuVb7nQ{==q+XT%ݗ B5ZZVZm/&U?WU>c8CXJJ㨺IW8z/GI&W@3ۿda-*Άl"N jy_[%V:@%L WQ}y`u['~hI7sj 1.*ma~ۮ⣣B>2~قauU9G9rO ڝZďj,t"rUTU{UUI{ۂL$oHئUN%5w0>~3b h>VbU+d~XR(% AJ?[m6n!v&*T%Wu"XB(^{Hƞꠉ_R 1V{Zq|ǻ7"*> _~ 1VrXx{*V:vlαPD갺ُ?>1hf.qRdhWpa5s/TXUXiXVksXZ ^$+3@po! l2n,걊Ǧjj C KeT+HUG!3[:qQf=LeE {n 6G5d\jP*H p pj4sUUqQWc.LͻjVrUR6PF1IK4(U["Nt%8zE4oXeTٴX`V$OWJ|QW r*H 23iG'[[>/bбY'M*|JZ,"mY >5> ) 㾐='Zmhf] Qn^qUUc {; Ujށu1!B4ZNgE`Kjpٳ~QyX]]oZ%Ue0Ub5pM坛GN~ů>=t(gW=W~lЁuFf"WELkpV.1ZGV6^^0` cbC,hR#U&:x\8 VJ&a_jKEȽ"V7X;]ZZ{o%7rxde3שU߳ a2~񊚷UXQts" KV~X*X/a-`\Tr Ⱥ1ۇ-‚4-U߻|sqlPX'GC\wR~}jzսwM u*e r=zԪe2)iR xVYHf^J5B8I@  }RVV-Vmv-oxYV?\5ƪsaΘ6m*;~ٯ>Y4b*Q%~>(jʨDAҴ8< 4+QH[HbiBXe͐E4jr2UUInA\ r9Ϗ~qi[|F 7j&E|߭cw_V(]ӬOmT\LYj@q΄vvգ/DN!up?ͭVZ]>9> W|E{w'&nNzdZL|bX%Zoc wNll /4j\^t X-9UjcUNX%Dd5goރkפ/;=FEz`CNrU.UU4aXͮx( &P{_q`*(5Tv:*3CW7vWNt߃UB$q7EW':cENSG)rj9 )Ri6Vݭ>o8H$&4Ǜ^H(qH$U>}r_&UC=B~;*NښUtfdzJUYd*v ^** p$P[X!jŢ@ 3NGsc}uXuTu5;jqX]Vp%U« >;qݓF^6+Y|.:F XMPLI-ڲ_A %BX 2m֧,p7joVZU=xwK~Qg˽4%hp\.iF(_ƩSf׳f* k|u/dU850r z=UDǚVI*FBULVoJLA # Vޅݨ3F/GիM RLqZZu$з5knK;ZUgKKwbm8> V!W/]r*M/Wi< E8*Up Zͦց kP21=lV\XX# @2V[}e%ድ޼ysNov|^/:#`ud S%).'?+ɑ}MV D,O;3b/(Vk!7XJ\ ~ΞͮoR A{k_1>`U29 =90Kx*itcɋ&Z g*Z4hU7WUQnPWCZ^&EKT4L_> "VW#UPU(: !*cuMvyKU b`uXj\b3lάB/{N{b5pUXE@&?x*UHlUg̷u[Bj08X`%Ii!"Ro%PmiiYp\={Υ+vw4JJ_U{<|O%\Tժkz1ϾO Xawֱ̡*}Փ-ᄦ!@{bW5G!Wcz2|@K{5|mYv?*=jU?PP!੘5;+rO 䪟/h'CFR?14gFӍh `,0]f#GVHZ-UjW|HŸYY{rezj?% =C5]0Ѹ*NU Fre2KTE6]VdGWh)::U7܋?29z;UPʾƪvjGOc<y/V^ciST=\SoV@XWOՇMjL)X]hYp'[GgnM9[ YXjj4{,QO8qT==SqgF]'+oxjKEL5VOt[e\*FZ TjJi ~~X%u 6JgD`z*TJI`SJ@[ d֌!~y$Fa^+~ymm\R0T p{`UjHT2hYc!V)B.Hh՝d4a r$|XJ Xս\jKL~Y @d--,s;E*lNqUg`U$XS] MNvT *P"^nͫlECՑQ2#d󃀫c-Vͷm6 hV)B* Zޫ1BdWTʄO|TH8\Ո N{YrL{'TFd@x1X=@OlYX v5IS^IV22t4vwƯsjKk&VB3x3ˢ@# r ִQ)FIҒ"cl (EaU"@!! M.f!mVs}}eOI[T4կs&_WeOhht:O]*Xՙ68a~k IDAT5M(7LlٓxT sPn V X0@5UᡡWk/haRn7DH\&} Fh[D`.$ 1VC^ObCtҋ= 8NZq0Ī*FCUՎW TEZZS| ՊWCOMwMO 3赴X0Jwf4+K&`Uթ?]N,]3ܢrUY2Hf{/S5SUj2CѪ$Y{Rdwnf\i8V\/zկ`uh$?UEuƽ zLEoUDn+7q@GRbծ lW.]G2~gҩMUZr(W 'W7R9Nfȋ 7Njn\u/=0 `yi^y< Uv iwyi4RN#6+CajVC!^*)֪]k74  Xi9!^h6z͔_jE}ȏ,yo`Hm}LġDNowz'voVR,{mYm" і\w {.ᯬ>ʐ3`ޗU VJo[Ů;*j*VGsURaEB`7G%ZĪd:?"G)V9gԒqu pwY]D~ X&Mtҥ&fV 1ui,YA:X:իTr%Vjvl(Ê< X/udmުt bϏQ>TŮ5տ"}j" á7^֫`߯jΦyPԍT79ӰE,Ӟ ͳ#V1!=qv"0V{'OiUFU9m,N~99aVOuq$r\Efi#8ss9kVv #_O /5m7;*ּAտ2?}?l{/2lm(RjR"جcLnnOscFm}Bu5\Kڇ,VZ~^ewQmj66/*4j`kPwPŢ=>˟w?mꜾ~W-.H+a(Ԕ O8J̕*OTӾZ&U/{)V.uIk^<>/E5vϗOժonxHr&zmv7nccdWI;f՟8GUT*U5_Eê>"D GmkD,̸hߌ4̧NXד傕Vo䖪cRUV?rsE{Fu{hQNmeY%]E~M)'mA\vT]r\ѭ٥UW,\Jk8RU?ګD6Tp&GBU" XJtVpľ؛ב]Umy95<ɑSu`uhӡco)%QJL֦H!x;f|S!-Ve$oa*%Zi`5f7x&dʛF%xrҟL9ܨH~QM`FV m,GŪG KKfOtzZr!{xnw={ި`OeVS'h(M.KiHЕwkE^@^@3m,ۮj] pz%Woꦷ=uYYZJz=`g cbaU+BN굋_jΰVji]UViIVTjF72/[m$V?cϊUveu϶Vij'$:RRP5=vPZg/ˮVR ŏKñ*k>Ox'cLzm4UWUL=9m5.w[5~qjV_="[%;i9U^gVT<t1#,[3Ez2ey2h0Mr) *<%xfPJ@X`A }jsWf.p%+oSQ va^ͅF J~KeMd %\Zz.En/z4C:m^vaP;'jH1dȹޑ8*RGQ.t-sD-TWV~VdЌN;F4*[JrOFWb_*d-ꭈVX5ʨ*%T}{8ZT:8-9`:juc& pOUyvj:%[r/ V \ CbXoGAZĪk&пy%תǝj-wPVh+쐷Qg[@+r}*z _x7;2CXuT瑢p}r:ժ@UՔ+CS|U*r5d1r:17ݤaݡ:A1⥗L^gyf{^ۘx01۟ŨZU|9 v$èjOmo2 /DPusժboah 9xjX8Hq&:^2!b =0(X篟AmZo"7|bvlֆjn\ UxF: 'NN ۄoUQT%9.Bg=DVHGUk|)x#4x 2H!5O Mׅ,JU#/搛C-ig ͠EH@Շ,xv#WVG}fal6 Hjdf0MdD&s BED] Ur!U$^be.zQ^sc̏Ϝ9y1KR X+njƽ9TX{_oo+Elv%Q0V_a$`]_zS&Dj]ҨX5&T_xvli{Uv8jӪ቉A=`+P?%*;[6ꚹuZUգ^cb5j[1B_XDMի̔X ߷&ըZ'V*jUU;ܨf -j:w_mՏY TKbUW#%5FԙxYЈlk^]\рU0ba:w7iŰꠊZ-~+W\gV}-r)[[;#w'e*}*+o ??%,/L438)XH/bTrOZ{_(&<%T,fn _6m e}\h{vthK|S\e 0'D<_)^U-qlao-}vǢ2DfuenˈAADlPѥ U7!Q+;Q*V-Z¨^_zjawUk (Rj[U*U sLug/_U.{)*~R{U=6%R.w^SoMk-B 2>Jxw"1r.bC\K;B,>,Y^VFrrv~,uǕ#:-@ ylq=F ,=d!C\rUծ0qitp['%5х&<x 649.[*Dь`}WsWFO_cxsBZuOLxd+bbF5`y%VίM9l(a7쳳WZUVOYQw G19}ӯ<ZNѯ+j[DYRzF^]Jsw t ׵he2]-_fX2_TddZ'>Tf/Kiw3R{|FXt*ѹ8J pzXݭXqSm-i j݉M:sL:g&KanXs4ƪz1#+٨ʋ *{{ [5)խʤJ_ՍZ\_[nfiG%ț]rbv9U.ݍUU0b+یiL$7\buQC ' Z߀L$VkJbVZfhHr̹9WIRZjsqsr sm#+[aJ[:w:᫙'^>*;'gvZfA°)yb Z2. uζ%x5ѡ64 ƛhAUJWE鿷k=V޷ոr )Y+ Bӄ_?^Dnz*C T9VC"paUr`#-@6{mEUtĪ{l2PU/:NeNǪ,-ƪ֪U:gWUaU6`IS]}T۰A, WTf梹4j$5&UUU-U/*.wckz!]C- cdz=djucawK`] ]S`{.8TX<˿hQl1 X7v\y &^J"2 ;ev֥EYOb5*J4.=+RU 跦 ` ބ=Ǵ*mlrgIƌEYw}c[|]ת*_,> Yc=lj iZKUnrUZ-[gU鉷AӟWP-WY)V߸ߋHO7bQ5Z6NV\E`Z@]un+WE%WRcj7ŏDR K~ƒ1 hxk%]_Zhͭ'qZ`Z-&@ʜjh^EVnHnw\=z'55QdzZk >N T`LV^~?{ !r-q(#+T1V#i[e*P͛|%f倪/NKD,֞:óuZNg$dRZ*|)/{aqA#AGW7gņ,Zb]3 Á{Z\.[\QG/L}h 3 3YRAmm$kNX=5W[xr `U tR59EJ˒U,W#]#_Xّ$[LJ\汨_5XzYY\6g+>EǓXd"#F2PJuRҦ* U`i7f Nc/vAT CiyWHbǪ VK&jjCXE Q48IUԪ VoGWeSUW" : E0V ehNgt5_I4bh13VYp&0VL!\ˠ*F"VVGGgz!=/ʚ<$)Ҍ\ *nJn65UܯXˁjTr#%+X7QnI`9fuw7Y d5[M&O\Mh7㛘*T=`*XHЏXakuruG@6(eaK;[鈪ОRvB50f ,ι"VV J[>ULX#>UbOd,Ig+J nT;wpV(|/K'hhCN_Ԕl&leVƀh8KZqU5!LqYvbvzh˪ZY[%qxE{ź!2U8 SwErxxA*4Ŧiw]I#Q`LZ5^is^7FJ]q38v#n(!kiezˀUUȀWaDg ?/~EȈ\]1bT- vy%z̲(ADЊAh)gHM!S]CA{ )Z@ᅒJ@H"5^xB=1 sZ{s^|}^{̈́ oYnő(`U#,9 DЦZoyɏCX}oX2(Houa^ axuc{c[X[_?gÑWUpPhvBvb+S;Hid%:?5ԏ?Sc4jP~ 6V /F=Ɨ6[w1XE-{bj s{QdX-:/+;{3*V)6 fBݽ*\FcTmbB4\W` WR^Y[nBժD/]zc^lYY7ݹ9j+Nl ZN'8XMa`^J'4Ռ氽J{^ŗOI~X!u5# WН3ܣ/zcfk(Q%lh#_5 Zh%α#{p+UZrˊ$W &}VDW =ocV{V2\ 0G_PNi4yHYaE{q\wH5u[+ IDATuʑJ-IUê7 ZJU \)Y $VXC꧰z7nןo q_G)NQJ[}\fǘN^MXJ/UZ;Yn 9fqX>Y)g]mwV V{Oj&g-;ѫUi' 1ixٛ7x|P"X:w%N &FHi/cSpSd<|`4jUXKNRjV\tY7 =0@NUVK%*j"bUYWU 4QZΰjǶeX%WQ{:99UUzVjQpZ~=;ަiK 5%UUCEݦӮt3'V"M9̖"XObUnބE$lr_Eܚؔ3:WCevi )!} q䁑~CUn h4չѵ'oQZKZEvö7J55Ҹt(ߚX]_Foj:k\nGO"ߙI!"VGVR=W U>7kIAk:]. EuR@khYVEZԂ"=&X[B6@ ![j>e f9.UVV:jzrjAǙz 0N57g.մm纮j fǩX g1ޏe"`;b5[dx`АnZ}[Vf$V"DbU;]6BY:1 Uw?nruڪFOŕYTgT$Qy ,? >=m?3iMpb5_^bHKdjXmLmZ%XܣaŦy寕kx}ODX{x}\e2)$,:})xmgRjuU^nW}UV_ҭʬ`hrK)ȹ;W][ne P T\u`P*#ϟ~T*`SUhcNk zK NV=^{tWqorD]c簊Bپ96DXE7ހxyjZGĪfz7x=m]29?x\KX}XﰊB麺/~sƭkg5U+եd_B)P8u҉32s`?余:Y+VӅ8kX/EX=UX ^Ӑ2U2!1s5)WCUP ktK?p4^a*CC<%R 1ᙙ=R^/mrᄎ.{{Ŋ#m}FUWgđj>m#sX{ڶkEtTs"D q^7jA}ΟjeT .jlp=+boc_̂]fKSԊmiQn _lrJU`ϧtժ,iUp\1S VsbT\B,MVMzV%;oKS{VY5noTk&txϘ}p12BUD>VWR_UMSpc\uuV7ۮglDը;P[U0?x*efDBT⮹KyZ;\~܇a5')yoB[S VU*;Q0l2Z,NH4@*P]t02*`p$|sT2!Ɦ/PrjHq&Lp;슞t-lk%KkGDW75A㱏#nlgVe/UVOuj)^+!kcd8gtDTVEu`8j*Xݷl%j1*ZcKڹcTMVJZW"ba [\5M к$A]ѓ$Ւld\U5Zkj׶PSb?K;.TGN'cC>jˣGϞ544i0TfꥦwaUG݉ X5˺ZWC7a* ō5#W=bw(Vgj܊\JX#k媪O8C|+ZY $G:råw8|9KYy^V4] \UߦBum0@:C/z[>*T%2Q ꔔl0XuT3 XEZuCՙ2eU ЫV|8nbQЈ"NDdXda-{g-Lc60t:U> ի7j-ߺqRtqϡZCUlk'~I#XU+|O*\}Z-XTuf@_ VfEzrWXgssME*HK2b[ZUc5$;YDV|Bf: UG.yzOo٭PsowTjuU[ҥk(V1T򜓩!+$1OC`4qK!T])[Q3iVW7UnŪ{0%RZW:RW7U,GrP#I~٬i5Cq*=V*Zq qԩ)_TwopP^x ߏ7YGa|ެiA8թX%U/:s\U]`5-XTNh! VB˫Q~1o]ٗSV.?V{@L {vP"V߅զ;*XC+&׸j:V[賭笐\Uev[:.6,S6t"99amEս%͢](V&$.{n˴Ѹd]ή.٪a\MJ U ?[o5u(b䑺Jم*XoOx9礆  J$GJժ&j*4AjIVȌeδ g`JsUD6Q*EA9*`uw4yk2T&ΊubJ&p72 ۦTz+UąלZTQ7]5oK?ެ8Bc5첹8N!?nP@oW\5 >#uvMd~k[r/ VOE*T S͙X5.[-cp V1NB,8sYӱjd=i PI]]ǒ{%4?VQX%V]nyc*djTSz6Ū4_QZVQcX([0'39uGbeP2thu2V9b3˹f}vh'fI\Ű=9h.;Iؖںs--LʅWcwhqHUqJ fVʂ kj`3d`ll,kr_Ѿ d"UY¯B"0C# >I0RjU9\<26 x7!ԿC}rC]@帪:վ7\ݵovp+* R)buԼLl|պ4j`:&T#StA~ NtլZyZ#XR'Փ*J-IUj%Ru,٩X\Օ^U{Ij`yK[ Ve2!-u~4~}_UEsGL*,XjՑàtЖqb90c%5TVA%sǟw:Y)TUX;&쬛s=ӓM-N6EkĪoVd>aMp-s-.Άq??ՐIMq\ŌO>#ZUus~q0["VޘX-VdZooyO$uZU iXK}x g*E_ )URXEe`tfռh:k7q0~q6v9[9:(Rh}YZ\lԍzwd[)8qoIp),/(Vߊ2cj^?2?: X9ۯr*qᩭdQxG/ZAB]"Pۏ*]$G_.+V}X/cA^F-TθP V%:bܷKDXpEz{Dk WەVvp*!J$U V!kaXF--ƥs%%U--?C^€sUv`Uմzg;n;I!X:DzrS rUUɿbkY^.^)*|d~Z ku_CHam[cvQQ\) PiD(V̯K]1z5R% Udr@0 T`@n :nj$XefՐ5tQ#r3eJfZʦA! pe{i2*g 5(T߀rbA?Qr_`XWvvN*{{\k6NqPW鳢Y+Ūqz,NT**g8;bҎ\%J*] 8嬪tZ悢jfAh}a^+vo^mB^f'`o>3m-#OաlïsöGci1V33~ ±+??z@ݡ<۞dأ|nVUSQ`':[/&(UD6Pm V69_XJcwnGjT譲xܰT9.W#nXS{SFx.[yxhִu7˽̊r_a3eL i+_Ԣϸ$kAePUY$, 64D6Ѐazs[[殸 "O?=y#ĚE%,G֣Uj8vƫ2ɫ $SRnL}K)i UZT@ Vuܥ35?Xj.cd d[S2{V*+y脫&O˚oN+zS:\ H@ar&b\vto},V@.1w8~AVʗ6j+)1X}UWU/|.Le1&HUje/\%Jq|I9Z_/'(%ӝr~`S6^*"cYY-5Vjͭ}\j& jnYU5@9+!fȉ2 P}-ðfآxʻ5 ET\ZUVX]u7f\2zQuU>m,;vͳNĨ?3t^:Vo^̦gV#UX5Ugݐ2\LSڳf=LLտSX=X准V X="-Y;fqTՖjw~Ax*?b]jb. )[JF)s$(<*~} tۣKfԱ%9T6du$u)heR)'j _gllskZ>OCleDbhsW5F5hUULC>#@qNpwۙZPcUZUULyzRudz((l8[j^ ^٪Vi_DzW55QV IDAT UU{*a3bUshZ?h%`Ș{AVڰjS8E~3ҏkVR__\2O"4*n]4q?9ǿN PU* =AhUNPmyR0d+R#ܹn3;<>eTHY%>OZ,*cS\H* tpZjXr5 [V#EyQ[>JUS̰?W8zSIƫlT}!f&cJ.vKZ}QxV{(audڳhS{fiKVXõ=/'ʜPu3 <>o\!k2po4d2s>Ң:o 7=23WkMH՘;♂A~8/,*%;v,ܫ0]f[ -,s';`N4F|ĆաNP4j 1D?j+\ӑ:UX-IxaVsRj]A5;=Љ:*i#4U]Ե?1n]^ͬϼZX!TT)Wʗd+V5F UY1g6yJh\ km[#x2,1y2z '*s=bOj+R-+S]Z PͫUTa%˦SwwQ3ص,k-͉QO9`v7ߴQUVj8Y}sꎭϬI ]LZ5S].+ϊ }zVjR`m*jEI R߻ǪD`_/ ($:LٴoQ*TK:-Jt|jp$'FIVW-@bR}J`l7+LI.Lnβ |.\Rvu(U}r=;b2gikm`k5y5ñJԖ15?TY7b+U$Yu]eCzUmQP5vFqE*4XVOX "pU-X NU-D-T`~#OZjգGZ!}Uzmek6QYTNd@/X}Qu[SX*ؕp'jp0!i쁫(xض2b+ U-JzDܾ\M R <ԬJZnjUJ7}\qT]|DtSHip9+ MܨVo Yð{7nss&V`Aؕiv/3͈UY}D=T b-HkJ6r)UVޕ.30SsGԚGc`ky2K8 :F5N4jITIH@AJ'$5`JBH M40 A&[7MVn]}a@?T=8ǘ: NO/s [TZV=\XݷnU5v7ISfh]Sfl%WV@j|kJլGFTdלkXM \ETj$O\/jR O9EX:P<ֲ̰]!N+XOa oTm`mM|ϸӈrY2ʌ_la4Iy1F}}+V@5RU͂ttOʁ'8Z9.ު~o1OX=Ĵ.~/M[\>WΑ.W%ء!hZ7|ny5j/ V'bb5r(=QUw퉪-$VYء޶Pq+*X}Y@UA] VgVбjru;b(mVSuV-j,%Z]Z}ju5^IA`uuP=ekY Ȱ z۩Zk 5Z,:VQ5=~Z*V`5Zי9 ԈU38X \@#֓U2@(tY4,V\K _a5꣰]ʰj\e*Vs曩 zi%^;{kA޸k+B9F'$#VJ"Ehx'[î}8kPhVِ*8kF3xIW N zT4>(Ij1x.N aւ64}ũ<HƏNj#;REpMRA)/-eE*{.eލ?`RjrЊ'TU|||J>ف MGV:0Ө7Y kvbtU9e;f3&YM{9h*XZ4XWc9f7QJrlu~ V kWKKj~WA\%R=)pکz;*3\2f&VgUEFquzy D || MYxdJĪW"EY# Wj^Vªwl5e 1ﯘ+U7u4*2* g'C#OIuծ WX4Ê6:U/\;{F}&)ڇ2hQCq'rEj-C7U(+Zv2Sذb `K\XureMx>h7TdU[>->xʟung+^1UfҥO6ckj֨r!zH|HX9 P3':j$18 H:<:l+ |U1'˱ʧhBaT䇕<_#WRh*xI6oK0w` 5x}s4a)5ܲ:+u%F}t RUZN2t\.2uIwmn1`uc?-hU pM#9 xʧREkI`v?[Eo$&>]OUש$Vg>o?H ݦVR`zy:VUh0U*WǭL"V-̺j m+Vj[dSO*:WY̥m.MdݓaًO.{>p%u[Zz{zzÃ7sAxrfX{h\U.Cƒ"VtƒkT}JMRUIhW.E -`/8{Af18z8)X7œ<#)~T#anhӗX,1id4-`M7RC>&UU_)XZffcۉUU\VcLjU~f}J^݋>R{BC酆F, \fkX=9qkWנ*b ,b JjC&a`e%ec 8zU<<]˩ՃDjlbEx. 6OkTim(oAaZaTj*1|]IuX _(4W6Y9d *>҂/xĪU՜Z3=NW˸1[VZ<᪮^ǣǭ@ VL/\i:KU-s^p !Vk+t*@Ы}@h;4mb^']zF 4!ǐ \±ִ Ud/,)VYPʹ;).([n ŢaU@5M ;x^Ŕۅ[n}krr.' +V~5{l OK\]{aGgl]jW_džEA m6H:'WU!" 8Oœ?EnU쥒fi]ne#V^UoJ駵cj`ZM#Sf&%Ӫ;;*jlZej*yC=A/d,zhz46rʜQqVSz3044 C6Ym5jt9ʮXȰz+UU8X›)WYH+{6s3qX^Xtɨ*~] ֵr>UۭUjՆ_cZ\Wf#1\ㅂ2F+ψlfog/n.=O)V5x){+äUUw+qFƕf_[n=i3?ðќGR `X0h:ޣ(V}g9"X-{Vy f<0* ĩUVЦp;J/k0S^]uZc@MX^ئDTn~Ћ{o)J]C3%WsZW5IVꥇV V^֘ bu)<%a5&Gt̰z/)+oPSɔO VTGkV3U/շ)^f%CW>ߖP,0xkЫ hz9 v%\a5|7gX* Jd?ЕSQRK%WU~{ИsƸ[ZJ~junnF!]1] \970ck<@iWUoX]6Ts~hڒdL{U3*V oÁ?`71e5Xsc p J8(VV4@UYjmYBKi֦4 :\ĉIYg76LZTG Ql!M\#7ZA\fUBo‹^ +<fv~%M;=ysꚾFUIY'fDV)t󰚊ɭ>nM4V*"_>7! *%g /^8s,ۅI.aQi( iմta8.Kgkp}Ra*x<ݙ _ݨЌ-Z +~E&]%]-L'%QwV-#CMc#HEݣ2~:wdW-";YBc5I\_4mFj#N+r/}w{9r: _Lk1N , ^3fW~W⎵7 fU&_Xմ+UHWŜ4˕;r?ZC5!a-څ) 132 4bTq[wS9nձUmU-raUV# TB!U8WKKAdUp'^[]#[$[]V#ªZr=xt/'~%8CI&P[)M^dQU (;q!6 ji gޓcϞtXrzvTc+oҭE/<'̗j.b1ZISՕ)Vi!@2L"yD¦V&q(ڲ^2 \:;VVW}uȳcF$@WT:8Ilu=ݯu5DNѹY:{fG g""+V9TUG|pvHLZVք(*f*ba]U8Ua8*""̏*Xѷe)MgƋPڷ?WK_̬>ryXýdy@U09))RQkNWhXoUON+FzSjLV*ʄ|5*W?ʡڪGb΢MliVŀiEUc#'D^=yW.+#ɚΆ})eDd'fЧ#:;N̉D1r XEЅJRf瑭Xnlz(. h#̀=~1z~}$B~j^B3߰$mp \eȐ4NLe:QZw9>$5D2{eG(.,삮VV,n_UFY1C,z"i\Bª4r )/&zߘmdT[iTi lƞZ5UDW0$W<&V]1[M(fUz`|b\tqX ۷o_Dh)kXe1>j6֫W>X9ҸODZr5 /kjڏT;|oc0q{QZ@oUUJU6)MbSv_極 t N \?uhH]VhwlyU ݫ`U<޽oXmb(z/Z RŪ<z~8-&VjU<9U*Ugy/vwxg^Crjd >t:htRXUy?^22R6VD$B|ь*WUt\ubVddڶj ju+w7qTY-hՏ7wՅɇtve>ªdUl`9Zr-'Z^_4xk.-ɪ6&ݵ%wD?)sVŰgqUR؈Ztu鶥2/l0)_VF.pVӱJO.]|JCժb t檶8tbB 6A\MukoT̚v*LW_! u%ahi. 3lJ`]FVUdjzPRٻxSp asSғX!xԏ Njv/rRqW\Y ܪZv낊U[[UUiI0a Xt6)Vpe2\)V+VUD]\vOǪ0՝7j27U?V΅MMR._ڋ;&hWU]}†0kX5@G2j-b ӦB?fRd']f=+1pm%`΢fVPύ&1N넰G=@V#Cګ!Y:_b_};9N0 *z,3O%H4/R5UE 4͊Jtd}f%{{82sV5EPрDsȾ:ʍW }=)i YW*ҳLfuenl'b*}mQ׸x*W `}1U?bտ+VYb`ej94JM?f4ZݟW}W-ĚXݟb8xGy?PZ׀&qِV3y⥳-Z_dz*۪k״Z)Uh([3Tw)auO*E'bc1SݴZ(;Xq %0\*0&_j3{M ;j*v"Pl{#VkhPYYPf1` R3OK"Vf+tR*)T{rav7Wr[\FꡱhX#<yTrU@HS RŪ/uٛk/WXx)9mʅiܴȰ@L)U,|<4aj1VUVC[,YvچGjm D ɲ'ڔ<#\5Ts+cKpk)WQ&U*V ~P I׸.)W1+T}ZU.--@vdU$*UR AՌ9Y WW=t/ss>UVvZ&X tVgUk Jc-yXEWzW(W<뷤\NV-֭33՝c'׵%u]f-O@lS*6ar{k\HE3 X=#~ϋXul`Xqoԁ0z~VaV-ŰNGk 1pQX:OyOe LZQw< {Tei_V(]}1Utk'j↡ӓ' WaIR0 8Կ D I\B]p½l794ن9y_;FtXj'VaN zPed]dNF > j5z $ioӳ3uMX_cb:]oO1Z~9O~g_]h0ziRSy{Ka":3u\pU+[YX{{PZbM8ma{١)gB ԙ_vUk8\܍qg=׬jB6:?OV{V/UBW{㉕*Gf{h6)V} rGTV*z /FpRuPuc\aVE~-^A֨|yw2NűܓZu}=R(r2'AfwB`[U]wͬP`U+E\D2tIͅWiLo TW s-#vu.*7VcZmf&; |M뮊# b(֪ &V-@UW^Pe+h1,jJn CQk5$8Z oWTE¤-H71jDZq %ƇVZ[mB*X1H*ݡKq%=q7*ksꮺ{zUBUW8Ŀ::NJ@UjjuU)1}t?w zbZEu9" eXL2H0jRrؕ`E !0e@sv`DEJ jA$=Ui9Va)s럥<'Qzyg]&*3VW:Xݫh{'l2ωc65)m+^| x%+Qb2A**1+U##ژX[bC`#WxhiYj]Uuلmњy=$6VKַaE5U3V1*Ykm{*@MTh ګ`O~j:aaL [T nU&j\J )P;SEoKű6wǹ}Kq4jb` $V+ւJH H`9޸ݓz]z obYQQ]faܲnĩBt[ݱl)BEf)̖d6pAӐ 4T^n-Ҥ^&&b/S}} H)f>s9yJS>ajصQ;XVǪ:_ZVViP}'™7XrY>buX%bAP8Oa-=_GX0TkSv2rCuV *v菿v[FN*>x/3IW^-j%}sTqheX— @W|ôJhDa)[v>MRV=D]үűJmnUU1lj. VGV՜0 zUϟTo;)NP GU]3:B(ɧ/]'pd+`^uժ*}Z>. JUkaXU/p*@U`ԤfkJ^V[]eP%X!Vmg L6#8-ǼqQu$re;V1`u a3LOPFG`*7O7+,WQ_K6lv[ZfUk)Jo +߯kHղ^<> ZʝpIݩ{<$UkhrB,*\}J'gTYh'e|}hlkAhjV\a**VS0CZ͸fg ^=YF2U9⩥ZwOSYߧF`L*@Vwl [oUx;77W9g\*MXIx{)jQ%QO3v*yU5W%/ *UUo^_Nuҝj=br'sZu6cm32G@^!=,_VfX?ARuԆU"UjW2z.6qb5jޚEV/"U cE`gTZQNƠtv?}tGFXVՉsJPcYRojw[*i{`U:t0 EAg#5ffW :p14lj(br99C~>G# n|,%?t  _|W1dC7VTYژt':Twqka®NxH==JEpo4W#UUyë2MJ';?.\ 6'\$4[Sjc+t[5La@Dէr]g$' Vc>De\[?$m5b1,pa:U pxhNzkQ%D5_Xd_b'FX+qqFDŽ*j=U@O= XXKɢUVyX5Z5gH_*jê6^fE?nsD: +GFFNPy@w2z+Uh@{o0{ѱZm ~%agz*+4"tj?=G)T՚wEQ57eo^:f#mX7.+&OǛ $7 W[˩.<݌NTW}JF<[LUm_X O^T$T@҃\c^//wu.5 (T/jSU+?jGs%SE*UVMV;jAg"H0rG|+e)湋)a݄yz|j juZ![M]fj*9qW} BӭB: tXjn\RXtUM䙕<pkU_ð=7ϠF)DU|V.X=#S$: \M&-ആU| _B*!X6~qrT9~ܕŴNN&7|A4pPv.Ww\\V}YSJ pڅSn-;P65NrJ܊ڮM'lFUMk,p}dUV X]mV3'TlP5o2WуO Wi kc7ȧCX}1t*ZHV/4*@3PRW:C"ɰU=:Su+djL3(yj vh-G_s#jj3VX,NqnUdx^Lmgg ⴺ#%UR{Tfs ݾ\'avXm(J*TZ؉ VŪp|*jy)ÒO_V史L8[_K̹yJg |"] e'oPUSl<]S*W3ч@-ċթp~&ܲzUQ@@ثVU˝JVX;3.mD^SV6^ym44_+ 0'k`X6[gC1+MX ^X] _DJ MSauԅUՅFDr**@*NC-Y\ĬGl`;pژD2hN& X=0W?b&L(Y?Nҵa?"rBF=Vtz5]f[OE IDAT'VY~fW>rUTu 7"Gk5r:J\>玍ː`+~WlKWkY8*- SMګlE.\ c*}añz׈،vZK Wꖰz;jU)42zu%c]ǞWI_mc*kәð֩bX=sis.Pa5J?_?@:~86!ӈUjcǎ8&#VYex ֭sνwɓ'IjN,g0\@i 0>pGzY^s-*i;' Cr[qȐ0:<ޕ妼kª6X&hyv4;'j߃G$D`+ʶU5`(V*+V4.lRΌA5TKi=F+4IU>!'Vzj]ZκO&JŽ`2_FUyIk <<;ҢT(9,46EWlJ8]%0zuUH?}PYV]`j.r2;lk%,}U m%kױ^@u_sV/WՃXxO#Vp}VaHUV]ȰeTp5b%B-Q 'zC)IqP}֭[=J8r70ig|A)_rȺP ;nMpakӳ#}t%VOF}Q EJK]kI2 D f5tJ2Fm2HB)k$!`C.d%d% RBkJ_9y[vQ[hy䅫咍SzHJqҪlm YuFUK{V1v-ݸ[ 5ӓ97lj`U,FZwH;TVe Ս&rr4O Wo޼sz8=|Ӟ㟉yW]sX`Dl ZVwE^eXԱ_/>vz0Zf,D^jVDt)t%V5xD#Bk/h]qs=}zXC>/_B3j UʰZq猍z čNؕT>3X(We bn1+?:Y̤S<;V710O y +T,KVըx(XV&.ݪHբbUj8bL+GKa<4Tra\GWB @*LlP.0D8U`;ccj gX%Ak 'XJ.gdVse][P{0crUe"U"`ìjTwNCgKTRUQ_jNZ%UO2ߤuߩ|G<ю8KVÇ< T0O*PU۷o XPw~ޑ?r! 1xmKƶ8+Ր>I)D_oVYi"bJ k4nd(:h3oCƓ2@@j_lm~lS|ӓ0/ ?pn#["9*bˠU{jM3.[ج@=``?bUrI]ʪR#0J{j0*V*f;=;l)UQjiZśȮ8ZF:pXE5X9Sâ,ds!&u)5f;3$rb㤫S ?X jՊb @:472~mH7" dꜥ@1 h=e,ZqQOY~{r2UO.g«jX5T˧^j)hMTӯX5RZzAOLGcɠ$}0i[()V9@ 8^a ۶P"W!ja Nh͗jӣ,}c#>DG5w}pXhlM'Oȱ9r2:sl^)V-b= b~o]v-X ب@r18!nXUq*`]ɩhmGQ+@rm~S!ta`S[m%K=J6L4+:T nyM "2a*ڋ;q%."vB鐽[33))2* \,LMM*V'fQsd 젦ݸePU]tu^W$fZoC%jDu\u*Ȯ>]JjXMVS^.XexsըVGS>UWWVQ^}~-xCjLiĔ WqktbHUpDF$LoDwvj>xހH T:,{f/+E*a$& yQjFMUw-;&+H n#.z2FX5-:M^tIW]V7 ++ი |><#]^Tg#4˴]!;|׭##0 Z=QK'U !IzzU5?-CZ0NN{v*~k6ibXq*^2:eVzY\װi+9 S+./0$G}yM 3]9ׂ1hժ!Ī5*vs *cX=|MϤ6m:>ULiUNL4*<:Ϸ{ՋVAzhjXC2ljEGa?~-.ɌW߰o%g"V=}juT+ *TeOg1N+> ˗Wo޼]jիow^UTh:V}m͊ AautԇE6$%$4P/X-,q*j hV*ZrKA(n Ur\(g=buv6CjpWKi6^ƎYM@Wb`\q!om X=R"V9AbΨզRb(-SNuc8Nz,x^S,̴= Q̣5E *Nf Aw@^%zfXSC <C$؉ IK TgHY2 VgLժ/L659m߲ JXS5I6>DBU|EVUd{ [TTwN cqͷ%-Zs._zUUU dEGX3j$>.ml]@ĐaU jPX2@!ղՃ5k9SJc<ͿrˍF|%~#7}a*ێU< jAro8.]l62F#VA:6vFgW3 VWr:>:|Bl@VuEuUfD+bojwԹށj땼d@V UCj%IZp`UvmaPkeETK;yj 1U/<˘M֬f4ĊaSUX PۂZ Xbww׬ok*7*!b|3SQ&f|s"9 VkN`uDLǪf`E\d8@ՠY/2@^XLUĕFX}7)v(V_Z25) Wi;GFNkT6ϻzw_|q:Fo^\>p!BUjÏ|7Y Fބ8vUeU2n.+ jgDQgGcԌxuxf-u X}xƊ(Pa fnxdcN65spP>^!r˛-Wm}o UjTlFp+ȨS]R׀x+[(H%~[;7*H"&BO|L^>{~=0'+7Ed> [v'QF6F_J2z&O!{<OpU%]ȴD{ƒ{PU1pU&ɵs+bUG|*p\Y#UN<*cD,#+ϨbVg}ؽ$' :[eoK;Nf~.a޿/FDo7Lƕ֋V.{An u, c첝:*#JUVd[C08;1d{%.SQ֫`uܪ/UtŨl5gd *ru2XԵٛd cDoq׊UrcǰSN`OFV.=\Z{ujUJ+W̳uX=H`&l"Z:j{`Xゑ0r1$gê{G90ʳ13aVtUp\ˆӇ8}툪UbXvcWex~A"X-zRFX]6y2F{PU8$UV&=K({\UL=5I7n VMKKWf=ޑLː꠺)GW^gA>QׯJBlVϜ18HJ?1VkFz7ryZmm:%q[벮 N!ꬦm+fK?G$`5?CJfs:X?}"4-6f\LZr:lPFX?{VJV`ƶ0E(ZXeibH W:ͤjw vyHbU:OXh OB3 8juJUQ2[ZtHvZ USod 3`C=uS+-B_Chjrhu?XP t|]F~&2Fd(!?L,V?c/,U; ~d _Ϥ_4qԨjsc& IDATO 1]^妴_Cvb;9@f`jEB<}\Z7_V5`~:,Tѳ /s5E2fLI^]ż%T2.UXcÇ+k(UGMU-U^V[><]5.@TYdPf 8 PpzUMXXDxػ]Që#$zXxJ 5սնu~?QYn&-V680u ?ekYHPlIpq즔4^lz׍Ƹ1i7{כ^}]" cwR7jUm3˴4ȺtVWQ$Dj ]4jվsj9V!i1VϿ L=z K0.3 -XdzcFb]$ txL\J~y&X8 5h<YVt*bXmXBF"Z+ V/]xӽ%±%;B_Hz/P5`Ek{v݌UEt3 0<1;=,mtUKhQ=b5+V $/wqڥxg1ʳJ}[H[[ ;@XU`N,&տčz F\#U!p,V+1bx*j%175UiDzVoF= X[b o9WTF*#eXwj7rN2xBBM_f F%݇vI?׷Pyz 5gTJʹuPuQ&U˹/j=]M,kG,rUbZ⪙=A'Ы[-."K `Qu[P|b/3k^{b9U߉OZԎB XYFfXuRjoؼGwc5Vpr<;壪Hwb5G !VT#Vūg[vVY9_RvƝX~ԍH.*Vo`u{`;z&]X y~&/ts{0jVoVj>A.HT(W+]S2JQy5C(}{no44qv6o|uܑc@U۴#RQ3 w-V-^`bļh<#5?uz~(UUam] /!fhlh.76Y9 F QD\uej (W/Fv^0ঀ9a)l_D?JhaSqfK,We&@ثw-LB^UT ,sbi$|(R x(WFmɾ֤=D:U+[VM K*F߼LcՒ媈UUj [4D,p,xVD3-Yab (V{lhZ._T#VWV`cǘceT+++7/]Bi[K?iy جb܊+jU ]XLwU:bVjYmj57OF`LрUZx81@M,Y-fXq!}Pq*/&7ձ~3{'ZY7V$c^jYo}(Wj96=η@owȘ.عO*`st!Ls/ss⪰!V629 ?oȈ8)4)=w=yC=/L 菌Œ@tJytA˲ɬL!e&C'L +U Q$`ցXކ ƽ]*FU 7RYXM]JXf`X- j0uu-ފy S,WsѰZuX}k8;VuB\M><#ON_psOYɊ)a1k'U9.+JYAoNZU8Ju%a޿nA>tiEo AuV&&%Ch |F)Zm3Ui-Oi` 2W.#ߘ3}{顇R dVX VׁU>v f`jʥWĪ'f^l("I]FYz:4lm\!FZURjUH3VO2W^^lP{=VO`M&:qS_r/M97Kc,L;:UHsu[nM!\+l 槴6h M4[wG3lM`?a.a5l;Oj kDA"_f톥k׀r9Y䓪Ӯؑ :MգK;h)|Tu+V|ԬCKT+`KV+UMiNYX' ψU>UhX0Ʋ6isr|aeYVʟKҀ<5r$pv ꂮҩ}u.b56O:MgjV\fQ)DWWJ,òwLaG-?M,.6ܛpvXmaDD@H PzWZ+ NQW\ou5I"XjrApӰҫ8bVsqj\eőϙzoezGXUT&@}`U-ZVStUa{s`Ri ڑZy?d:ݏLz^} 5 ';+Xzf%V?ZձłT Xw]i W7+{9A$v϶[\ȨބY`J"VQ pJ[vV;:іJϟKEG5WI5ޅ֜+[cL<M|O$f,W=K\}ŏјviVU|u njjl;u/,Hĺ#d~Ѱ&kC˒[Zv՝\)_ծ|)@@V`5_zn UWLK$KP+78XohGw'`rt>Tzȓ`ݵhϿ$Y]Ko}8k;A,WDaQ8F%*f ΰ"-a)Ykvh;ŘPlv31Qhb d>݀ɆS/ssuysN:(ݯ/6vV'FfU&+DOOSplsVZbUժ)!4ïrǵlVZaKVǟhՠVkgЫjqv>.*DSA2ZF]ބe˅6a)V)HѤߋUƖ]ݧ"y?zg!U$5USZՉ#SB;Y*VNb S\b|z!,GAրU@}xb:$ -WڳpU=Ç"#(`0zh̜VM!W]`L+V#r CR&I*Tn 1";5 Pa XWyœWvSD;pRA%peZ/Z<*̑WmlW,ʤu\5 Rp8OE'6XH_ v/b7}bbJ$Y w=[ pVV f5w:CUaVR~UV녁~`u,w{Quju%V^FUUVUVjvV5Z 2^=X5> VUKq ! UjEAMrR|0˘M\Y:վ9,YӇ,*V|kTYMVFՙUQW!~&՚$S[HǪ~Q BZM~y ͌6[Pދ!:I;  VǪ0TNG\e25!↻{ -j*vr5Nj CkUӄ9)58{U'-XV#XY /` gr*AAs|c &bmte(.β/aU[ u$us7t8īx^U1٭ݗhZq%QXZE:,TR<1 3ծs WHƺ[bXGXUG+2aJa.EdnU׬ʭsd/DY*`ajԪ5Unm!:`K, z*Ӯ՛jڙlοE 6cu`.X,0F*KPZm3jj-zaX*>_NN2 >ΉJH!Kj/_30&際b7dcGmQzc _c<ʵbuc9f2VU36Nr lX5W^4D 3h 'XGaXԈb5:d+B-NQF!@XlV3Lz]"m+zxE?j{ Uz2r& zxrϛ?ڂiae*GLE;/fU꾩U[ݍ @y[SzګVPVUUN`u*UBq ƞX \}V.j/_:vc`cqA#:rʂM:d"@U\VAV_Td|K2vɒYɯyɿOZF7Kf@bN{KXj?"|`䬆\N`vVaP'<5"Sdim*U'bU~ߢnorg&tJՙee)VkO=]QH.Frb WWk%Vm\/FR.O~FwpH;8N GX>X}aSN6Q,l2 cbXEXՉ:dj6mh!ǫf TiD ٬ӸlU8BȬތO$iL6SژW>"W wIBG UaªVqD)VmUnpªPCг(`;6r?$;jhxr1$͗k@Vumup0-ƮYՐ'BŸ*d04+&ZZZ.6*UbI =9>f99xo`DX/j_tj.Pհ ZNccϱ@w&TѨ_kF|9["auc5W' ;"%sUyo٢! jwMX=b =.iH{ANkl+W5Uy6#od\`E,|sTw} jfU4hF%S$lJ* %b:EN!u<}sV9d$\=zdZ3e[ v}]*3ݴ??X | `JR֫UxF+Usoj6Sr:ÂU); XuT+UɃa1V V{;Z`;CK֞Qt<`ʤ%PzB*+VX*D!bb.-7o aM+u|p|n@ⱊډ=̜@Z6'Vuꜞ27򈚌~.EF#b9ǹf(V'RT*Vj69/߇U;'q]pn}V9\ű2jO'*nMgqg]X \5Ƚ1VٯRZÎfz*إ]Ujˊ&Zhp*Xən)VÖ|J/ ;?Zdﺁ*]\f8:rի՞ivZ+jE>(Kװ kXp01ϟ;p:wò j*EрURu97pzujUJ5\9lZ, KK(jmoS}U({|cz"WU/*WWhD|kw=~r3uI&(VMg ֝n'XGee42 }SQWc)J_llu9G*XկVms5e;ڸҚK|-:BpZ"@\•T*ys`{>;W`UFuUkY)Vm.Lctf۵]%bP)YKVXk@5tJaW4U2uz5 ¬n=B%t5jc؂&j>`Uz7FgBѬbu*8Ҋ4F)Zj$aM WXVAw#'~ߛEF*?Sx2Gcja5'HugV8VCQ!gIajYV2=xj2fC1]&t;ZYuFQ)?TKC0#!&h5,$J$ *I&|={I +ygFmQTp`~s=ʸ 'l>48+9fcgX0nсpXZ\W0k+Ȳ!*ܬug@Dq9t氊2ji IX[QOx*eW\q$@V էڲ]J/u+3l5$y mW;.K<8Gj;TM:hjY]Dvq5U:ۆ IDAT[ IHTE[qտ)T|xcYs+kl<o[p XqUnDi X] e2)}O_ݜ%Z]{\~;;GߎΪIJUXqsVp@ў4kj0:k ]/R{''w\pn$\}Zuݨm$*2uͱlc<^~ !I!`R ٌZ5_Mu" $Ą7?\_Tg6[ω|[)rt8@T=~ }o"hW^TZ.Sڣ`0+XbU^#dt'P$UjZK[ [ V9d5HGUǪ.i_홥2"K>z0TgI%XOgHѥ wfitȍN\"Z&jl6*gÂ~CLaT! rJTCmH9y%v}f)[Cx"Pus>`էP]+_V]Qj[iB'^Wz]KXe_g%敚Ch0\`! U=a*v;:Ūnch%] U N\qVVjt?`u4UKUHj!AAS*BdfU;WcI"Go=r;Pj* >kq? aUdԧjedŁJ0:Z$U,] pɆ|kfvq%Yl6"9 +WQRZW]]kc.xKF(xѐ&:b ͣvStgeHE}Īa@tCU*73 6QV/kaqJ* V]zG VX\ VW|#`WgX%X6VGff=^F:gkWй,7,m%~^rЫZ^5R?}gO(XmzlU͜XMD~Fro~@AePX=*]QTKʱzXy/zTs 'Y&~p@ QLUNN?^::ruʫj V۸Jv׬S#EvN'غq'GDT}E*ziҫ묚|׶S=$$iH9&Y{x t?FVΡYN>dF^rǖO+p|bUVDVA4[yq* SgKrq^'cW6Pw䶹d1UMqԪQҲzX-4{ *ܥ`*/egdvEq H=%. bdBJ{T/6HD8tE(ӑ."E)Aâ/s}}M0'8{9,VUK(4HP*r^ϭF峫U*A:;#|l \w`r+V͐5꟔־P-jvJjxj>[`ڨ-弧U;u1SD/%Wok+Ib+,RttNΈ V %_RgA5"~TGªi|_ܰŎU?jR`PU8N[iF6gRu9P>3h^(ǪZ}my]_=6՘fTC ߛbu#xJV,x-z!y?aV[oNǸaUcD)Sh5ȴ?U 3,/q#PokS~O.ʔE}%oG$|o}n:RL`%AO ūX.uҴRGHJwwH1A>5W.՘V'@l<;Ǫ+| .*ԤF$7D%΂>v,-4*bu%X*U_3=ڸ5 GΦ{wo} @Rbwʨjoi ԽtҖ#y\$*a WUII@ꧤ玸48Wd[z ėж">V-k~Y?Ѱ$\ݻRDzZKY=S|qUUpYOyS*FŽk]i,p5Ԫ Cl*Q+X]\X4s@ *c;TXndjX>իR&LJ,\Q^(.0I[R IjXeNe rt=Tod*!W޼9V$j% VEɽ&{Q mŦV(:I[x9{baAnAmK)jp=UX$V-:&DhaUV͞DoKڜ'].*WUԃ1`&VTl>< ScqUW l&c0RɺBzq2> ۪?/.xO>Ha*:CewU+V_x:w0* 7X]$- UV,X}Wo yz?ApPvEȕ 4)̶e뫱dk\e]?::yt"[e,BRv\-DBuু*{| @E:|t$C9r߽Wp@0wV:|1|ë@Cm*W) Ī +IUR` ߚm_%u5I˧hLܦ676 `}|5Luo$=BWUdYzR3pUqY9rAdUK][+Q= ABX?Qm󅏫VMNJcgeQBqZ&uwfIXO&Q`aubCYXCv+Q:3`UU kj/ڪV+hP5mq.  c梲,ymJ3VV c,GwZ LW#z\ *V 檆v= _ XML_%0l bu\q!JV19%ⲃcW=;!$=rU4\vd^ӮW Zb5eXX ᐍtCpUGXa *CX8xg*U+TjiڨkʺW=!Z6-P*r?%ORWcXјqNnӥX1TՕJp*ê|au15꫍TA<)<WJ\k?HXRn{>I@`jv0 ʦdrc8xR:8 NJGȲ%2+6~d@,Wk ;l6z_J?a:aoAc6'AVVu?\ߕ*>ͱ {3CIR_I=R|t|L@j"K~W9SjĪ=Ftt*w} Fg;fWŗw ~uHoyKoVU[m2csbwVif¥~ tؗCwfi~ @Xhqmcұ\з傤3N|c=uME؎%亪,p+êbH @fYs[פV⢪>-`UhU?:u)ܧ<^va}4T\UW>v yPQĪaNٖ8oV}ԑܝ;4u$*ĭB XW[MrGlH]VdcDB3)b5q`)A* µWChg|{L(M$L:# nJJʟ$Bd}!$Ǎc MͼZ^h"7M~ϱ_ay@2H=Ϲ Xr(fk)B a K%UqՍ^ P/8w6lFWݟb@U+,#V_xwu{,ߧ^ÃC=O2h 7F#iW++S&Vu_C(iч:Vf5ϛ4T}!ptVvg$:zX5qJV_b>' X||Vwmp|3 aV .d ꛺齩/god j5XՄZW>N@)ڜq##B?%ZNK"CGi-|{OK2 _X="D$`u°]8VU,Vxe5Q*Ѫy`Aa/'Xm~`4ˎl** t@;9XŐӦ: 4lR6-@LU)_g3 &Y IDAThђ݇A@~ \U3`uƿg`uzUgC )ȅh ;?\ B]cU>x,YHF "fcu2٤̻4Bow؟X,:/mP :X- x`QA|AS`HZmTs,a$v.pXo {z^B$?.¶/նbuv#UWWc@IfDUJƫN=e3XZMbD}+pcXmN/*U mFg4;zkX}T,A)R'arnR&aUR\=#X=j,MWE^ 3c.LìVUUVV3A;k$VRQ) fXugb" װ$Xm,e$J흒 v܆o7PdB>L58: ƢB^QUɪrZR 8$R<ڡxCQO,yٱ,Vrx+{Q<4|b@j+3jUHZ0ScTN.W*;V5ݵF ;Q#;l "(xa }Ѫ.i+Uߜ*pX"׀ז]iՔ';~qmcJv-U\bV6Rn:),#bV?'6=X ׺Ruo5J~n]Nӫ9`i}Y-%Dj5ÑoV={SVC^QkJ̺Z||4`漵:xj;c g\:4*jE(ԫjVNb5mSnTݒqٔ܊V3 Z 3$W۾>T:c .jymJ`/⏩:U.ցcouϭT%Q;ju ;zlUC^*q;Z}tVpuc̆\e*k)m5juzF2JfhRq6mzͩxOfcQIhTrE\Z83#lacpW4WU&<^UժjSuY9+A϶+[{WzQJj&a.4Xq9J[I$'Bna࠯ ak \"kp5C6WLwUW>ZY*VoZ"W?:h$YKZdA VUUXP||sN`IG;|t+GV~wCհ X-Gh3xtqΊ8YJ'ŪvT-wհZVEP;GVظ|VksUmXsXOǰع\U7ci+>/_0*[Nľ3)r.UeqUryVNFڀGKׯߣШzp(rsX*A}09J0mĪ)Hcuz&@6VmLmŀQܥp9X- HaS:9*X%bUcU[Eza9NyU+UYxW귊U?j 76ty{Af\"\?' [pUr8*\~w]!Pپ \-m]I6~ܫ#YtoW( Xe[۴6X5y:}τ||/yC@~-ˌ#S$VQ(g ȷT0}p6"^Nhq\mPsDYϳ |<&T}Jh *UU]ުeHHZ(|36Xxٰ}>/õLKU:z]dV%Ԫ(9bcXd>հ U*ҭ  Lc]m:9LOq jwi&"p. *?| J0Ql+KщV}(ZկZ ^TrT$^)QIa >>ՀJ9;&4@ai'J$:vkK?6M5D t\HvF0рG [(ԓžN}y޷cM_~Ī&vPm.LY%YCt%{@@E=ŠY }NZ_'ggRa@9ז0B":J}c#wvZ1ypBt*yDԼQ,w [з*Z9ptkI`-blŏIOևP\5Zu![N,,,Uj$`Ymܪ` gLdRhLp5kakĪ $K/)lOV U_v"~-X>etJտJWAa Bҩ\Pj&.&<@U?Ze=XHi =Y/U1Wc5 -*QjwjKg ue y$b6)!񈫎JI"sP5bdЪ #U ~>I#%DdjהLY90mMz#jUl_~ VYPA[}ve>LU켿?e{F9.QU` \\vp|U=2R~w'SiZ9IB^'~U0ikcl {rJ(U-Ƨ#UϟTĪP /'B%PSBU*+r{jzV䪎6WiڊWѰڎ\Ŵڦ#ΑkUڱ[*[bݭպ]؅ъuo,yƉV>j5jI,yܩE -|0 0XsΎ@g[>q>:VQe/VԏZ\91̷궘y8.{?VaGWVKUBR lΖoHH ?:N֞ ظi7^Va&ˎ1=ju\>t<;:zZ;geȐ{Fxw194ͩuU{d<)#Jwãz:%-:SfDRLM.\_ӟe& I]<:{=˾8?;q\g :kZhelXl1aEIn'TN? Xӵ;D}%21gcn*woZ5[Vb zoV,rm*-< ,>@Gqu EA*`[sW5zX'ru*0nNֲa`~H]$V'GaucbjC s[!"W-[/?jU]Gh;D1PlEY!fzVрG)$vrf(H=RL4o :RW{͋OxaQhU)[RVs[9 03t~ V`lZk% V?jh0s5a9z3aMҥ6cʪVUJ䏟rȗUk%\Nc}xPO VŅT҄*@h/ҪܤT13_+B~(YD5\VW#O bJk9\v,*f&HXa{/Vv%+5@ր7fY%N'֓?DXبGjUWQ VVddq/UzMRyv~hZ*'V*jBeImb;]2^,"dhSTVXUo+VKjwЫt{UTe_^Ezceh/KbJ"Ȭ7Ux}l?,kAܳk+MZ/3p "*juCγV\-y3nUUbg8f@պ3'Du)M{9XmEEܺ?>LJSa*(r_RQU÷ ?R]d?Om:;wĒգ65,bĪ=)EX nE}NvƘ­Vw 6;~5^6.) )1X@ VYQ|3{b>FUKa{LqT}u{;xcOULz" 0U )ĪaUVd[JqwUSߊ+Cf91suii Jԭ-m]UZ]-{9UEEq@ZXWj Rb8(VKر\ ; :@Y~cXVioG@U@Su7K恂Լa5D>zd1wPƱc[؏Uh7*&Q6$TcՁa}Ui/_vTER `L?~)MuEʵJ*wo~Z\wb{kk4*4/V+ĎpVmTcYN骁5#U3NE#ڦ(AX{@-jJZah `G58m=<X~qb4-NCv6U3P2f#VUX- uUᭌ?P>qUUC`7GU\X@*RU==ŠꉗD&޽9XdO-BGyixmX4WyѶ,UE37fjV3rXͥjsU0\&iE#Pm8G*jR^7g"ܿqV_/x,K2ޖ5-s.KiJ9 pTb a\La55F|g&&e*+7UQX_z]1j;خ1{Z g^TXj<)V[ܑfd䌭ym~ܝPv4'UʃjJ^XӪ^⣑Ƥ PB$=pZ"+;;j6**&kKZqIHV(XX@!d VSA^S j:/I#c˗t08?'3YMq0 FtLKdM$d3bB!Ha%^4 ƵUcH+bퟲsFLYlFp{s8V[liIXV})9lT@իU$ްhXZ M[\M`uAzΨ)"N I 0cw VQ;V 9>-KE]AYË7:rX[jMM9N3{X ?jd?[Z3FvftAi@c -O8qupۋjTPru\W*R1e-#RgP*/Ve**j逗Ug%5U '/.V޴!+0Z8] rZ-If5ߩ@i)(D%;eD毢qUmUaYXe:a pQzGyr$V_~WJ:7wԒRquj̊k==ͽb0=6lEup]审#28H+`{ZMMMNZ`4y U NZxJ9 [;ΤGjN)U"xM] ε,eQC:^ASqt=`u1Β̦?Ԍ  !g mN4ZQPR6[zR#G`VҮW% Nr#)]7LƱP5[1UKge0i޿jX론*,X*rSa;M|*"^amի5UUUT\}mmTuX rn#c\j_Z^(C I"sJ?{JNA:aUϮ|jV_E:N&+Q50XCؖUꙠVVͷkK!ªVŶXU*W)VbH}k_އ-:Zdh밌눫N\6 U)Fah_z֭C`׀Ϧ͹ nw3pJϓjՑeZ֜+b5ҕ;I>\nSҁaz:}:%X5\=)I%茉2k ˓+.*TzB\|j_ƍy!ma~IXzXu_+oIV R UAT+j0&$^eŭzU~)+\4>O "LQ)ՔO{Sv/r0::"O;6D;b8*2R)b+`MB6)>r#X W6 v̓Dj"u&􎌏7VĭżnR X9$ĪQ`n&vW/fmc~aی;Uh^(nV:&8,e!Ί +%ܣ!b o@+cx_\Z(GՖD^{'kH)V%, K-uuyķ\s RR!9G_Uɗ<"7p Vr̢[X|]Q`߯\9::993yI =#Q%Iy`~aD> ! _ eVfĹH߾]Iz/N7dy쬦T'F5*1SZ|buUIѪ:[KJ|T8,xD|HUy޸eAz5 \:^$6&~UW*U4JFl+ՂOEVu=yEeVRl@>< Q#q reê#?_&JUNդU VլAxT\}gQ=V8 BޕhuXVbC5Jo/HV%ªΡxP"v%K[%1$ꢧ9j>4y{0OzloAժX7Z Xfc5յ5TV׶6U{xM%,(pʓhUϥJ:ڰ{{/ut!^v~rXZ+QNut0zmĬV6A_akQCv:{CJfFK,/>,5kWeNje4Rffޠj5PRڪt"Vi gXe$%wD ;sG2 UZZ\lRoa ySOBqZ>eǏ`?(˅-pD`ԫNU E~'|*jU~sEKR=7#ŀ Ɯ]xxJM W1nαh.s\ h[>8Jޝ} wpuhՃcQW]Bs/9 Xm~R[ J~M2o;NPCg~G KAiMH-bU +Z?U҃ʌ%ΤB\\#PmNbu>4M$Epu'XE,ްC9.x'`р.N !U8ГDf<*UQLdzhʩr7UL  ҼX}U4^P|կp)}x%WR2:ZV6X=n??W_jnvsJOո*ϟ=R_qX٬N qz@JRbD}RBg&YUSr"eUy5yoU9;WMXDXUf*lA4cЕjp*XӾ2UzzيCZúӿ<ڰ5CIGԿE^~qLmV&"}Jb7Չ4hՍeΰ~IVg=fffix:9u3GdgB*xz,/EyQ 'vzJ]jA('&GVFCXVH1sp\ToOT8'Y1M'U}q9;*z/4YRjOV\uG }AW-.FjU֬t7?׽iV6޷lʿx*VRJ䥬Uj+pWt1Ν܁UiWqKa`jP{^3_s\P3? J@=;*=+t*֔;\?bi,ӨL?SNSCIE+ΐ\yV+zOuT^r"F/aO?$*MlMKpRSO'o:1mgqOm WWT&Nd`ԠW` Xi` @;(,GX&0,D*V#Bt#B2Ǹ RVB$xA6)\ޑdsyҷD ~WeHcRsUEZ P U,VOQWCCuNq9˟u/-[aV:ere5z5JwrqM[MՈw#UFdp%+ U4`T Ua @r%36)XZ~V* + RNXͳtn-t.`uy_^8 ʥUu2(UC6ZI$i=Va;au*Zh઩Vjcz׍U=joǍɴo#+wf`i---SXTpfͥ1j[ej6Y`𬪃- P̜y$j/dsosV97 V {2a-V\mռΑu\qxիzLJߜM .+ΉhYXvHG`&ƤOemj"TDV!)^yAW4Un1EZ֌S^YKq^R?%ymٰGyjJU{G p۷ h=|y.S *åKG%O7Őxa-Ԫme>o? -h5 V_c-W5Œ^cؔM|(4UGԪt`:즊8sdp`凯Xm>5\r+˵R.߈Z[˗\]uUAoq9[STj1Vw* $ZWd>U5c_V+ꋋ70XrzQ gSeo+h>y c"tZ#;8CGf<*r5KvTٙՉb~%)?vKpNÎ'ET!jX5:M '}|bakj+jH'|mX#XqUxU%2S)*N7Ujpug6e Yj'Jjc+V5Y{G(+*B ߅[=!.*õUBd]5Z*X?:azɦroπ;w$:+yDUx9H~;3Ы|32ՌUdm1 ZU֧\$6U8VwZujAP`_:n/bJ/IMkd$ 2VBaV\-jƺ*UK$*25 XQ*tJ+ՁeT1crm i%5ռ~]( X%xq͋_X { Ekcuo Dž8 <YG]:jX P܅X2 D#V |SەHDprV=x!hhS7sQAYIGXi[9~xv3V?*Kc)rCt`/}E7i?Ν.*# Pd!O9 :#8-`Uxg*A #"i|KV3V B:9x'TVqk>z3pi; 2TKVG/Zs>Uf մ~.xZr\uGV[&VO_{ܹ%ӗb54 jܕuQ5`?UZ0?ɌV\b~-9UT ߷5poZI&]۲jl].z⚫DN`ycAde,DCV +/.VUԔo u+Oiˏٌv!k:hOb5ǖNJg>z@} c VMM}N[?Q IDATxTL"Պ:mdmuAp/j#Wo"6ۼbV y&cztaUWX6-ZV+-zb~%WD 7ZUuXvURfJT}سhH4+*XV͉J²Ѓ7O>3IOX`+F:]F k7kYT^.47~L"RDWWU_VVh3ͯQV}Z?_Y_k3Kٽya\-d Z2Zqv4#8uLC˦Xj56!BAZI,g=9v!d=ۿ G\}?}%Qss׵c&p==V+P+60 V{O`uFc!FQ+9عK*Z?a**5w=\WoU d`o$ק~ ɮFUY%09Vg[Tf a hׂ[/NXZH== ԪPQPWPD.] oC&ji§<ΡNUKpsдl\zJcKK׽e٥%Q{% |R^[1VS~`V>܄߲UaHZުT W!W53MNgZ~GV/^7%UF} ͈TD0" Ԫ=ozS&@TiUj5Sa|(j֨t9+(gu76hhR*V{`U?tVXj%:Ufaꦹ$V{vw֓x|?]IsXU$nIĹ^X V~EY|D`߼ma1ګi1Nǫ V`5"KCq!eιsڅZ&,T5YQ qE5(TGӉ-\t|#V-6!GD >3qf3!*a:V k5P*]3{Q)/)V5Ud->ZUDX˜ToEjt֪Շk_|Ho9X2ߙ X8AJ*-2a-3+OEjV0~jRC$iX|{?xn._tb5Ī[r;/IҺ'PVNѾؼ,\域vQ{1ˌU\#XQbk[]P!'t)O6 0@U=iY$Б 0"UgZgU:Y@xbêۛ VE"/d3?D5CzZ_K2^`TpswOlB={T$X}z[FFi`uJ*I3xP9ϘL$SJƠ;Mx\]bZu=ѷ/d }](['PASTNxZ._S{ZQr?4" jYi:*:lXy @_)*;19ժJmlh`]}ʇp5*Vl$Jmj+tk&)Y/=`\.7[[ZJ?`U6(V?A/X PU?#kwRPD=0<-@R&%Ê*ڢ ħNքL?p) ʊUUdZ;kb(Shnf`^q *DbV!*&`ЛyI T *Jk#lei E2_U$ruF2[:2)9;J]wrXm!Vjek:RDTkabbf~$ =ƶ(#) <-G ;bTO){+WZU]%n '> SW\z_dK[A.0ԆYiĺq0{XA*ꪒOMA~wNz UV VW:C^Fuwl2'C{Ue8k6[7|.YY{&עďD߸&>Z`7ncrUdjV>sUjZ ŪfSq-0L5Rujoog X{O{YwF$]bi,|Uq 8f#Vۈ'mUT/f^TX**Xſ(nέbe׹97w*U]+S?B@U+V#X}XpJ_߿~|㛷!@uD밋wr\ RO TtjpPn%[ 3}78 %#[[BurS*mL2fgʰzXUX>kG |pNwE Vk)s?C+%6R]_>ql]ABPt+Fd1$ 9xښ Wi2}˩jXm\X5?R"X 0i1#\{JFĪg21 a.Q>RcV?[Crg^U.L>,`,X-gD4|R%BujLC2t+5w0 w5v!]-_Oy4`EJ&;=5'`](Vb7Pd*`끎j5t~I>398dl\#T%9+87+k77En3JrGxi$zfKV2WSas9fd@ÿ:vMu8ĝOYƃfR|rRŧhAiA5дҰTt1B ͔0f1!q1#r,&qI7ssιݻoYтUz<WH (!&NWoXl(X?˪a^"o{ZʐVWiUàTull^E>g[U}q{ЙօR2Ž*]VV+V;ҤV;Vb~US=U%x[}W,aU ('Kwjê9nhhb~NWX=]s8$)STFTN5zzJV 7I9W;~= z̮~FѣcPb"ޞ'!eZ-+bH%Xm&.2 2/G@j_aH*'QŷxP`8s6#6.2$VJ:p#el#U L L-pɑ*tZA^[t[vUII\g]>_yޱ5qSrYm)qN#jX_ʍJ߈ZNB\dn2hnۓZ V[}3U&+wbx>OKw$"+XU*f3,νLKeG翭Pw.zĄ#^IÉGaꬕ ǯyi\sGS eOiuTDVzܫU(VUV{qD%W~K V5Z,d>0Vc;;MdS훝}֫;{ L\_W=*} aQLej8>׺ rV9P3T\n2FUV4 _:5ȏuKC8{3+zѩe4H*_5g|Wd2\sf̟Hj V -3 j|&1"`?FTGYmq`P0jCVW(H֮e배Aɀ΄RqPeTEA ʿЪjiNriM;U{eRa~~va:h|hp붚Dj\>M-?E*au~cX%OUP6yjIf zcb-2֦S 5J&cX-3+h'w6}WZmVk^*ܷ眑_P}|֏lp$֊Rc_ք qk:8ZjFu}}qWSUնN*Z@U2U,0zի;<<`ju nPrX^R~\~*$`n0rf6̊ekPbe:+QIi UrVe1J`բnF՜R LJʤPYM7DW.'no$cAd5?G.S ~^&*ꄳVZbX&j\܅X-pksg&2 1|pc X}M ջU'4#U[SQh c5JLj4Phֵη5k{U癫,W;n7d*WGg:|r.D(op V/]frMx}Vb2Z 5*ԪTe)c Vi)oԋƢmλږkV-n0_'M1ei;W 0.vbрzr6(S TUO"EDc5r͵ⅹXD+L[N.Rf*ȑs׮tމkcA}iˍjUy(W58J6gFffZ73 tQ̑`TJ$bjLtZZPEƘ Vj%ўӹIj2t\%tEdcuaմUz;WUVM.q0ɴbucccSYN ‹ʊXz}p膺 UXml8 $V[(V}`h"OZ)}ujBmuU mGg&W U+n^zU,Rwl,EU\ݔ W~AvR#Fbj;(QpcV#?+RjUk-aJ#bTZD_*ޘ+ o;w= 4e!W{ |^ѴrN:f2BVKq" `TK`+aаZ\*+X-s.CJ%eJlٵUwؘ x0])Uv:A%}$lNZ9 U;k$)y!w15e]kUuF8Ka>X`NjbSd?s?Ԫp1YUu}3U}X FLmceHZ|oZ-fMBu@uwHbZ5n z0E P=M-Ne*zX]j}jAJK0U[O@qZV_VEzj 1ԙoTUSOƢ"ҬU&YPZ$4k8oېU氚=wR1(LZ9 [v3: Q'Im3=|KFWՆAY`yׇ=V{{JT=Zett TU=T5va[`uU(WXݓ U챸W9ʽ|.m#9+jX`G<+J 2lr6/iRuuuS JU-r3\K2}B0[V' XUl,Xu3˭2'X_jJ %թ:pŦ~|L926Cհj&LM(]#b1T_6gT= z04m*|ut4㨚8?)O=Tto~VUPWVA.#[Ū4d8/v=!g?*tjlkCp[-NO'1R-L`ɴݠ\m=z۹_XZW߮:z<TO`HSƦ2 UrWW=Vv`E.m8uXP-,9KwB.?X*VWZ[m}+}lYU[}zSU\)2^}V=^c*XԪhmPOjc2I-{9g =:u]&3u&#K^aEtW]7uƪ94uՀAnY'))K~OB=j{s/R:N`MLXV=Wg[lUXmNjU}/J*`+)V@ZWQb؈*Fu@?Vͩk%#XCC)Gj;Ri"%rUU9k\IyuF ITp"@~X$`uVT D"T+6_uRH, U_! ՔZ2=.T`u92a\-tZЭX-EVV' U9Հ印eyH[Eajb }JTvlXXs---^Rj[L?2]5J!Wё?/8𡗫}o^>æ/^ ZHuC;Vw1 ؞AB m.j'alzҎYZF6F67Fh*>@ԿkM+N_ ]B;eNu(.w|C:Du8ZB`(N$Q= tTn]iBH M+Zi% ҫIjhNVRMKدM>cjSʋ jXV**P}5nƽ/kՙ-pY48VHZ*^K&Vk+!K)PCKɚ&ulH6kM~R-I4&9 X04~ 7{0 ÏyjwXQ=J%m<g©Y@ ]Ɨ;rIunG%x G_ǟJp t%jGFã>atgUj&B53wv_4Րîp#gn2i˂ՃB!AqJMVga?|YC<~*~ |+>SR(oKU&-&+!ѫ`npU$.j7U ϊWA 9ʡSUp!ŀJVUBTSL5Zna˚+j^۶(WV34}JͥeKVV2lزŘrjuc#*] ̀ՍEfI]*`8AbS$SUW[]Oji} h*P,΢67 NkH_bG U1{9&kUQh*Pkh  Vj9y**4:|p5T50cӟn: MuqCSŪ9[ G\y[F!f::sawbOYͽVz;uhl7l.bZt=IEY'+ C$VCGw: /zW ^{Z+"n%bʤ@r5?ۙRbzWV[0Ɋ:Zk᪈Ui=9YDT]V1,wd7 tEgL]-S"XuV)U)К sX?$Vei}XfVV*wW۸kzzϙVyjZXĪQM[ӗ`W(QEڥP`fN$}t|9Vc (V GdˊcjiZ~{bullnjܟ?=4?k6'ǪQjRw/-VTv\UQ~[5&fL*g;FK1K 媨!<*_ȝ[CaNoc5+;@vQa֤աwߵCVz;)*qJbT4OVL /v*!+d жTjio HU6N&g?i0_kPU.[N2Kk`g yv<[@p\dW7аD—^,?u)2'Ujn+h@+RsRVQW 5,@kST+e*TՌO-*@VS([ U#WKڷjV@V^Yu}}9OjVªXm;xQ Zh Q#Wϐኙ©.*y mI?)+7êrU3iqj/W##VVQ>0lʽ}XJU@շlғE<4 :;Xjs>Q-<ɚu8:VVvx$:C@]H̍R)`jzdt|Դ^?ёW WVΠqvŸO{~쯟θS?;ё'?{׆+MOCTUh[MVQV?'`$~o{6ca3wb/A3[Z2 ?bgiXL*HZy:zF:$uOҼzSJW U?JYLe[VڮJT=f/(|D[UXݢE"Rԥ|mZ^]48SJ[\ p5âU5Z(X]RY%;ک*b ZX>K_-V' 7ڴu5aՈUYcR0+rxH\Iג*ߏiamz[^YK7ޠ4@i 4|AU,>Eק,j@^ kZF@vQyTJ>j1*SyGG9PT6rf[wZ%QqsuY*SV[,(YhߊzC%H"=PX}3sƢ"af17:*V"V3}j5 m Pe] #3Ŏsn"~sc/*U=UTj:bI [Q*SȜUdmlyj*9b`ژ3Ԫ4Hm55uSSSS kDV2Sd%S2%)?É|be޾]ؽ&FGy>7 QTcy.> a {]5'h5佀X}e3` 'V7 W=VW׉RU2G=ׅG/Li h{9j Rq&@75@f+{r'+Q+3βb89vJmՍ+VSrCKXtaUR5S9Sxx8w.Nr<885;E£o<.kElZ1Ve <-w,^\%*`[aE`SI*o_K<E!k7S`d&9hC:TUӀ0:) [=LEu)=Z0jUlOnXo*`b-:OKV};Hogϲrp[`J5ܻ[V*URؗ+jWXM&,dFո UU*A{ţ*Xb݌JCV;S] -L-Ri+|TnͩHHULY|zU+ UJvm@]дuaD ZN=@]>Tî"dIX#xd\ ȡLcєLJTw&\X5pA΍_#X#K:rpjXy4FU(ZQ|ce XAFce.Ӷ}?CRmwNfCX;40;  pc57T\*d&+'8j:t\xc#|c>8>I+("ȟ Q4Mź*qbI荔4MLLM[%1&/bj2ޘ[ qak *` ,K_9=V}~|99@+n_ojV ^=VW?*Kl,J`PzіuW1YeoA,QT^u5TW:`]FU!('`rdc+VfDXM5㖽8"r׍mxRͻj^?V* bL-;-Гp{ZUV% ap1Vj2TJJ٩Jz`UVZլsJVW`UWWKU1ܬ+M.U;BL`?q'U!r/U[CCS[US]kë꒒^5Fn%jSss3IڤW,ĪOմit [In)6ZVUUEwg __E2M:Ӣ5+T d߉m&Valԑ:jU-x$M R4'#UZ} 80K wg.q*ruwY]["jN^χSO oCfKUj qB +NZ[| Tffa%U%" IDATE?$[t-E1!0G̅ ٫UѫQS;Q*Də+UX՛LJ.-hH@UͿ8$VY2Xʦ{N$q,*am4}t.ՄX]5꺅j\] Xzj(D߷c}%6Vl>1S*{T?ñy˗|-Ɛ< !ѸUBWVcJT VUVUrhO`]W0C[/7Z\s#Y ؛J]]enYMq"FǢ˲*XLؾLn~Bl^M͋gCEX}XUT R&fs?99oor|zbuqTԪUE^jrBXMArB?&gݽ;zfAV'Z\ U}X+aU|lV1U+ :h5V?kK'vM?2:!wT=UVV`j@J,ԪnZVֲ^%lj@V*e2AƪذՀ*PM`ݚثl+3F}j`ْ$Vj5a)U ~SYЧ B}=c  e2?X e/8TGUHȕ?")1Ti0qj.)Q`5&PHZ *\ ,NJ6!~F̭S7m "+mRjXp$RcXKÏmUV&7=xV]PrXG uXWjUbn#+B(~ XgXh/`5Írtc3|k"p&_-HN..(y@F:M/ P[Z T+.fwXv % X]U4T Z X/+@Ux*Th Ve`-QV j?VV)gVB D<Tсl`Z"ScUV_Փ+1CWWeh@ձHzX *AuDmUCRB=,t=|X+X0p diW}>(J+9O_2JT=}:B<Gѹ<*힚/lAF@|b>qٽU/ZU?5 5*hXs ʊv @Z;纩z L/ҧ4mk%.n :bSbyZ,9T+]jv6e"/JX]Va.LUҌV4)e`իljPΏJ+@6r\RVW%ꤷسzXV,LU=kIQ bʲ9]Y0U˭?gi kaG  @ft,2XMXxX]+K`*mă7 yep544I0T쿷Om1V?YU"d aQ*CWRRjP(--O^^x\;lm0Y!JJ)!8VVV`DleOm Ϗx빿>Ov [޷DUIzerU EƠSi "лQ˞X5O”UQj*k\ z) ,'LYŏ+KoG' :\eZaU yorj`FΩ(9YU< Uw]P,V/vZVTY:8hu\tҺ*[۱ooʪUIKRq>7ͽ}mGU N7邱!6U `ʖ,ӊKհY 髡xBU3JcVG=JZU>t{Jpצnѥ~G[iȵ}*Ԫ "pVd ^Xawj)JQ f@kEl ()ӪC"U?GV庙gFXVՀ(WGoj8ªSyZ81#>zX*lKk RzVqRyl5WU*/dJ+fG4U(lYkZ{Me=UbԜ+HԊK8T0(' HF"Z_"`UŮj7b|>jGބcaUk4tecKEU*7$VZT TNLUU_uCVf@jX;Jֵ rpzucuUUP,`pqa`\ XëX= b5P=VZ=A\vWEww6 EIa%ˠ*MWZ;RfXF֓TR `tPBz=#TNVX9NpEw׿ dH,pUZxmXͯw`%ZPOLl`Zuज़pT1]u &EX]Z蹊z1,na`hO2py@ OȕiyoT WUfrjuKrU=xQem]}tSR$AW i5\JEHb@ǦbZ @¬ jXt|q\aaY위;EȈZ퉰8jnX=jZkږ+Y-M7cuW.Vج9~@UV%embu=%~,O+Z^Ӟ WxEWˬ, X(pr5ݖ.Cv7_Cȵ;:x哟ɷXmyA\j5+p_Y#/yg*g\Ue7FF-YEkVєN'8<3lDe[*maGu4Xf#nu+]T ͆iIwNO1Ŀw},k䰨ղ"Ԣ\1jCnUPeѫ9j5'ȚW`w\MI̊8V"@-YT!nim%;,8Uu`͛O<` 022$R5rUɈEW#vu} 21Z3s+-ՇͤO0C޴ SU%Y1NV`'ډXkꊈJXJTr*SdU2T()-bm9R`jVYpˆP5)jr`h]U**k%k%y%z݋Ybr_?W)W0q fȄb#V9X=q۫VUڇ DcթίmLH-0AD꒼#SQ$jcq`+Q95$`=>Ob53T6x}o\nEzu+`ehyGjtV[i"tYwfZ-MǿUP6{ZY>ejKuuJX[hA; Ū ܐIs_+LMOew0[ (VQgAR5Sut0{ށUԫ3}uUưFj}.QkMkj.eG3ޕj #+oƪv-$Q rZmIV]5M`UF@UVE@b՛Rk0FVg1E:U`juV*7HUru\jjIce~A`%2kt@-Vg%ߨN[R+~k\-[)*FV5K@ d7_; 3&<VVZ}kv*vê/~6)߶2Vc#*Pe"Xqk$WE}YWy`9X9 VGT-dj*OV@W4e|MZ$!T,b ي: @>}\&ejhNݍmVWrNNs4j?zҹjǺwX_4VGgfjf_vO?zFXTX;޳J^Uz\Vr"5 XVOVsyXm V7 ckBWQ\[J\}֫ ֊ֿ9 z4q:9yql΀ɁgC5* +#UwT.,P1'BVR޽aG #NiUjQ*gY7@kUWV6bvjUjjUYF~vxzxXTia"1f|^X=Yby[ fBC^QOVԽ!,W}>VՄDv.=E_/dZ0FAq&AÉu4};ހ~_ PV*mCժZη+*T"*/qXW%@ff5*Tє-Uʇ [* :M+V`U[U^XO,V0OTi? $;cmE rW3[K:֣\e`r\={64I~ׄUi_Fz{{'8*EVrS[qn @ݫsnF rZ͢@q2/iXuʮGF㶸Q>TFW8zqO;W P:|ZwQNMJchՂhC"VZK ͧ[V6'WNkDAO0e[C>@烒JAK^2X| W}jqp $mn1P0߱@W نt'<(;__kyrԪߍJMRȘ:t9Y}-+Vkm@_T%&MEvBWHe*Z8f |NqV/iASnU=MȚV V5MmMa"鲠*V0[1rOΖs >J IDATVMyXڕė+aT8hJ=*Wh_5JV?חE^Vz7/ U`tiZE `_ UpV" \fjx *X.̧[ZVV-W NWjީQ7̓ƪxX"V0UR՟éUO@$RM F8 dUZ;ur*u'p(5qBEVի,W!`ó;Axm1 UUUC֋!:2)_nRm*1 sDLscJUM:x` V[nI (WPNk}JX(#`:*c>r\1-Wdց]yjy aӠjKJo8Jk|XUVx+G}'*FOmPՎFTՆˇDuVg`s(XE<cZ ([MDjua K;JuUzH2Vk:! )Uɕ+U1ePYռa#M}jeQU cue3!`u.V-TNZevuIw=͹mZ4%iUb ~h:)[ۭ9$I*ϰ/y5TPpcըUt A.V)t\TUN%LՈZ>*n[ڔM[fP5g^U^yBkT+*cnv:T7xPUH035Lv3V GZՊi+>tZ^o(`\6qEnSsU̬nZi՗~^Ccj/RV>[ z͍-YZE9ЪסV7"j*}u.@5X *)ZYGIjUUevsI\`5WmIj9W|PClZj[X=GǪpVz"ԞJdR[yAX* /UMX6\Ww8Z|cK%[0` 09Yi.'\U*G!~v`%(%{!l5&aQ'Cj#EE j/egq-vDcgL\p:` Pl`"Aٚ] T#jRjRZĉU5 /}[ߚ>e>qs3u_` 3y{+ڼe"aT%SPU;MU}"'d9x %%D5[sN=VZ Djh*&;tTI)@vtw!!!ҬlY`D>vTk+>|w W;?%W[ד :OI{bZe NV5* `kX!.gTh՟JVqfV8=jP"GuU@RuR+սn(Q7ѫV:=Pq<~YXí?ر#K?J[On!5'.aͼCVZubUUQo.+<ؚ(+pu1e?`"j:uK5EUjZu4lb5j++VT=T>u_twIUvdMMZ%Z SU}F~; בıZ}5"V-o`*Amg"VURoSa_ԳʤZ <*W @`+:vEfP=TiU^{A8sa*VC+VIj]HҘ2Ec#uoXu Ra;[)V`~Qy}__z54LJI 9?aqi'f0kP5mB\mc\\uS֭UUZ V X5O\YjX]HTgչ UF Wm*^pz/vgWUPHV3`ՠRuVq<:a#$qE I*UtJd`V}_:cTh3ؓJ0P ׮\e/2SڙSvnwuPJVUgmݲe_D~jCGb yTvHZkGn ]iH|J5qU[U^ bi~WêڱcuV#CXiB6GTLwޝ`VU"Z^]S?`V*+*xͮqiww\szγ;'*VW1 U/ΘZ5?c!VʋU8yZphMj*4rH& 6JѴ(Wպ]n^ jz=U[h NuN 2V[ TJqj_(uF1 iE`ڪxpt\G#e۟ϻfy귞]NO~V0|uyyvIn(+4 :-"XmX׃ MWRʖd0feDLT. oUy!V(\U뷟8/$zeBUdž]=j>y!4; f2zs)+.| ٳFXJ3&BI!Ѡ*JVaתyY+U\hưAE ^\NItXmw&Qn噫9+%XTK\)yOi9֤SEm&+ sUJ zt4tTV(~ `51T n[.cIXt8ϭx۾[DTy-0W*UΥpRpUs0@ʁUQv~?QYWXф1`,nYa576D#Mְd TR/HUoI7Qn«U/stt=9Q-T.^zxruX2ͥ ZaDɤ\mB 'W瞞# Y"XѫjU܈C>=zڈyb0uv"PYU֘\q2j]*yjPYWcm~+V9*. mruQ~fYKզ=V*X1m@ZX]^Zv_v`✟_eobkT3+bGX ,VlaR稲: @ejxX5J_:Zb[7'o`;@a*6O6n8ztZ$Ξ ]Vb m?VxT_T.>a4HE)wNM6^|?>??mXİ-fWYI&H4! 0-/f}!&gQ2jXy[E9{j6*I*NUiXiRg}]*->ZoʃU^O`7|b H)jմ%ê'p)Wr,X$XIz2ĪShUE$-ڝ{U/˺gEJ"!X/Vyjd,]6U_V$xEeHQ(08bU\C {-zT()H$ W_auVbbU ,rZUVVZ*}:uŕֲ-+Q]V ?VyFXiϸ^Pc!%5~wMbME~~zkV}\`P۬ nβpW Vϝ>}K+̔O}4R8 5w(pROS+kT 1+8D%~'XLբrA&&V-.̇35VKŷ xlUC=UTI" bXaCw?;zcc&e"귲Quaa~~u+pZմ#; V{btJAQUˮ=I(߰vq Hͺ߽l o=ej,x`F4jKm\76]+[A!_8ѯ?Fmڡfi〭ιUheuUV*WwH"VmT}O}mUB}"m $Rz* YEFUd5U!R5%~V:XB}ӝ5신Uuj_\aH$ ۜ/д:x+{4q\u57=|TWH^F HtxϹcUF]+~]]j5wzVIOY Ҩ~q ZXegչՖށMށUOB m>?R21VW U"9!ulRbU5ZyzPՁsiyyr 7qčSEZI\}6<495dH*jWuHUZcۊ-~ 8S믴O VZ*P(ގ VOj{|q5bݮKcjUsz_ZBk %%;P%/WM^N)w5ިUEXݶeZb,j- sx).ٳjdPWVfV]WWHn@fVe ThtdcE Pջk\>>U)UgfauVV?QN95rL:ljZUf Usjoː-鋨l NN`V)q*A^-I8\qP$VI--U/ X wy C{d9]Uʼo5*ZYմ}G ,2eF~Ua*\燓]6UѪ\]^u@}PuXE̜B*XWωao\"\ŽH?*0*33/ĸU3- ˫,sI\5A&Y*9,F(FX5KJ6^Zj*(Xj`UKRY2/Udp_ ?=[B%* 2+LՅO%7`'U^\1ՃЃL Ybʊrӝ]?ݦ*R;6Ū׬*/ҺHPƱ0qTub>6aLJX=V9)Vߊ՝;w,)H_E<7R˔yLeU:Vah?픫QOT1CrGZuXuj4h_*H0:j5*W+ Z82c&]ca@|M?#PoſDX2iUs~Uzx x6H\E5 v =\Ԫg&&&@jʛEթa-"z4ڼD^aBAPV˸B`E ˆVt.BJbq77KbiMVMc+DIlLM7ًjpQmjcv6MMUss7i8o9<ȯ*Ug,Wũ@UXO*K\''I ^E/PU0ep2A"U/vYTZݹ%V)ĥ?ZXMAYV T*w ou< ]|ZR|L_й -ک$꣪ekVf 凜my)p1,[ Q@Z[cT4kMQ9&JbXd"D$.]c5WwWuh,` q oClb1=\aBd:ng-'FV8ԷU~=ew*SJ_`:o~ߓS*d-Sr2?(U;7O`j|}aI븍V$ZUy"E3a E˜޺b}{~7o^3Չ$VGN^>N*bv+`վW&j\5PXϞEUa*KT*]mZ\V )eSe>̇sϳp(lЪ<IUIo XW*K,z5.7Ī{(VHZͥrQ4ՄV3  զVrGT:aja\Ә4F8xL5 .!U= ` LWUMR*VN %+Nj?@C8Ց+ȫq=֠Xզ =QK]]!VE.jUwXMrTe79юeTZ6+EYkܩ`$jfStJ7k!O;" ۔,Xhe. ;ɾns=:1:B8[`ĢL[sm&.J3]>re %)^E/V^b;,- ZuyMXŢI*V'к *k'[Z3Xώ%6ԉ_D>+hy`U_e M#MU CU I_A$(첲̼:W >UhicjS '[\g3swCZnDk 3W-#"TE~-FUuW):A2gb؝ߦ9Gkx XUxK%]2Y@2P\嗣*R? a4z5uŰk13zHJVW/h`[[a*G KVЪc0,8@`5XV+Q3 ̥c%Cdacǎ5**/0(mll!U_utwE+TKwc9W[U]GYb<@'S^Sۃ&0j.). \UX-#XWV+x[*VeʰKv;sbrʘDVg1pU9Xzr)i@DB:W`>x\g[9HrΦ'_/Ѝѷ?%{Tk)U# 4lΉU6wMEH۬,Alv*p~fFk`z Ya_Z*tQk1n3[ƞU^z~/~,֢QUPţ_l$}矚@ըj0-M3vVXUwk- mp|[5ZrM%{VzP, =h=p`nVGNsH.36V7MI2K/7*b}krƨZ9ۚ+*D" /hKn0ZY)^_ͲE NjЪD:֩T&JL8xl YaTc]]SU :>EV&V ԻRuVҴ}#~yGj.ˤU}Y*O9~:RnKZcWgbdi+'Z\GAu(v0y-G?Lkj"(G Պ+)r40 'kbi,ldAHY沤I̘g7L4cg<^)X5f<=t{NE=8@^~dRgUwөGBPD25+5115>s=HwO4"ujba:5}5>O/'N`i*/T)?,WVedaTIqTﭾF*ըjIPk]0OT#G:h< \E#nUZQ( VѣZn7L\4V+YPTY$JTb XGߗ!L)ugn xjj*yUTWWS4⚺(9ۢfꍂUNJv?Xn`8oUW*6Q>Qz{Oi sWoTp`4cXlҴ5UV{*ˌ&/\.ĖiZ%Xe(ɂ-2ٌ^b_V qu<n$xj*QUՒ;&Ϻ+.T j50F3W%ޥ87QQjPvP@f6dE+[rͅ/FU,Z%y-TOO-8 _*nLVCNªCX;ĪU'h^_r8XHi vmպWYSĪwLIVw_\WXA r:*nQ^&5"aӖBmԩfjUa%G+e0L2Ѳ;+U^wV]R5ͪjTUZTV5U5VVUlȎ}2j̭7oڼHX]6u* bivLНܓ*VZ9E;@~%cuYV1DUc&*vVvfj{tKpFȔ^1 %X]mbUbWiFEe, AJ(tO_*&RH>~/N7Ըt@$24B*a@IJ ÒʣV#[wdhMpR󨅓 e}s3Jhn] ' NAЋվP˯c/ }N65&TiڟV#djZw! V8^$F;H"JܴE?yϢV?2^T$ث':X^`Ҡou,_Ae\J L=8Y#ɆVN~[=qYKH(Aվ=+,|h]evʓj:qa0ѣ MJpȋYf|]{ZE[NZ>5"uGWuʰָ, zpJصgE*UZR*txUȘb+5'xcߖG5qyl~ @b_[x~Uޘ4>M}uwTREtvu%^<_E}иS71fyy{{`V{;uAzGZ_arCfPM4zhTZXWOm?Ej@R )]iVTPVYu{꬞Cc\T"M/\_(',>]|]Tg.pڕ] rTuY䕋A/)%T&L[rO0Oq#R]RMwsUCzselٝӪjUTR`*uok@ײj x `+_8Bjjm4{v誒`s_8$ܽV)X iW[Q TZqJzJXՎ:R} ,nUjjKYֵ^3^nZru#FU}EglU.焬grg8J]rӹ֍u5UMX.hȺ&Nߋ=ɰ_)J \"Jl!8Ku{+r}~.%p8 RA`5S2~*SO0:+η%V>5J8QU` r:~$`U*-W6A3>Q:SFy7*¿DbL.}΀`U bP]BڲH֢jɽeøm0˼}Ę|;Z!+X3HՏ`_RX)wDzZգJ&%WOXu :_}UV8VO$Mڵjz N{Զ{_r*UA XPM: 1GBnW9X $+ԷRѵ7]  n_e,ٳ|U*+jjVpz18Bf)eҕ‚5Z 1Xz`nXA1_!FyNF3j8VnU&TzhJ3V> (VIU VSM Hjrץ(u{fjVKj5Uᤢ\5=KSCʮI講09|W3~TBF'Atr ^:2S Ԫw5[)]?N %2]iU \ŀUV:Q;:ڣ [䔉0=;elF r̭;w\Q=u/^J\X& ى\_y$S}kSi \!XՕAΝ4&j5j.V[⋧AJ2㩽SEqJ֕V#TkP';NZ5+`tԪO*:hǂpZ[#T >#\ au|8 Ӯk|B$j(2+Z#Y eZUxC]`5V f H`׼2ӎ=s5XWPOZ`MpO[5X}?UZɭ/vo?Wp=V擳TSξh,KW.SUӫ]xU|Zə,aUdՁNڮd U' {\brun&WER/êp| U'D*j)V/s{.+ P)j`XcUtfbXteRRVR)R4]a sF0a!UnꇢU8^fXRg+S[!5'(F\N %hθk<8 AYK0[mVV9U6  -XvXT_ȎA pO`7:2o@/m|H}u.yfQ[@"fbWӵBsjx' V[ @sYZO+F0+౪5a>c`u!V󎂫a~}}jZݾf] }{*jou'sM}{ vX٫~g՛OcUYժe VLX"yӻPm~h v_gQ]ɸgn;'?{68?_euȃ˩B`UY@Ŵ2VTx.fF䬜z*[VYuY*Tʭ X dY^U+lA>$8@WS;UZkꠅV\/pnZ$0(["F ڬT 1-XJVx˔z<~ϊuUjUq3GTߡΟR?1`X-8U?Ճфjh^'+OojX:͋(/,.>6#l~J0FʇBDI&#eȐN2 FCpkDFRȖ%VEjYB@5T D,P#.VٰUfMW.*tU<9{fJp=9Ó.Z=r -a֢R#򑏕+WuXmՏ&z@Zq7Y屪á##8eXu@ x݁ZцRP5Ī~j߉gkvnסH@TRU*g-:&WՇQ]8ǪjxfoE걪bê6 X\34*[&cMV,ꭹbTWt! 4q wZ]bLB 2$f!YWz)Vߣ+EЎQΰ:2zEb1cZ^@7]돆WPsN`1m9z:'zVT{*:.jﺰp5Z}dgVSrò+bjcjSZJ+zLƱjuנj8V1P.T(}:d&0UU*MeDvf c&{AAjOQ[EA5+ UN&+%f5X]04/$p5oX oou`kyy߾hV<6+7tR@3eT$Vj|""X1yUՓqzU=VHz8z޽9 P~ZǐݠPB/6UF@GR$:?Nfg[i:VwE[զwTm[D_nV!D^W'xLV6u'EĭW*eGcލ"䊼EmhҒ"X}jX ǜbj N!\GAUTFs5#%lIciŏl=x7.d"pX {pj#V~ Y97.k[VO?~]RV?UO1:u?DV?jNh b+֭[!Vm `Uy;&4gM'qdlAQ4huVz4elw@`$&K$Mx5s{=x|{߷mɇ{k\jUgC]w;tT@7F{jXb\mUXٜ3,F+/d* V=AVSr+J֚RV**Rl \(W^~E/g+<XǁntKx-r ? AfUsp2F2f*y?"ɡp]XT멒I%Vc`e/-{Ǫ~GaubNXMO/+WƐ>R(d `Z5f,}':QO%EcU%>~ uP4F[ꢳbCOA E#u|6}*3:FAۀ@>n'Zo !4_!?%RR(ds|=P5|2-ɰszwX}lQ'6ߒsAT-b5U ՐN"q~HQ :c IDATTuRT:OԪ8RH jTVl]pO*buMGmӂ5i.`,vWr XDRjgdp˯>P?"V;IU`[VXQG fOnպ_Z69Uձ:nzVVjœbDww=V?A33YOdѠ+ry4-Uw=.PթUysՠ_U$Y*Z%7Z֚ 7F'TŃ܇ZLLD" bּ07=$R5ͼA\ąXHRݎVB OUAKgYP+ϧSٱZHU!*vՖm@ ۓR51V\~H= qlMr?j(VGJEnzˠJ =:7Udk+[uìAV]:J1@.JYr[UG¨V-˳r=W"."u"*AbzPS"A;;;C*Ue 7bj a=@gT9`cJڀU!Vc1i^ RN@Uvb=??RvOD `*]b -jUDbU9;Y2 Gά8hf4 z,J 0U+W-QNQr*k <+jUYZ#PÐhx:楺Т NoO{Cb51\hQ@p5?ΊV]a1:1qm}x73h;ϋlUkR3u7XUL'; V7XN3~ )7Z5TZDUUpPB54 W[6uKnju&UZ UyʝT-EU+Z Ū5 DʳGThHc` LX- nad`jj?iX䪍QѾgX1[(֫zuT:<V/5bUDZX!''%juqu!5b0U*LM" ̉dF\izDDb]S*֚ТHhbf"Vr^ yjV3h`d(ҠDG`u)z>K~\ S?@_Yv T}GiQ5oG\ry;Î+7~s\3YMJmpub]m]9{>3~fBBNImznVxUƖ1WgF#Q#X(:N{bU&ٷnM7N#|yZ!Vhc lSgtnUhymmMuk0<3S.c5kYN]mwռ Vj_u[.uXW%e-ң4vp JgX[m9Y }aTMr)*fV%ڦYmvUd^"8?|.as ~P尴,]ˊ禾3*BXIeɨO5U T jUo TW}:pSkEnUSUZ.G8&|g8% 2E.$s] .j>4Ud C3#yU?.o&EZ? O갢s`|^W'PUE\m\ݕ Vc@,3*nݶj3On &4.v5*qlUՍ n(UU\]:?ښWGGVKzI׶V R]fgul:Waa:IY7(Bk j5(bMh,f/H}mUS V9ժ,X2WnU\UHbiUHս`aG-FU(dC&VAnt ~?ѠVcV9#``$gV5bQU!-+&\֋]TUQֆՀX݀U=:]:]2ŪXee\t=VmVkV'[@mꫀ oY4+xhƼV-zs^X/ \!=7g zqيE2Jqjju'/ڵ V7 [!(йi>hbu)Vz W"ƌ9GpS}a =ɊHSxI.jufQu~7Z5dKKhX^ZήiKy~J 3`buj+:dKxQWZVEkXmjĪ&T#&{n["WCXc +Up,V)Vj r?J=qHS_笐pW:pv.ӂ܀+`0Xkſ V:*Su{#VSuXŻJŴLڙU[}A_iZe<ߔx@{=*UKDa<RPЌ+,V4:jqU֐&[ۑDH2LFNx`JLh"1쉇r"{A0md϶Ey;|0kוvS">5+”.=@jeʧBUܪRRU RJU뼪[[X]Zň'~m$prjxduwZ%W^fg2љh&X*'|لXw`*V(V;qo`U]VW%ʴE_Eʖ? +/Ik6-e{[ )XM&r > 7X VUq7Wy[UaUVWOmzrzȚjOU!ݫ*X(`5D͙O* `LyP;`cjq$oѸ, H@KTr7|ỴoʲePn%@w]i;[aZkU Èc_j5 s, Ū歎ڛP]@svTS:@jX7yͅ QdVWPju>DPuḘoy +p۴ŜUVjV X 45 V[-XwVX(p!dXgZt^Zy͚X*> ]zj6*%{*Vh"AQJc7 gAIy*~y^5ՅUjْLN W?t嗱XnׯZRGTIU+WVVZedVs%O{ jZZ-AUARр6ƊB+XQ91P5U[3jrJUE!;WgF8Bc+A!x#u?F;cjJ[6ڰJi9Vk 6ޞG,PZ)UN4j巪 }]bVZ5ZiչV `eZb*~,JVuhj|M'ucQVbUjwHV zXT3hgHUYDv궱QUPUbJkZe:Z#`W]bjUos՜4*[SBPk=XIU_"@2Iϖ0Ve[YU#UWq%S ,-ʎOzeJ˫Upe:@䠊Ue5-_,oY`|g)ES񙔹H\[ѸS(Wl j1 2˹ ' U3K\j0Ͱڰsιڰ***p0Քq{K|+vRmV{#%JU*A__}9Yg\]t>f`aNXQPjPepvaJZ*o=Yd, LK, CR@!+DSG iaS `u+v?+TC`:cU<pТ `J p-+ױ:j&[yƣ`b]wQEϿ^M>X]n6둤R{@5v}s@ `%q`zYՖ*5j!Px| 5VZQU%TXUV;!LPbͨx|i - RIW>p9/_6xc@(XM4bphd0e~r|(2cc1wn4L:|Knl;ת?y_l¡gja~4|C-sEР*wmL>v_\]x [J{B I=`}Y]1@W9jj` ҼzU +n& P̀W}+Mr\%VoIbUM WrKj3,zS{ Cx8F VX= XE;|SնFΨ'T%V!VZmEaG]LEUX6*JȮDAҦ X^l~L>NN_3`sQa VX)W{O1Iu_ U~a0k!GzTTAiVfWu4)cr *hu275T 1+ƭj/cS}. ڹM"VgUV/z,n*nhTU]}{X}#e",W5mqdCA><|܈vxjU?4}tD S~TF`.;/%\@ZFW[V ">Wx"U^W^mU  `bUYU6kU!fX(3cTGD+ջEUb) oU]A%V!jS,2}mW gHd,׮rWw"4m49L{XmViI*h:W{6jOONzeb7^eXEa2\9)l"@Yʖ'9+VrrQ[fVVK\h()[AQCO,Xj{) k95D”%cpzY WXmxڰm XeG#{᜽nUAHUHC Qz~[ ԏ\M{rU(@}5|êTAj0JVR^eJժPuAJe픛ªj:p2`~w հ&* כ{Y \>V11X`UV3GU  VT574~ս{ށ䷗^@Up%/ݼjVfךvAZvVd|JժD?tW0O>y𫯾lP6Zڰ–VիVmS1\'Vێ 0{X Pdze"5X?~Tr# LX5ЫkmYQXm^"g4ڼKUTKQﭯªXu^RjzMRZBKVaU}?EV0:4\V\͎\?[ڰ\ AԔD]EV1[UXfRjT̖2Lн2\MQZIU4Vա_OxںQp X%8>?U8u11< ?tMi+Bj*VyUBj`5TZfr;ƾQiA&Tyluje~E/Sw]UBUpUbIbbjc z_ށZ4UfOJbs!8Zo 3xfCFS&NjfVQclMDȸ;eHx)a )x`jlB]P!{j3CiI\u.?wf5Bo|<96og{|/K^>T6ikY6În(}`zZLEUqrfU*q [TGz|*YQTcCѣP0r5>qGZ _pReޕ+{Ұ$O خxZ`r ϰ_j?UeEr8zuj" $aF6õ |#$fg'*jզEhS}f6 [}6 &aW\m-UQ1dšwGRenWTs*zw*=S\Tej|Vּ`ulaUR*S\%0Vg?GP62Xk`(Vj *ir4XVC8 @0Iuu*ժkiYYӴ7IjuEiׂx.OLUF+2e~x:ڟjM @ X5vᶶb0XEX`RV%V9XUaj{~oz.VzWpjg|YH7Z*V)%R5m+(VGгTte ж2"1FB`/B( 408 aM dʓÐƃX.bY'Wm=`nvWH-jM1Z? / ^$V-So ktjVZrV nEVUݿ*@[|a''MeUc5%VZQ_!PAUճ:0H Vrc VSeӵ^=\u jNWO=@U?o*S/FktnLڲXQ,j?!#V#ZEh,` ~TwMC7fjUa5cjV!Vg&*jjK> +5iFB! fU:h*h{E+' dTl8˄?aNB [[(w-l{ cuڊ]*g4SQR@2VZ1^@TՇsj&``*UW bu@@˩Dz)}FVBW'OHۛf8;N5\XU\EWY"UVB"{J$V;*>Y%+ UvEgժDwɏ\+?mYV/<,>(XU90X9\a+\" S`*.PkXauffFaJSǩUQ4ӵBj[*b5N#O)j Tm 6 6"$JCjHH9ti5ë`TubzՇR{!/!WI\>sUnqMqҩJ?U~);5XmխhjWIF1~n @8<}@**9cr*LfU.>=GVNjC:hELY_f >q_.`*To?}˪sٹj}.VV ^U\]ըVq7 IDATT-&3%V;"VzWKVUlUCBQĴ]C5`%iD~ϒ^XV(@aڮr7l_jWZX'rV*a5U+W]꺪]mi5ʪoS m+uq@d\Qa GA BѪ!TS8  !eQ*)UC=TEF Erm6:gyjjXmg^p D*lkb㫨V>rzc j5S<UUR*_7A&n]Jj*_6I%j%W`( ɆXCjӵVF<`vFjMJT_JΘU!cWF4O1ՊOsE\m`E"cUQ y (J@X 3J_md!_i5.+bb^JFVz+sUhYohclhrp5M`Mi5ўբƥXJT?RUb,Q Jb^=Vr]ݶWE:>UP<`CDFSVscUח-YVa*ՏZ _Tj6ЈwҳN[T$57gSQ7UԪ U'iT):nNXs˗cpV4[~Z_"v/*N\ VZ2V]g]b,~.UWVV VaQyymVU} RDih4o|N\W0dGՀXٰ\k*+WBU]wsTVJ'=ʱ5Lw%~N(!OJaXjNB>V7 zS[v =?aU*T^'KKO#;__7_ U,6E kXK`:,;s]JYj*㵣ՒPHO@Ieq Ԫa*UpރZ-ӍSJkj^NeCI&c{V!W#͊_5TWZd:dHU(`*ꅥڷھr^*0g ;6F{;u_UHr?JF@=X= U7LmJi5|UU^d>,8Y_ Vj/ŪnY20KV[njً##٩?M,V!'ZIj,WOFjy%1U(ʊUz,VxT6@ܔ)6͕URǁյՂzW <U\!|jUkXv2V)JEpbp/*YUjdnZڨl2 0;鱷JЙbjm[4M4w%P%83:ZIhƨU`8?rHTЪ0VU !jzrUח/R+Rvy͙'ϟ=_{$VGN}rxoN=UJUbY2Vͭz4)g ܸZk̴&冧*Ҷ?%$J}|g VpUSDXkXQ5b@d)mJD w>w?"n$%ϪUdjSDUQDBWI#^UʻT~Ϫ7Y{cpUNZX5+I[+ Z,jФtx Qi?8N] Ru/Vw`u0We~((VO?tU;jdʺ%E333ӏ]xOqo&aOF75OU jYe+ bd)ej?ߨ U[Y+Y(_[,T\ qDOZ\ZNV h0bbn* PUkW+0ȁX:ea[^le,TIVhC)(/Vdg.1\I!Z@۪ixjxfmJٝ ʩSE ls|'4'Gr,wpVqhA c3VdCjd@v!Qy'aD0Qzr9thuܱZ1ҝ r50NrBl6;gI ܃ڥ>ZMQPc nA|~3z{7nl||>IGj1jm\f[T?ݪ8k6Lfrj@5Cj T5UeܟJj[TCU[34RA*@LnVS&KnU ]~-.Vs`1cJk,׻ƻ6Kן<|8ǛN?\__\]!b#ei}!RdCeeS*iU/~2KU5p%|_B+DKY,+y)v:,=hzVXR5ِԅ3C@P-.ƓTO;ʋZ;",Ca*ZʖDd%w*WtVh"Fba*Vsj&Hkl."jJF̈́f3`0EXLM zWێ, <ϛ 3VeI%|_6LU%Vy-T ^ Ug w[$ځYjYYyz5k*0u-U7+vuTE?d^*9U5}LAs*uգ%lGW\>V5.V+.NL.z.zۍƳ//?^OYhmN&V\%`zJVK zRC=T2&pʹn/#%%c\ aU#V#45#VuЪW/'HUKxq.AZ͂J xU*q]cĵĕ::ɣԈ8$TQ3*-V PlIOW9L =LTR}ygj&\yQ[U:KU_ VjuHFn Rɋpi Fa:10u@zL~!I#2Ma Vwu_yЫ7xVg2G:Q@NzyXJ<}'aX2}4vczaT%ѪT /?95X\jlUBQ'NRV2F`*Ҽ*\~G9ET9%V%VV@秵kGd׫ UoJ<+ dYL}q)>m*ƪP`e6ŪnTj{%stAfaHTHv`eN;m_UĦ6}Ŏ \zo\VJ3@\psDwtYv•<*cXQ؆DRyzf@N*nUb*UV<BvFEjJjsrt돻ެ-0Z#$k*2VZmU0,~azΈU-[/e\a'F~ZF+e(UQJZLPuj륁XDUv&+[-Fr;Q6 8*GZ@hS-Ur/JUרpTynREQF@5Ot+*cUSڨU$XSX`ueElW^m6>yf VjImB),etg_⸾#S##Sܧ-J@U6 Vp/dm&ER(j`*辡vX TSuh~xNJ Lr݇ @*۪gV{ðyOBmnY4d@uaA:ks+OqWGʥkJ]?}g&E Sq0XIX6 ["*;OtB>}X-EՒ5֍`K(i78^PPTȦjx%+ p@\x^\g닫/,V/-V%rj:)%Xe^>2\-jo\M`=$XPUIUU xa( jS~ WۇEJ2)5h:!eLufU|pQk5.daG!^~W^3qJJXib2Xh9Z#ą Z+8CxabCmj|aU/ϥPV'uϯJ`zjhHu`1ծ V;dD`yqeث<+O~5I@l fB;PT. Pǒ>U**^C!i U9!Մ`"O%nExh 5Z$M1IhMž *\>^'24}_l3Lͪoy1lʗѱOmnў-΁yˎrYnOMHx@҅|ot1V M*Sa< ]{puvv2*Z^|/0.gtWWT NE ?]hg iB݉Y?&jbLdWWFGò n0 e@xa4̲R& ^]XAl5h/D?<;Z}f&Nfa9α}tFv&6f%?Ʀ[*rD⫪V͸G6 XUsϔ Ow qT;jjXm`aѱ1b[Wc5⴪a@k{tZLNy&}_&5L R? wĕn3oGYqWZ\ezE* Ro}”]8{ Ftֹ|SӶx {Q窺Z*T*mr jw3ח ґGj4UfUj"0n:s5- +t{U!VW{Xm=~Vj>ץn'W{lSSmb@>U+Hi;JW(sثLjه*뷾Hpo).Vpg0jnIWrބVW0+nR) ty^k;Y{r*i"Q(Q*YzsSWUb@lWzqۻп  T|*.LA?KUΩպ`-*C5ɣgoT11`JmsP|3B)SJA5cbqSq r\0qEVvܫ?^-EJbkbsV7ޚ 8Kչ|/c`6zQiP5D1m'(ep*h5L~NvḃX&ҧ m<±jZS[}]MyjU=Q~FH2*-Oڼܳg\ҷ.kjZ[1FXqWepjz!8|C,ՕER:Yix%Rv (\MԣG]!nٲg46bUI" VFe\-e6:iKH0@3;0JLeSru%;fհNb^R;sV7Whɵ`E\Vn XTb`u( ^MZLErU+bEs !X%jU 0dM)MPUf4 fڲNZ*U J!Tb-`ִ~́Uly ?4Z Nˉyc ObMEVj_8dQDT|a*=VT=qTA&Huj(sCԖ jj5H  J==^\|ٳga''rmëK\`̙*hja?gV1Ƴ0U+%e'eDZR: 5!w(NeWP" `Jj+\Vmf Q*ik瑨:6=PEVk$Vꕬ^K_vZtN+s-VV.Ŭf&+ VU u*V ؅Y u U<|֒j$dH `[j? -<5KdCcNU"T[efY@㮴,Eh:x)B,YWђuZ7wTN5i* _b)eO_V5sue/ U)UO^UV7bu1`MNѮϠb-PoU^woE`_X%XYMk^H^U.*Ub%dRt/W-QHh3k;UxJXrelj{jl%gHS;;Q1?cJ`ac{UVnz]i&6զ\@2]h(\%\S*Vq5fngWPPVfH+բdEfe WCaU],V~ZM$"ƫ3շժWb3pȃ\ YZ!U2>*jxK@*~ ^Wssn UxTZWO1Wl `%5=/r T_΍4TPP[h;tXn3XTSG'c;+[Eɦ⁹U@uf'DY xP6OjkaՐXբW/^]=˽x)z\N{4k}WKRnjbsL҂ Z)V;Tmp3ujO0ȐZ}>co;k9"Xj4AΊ*rZxKK\PXݧX5-w8 ?q?3|sSGJ`ըbΧq!le'qmuFu$FFX'ZVVfW-n%WgyV6YDf{[:ȇc訶td*ʹd4泓?)}j4}3^ V3 T,53&m^sX Ys5Ov@7fE ]DՓ )!*3zJzoyqRJ5+S-Ke$k/sߜ*Mn.72lVJNMPTLcr9f% }QVDkU 31P\UHuj՚,7yx#: W Wz`IӗKs>VT]u`0W]r;0mPJx+n `[q.U.jo U6$_bV YVHKkK|)+X9:VV-~P%mMT{iKǥK3CRQ ^ VYrADg&W'&H"ڀ*Z QJ)nK߭+Af zpU9v{X-x%V}KEh8hsOTUoVWj|-WMoUML;eGտ}*;kE 4I~d`Y[y*T={ٳՓf$RAFnS=8~|XC|t|1SOJj,5? n.@ =6X*䪂h;+O9ދ@Qd .*wavl(n(Fhcm{"0m .֚Mx{fffa4]u;smb 9{RUa갪Wx*ۨ>xˠ4BejNU?\Vn`[AZ:ojzp{7#65-ڇU*з WaթU5fE M䚴rfNCgf~(YPwt7JUjP*}VP5()[3dΨnεT 7.Zji`Of[o4 :1\cwoM*yKl OҴlE"$`B\%RJ:JjmwXXTUg\URinq'WB!D uaA7\W&&P񿍽 _PZӚKT=NU#!J1 p,UCZKe,ZZTUpOW*SĤzjhURIi{j6K`EF_ V%[=bF4ɕJ]˶U >ܺcH~ժ T\5\m9D-Ve[5buU 0fև <<<@T7 WۇOD {-V:nm jZ~= }+_SRDâWq%lf^A!t6sa5G'T^S;A]T>q 77t_V߬Qv K*q^D8mw*:W:Vm#X)V? B՚HURxT]~KSD8E\ukSm% wXPt Q܉X"U<94PrXYS9e/ P% W XWW>Yy\gh=\ lZH{jjպ`DvUˌ9@*qU֯zXToiP5V U#U~UCO#2Ib-DUzVmEu Qy5iՑw_{ƙ3g^;FIma`R]dTdt4287HE ⵉ*<#IKWPPUt_E }/Za+Gª` y-&j995V+Xϣ*l|JR {5 8CUUDU*&b/Ý|Xe;Kj;@jX}@*|& Ujk%JũʃV\cyRmuu>[%Qcuh,կY*OYЫXT2dyٱ_XKm\%zf@*V1ͨJs@5cvvgpxj2U7P cuu ~jbu0 P@2hԪbuȫ]:ju_1Cx 0X4ǛaUMՀڒx21ꖪrwUJ*. TRbr5V\JnNUtX8`A Sԕ+Ǭͤ@j\mk r wzH\{_֬]UrըUJ"jTjVJV*zQȊQ۶' `AHYwGneV*WCZJV2`KJÁk@Zs{ɪ KVVۻND4XEYukB?UA+*RQJOD `-XuNM[F:"!um}d&dzLL[-gVǪ/ j_^ֱwy8.si^LL"6hYgwYybUj]Gͳj•*JWMڮ6cˬt Lit QLqr_HfN@nΤ0Su)"A图A+ujIVYE'c6(Vs>#U͑ua5S[: /3P)kPNߗ j kZlQXn btXz1}k1_kкLRT_PgV@6A᪓ 2 @U ±wJTAbfF W!Wo@V`j4NZQzOY5bֺ]v  V{ܛ™cj :NZM{B?dwI2!P(Iճ/emV H`ec|Vc|(,=;e)i8OGbԂU +(xB=_5%j*lk C\}ZRpd*|7&m(= ARETjt<?:×|,К97#PTf e !j.*iQb/U7Vp_9*ܰpUU44n݊E1(Iݼ/jj=jO|ʁR! &a%WFflxO/;XeN;hIT ʔ&bWD4pJ1 @bacU\&fjU5m=gab`rUjU#YX?#F[T'yS/wY1m`Ve*QSN$V1,x2 UI-qN$םv5;d=~ѓVʂ5 31Ԋ@~!`֪CfakjnTRi p_ _8NiuARS=ȑlJJAxE*YLǻ[^h#C5Ev "I IDAT.ljJgb:-1 K̲%Xӆd 6i2MttܜŘSbk_};yH8^yżiO1EUpd!r^7VrRPɋ*YED:g9Ǭ[\bLKbuXɅU A`Jb=ZkMf@A'VU9Y*yaWO1W)&UYΟ8%+XBW]jb6VH/6:I[Q; Vi $N2VOX`u#QusX_17RcT%F@ZPq]]Uռ8o PzǤV ptrqL`m,W%$,I!VggϼTZ]]'Vbk[XXuVKҫZuaST\1UaLZ\K+IXESX`~iWX"'J}LU3U6?Z;ŊIns^8١9j,P5@H%CmU,Z5:rUJmVz*o U&AI$Vj+,-0|#T X~5S~UXQPC ,sP+^-EŽU5VI6_`u:>pl64lAH.vXu3Tp=XPU'U*Xp8H`XFhU>-XjWWTV U[ :d˲{X1^eUÝ3Pڽ+[;Ol}tƲ>{2a%}7a~IEɵ;6ng\jىW XBjVÙCڤ,XdUoJmƪR^]r S0[o K;*m2KXUW:?dtGъYiCUXeR#XuZ5G(6Xq(Uwʪ,ܔP⩗jAA!-DjЄꛤ3qeU-[\vvnTU|ªUܧ8 -5 []#X8.!jaPxz_^UE@jTes5)XlVWB`+?2~a`zqzufjDBW]e:,B5`ŏSmFpzVgAZNPv1QsY(xgj>{YSO^~X}f_J-TxX a鑁jwBTX]ULQ rꐊ^|<,X5 *-&m!*SnjN *|q=_;|gԫlNSVF1&}Sܤ(w}TlcXm~b\]U5Tm:8(V)9V^"XUVGs%W[Ŷ*WlWd߯KSX#)U?j@+PO7^AXoz`\;8"+'N8*a\=%˘AI0ZRZD^]&@zE~ƔZ:~jVH#ut,cںAqU aTױNZt +jTo dP8]kcZõoWVZ$e4mr+_%8᜚ʜίҙ3̄ZWҳ3~J>tn~6 ?@6vnwV)YeЪcKƳ TUue/kӡ!PZzJ)m>\][T}>TА$UsDm/IU^ *Da;l򋣒E-kS.wS Qu;zv7O"Ve?qꎭʚ]Ua6XٵbHUƪ  +Uau[?[MKbua`&!X؃)aQX4"Ʋ6ID2lQM*l.rG8וeU`(OQbY2,'P-Xu4h%F?P5V WiZAsD61N\1=: V ga9@[E۵aUY\êڬnsJTBV#PfW֫d0P 9>*V-X;=W4MP\{\wѾ/aXEAaUjb֗s'Uo *-CKZj67v)P*|3|^ua4T̖SBojR:X7|ʸOժ(#{*b>jlW.b;Jibʝ~a$4VZz9Hd53th*":%U{LJ*ziM)" MU\w*]X.J۫9ĶT/X .Uo$ە70Uw X6 @Z|UV2mOt+DV@P*@5YZ VIuC*+wq֟XDJ{*9l6ΪJYBz<-@So@[>vHzX-WG.xd ԇjcCjv[MO߷ҏ W|V?sc5bbUS5 |,.e\`6OtcuZVzǪ^wo@D&⊙%Տ}2P嚛8]~"T͇U)UYX%b;*NUGo$* i;.JL~ZK,?* &4*U;WEl튚**'أR(0ю" lۓ,smQ()GϿ_c*k~~ߥjMVL2DwtcJcSW]*V;ceaH Qwk1RU?[s*Z>}X)Kb5gr^*[ѣ(b^qKUmC65j X \Pj]Vn 7K\lct(|qO_Bon%B7SOU[Nx_;:Z$2X|+jVM@-Jz*oYtg^2do*: cU>ZLlW0!b WբJX޻c?=J&" $ªR5G.K̪m2d MsJaujs9VX5JCjXT1Xb9T?_Ͻ'-jy34]HX]t&Ճij(X ʜZVb`SUUጠU4b/uS4nY1I%ꀤuHc57$a7UX X ȧ~t#nV{{hZpQFKiN,"O@Ԭ.VQrĕQSQWԓwEƉĮ"YWYUpQ@r]][&P:N}uiMdsCNC dC!,nn= Z%X/<| F-P[y_ vrq.꘢iz-V]X!x-->G[%>/>yƪ6Ecur=#-b0K Vϰ]JnTPU VZͪ.[ⴕVUXulUv>kcumD\Mզ&,J/N.NQA%C[ *U.2VUjp`5S_PX\ ,t`b9;.׳k:|Ǥ M̤;Ӽ QTna𮶂r.q ]NdzcҫRwY1VؠWR"pLU??4ԨBJ$ЊZimLU͉YjUQVTc^=f1NkB|%6VHzv!.l?.`WFauIF|ӧ`8L+{$u`xaVEZ2V$&ZCAj30\6 W=\Qy9}|Dϙ$r=bP+8 $\~H]533aNjSůV{`[[xHf`ljːP[*J3xlRǿ(ª. V힮v/z|LjUq;fMn_l]#ԑiGOV(-G |Xycg]J@"J|ݙ`',v]c$UQe(bVvUځL  sWUoJ"K0*\TE3ՓZaPM蔼tb1Puk=Z#g,kU Iv]}#T*zJ|󇊩D/l |~UU* UDFK 8Zi:`j 8}E3RKʨTmUx,ld薺k!w{ 0K988ua&˅0YSDz95AkV8?\]ay5!zjfν{qb,MsuU=tQXb15CV朿Ū1Tv*Xʢj.J}vPVqBWN|g=rXS1W7VLR,EiPQ|igj' pؘҪrB 3$ɐb" T 33lcN׵ c*oC*Gꤣ;0}s[l?.4*h%/u`I@MCR.3HwNwؐd 9M\'^x].qd27sW{I}ޏӞju=:P ;VWA&X֝O J} 3Ym1Kr0%VՁU xgPuϴ>X {g̒`;[3V>?h#an`HSa6bkULE}[8JvV RzI{8- pX[KAUNJRX! &#=&a'V1=".Sr#*\ﱨۍ"TqGB}UՑ:Tͪީ,|%^.Д p9`&͆! -% 1 VQ#=hO ^Oj?F1ꪞIQ3aR"p$_`<,jTcm$VU #Vﲮ9EBZ(ҀD=Xg8Wq\eE>auwXOR)y w GPݦdJ2=k!R¨UJjX_1W8tnGueuheJq `u5B `sT*)%PUWS2뾘t3WeA7R\YR^N,ߴfJr :\5jUnETy"@BU>24-`*cA,jZNVUUZl3rX^VLe|5>UqToPU3N-ja!=8 yej5te=] ~Y)J-*\m"@ba'Uqm 4UaW*<b1bIK!ʃܕ4:"6-wכbDD$@9+?dS34 zTW呈خXUɐӳZ4d?˚;dLs*?@zjЁAA-V:Ȓ^vXϫ+,ܾ#PS)sLLVe|M55qrq _)9OE`gJV4fGPf02VgIþKch\puU)d:|x,).]_>"WQ..CctP̑ F*p^UU;|dYqdxwXe' .F8U3icD@Z5&+z͟0̪f`՚hU_aUhWVv&j]Ϫ_ϥ$ ɡIfc@byuW}j:$=*qZ Й6"N[4T ֞|VSUTyUrdmTyo%Sخ R[MUVYr*VɲJ.ږ*`ԪYUcժ*~E#^-CG@z^(T@jzFmwPU&&n spbUJC U\tLM?6L__2^kf;>XoBK*:77EXmnQy2nUVww@>epuXqz 7V5sbf Y9VwVHKvÅ5XZ5eFv䵹s / {rqz0S7`^J:LOª3ZR`>j XA+ a梸 U48\)yuZM lm\7H3~5s_R!ËZ(+XK Ji@_ +12&jfάUij`Rb˧\(CDV V9zŇqZm]Vd'N*Ջp$TTU_kA +S<ʇZ=`iIpjR pVUqըՔ:/ղ?qV7UªzT9jT#`oCi5^n7VXJ U_쇉qGz*Pri~qxbX劼 ž`0-T{;eR"Ji|tD/U*R1'[zgRq(ldJ+l-NVH#~+ǎ:aڏ~35_s-' v%Xw}NZMXSv%#-nCm$Kq'OwCp%9O|Uꓫ1PUP8܄[,X%HT5s,K\y;GU4Umbߣ:U#Tg3=jX˕s!zP(lhgg?*Q\VݟH3JZXŖQNY= ծ.Jej6X=#BjmX@TŸ>@ykS ED+aٽX}K޹DaPG7z:B+Ca@XX dtN(lҤ 7KD.H\{gxm;ˊ:(A58*,&'ep0ixEE('Us9+ wvqx`(ulJ⮕j0, 0M§[]W'Umt;hSQ5Ţج+M3 t$rjy\,c;cuVXuxϩ4gOHU{  WA<%;ʍ~T?#r%3%j+ CU̬0WrL BatP<sU=zTR(M6o HEڱjԪ bbUT7A$9FW9 VZ_QNYZ!*kWKfYX-`uIƒ!EJXVx 5VV6j*~io9|'B j(zBP!tNO?bURzun;qEJb2T/s/QG,&l2+<;VNFȧgS``*ͭcvĪrX.TVVpTMg vjչ{E5*S]Kr @܆6ҥӜ1Xt௔TYVy U=O`-2lnUeUU!~hjğ44g,jXW6++/ynUGZ@k2$T4 TQ$,jֺkjk K]8BrO6U#BV%VQV0% "W%Ve<F<)+syc =6 xK(cMVA +Z:FT:ŊqV]Z֭x6_⫒p +WnPW U4Qɧd:ଔ98 Zuy3\*-*|,0f?ة:xCQUªjUAfUYU4 X ;`UIW@y#)gƛ|#x_@]RJiczT^+₞.Uzmm[VXmӫ6٪jv4`'`*quIag/ΊWf&G'w(2X~\U&U1V/=* @l*R j*m{* .>Xea.ִZT:꟯\SruqZU}\X NX-*K%ҠERqUe(*IUOWꪐxV1 _s*`.#hjHU8(WowyVg_XEsӦVi^P$V TVl`nS`Z_h`W٪;Lڡe;龐} ƪ *Nn*7kTU^' T%O%Uʵbm/2(Pl*^=t 2p߰+ª촀N:8feYZ˺pr~}LτܲsyHA{qqõ*Z5Z`e &tV1eT(*Rw .AlW?:۽Y+ޕW ;<PMWtwwzV!*MFdJeTTzG'VUш/:"IU5CB`bVBޔXQvsOsnuV(WEU{R[3BW*Iվ8R#j3۱ڜ4 XJ-SڻhW?PX\E1XEf2Y)MVN4fqa"2mEi,#;2B343[q71#H7EK7l/d.l7ċ: ɒw9{ۂ]gV+P-Oy~ SNb*>2UVZmDDUczl*9'*$U஑Wmnxs9 J"1q R!!?S.O!uTZ>F .)ic KЉ\rccUWewWV<߹Oڡ;swmUC:$` $哫q\$҇eUq_D 4=t8VYj\$RaUW'XVrNYT])m? 7aW"?LTYCTE 2 $UWj'X/ jV@KXjU />WD z;{V&qP۹Tm65u׮Ob6Z2X Vi4|\=>XƿZFsxɇZ TdteC.~bV @:ՇL_RZ,]??t0[{M1z~z3x?3%W\~;& 5U ]%Pm)<ڍZn)!*S<"uKJU9fel"mfQr`/i˻|`]Ս$O -^'S[=oSdVq$o_QzFX]Ϭc olU S jjUCJSc_B587NVg9:ɮHK\[- AN/"Ëk aj92PS=q̖]12OӰa3EuUd?KCu:c ^Jt).u9) UOQ'黕1s$6f/k*Pwo ,D*y[UUm{{1Gu=Z]V_)Q?:Z^QjLbOccKGYSwZخ+9gm$TWvbz^cVQ98::&j%eg]e2\ d&VϞmycZ.x^2A %W l>Am\ VUX=2gϩ?m=hlzZXVgKQV `h;wzBWʻOgV;Z:~EU~QLUSA=3aEGi*9 . {PU]QyEp[2[`fF' LHʕ+_Ċdp@={g79Ҧ8ĭrSmoQHIFW\ejU[3VjxV1עj5H$DVM_5X{64ŶQU_WBuz,V0uj5,aI֔ʓ#R>٤`RgE_yZ/t WCʝE"f盍UCP/Z=YVF?-k=~dA1sW@Y5VUVvQU:=VU>)QU_IHd±5ѫtN-ꏼ̰|vW[^ufXչ@6A|xH U? :22V5/b\]Z) IDATdcWn {i j2U.$AkDVUknCQnz[^*[a~/Q4;m!" UE4vaU<]G^j4XbLcˇUtjp,|Lc5X\u*Bc6ICxX*mOS;KTބVY *auX4jLLL.Xm;9;ޏvKB[SSiiX/j{$ZCL2X=ϸ\"4Vj/SY` }+kU%V&IPNmL7$W ^?\WUK/O +)UNXz7_Uc5Z%=xlzX+2Vьx;j5>-] Te:fx$/6nKK PpXHF"eu﯃ 2lDRFi|Ix\ƞʬN"pNcV\8Zik$WV Y܊HZXMHAv B9*ܘڰVUk*Wo0zmfYf9Xs9y\`{aH'\LwVV&9@0^QJةGMR XUUT{GPN_b5zY;P(]Sh5qTN`"VɨUͭG츍Zu~"@""q'*pfDU}7?eH&zc.JtjQ8eM[Oq4Xِl<;U<X#MYG*U0%j؊;wn;:ܪjjH×X]9#fsz`X=t__~uS>W[ƅ&W+٭ꢣ&@_%tgnj5s5N3UxX\U*3`Z?X_n~,^"`uXLd!͕ UOxWXV> uNT ?&U$zCYA_W\]ݓpi@"zY1Z\ Zʦ&5V}՟ϱk)uR5`*6TڗeGEJPwv {mbQD,Zcc%cglI,{DYQ&i ɍpM`dźB.{!P aAvs׊7{eMdI,}s"[MGu*T>V|VMGrS|cM}زI˂ky\ՖU-5kc5fE޻ w7ZֆR{{&j;d#,Z`u•KJK0dT88`{bFJƕU/LNV{~f>T^)+kRUV%`'T^9<4Pf m>i~)U`ҎVY w9OfՁ)suVջ\#+N>}dDZZ ͛tx*FtH6'WUK%J* [iC[6A{I≇Us,4R $V]xgWZphN]'DjAWXWU1PMcmnR[O'cc)n߷^O;]ʤ=XjS0A@*RVb:3αjxrف=PfnP& {EmtU<:[48+E=e >)*y 2gb2 uv JY=E?)t*ճ7uӃj,`]aXK{[LQǪF\3VXC)3jaUZP erL`LH*sj-I6]4},%h8wvv<+W*'fA:TlC X5TUU\WX;:.qU\Tb~Vo`{CmΈ˚<˓Յ9)%QZ *HVrz\I<7)=L>\V/\%auH)y WiRMuQ#$}8gNjL,ɋX%2Hy4T_:Z#-KmllЎ/ V1U\UɥYTtjU*SI~Jݜ!rHgX:oV&?dǾ|j?AUA'em@ڷ/Vͤj8-t'5Thd**;FaihVjzf VߖX=i=SWO=y0K3V/ݲF~`%zU*1#VTl!\n3**Uřu};eqzYdvOUqé6%ʕ6pIՋWN7s9i!UW*MW#cI>V/'4GZ&j'pfU"J, kǮ@+Mdyy\sD򔀻(s95@+eN >.0RhOݑjUϮlcӗ#$KDH S*TƲՁj5oWDf./{`rT9Xe7X^.i9UF՘U V9s{ჃUJCmXYgVMs**{mUʯdWOd~d rSRH.c`*X VF$:2*j%C,b5TŎ7gtyπM.Ce~' op?P2ǖH!dg\\A URI8H*Aub]źiuX^_01hkZ[]a -ZZbuE4{+@JMVWJ|.VreVVU| qYkw{ }Qߪ:>Q}Ho*ȪX]؉D qY.n][mizee^֡,hyPZNª:Ҿ6n6fa|HvhƄbڴj3m2bJ΀Tۅ" 00 {rf:;02Lk&&>[8 anyj&U@0VcG`WzPV:}(YWoU];DI[ XZ%Ӹ4Wa{ZEn@aSњ3&\[jI==3*ͦ*"QusaWav9@t*늪.HBՏV>0_?}hu`(5kM2znU&VGHZno!VV(C`Wҫv? U+2PՏeՍtaIg.j8^' tcSr$UBUs>!d:5PiVkP5AorrV ՑRPЧ1F" HS)b^TM'R{dr: g2KΪԪva6jOH`uGZbQ573Mp{ 2U#Xr59ZreqqϴW<ցc8kGRQ:);PUoWӅȚ2VkߋVL!uy"bUo7TIY0>pCՁ`5ĨUCn֣'mu^U5Bk|U{gT SQ`Vx|*%WYTV3ՉԈ.D 2bV'?T=Ujؽnkk_J܃h[S㚪&zYVGL@5XrK %oՅ+XEeM͈j#.UfeO+HNC VWZ{CfWޟ誤n{$]Ww̌`7.V.|f?Z 'NP:kԩ0W\X\5Xu\꜃U #K KUPG*T׍U-@Fq\l VWav$hjrsh"N~*< ":ܝ`^Yvjj뱞rORE9pJદvDӟ떫Ru Ph=PjV˂ծ<Ī֩xYnyy۪lU*c++!+ժW(ҪzM(>j!ȁW.$:Clءz5],Un;qXl3 U2{ [OoC|A\\m@`URGZN06TԪV}:nWh{2NgJb"ĒL099MXźXAI=dP\qQ[Ӂ?K8RONU=5V VVjJRu4[-20z)U.S/^Ev jrN=V&s$WSSvk\VHF^ Tt @b& jVVJyNʔۮjXӮꀵ\nRU}ӪZE,+?a]YP.V}4PjQXcT}vcuH2WKEj*:sJ*]V6U}lCeVՈ>臠q*[MNj*g* 洴yc_' 6)VڱgUX хwZQwVLiV9]򆟷cW$c))XZʐ5L@!aVVZm4 1ސ!XU`z;P fDlSd{)} .jhN߇j وUDpaf0VzR<c{+TZWPj޿Ydj2Vg!\/j$4W9( U]PXf ʱP]wUL(9U@()(V*dV$VGu9jOt*tn``6C[EU>}*ZUvVWs}%Lֽ$J WIVQ!X뾑mQ2x`Y7z[\0SQhb5D"ᄰzoŪRZRw~k]v^[+ `Z^ECQ$Pf8mm6KkrUܻwL'>g CIշՍ%duc8LY㟵:z~s'*|o&sLX^%~ .;wvy9 MMjTm=OLՂdZj+z* .Vv9좞l/8IT#W1Zm!-֫T5V^WsKDR`QCzwB0).=aVӅeR vVyu|e*`mmBiDlC+q;a˸j K%x3A? W<vέsbu&k='xtpQ?F2`5'W\oJ5V糎K܅շ;=V:ۋg`z*Y SL OX~clɗI Ŋ^ Y`>xl`+qԠj^UdUm >б"Iv.?aڅe,K'&rr2[Y_)`*a`Q*W}H?TwyUT]K U2+v˦T7BC7R%=4$z u])>oHg?fUٔecSd*١bA%f[󦎺K3ZB IDAT&b]x$FVX2rnXy p3p;-$,V#ӳ]IuIh **;ͫK;QVa|\jU*O Uw>4qAgLL\JL13<=n ʦ(fRB迂*jQi Ȩ%U0aW;|0'V WيU,UUiHk%O~4X+v`.qU&xwxDc~hJ!*V+VMoAZ錡H$ȵ"/ _ H(ѡKc5P'Xs*wn;mfWH.c[X^$ccBM (qc" H΄ T)d><OЫ^uk4_Uv4fjV*GvW37,V77XSz';EV5/]R`Ֆ+'`r*r/y>UU j>5H?ȕIFUmvmF6\H"jP ~9wȳhժl.VC\W``5WIHYmD)CU*h798+ Q%L*7O$*Xjи D^HwZͬvT4j桹Z\9т8meig7Dg'8bնlZG\2EHKuZzj@{U89 G[^Z+jXM/UU*X5 y,cW%jl@6XW:(GA\/ \XV*@ZjI5k Xm>nFZ{FZo|Z(*TFM!Xoџ]U}kI\o(O||VqE`]XjUX%?_*FV]Ggfh;e?"Ushך (3)iJ@Ux*5jz!qudFRA [V8$׮*CslpVQhڪy;0<*-jW+*oY JFE_]gf^izbժ%WJ0VAŗ̍#|eUoUwonX[}PU V*-#U!e*t:TFLՖU.rU1X5*N](}_aUplUT.%1>AjIk ][%KK*(tzz+(G scJu;0Ab5?|Gzᆗꄁ(RあTe p-Vջ;bSz s<67tUL7?VvEk:<T* PvK֝ͪ` ׀jXT҆\ 8wSSUVI>eVΎBXE \#V ӈ+/-/MJ:;u#v^haujuMXVUaf2V\#V%hXe*N V*uIdUgj9<ͣXEgC¾ A!ՙ Ԫ- ]CjO.,wwRABScƢ`nxU./@ ѵd,@5ꕑдUR1Ŭb*6`il{DbvP1C ȷ\Tmۍ|8{|A:6 A \ WU U4{j*yWUwYLz欬 qG)u WɬkPL3z%X-\VmTqCfHZU[js%Ң=Ngu:p߰zUcռV[CѪXhlpg햶V}V}9|PG*>O+$+CUԪ|2 GlYy4pyK! Bz tVN:XN{Z((VqѣEiB5bV1[yimZ;6 |8Q:P>ҞCރ}("VW-VMTUjk),({|_#QVpռ C`m?hUEF:XXۢTj[5bu0qozى*Р볣~JQk\ buZx O*WcV(+&ê U5a![ʞ(rՖ é|G$?-VO6^Ug7;|RUJ:bG&Ǽ)BYժ$%vf Ve;,`-*mCy`P>P]_ fl ʅ{gEًZ|A/}UVA_Vj&w SX ~OiRWPDP*h#9{~:ޞʽdcla ZzNL\0?*.-CO\{ UUٲTWd U+՟թVY8z/X>U @55 [dţXUX/**ɭV"j5[an*cJ>g%:a>=+TFyne`* JlXuP`ij-WTj-xX%SAX&h)oW76dqġC+xD9UڝvgH.VZ}G|CU+ݫ/;{"V'x>;8.[EX8%+Hr5}2@mTBV"8LǬqp]V2Iw#XEsz'VF**bnDv6`ZDV v *ya5V1i `ڌjǵZ\n|ԼҞJ,V#X%f}'5c6܁=V/\m󧐑:TkF]KTTUK9e%Q+]J<Ǫyg-S"?ҏoUy1Ͱ2Z@#! Ysê++}Gr_n#V V1Ku` NW VujShj^JE, &vWzŪ0 JHeg#j*NIXCX `5%XX-bMN aUVWTWa U4Tm?Ԫ\me?4*xORUԍ'x`yM)+WO}ꔶ6զҎiyM|Wy9/?Ka m9aOW,Ic^-|R Uҫ2oZc X V=J7 [ HXq2r!p|GٟWU U3o:`c^0_Xm Vk,8]M tI4Tri]&kOirxDnŊ ajh{Sk, uN(kr o,4Qnge\jLnBXfŭ'_s} tƹVڂ§{ܢ%:8OUU.W*U*Vw8&qѼ{TPeC̅Bg(ug+qX[o˗OXjލN'd0:VTLT;NlG6+*JꑨVClE+@*BՃDU#V[?~ liпdGXձ)1 V9,fHw q!oq TQũEеl-Xl!Fl>̯fh;*iUXXMqM"S?l4EUU\ azJ ܹX=S7V%U85S3Ve0+eusQHBW>X'v-XQX-GU2Di'Ƌ<R%t@#>'|R%4k.3Zw-*V/m8^6]@^ԪªF2/sh4ʢzq!!ܼ("V'R,_r kj*J ͋0T^۽yktԜCi^WEjO*.qӀ}BjD\uĪmMUT |fOA& 쁅} RqwTժH^`a=o+J ji\n6?l4vʾ_١zWaN/*ςJmt5?J5Lyr5ױ1~\im[,6jaV@uX2MUيL5UnUf,Ic!k,Vs3P֑L_=bR~>'Y泱LZ(J@r9l(P#|V*}eOMZnB« NHVuub}ޑ X%A+@qa@brá;@UNXxw5Uvwo&*T8i;EP88J*-nWy6oXWnW S6U$ևjbjHg7ͦq]-G=?nl#4 #VexbiuvUε~t$V9/jUij;dX^jhj઒`é1`.YGBJĐ\*f#jUauQiJr9)_bl\)VpuV@*Qqq*؃d*ZHUVsJV`qNŪ:aW|ɤLM{߬&ibIXA@`IVDB#T*=B38Ag/·CU*A :wMU 3b"OTj`l J[R@S1^"i&ZЩZH.Ԩc,JђPYJD \A Z/o4oq&2VlekGO!Nq1,ɑjI3Z;EԪ 0 X%ZJ'LwF:Nm˲&p}drժܽX5c6FQW6k֪n .Jpѩ 8EMO ˁ(=!gT V_z]Y &V$+J+,io pū|?~ #5 egx"嬀j.UWԃ+,SJwGYlYm2VK,V`zs~~x;h% 73J\ŬTi8VޡZuTP$}hggCX:lMTaEQȊ]m02RۤV$JȺ՝SMXm鿡*@c5깁V5CcCSVWlϼU[*uV-p*14busV2O(9x+,Τd,Z|Ē:Pd%VD*XOgN;479_x [V@*L솙r|@-R`DZu.VX5T;EK^5TBՉ $VЯ`RXi*xР@so% Y;pp*Kjc㣏V_Âx.X՛O6V1:Ag5' L6`U["2X=fVGjb΂{oMkU h@j"GM(W_xi*qѩTe2X7(VCj5;m3N*nӉ6VN 09$VrΤVz4 aHհHE|B+-K`5"<Ua* |pVS#j 4kϯ ?]gXxi|(ʾ%g'RD=x޲*FA+E _`5#*\(ݔzvYjrz>Xzͧ*q5EVby*VtXѲUwH{\U=JƪPWo@ ĪUPO_9'_}G7?\-"ԪBfPɓJ圙אu4[ ߲Ŗ䏵My8W uu;{n"NnE)s[uYkD"Vۣvlo @DU*pUH9Ilv]iUyߪZbvK̀aL` ViXBҒ9GhxVzrXEjOc 2`UUUZ +GD>ri+\N'S@ IDATj2wEQH_5au*zҥBiVuy =@5VF7HtF}}w (c-0tka͢jz*bk_;\Kk5SΤRLZ%VsMR7T3)@ĩ* .L+g)^hE#mp,{(-} f`U,SjZb5W~URqPZe=Jgm?۸L@UR8ͦDcEdWcU_*\3P]./UVWV/P(nEyTXȱA#0ԏڦ$[5S,Qk,kULuB:N2Yrxf7 ~YLkba8cѫ@0bٔUsuB*"UE!&#ꧯݢ}pN9ĉSV\XZVѹ*eUx]zu*RvT\L=r+APtV'˘W?IfꉀTgjJ~>C6@rˮ 8wM-F3,&?ʚd\6b`M*<\Fj]U<57;+N٫Z[?@sfJPU=UUNjJUĪ{ӝiOiGeȩS^<~H pIRsl WW+VĪ~Ӛ8HVCq8kf fYlj&(*]xZau``~Tp>Յ&^իRŞr+=sF`m2WmzPyU dgW;1Ab5a g|@/tNՀ~}1)X<5Us=IӦгzR\9~oհ)5jVUj>H*aJ\#/b4 7D>r "EUfo)zx!T2*|K^*U`8KD%@h'!*4 zVU p VBU0 =Gvw@v X1XWoZ5gXa ,9+P`뱦Qg 0`a'! XjX"d fT'N bա.UXꢃU Vojt1%\UTTLXe\UR`mLjգո`uahU +jA$XU/iuXuF-#Pvv_(Ra\ + 5%u&Xp;jUЛ IXu@RXlEn5%gZ\wW\բ1Y%XզR60XU$pek:{!\Z n@VmOWqUKrQrVihqNC8RKU\Q NN#Lš#kM:jUzz#=+DUt~&R( `ձWXJo?!b*ߧ̴>jJj @l'VSɚK6Vu1&ʔ 9j>So<_n}x90ӍǛϾ|u\דg*O~n{l"W1@V% iug7*b< 6&[qE$\OFդ4T+TmBj$0*|o%;PA(}3f]NU\(Y,kd"VojlBWE LU" KEZj.m a~`ïzW*۩"V9TQ"ғ6Xt[0'bV`RNu}azھ\M_9'ny1MNOjMTGV|Y* Ca/Ӧ*2^T()7IQ|HBZPݻuªVa4*1{==(JLeJU L'%V_ڣ]@F p,2aw[o(:7sa||Ty.,7y)*WWs"X`PZ7;qՙn;]fYɗRUӃUnj:jio Kɜm"R]*oN7R֜8Ƕ6fkhfxS*>͍po<@ދd}J[/^ZiRMR7[/p}Rv/●GR!X=jܓj߹`5mjLz]_+wu]+~DU4V 3 S]5#Kv*X͓qJ(Pj94j d`*O|[AdXQRT(M@a]%ZU *VwwүuVV_AZLՂU* juvX%KXݵKxJQ-"Vssxi[gz{!x#4V0XA~3\b%3V| ZXr$>,r Je*Xj52aLL81rB&BВ%fW PXeip^`JcKR`0V[{)MU:NG\E$C7hL񯧩[xc]kV!)`U tqi5*oڹZu%0W-=o^lrbҦ:.A(V 3v}5oaOљU\UVK|句WM VˊxC;5T`M_ՀVOI_rT5oм31xPo8V NYV'˂FGZj ( B *auQVyjVАe8#DqT7XV"L?ptħHt@VMM}~Y2X`*`gڷ`m'Whc 8z r"uJ`mU+akFjo= oml1x{V|'?kjɠ'@4cݦBU=/y7OqWtCb:䔣H 0RmIK5Q> M"qɴx+j*OFZR8z c@@޺aKT蓫^dT/Ƙd*5m%s՞WZ#XX15?۩pVS윂*=m)jWݿ{ekD/2SV“wn(_ rl!b:WA ;d$J_XKz(YNLS`,hYTN0VV4_yªPjv9 []# i3qjV^S(Q@+BV`)hU^f+ju%bv `W'jGRj*MT >}k=x|4Xzbd׷"Qƪz4iWy Uz``a8*;l[uK(W9J15-LO:1j=p'(z^(Vu4rZV^j*:9J"c`@<12l=vT-^]trM]\ `uIZU]˞WVV{GxX ={\޽V Upf|\%M X̡*cG(5#1G> xU LJMbn.R#&X% ՛3؉%NHAi&d*WUUQj'? ʚ:WUm%4fZ*l[o '-v:x!VF꫹M@RqTۭOj󧵯_~ XB_` +vV֌bULתor*t$ZMjn aLJRU(X-eW T"!OKiiiHP~ȳruԪ䲪۲zU.VqmWPX(c#WbeVL+ΰ*`un\/dArmS1 TL*ɴ'nt JHMT&¾C Uc(pټ Nnq*%8Pxk_VϜɾ='J+Q35V)*P f8u_ 2͜ʼn,eX].b˵ѬxKKF,r}q$V?^lGCJ[3uj3B*!ݦǔ:a XiZ2Vѡ\*uw/`}\Wr1B?Q59 gQKsrJ6ѰvDZuNY) XSUkzf|V2JT%SOB)u~(ZdE%EyPUwUJoHdnBV$r kRx&/בJ T>}]V !Y`r%24XEsX妠/=dS՝GEd_O160_N+VZzMza9V7G!Z*bp* ]'~,=O 3jd^_ 鈫.%iUi?<9J/1WiRU&V;:_v-Z~X=ZuY"b}jSBRYVF:H08(az G .x-!她\l.f ΤR^5Ol. +OF˸Y+وZuh18|fK~SGճc&޻ŹU!V)k$ת7v@bU Vr͑,JXVGe, XPX5ju N++A NR_cc*jqO0]UZ*jje5V?dUM cu@0Kې%[cJE/q9&c5 YiWz5]7cUfkaڵ^ 750MIumN" U"45-$Ij5gqVޅJFͯz!$ҚNJhKgl~B5fhqV3ÞsԜW.0Zl'SiY*ĜT;`UN+ʌ: m#o56 _XU`u Ǻ|92/GF8vCWVb [} D%dVt}:*s(X8'X]lM`}9O;N[f<@8j}WcZ U69OP2|ZE*j"J+LkoT,VQSeaz൮uWoC cQj02VW*uj˒o3d:te IDATk"Lz/\X55)|r~Δ`!M{U3oV9 r,XDF*`Ek:P',R&CSXg uZy98H#0f(XN`>7 AP`i$6?j5j߭{PΩ^R}bK#(YwAٕp5V}j?6*8UU~`E.1VKfUM'5vcwWCV ;YC? UЫ S+uu,|@id +{~s7j>oMje$j SZGGܳWy*L=V9iXtb*ͺaMmq@Hlg6>1$O9$'*{^lKOh*1f MS DZi8!.[꣕8h?beie2$rPxA?Wa9Pe}buipiX'hWUjX`AVa8*T%Ǎ?3V/zJCS9 n+)irU@S^`\{dr*K߻ ?OP/Upu*5e ξ0\U8@rSj! CD> NR.į 27GXEmԊu*W([*Y18'NMNtTJ o͕#YWzuUtQ^(guXiXmtf^=Z^ի>]MXSu:[T+BzW-5z l`@Bz:'CL$~.4FbM84%PtwXnoUf @jvc _ge,XGPvw57D >Bl!<+Oҷ%|#BZWp&**W)qWJ0*0f8 VժA1;#jh5=:b3 ;*?2*w2ru5 ^꽔pTV(qtmB KtU!5#e@f//W1Ϳ$\|ܙB bu`bapm?aVlP2Yջ\q#BjlSe;B%S%hU)QcHgxg"mUQ&㟱o.`Ql…䭚ҲHo Y{fq\ԴtFaFk-[QG7M:I1ؤi:$+ er((l!`a/=-el1"AU:0Us5-V\>#" UV׭@@lZeVU=o0܅ƃfY"sW-`t*77^} 6zaWSm}ziW2JU>Uy/DccWZلUX, *Yˡ*T~ϫ/A X}N\^蝝;;tv*67wn܈beVH4>\(*qVzl&^*XSVdOKzJ={+u [Nꢕ]""Ҭ Ec}>&X%T&< b57"Z=]?sv}I{Rwi~lUu9jچ"74 E:1ZPW5(t\]'3Ԕ^ ɚlڶCIBӧU';EXkt-S\5$LEkZZԗ/_U|}ؾ X Y#W0`j0lEB+ 4U<~,䬎zLAzGOT3Vf*QV 4! KxߐJ'VUdfZTHc`*J~(VpjQfTG W֔@EcjAQjUC.5е?KY;?};f`.&kYk*XOP*O WeW9jU+kuz)?X]êov jʣ[XqgCzMUUzט Sa*9V*gTRtUu`k'T& U)ыKj>Kv7C jQ,BRBzRYR٣JS(듂ɓ(Wq*VR~5G@{E,4R5Db5jP!_A{د6zVQjnDA XYVNIa?y1LY7׮γfAPtZ*ӜZ>!kWj^X:X1J/4S'Z'!XCp(V@K sYevjA5(T W V]3joaJnmoyXjՖ`aj4a*TNkZEJm| \lj_+yqu#\ grъ"*B\enn^aB ''`P5-NXfN:TƏ0c1mWQ2}<@j[)JXbɻFwj좊j6zHș g* E*s X=],5/okB0u1|H9NWU]QiZVH2S=$*i^Z @3R6VX]ɯWda*.4P-@%KNH`KVR"`w{3?A/]9)Pe_&C ecZ%Fuw)g\C :*JU* UXZ>2rK\{DbҬ4*Q\?9֫02;y,Sg~Z^V#$YnO:US⫲ZMTZ;*abj}S $hՆRzGXdlo Zg-Me(=YO\h?;95%Su[K}bU=FS0_LVJyϩΧ-}7TT߻[颻 Tև)H aj5Rukk徿^Rwi ƪ(Kd*\0H V &(WZ:OwVU!<_{ҽ`jh*k4:?w9q:>t}j2~!d2D,eW۔\SXVCժAW &aLW`]dV4h/)^tW̼@#eoUYrX/؆*K]jUM%3bwq)[M[1 R !~|^r/M?:jpŪ_hjivVڗؿX@y]zvQ/ۚ*9_ ZEY M2JPwk5ʱ\yUgR9 !6e]c]:AWjpZ)hAq?(-c6h VXUj&vls}TVSTC&0UUOZVeQjՙjYk' (JoUAi}i)eT(K#|SJj q6BU@vbjwwš,ALjiia*\Ev`ur|EkgP'6p)k8 v~OT.Z zi:HPPLU̬*ͧjS{STU(o5h;wsP5O͊- V@ƹ{_V¢6RY*ZO՘?#8#Ou$Vne$c[9, %W]G]YM=UUq%q4F)U] YK7!TyCOc*AVsU6Z2BDer^T(n>LYXjIV8IwUVV-QxsԛV{X9l뢩OWlazIBXRSIRI@X *0s?aR)U֫R 2*E8`AJBGB25q5ZZSTղJUTԪZP/eMKUO[Lk>X,vc37'MA*IsF9V VTOVOZX\wI)3{ljbUFq&zBVg.pUPF*S++:hi"J(S\*&+TV }>2TujmWG zB\4,EU*E)"uHϢт4~ya+qofFU Wsjn*Wj5jU0]XK y!x\]};Z<SҢATSBhF٢85Bj^T\ MF@F(zE%’9Wn,bNB;|m`Yv U-Yݚ#OsTJ25NT ijUbїcX7I** GλKq `z}w{CLj5sUwpP`v~zڵkΝ۴ix=4jjfr?@\=@2Y&䕆VPT }鳛o-oU$ ٙuI& NÖ#H6B Vo8SX)d47+XU;®J`zq+V =%].}j`eZnMZQZ=uXU7FQs!? idHO$S)kY5Xvj l<~#ZjO%cr6ejЁPTͰP# ɜs^hUCM(W3+2HmQV!FnVm&l8n S?u¶bҿn `5/g:\TsZ%Py)ds80>I/-ccTyXZU([g&Bh3x3SZa1V-**PUp^pU0Dp::$D0UrzмĪ**BVGbUDU:gTݣj3wcwU'0V`Wcx@׸jf}X2@/c{TО7˹TfyFMk]rc:XZ0Pf:K W&Ф>O0P"W#l ? e&A@l 2dGJA~Y\=c6R*R%CTЈZrz` Z&V; vaul6Lfc VXhXM̦3هmdJisUWur>CVKU|: @uUnjYOcXl bu@#z12:1 Ab} j[L V]MKjzzb*eb+`*STezy@ߕ5Xf%djJjky8Dv*-_ҵk;]S յ qq]\  5O_4.U Uty48 7VsjX85Tjs!M 3Zf#w*a Fq}Ҩ[LRC?- Wd*NtTMՈ  DJDTD+*6S]zWYBXAVNQ @ ZUZPHSVc٤ط8P B>˕'\rWc-oRu պf[$ hhlQj7,)>M*V4`UtPT:r;OLV~{nV}9EU[*M/ogezq+%&jV$k%m+H/$n0"@IZً6ha)"`Yva.ڽћRad I`Q 9yG;r&_9s-~-Sd_WrٟSp*s&7,@TfV=VE|  bݶBZ%G*C9C4 2qRl+@Du~Hkӏpb0J@~qXB3OKӁSg?X1X=TIL'XsFLji'N0y S"NJ$=i'WGp"zشĀuO1^bU2`jy*7GU .z\uj ȀPHj[1WZ@$ZN$`u1Ve`"jվ"Ry^ʧ7ZXT=)XUB^ іLJEJ\umjٔE l"v`%,Ss}.UV0OX C=]s;SA})>5UUVǪWTΡx5::~cJ(SMwogF?D i9g0oIc70j WKR`Elnx.LCv6;QrL=ALUvT5-1bcդqŐ]?Hn6>RS^Pj:X0hpQtU'B\qVW>TDΛtG=6l0)+CUI{HE]Q V? JbVU|7:1q0@mϚ|?W-vqpUgP,!~~^JV*(V+]̯j7|UOZ+2o FC k6knc(Q3?ztkZ LڤDUueU D*gcZ}Kժ}y~@u ;ݦjj5\[JFs*J :JJU,x} VFOO ZbX=;/|SZ4!Y"JjZᅪbp5LFŖ/R"*=i RYto|V*HӌЄ]=t"4keW _+.HXrAR5,R2`%wRU*n&*N"v^۰WRxm%juBEWO<@۪t{Pv~w>?_ 2jVƪ)*\#&U?өw; We'WXU/U!8. Pm UBhվUOn~EK)V WuUJ.'¸NM5[838 z6k\M4 .Hvn"}0RuxPuW Y|V?|Unz9'k7%z:%Xf{pְN)|gŧy# H;-DW~FRYєGV-(aqʧ*ge AW`j|u$M+#`*C+T`sT>T Vxn\k682]+`Ytl' cco| ub_yiU:Ə\jZFZUHW#O\oj~O=mZ◌U*U28qcsNeJN@jO wpݕ**`U a9\YU2ϩMvDDT9pj5ܨU 6 Wj\!V\<а|֓$͜Y`j)L?跅_Zz闿z!?UHSY%U7>|wEc(VAVjzlÉU؏H T#OcB} * TM0eV>fMVZ5#jM5CְpNjǠn $ Վiґ4f49ceW⑭~cH۵:xtN*XZ"o8:n[=#՝;< x m wQ/¯ZUU/BUҪ'O^e:XvRm'+ƥ=b zC4Hhh}<;ځ[o5PYzj=jծ#<;Xq*T1VR_E`@d{́~:KTUq`]U/i j5tj,1z:'OzSPQZ}x~}O=j=ά."c+(KD,TM7YqTo:LθorO۝|’x_{zN0NEWӤT[-mj`*")8FsV5 2l|,֨h Vauö>3V\e+SUbu^V!UI::γDQSVVw0Fm :eUUz**f)**UOO/-N^D6U{UԬVTVk)%  Vb DZSUVhs~Ͱ2V{X]SUVaAP} VYVja\u0|Ƨdfj;1cU+SF-X1V.ͥw:DKhA5VQfs{eg TF  =Ru#;*ůg^se,Jmr]DQp5kΐ JPBcVͬ$FڣB5r5`Rr!&"zTQfjT[ui:-Ϡ#) 4#QzSmjBOݨYԇKP9-MU Xl梘iDŽ)*$ʔUvl7NT}sd)Y(_z\5i7>V13`2U+V gaCf `-[v6Tu3VȺrU*:`ul@ ֪2ˤFjVMk­[1z2U onC yj}78p@&zcd"Uq ]Jzv,fH SPOr WB94VA\oK"i$; )6%Jpb,ٓ`?{L`T()=8@ N L(@ê W o%,Vv֧x aR[#v9WUSƛΪndRY~dgWccVIfYDkVPkqJᇂ[$jkVuezb"EʘȂ(KpepE2D:L$&7F/7L1ܕil"W?U=[yK88zc5x5ewzCRƒHK+м-FPGWgCYj; _ ЊL,'HjP,X@&c?wP4(~߅R%2?'J٨T8\ CNIݷfEfӟWT.]N d!Q`%2Z|UPSO$]v0V\jE DᮟeycfVud*"7//{bT*$ zzrV4VUMU9*ɞVJʣXN8Q$Vjk*1Xl:_-VQX5,BֆJSr/nfUЩ6ZmRSW/P*UAgӰ@ )@*cr7Tf#wwXUJD# Pd|j$XLvUo鎿3= W[$S-_/oݧj V5D_+,e|m|8jgKe+"̏"~D֬JM%^*On?ΫGeNnȜj]Nڋ3u\R]RSU>1[)<5LaUVfpbA*3`b5~iM%jkh8|} jzdّTN>7PmzH˃ U:uPDs,W{'6lW5MᓶӂUm퇉U'OJ 간<@*=rA`UjMsaBUZ]4T5 X0Ur^'[PY;xn*R&oݕ~眇̡`hbޝ;:ZX ժZs?Yl*(@/ <{pu`4ctJ.X-li,V\UXYIǪoη|NV}8a^<*kU # k*Du#m X 24wU컋azBIZӚvjL#4;|V`՜GPLYIcUA*Z9l#Ml-Ԋ)=g^ӄZUy(LNK~OjS8OQK샟<V?ZDzV3껷!S!y1g[Z]X}P,CվSN"*~}իȻdZ:m+wW;Z9U4WVтepUπ"Xbus.YkEVu6YJ;0խ*ejw@CQD5KH*:52aud?j&΢dX(ޤSB?,VXD-b(RKURrLΞU8VםZXZJW(\'OS䜦*k U.eZY3!V+SZ]HJZulp[P0$.kj__=c0b`ew $#A >_XVGV~O4(@f8 Z-R\N@Ud_:&`?*w>c[ Y m s\Znn/^EmÓX=Oxcr @N-чijު:?w4/.=Sa5YRQޖ98Qu*X1+xCa&Ժ|+vLUkTѧ`UW4iHժZ-G xzRVڊ/_sBkwGiGBHģNՙՅnt^c8Zűڬ0Xa5hQ9ݴxX-~Ijs5P[c5jK#Dg+suV_j?WY;&X5\NBg 0獴!aWUj'*'_EskZshU c-Y[аOZ,[(bi)~.ռz!OR*# cGjfO>R5 Uc ؿG'Fk9*j99i(ij꺫\Z׌[LUԶrQ*jkxFni&I:vÂE{NB.r5REjs7e[OOa>aSr|a G54WwjFJm XͣmkzPl+Qj۷_ԎTF ` Uw'sqH_@gQ۫<LrN:Okgd`9F,a͢ի/ b8ַ( `fsյ )j)Xm`;d,V\5P5_$X +O?hjj{UoZ7N4aV!VIf Lf]3_,gݢ1C؀2 IDAT X @뇯 WU'NWUqXgX"?,kvl,0ZZ/Q2ũU 4ǫ4TZiz9|5D5~"EJl>b@ٮ8tI*1ӳK"T^W{5RZE zWi&6[)at-q#릊g<ڦSU# SUD]3 j4Ui?ԝ]KiILh+UǶ::kBA|cv#V=pK6M)≝.2҃TC?D0_b<1$i5*՟V5TcUk7&D&R!BuLje%W%u&h&LxWkNdM[iiղPJD+W"XZbSxLՏ;;[$UT 5IV"-@Uq25j}Irв8&_(4n2X=^*Z W̩f VP*Q¿U7;7,} ":rs]\T^"v鉏P<)”`kzrճVU ]ʼq :X{N Bsh vh*`jAu^*!XL>D 6@[jU1H4v=yx6T=%mKF_8`> XbuUV8 Z-H[auJb$eoau٢jhe;TaN~KcՀ\R& 0ЂX52jjJb5=fq"V맧ldg(?[̑Z m bi%G2HzOyN+̿2F5\򶽭-Y:a qYV10.Չjj(X4_~-b r5_ Uc£QvL_* TGMl !~&IMVe jehJjsDT_.IUƒG3Ij-6-WRC+VCUf[%o%[""۔e;, -I {V`[Uw\uBvvT\])֮ KUmx<_vX} z:{ UcLg:}\-$IKդ{Mй @2j >aG!T/5`.+bŝQh~*KՇ,V:HA?zzP8kTҪELӫ:[g;TXUn&p'iw-Y=kpz `5j dWe Vԟg VyOON]\mK7ς]*Zm 4) iX5"RڈUOt8hjx[@K6Dugɺ4 *Md;Aa [R֏׵J}1uSV{XEgwKVhj'ӓ* s@z\[GݐГTe7MT.#֥@!AVj,YdY Xӯ-ⓇU]%~yWazY.s2ё7 #ՇkZczJ#lXB+`O7X+'VU:=h(ʍUggZ@{.0_yӬ&zcssϞUd!GT Y)JTX""K:0 T"wpl ȉC%DVLM6jWF7F֒+8TQ bo"'\kUXR  Touiī'Tׅa:9g+Zq{;]U4 1K= nxN̰ʢe~2{*60V:%yjp(X-$ VXKHb撂{bWEՖT:LҤ*4a7UPXaPA\9Ru ,O\-9U~\>gL!Uu\GcѪ?@z_ѯ5B5 cC L9g<@&Vy\Ŝ4@?5j;RL)Z,RuAD_О GVD"iJkYhTT(V 9@DbՈCњZ~ v*=0cBoWtV)DpB/ZUMREp1XQU󊇪5ս*"]V۬Gsj-TfYmΪU qpuW^EjG\+_~chx+bugy< U0rUԪhUFauX$U'JWU{*U`s$ bl!`u:Ul`}UUhªYj0W[Uߡ{qGcy.h!%0`Kld*,]WDT筢M׊+5S^U1 թ)K|DTHjKmK*G"TS(8uH&S6U6ٻvĺjTS_Pt"xzo`GBukMYVjV^tƞxjXUro$lʿXj{YQ ܿնWMVhت:AW4ŶTV榀՝Y+ VG!Ň&PZ%+KRo޼~SzWaj*L\8) FCXVhOLFUŎzu; /^dLo Xy%n-b~eS~QXtu86'HXOVTOK(W߾}/Oi3VP|6dKPN8)CfP JLE^/#uybd 9X]'k8rZ$7`̆_G5z@}V夿YTY4I)4bJZp7d Wlu3=?e[u8 PuF]u;3 TsF.}U-. VWvѪIe/ogtN,E\/ M0nLdAL @u]@&kL& {AiLSb@Bj^4%7s׻ n>*c5 XK`5)] MZ}/^`{0"WQ`Ψ6j 5 V,[Xg JW +[5 >x0fPJIZE~RLىLUO˪caUN[*LWV߼~c.⪑kk4РOOWmiUe$iJ+9S>Rn|ZA99(u|ZQ w HݭBTZbz߀wm`~8VHR/u7[95EZ{PXզ*rOJZn,W|ߑW j^.wNDNB$X%= `zj,rp6W\lf8"M&fu*A$B; f| | #MVۻHOjN|xacWc6}ۊu#VЁjPwé/0i+g r avlթa ߃P`)GVcܙ_׆$|Z+BFVqқ0PZe>⪊AſIۥ dIߞnm5b5An_?H2 `rt\.AqQߥRc 6ŴCVY: gWGYQHt^PewkqHܧ)ύ=bRtk>Zc *T}{6eE[CVP-t0:0qJZU`-sOTk%q+afec!ȲR^w`ggHzEsZ=uV aj5~wvpp2+AQszՃ P5]_cLYj 9 82WVcj@cuV1VD`: jZe UCc[\dDTPI^I[1^3f\T(zb >> >R.kM)/0Oi→k>r#JH5kOJUw:OͪyNNՕ6&%h+%j`D#YYp?g&]yRVOt g,Vá Ջnjr5f0j%a"_Z|iJd%:{ j*F#*aTa'i뿒pENQ!YqG^@kL #`Z=,;o,Wo\u킝T(y,GΥ39,m:ڬb HAZ~"P?53 $25-.#&JN6t7uIh|`( sB{6ЃqB_lJw}߉1}7i#빞6T&P]IjpaMPʹ1Xtk5ZZdB$W;Ϟq0 UVX=ݱ@tl~25X=ۣjy]]**͜jj ub[ qPy=|eUh%ZU[stUŪUPuk~kss!{/\'oh2%8J?%W*){]X_mj=ήMONZv5L1gZ?5+*(YIMJj覾ASBMq B UVwSAV =/'h̬h}?:eé(PKPGWU*DUҫpKݾVEQCU V$#jj*:*ʓ*hV IDATϧX1Uh\ VUX5J`m$@qWIjQc|C/Vs&ı@fBN4ajObzt}R>ձ7)G- TIĎTWgHRU lbW]:WI?x !Ր VK\]|GiU}JDUUC!V+V'oX:AX,qYJf*zUbK֞njJ7UrU5?ê#նq`8zxwtZKPi~rB>\.W3 &*|Nj^[JOt^rRppyBULQ54Espaӂv^IwV0FX}AdF՟.>i(q/feUa=Zd% )Iz[\eW/1ҼNj:y+C2]R/pWIƱzX%^G'N,VS)Y868__75U,EAU 's\1JK*ѪuwkyvroZV*& [ "~UK[9\VAU`z V=dr]6S1w'lj}/K֎}5Pt5g$k 1@ {9uy$uX>oxyPsgFdtZX)t P".sQװ:@q/WĪUi=U.pHcXխNP٪`oTI@*a9ryj}k^ծm tv Ulxc5ruXrh,X|cU[ZV`jzW0PR5E1\Ob@mBNzX7} ;U7JCSZު!1y^3+mUbD*>}vXz,*1@b|slT}8SkO=ZBR#~O-QVnbR&(-1\Ujja,5fÒ3\qUHnȳ+a;hYq\]a 3-[Xհ,o9Z]WU-+ *%04aՒxB X08otFRfQFlU0hջrTu&ZNNRUj# *Ԣ/OO*{SVr9'j`m%s X=9EU{U*{1UY^1b6rh?x E"-Uejʩ,WN0IAU2Vk\՗^KEITR X=#*U5`u0u|,YkR׆)tp @wC";^ qoV=dEJyl %Oe$ɎdBE\hx 0 +|d\NjqSXԪU-# T TJj5ߺ䠚-/Wj,T_YU5 P[>G 0ݫ<3U+VASJH0H*49$STw.X,:&H@Usv1S_XbU4ĉ;ЪY*j K&`*j%cj.VvFrU*uЀU[).cBUfNP#`.WkuV{hz ޸Zʷr`$U͊ PPNmz/Vo'`1(\Z3T5(崃(/iU+9U#U^V>_~0LUxTo}ܚj4\=SgS!uΪ\<7$V%WPNYt{ BߎO>V%K\R8ToU#^jnΩTFmԝdA5sG$rԩ<՝5ًvXݱ)S o3Pv=5ĺ0C~*ڈD;`&>GV٠{5V Ir5Ἂ +R]gf7UFUx+ .@k,ƪUX*sTū֪GU"M2TrĊmbQ*Ϋb:vTfgT9r7TBӛ1+@ZZUǕ*@I"PH3[c>V;eT (SOA׃M6PIXml@dWM +VpƑѪ[%>}t:yl6q`IXigוqLQJM$!@BL"IM[%#;$_B׎u]bf+-H6fF`|S+tXhn?̈́U%H=۹ܹc^LǙ{@} n8>dj-U YcOeXU0b؈b%ZJ\M'ZSwgrbdnJ(EW/T.UX^ĶvHgTRthRq KO鑪Z*w;B=*yhroc )}c*R\էn$ƪiҖj"8 g,\=7<ѹlC(VU;]V!>\u)i_%OEWPF*]b}˭&VUԩ/pv tYRWYf w*i+W,**yV1Xw!+5m]y_leaUsCXKWwh VoWJC/U疊c Bkъm+ d 끪i0tVR]mpZ> Uܯ: $7h*JVV'q`VM+]%4êpUD,TQTmv7r: UUoZޒx,{M^\˿Mi)ETj A+al\]&ҩpr\ʉlҷUuiOJkaǩV Ahe}]欺{VLKojkXޫ3U V(@z\jV5XʮWPm]çOVTrjb9r2VGK*ZU]?T#T'鯒mz!?PT2@j3\Zp0drjge\Eb-,Ā,Y+=^ܦ֖}[*P' ,<+T \8+,[]8:T@V`vQN6z[ZR@Yh:mUT'qʂj4mll4*YzTeH@lf-R\ X$tkVyC\}V/7|~prh)S}y 59h3HxLUXrա*䙔5*jwۛ^U0RIWNUAB(K"Nty\5XruL,W\%Jtmbu:@QUt :,`HSM5)T\) ;UUeEM%4 0ZDLmI`${0Vi\E*տê" X@Iɏ R ;M7kU%Vkj>۷~*5ԪWBHUF!&*=[M5Q*QPwtB * ՛Wsz옗\c \x`վPN:9=Vݥljj6T\[Eeӕ7޽4.*2I'jUćY쪏3*`qjT&35|ӠBT: |~!+]?CDDZ6VXX` *qB?:YH+b*dQVG1zY PNt0uL[9`5 9%}ُնK fUTV!Wq 00>֕?Si"T VǵeH՞2:!U* 2Nhf漫KrXݗag@\"Ua k;6QVR#۷m _% ߯ x_WP*s$R44\V> CRWcWU4b!c8V&juH5*4UF ZZfK3,=q OGUHܡ2h j=QSɬ)=r*nbb]OkFG$P8W+zx{*wŏڿ C@ejӰm\}@Ҳ ^~jhUZE}hZ{XI+Wh@b%6R__S `,ضZMCꚿ㏃U9ac'6p ~ZX)hK>>X*-XIzS0 j'A!%W QiSSP]drEUT=qJmmEU>l;4KL%]g6^"=|jy%W;7f4LMo̬_C&fuqZ9w`V/ǝٵ"דkw.n9I~SxқM\7v;XL0;uF +V+J5ŬMwLX=R+yIWjd`@-|~<A*.JuIRWMQM |`}|Ủ*1ⱞPj$@h&)]=;O ȁPg xjsa< ْ&U֭%%IRU[=Y&6VwnњE%U}Q 9Xp*Ы/X-/,'j-QpN(kOBQzEW-++ZXuVXA|-W֡hTK]Vіj'XoW9s:r5Of4@Z_ v]S(g2Km??07dqnajiyo}~aqQfnn!30VZů;Wxx|f7ꥩuxKiCSK}<[8gEzS[3j[|ۭ Fl[vl RtWc2YO,9gZwl V aVY/PꥫɬiԩXv K%XJ֪S?5Hx#TacmWnXRxƕSu5A<<ɫD_mU]N 5#5GŖJEE &~t> gA*b@aC\{LLyۜ𬮅AXajZoj c@ON4PzyrTj bъ:o83TUi)_c *P"Fh3#y`ê)V/"@tG{N ݿ)V/R[o9MO3OƝu֟s~Ru_z3+_`5XRϨoQݏ Lj*xgJ*"Tc2??$1]X;a5%gQ\]'wz^4.2JDХ\%^S&!q_3Sg}GMgFve^-=ϳRXG/LUվ*.FTSg4SGUA*Yպs|@V5Rx72$RoidA5j ٪Y{2NV5X۱o앰ZNo:jdF\O{hvTʏ՗=E#<2 JK ? 8D\fiarlnOCܿ#?IlkG]=Huo<}LWwi|q&Z{yFXu} jpkW5+Z/*^T`gL&XzV29PXޟqeU4Z)+VRAYaV$*T:yxzZ Ta kx7<J_xcGSwXo\tzQV[mzFu*Yw VY U/cHnj"HM]\WW%XMZ͉ZerOx}<!U@Sb9)MC*QWp)r4-jt"ʕp~bsNgK SK+_/ѥg0N_M&F"V$XMZ3% V0egDYTRZ(TcהK2ZquT\vQ#X9~U;dRrsj)2WbUT:Cu@Y"\~g4&#FLGUIYU@jʠX-VmSV>jbpeijiikMέX)Ī5<ٚ*``Y$.j$O.tDU/ <`JN3@h 4ww7CI= ]{*C\ͬ{ӊg]߂p[ o>T}@X]F>^{;`H,UHX(P-AXEgp/u^l\<;IUG*jHp9FP}XyZTѪbeU$Vo6Ejl)?X)>VedͥVd $8s8"0FZj| S(b"u~6"^'ѲT饘*]On a( uSE~ztE`#]ǙtXt_7Ҽ::Z*WlAV[HӤ[t`^Wl;ʹBq?sURo)#QUj-+O/km=jj7fJb[thk@h֧`.OYS+͚)jtJr]hz6\NI@uDUQ> Ued&O71aֈY؆GUhpec aDCN\? U{pyvX^ZwRLY]%v/jMd`YJіQ)[+rr TTj"b,V<:z4fYXLي4V{mjרNO&+$μR;)a98p! *D\~P.{ 08#*J V/MLZ=84b 4]m~tz>nniª"dWD7*&*Wu /^`U*%55Sm**4N9SR2uE.H*ƭգn`L5QASqT$dl?[Ry_.쒹G`OX-k_V+pkJ@PfUX]꨺ޜ[?W}jV߲Ub?@Ug) TjJ*OX TEWVC^[JQ2V*jVi(:Z$z-JjtUo۶mU[rUReX VfUU Rb5 hkGW1 WMŴZ9eYq貎N\?mu$E4+-qټ_Es*VfZ-PdXZ~VHޘ}ƙT7 Kd *5+gئO#}o6lPC WUIVKKêO V7XnKUbuٳ$WZũ!.j5`XݶUcGE== c{^T*fS)jHz xKXL|SUt1 WAYVƔ pH;}p UuLJ|m[__p6H*83t/^ _3f=QX )Cudt#lZ&SQB:89XqJ)\_Dm>}>[6K=_~V!oZX|;Qoe~[apU+Щ-[TmTmln{Ϋ0+7bX]u S_eTYѣ5颷KZÛ*NjV>Gn94_$X|N ZDI UVՉX]PF*@o'$c+}(6Z,}Lߩcb/UwNμ-QULR>U\8sOzzzlﰺLeU*`֨dU~j*}N/ã7 ѧSmޟ!/!;]e#Xmjx3?YGVs4wk(U1PyʨBېul=FukiĈ j0\$m-~ꬪ?T Z{8$§޸617C'q)2rY=UJ*eEGZ9j;eJjQc?V3Q-ګ6xTqhBբԾZ {B[wWR{YSI+9.M%gME%iVqW,4O?DƢY2 MZ>Bd@ x)VK59_UiU'j ǪyMT"Vݬ Fjuŵ UDd6)Wj\E|FQzЙ*w}RNDUpʚg/,$?7Q-j`1XMH!\A\\9C֎-#VAVjzn:VҲO"VYq\7BRWs9UlVEU3j^5yS)vqc@vs^pPuJaW /74BeަRͨ#TDɆPT+e/B+WEPŇTQjv:pEn\ [ƛ_Kί 4cAG|kW-ZY#MR\OETNQY+RLU58ۿ9jP$UVErP0Œ?zjM-a*}d(VyQ5j*ZZ6wu Ө}*XMZry̽VEGGc*iup^VۨV8 (WдZ{n* ^eBX:gjV!zAw*aCL9NVXpɬyĪU h5\{ Ξ)]Ya*AU-ZE:+Ԇ#gMQkLTRUdjvXx-{-0Bm2b/p{Dx2XɏzRqej-g,T./VXk05vi䒁RG)H9 ֺ4ݒQ`j :֯>囮U;0E(IN^B++Ieg"}$W[Z^?`=OO c9 a=J& Duz*Rʚ&őj0ɩDRU! b\LZuU`u`Bg0jЇR0TIXu/ZEZZ70fUY#Ww=vԟ.Vafm*B̙Sju&TsiZn?&*`Ux`뻀Տ7h\AԠrz-ߩU0qMW.buL*UCM,4dmn0Fz}s5ej5Z:.:-WtTc{F_iC[f^|DIrע{=NrY,QUf%Κ2@匯[Zp!e_W#z2}Pcwj2[_wvmjX5 Z%fSjO^>cP?+SfT`U e_T]g*@; Wujϑ~IZ+U&/U}MHq< _UV::֛UxV"8PԪjJ\sCU%aDXVѩdݯׇ Uw.iz8;U)nuY?<)X1V=rJVQQu`@1U]5bjt+L,`:T Zu RjJf )ɟ@~=խBjkgg`ӡZUc*n9ujWzuW*E0Ye\FN'7nZ*ruYjTз sm*3ScjaI8@kl@T|/S+צ*2| >`ug#GنqVkfftw,n\wvUZL҈(cQfZ² %KC1|ta&]٦҅i2kb[}^9[&ͽ77:?Pb,;(\ ~A;꜆)N9j75Q* 5<?*H1AUn`U GD`Su2 UĪZ&Xd8pjchWGY=qqR[x\[b5%UuX(VS4@um- H:%r V{IX1[e*Y%PM)!\alWQAe:444xӌP{V\ǪO֪JI*zn6;U @+HOìdQzV'Jc?4Q`WaK.)U1fv@_zNLo~-UR|sꞅ=b<:[xz׸j4Q句 շvSP}G8Gp9[2r(ksw,ׯE)R)H[cu*B% BP?\:VrV4\ N)pRVk6iL/j28D~"*X}[]9ԆZUy ѢE6]׋P| @zz)0Z߅'ORGT%-rR@յi*b_;GMSUUAU'V)lVx%=I)V+d aVV\:Vu˰^EW5;KeH.؋UQ8TJV7 r'V?;|(fO_ATퟩV.Z/obiyڅ ˗p0 wZZj*XfG-m]c߁stFd*v77KoF&|H_ZZ[+sVj w|GcxW gTTŦaAƝoz}2'5"j VlzU4軵2)R$TZՁիP蟘`NN+X(VC&VLE.d6=V&.PZ#=˯oЬRM>AV$71wX &hT=uZ5KhT=qYWI cʚ==u-S(iO!W{5S:I)>t#UӝXj cUjQҠX%o Z աru{fEC Wb˗_Tg|re3/V>mл8H>NptYH4vjPSBjJ2Ux܅!HʼnФXE&w֭oF<$ff6)#ۥ~ު*ZQgߐ<}ie0@}_t\`V1 0a`UV)YXe~N3T i+աmLBIKVb7EUDbL~:Zu ٥L&)e0`^&(4;_h()Ū"V4YpvCBoUF`_zXҰYuDzJ[/8 A)Ni;PEއ`ҿUYOI9aK,V|a3"%Cybu̢^2VcElDX, "/ 6&eZբQ TSZ iJZ bŦ2TWRa-6? 7%PDI)+ \"U%NV/5?!;j*҂께f>'VyPm 4n@U&`X(w)U=&V?yMccUR..UE, >~uU*&|B5EYTWWUT@ՎZPVכGXU*QŭVX5SP **&={ %6pROӝa9@4:~ՐZIkD\qKp*ZT8sORxyRkUi$e"aGL\?r֪BR\?_-Y>I"6t^@ڏlzGh"Vxыby4%-!1Veu7[u^j}7lz`'ls 2=㸓N'.nXbCe3CS@LgqROB25 !6{QEPbf^ )^W}N拤7%3lv~y-Vm{}Vxrwr8M91Sͦһ?ѵ$?m*yJV1+Ln3k^.ZU_$z?у*cjպ#ǚqը6QUj6JbUcC8>UXm33Ute [Bxz>XX23 VqB@8ah=bO\չ9<**A/Rd#0guW{=ۄՑ ^:\"ړVDQ]<T˛$@8fTV'kn`Uz?*0,B޺eaU{ulLY= ]|OϧMAZe\G~(C\OQt27)@{[ S;͝X'S4F\eSmH ɂVpv钡n:Q9 G G2Vy A9VoUZ9V޹$ [ YIa {4^moFWJj$feXJZUb?E@d7E݉y1 `#,u?xD!oo=ڊ<߶ {zXŔ _U߯kuJY-O1XϿYȁ V}yv; -c35`-؍ 1T֐C*O)bzeZW3XkB2sgLbT*吧)ZgE2 `"$\YyեJ85ª=(bZ JTXybV+~UIYYb⪆vGluQN{QIGU,5;crV;gfzgvBmUZm X Ug V1 jZga53Ǒ 'YRgkȭb:jcT @ h?(bg'xl`?.r+J]) SLD@X3E\},QhPy(XQ~90儚C5@tYsռV} 슭rq\>ho4Nzf GӋ RBze>˸-ǞI؝ᓰ\}ğ`0ueBtjWO>[ DuEZؽBhLLb-/K 834t^5=ɭAdߏW`o?[5@TFHbq} Qi*{1|'Y{*)X Cz<_$K?* U>LU{ډ@ XFM%2ަJ& t&v ".'U[i PTT့Lmb7Uڴ%^L lw`Ⱦ:~q#"j8ކD%X]$TT b!}DZ 02f8(bSV3V%C_9x|bTT;V9jOk:4&p$0H-~TER*&y)Ī(XޓI5̹M&ZI h\.Vwn%_^[M[D䪪'@qH(o{YC^[|̰v6Fx2r|.kh򜎘zX٣d9x#åzh }ݷ NDu >][ޛ&:-ֲʗϦ$UjXl14Mƪn<*/ ׆5:j=4wqXWYJVrSVV5vZMM2zRlUAjsV.cVl67W,2:jU孨D5 'jݧwUq: n @ \ ch{^c5X S!Ҫ! j1.BC\Cl HQx]>qۨ1z*@ZR2u ؕKP%+*T U,'bfmgR@vVVQ*|TX4R~]cͳ*u2ejjX0WasSU;aZUHLjEUehTT[ L&ED0NT:[A:mVB_)zY+VXU|d* UAU@֨~J05[SWM5, iV=*2d2ۯv" otRVUTbbz Uu}4'dSj^jq5y)wJt2TMjYX+VU>Yh,C]%3Z3Ŧ'X.w:5I YJ)m&U@ 8*|B=HSIvNՆZp*J9\jX%ZQX1Lf V+Ҕq~`b籶/DʒJ5N^23W15Oj9s?gxgZ=VZKT]_|UMVwUS=cMRVǎI{c<(\+e&UO:: X=SuPWGBG:\YBQ _ jz ~N3WEbPWVpgXmn]^Xmh[a0Vqj=ǨU+ya?ȣ`/@T6]*RZ66յxZӫ_{vĚ/лp1C]BU"V R9:;F+UytVmAXoc@&Sg7UCM׌1y#ش$CUmm` 9 ?ƾ>oFN\%J\XͲ\Z5b5վK0000<13a]@ju&Xլut=k<'[g_<>N(.||^0=yX٭$/{f Spt'65IՊ d2V,:84xX9~'pՌ4T%+S/ /'tIUY?y5 LSFvVE4Dm3ى4jǔS pew/,TcP5I>%># Pç8ݶ)xK~K.aʽ ZI5v媗\VՎoc PL09mfjބ<_`BVFŧ:uVC՛Сm㥥 9S J,Y 6%+} ucݲ@ZjVY}?}{ܹ*X7Z"5+>f*Q5^@j#H*sꐝr`T:̓Vtg_kI^RN}L5|_kMwı<>SZu>+q]0607Yb|15•>ܤSϗ}ҜLϽwSP?U[Z3%R*SN]lBHf0$jSy)ICIUu&u.r?CJ"Ig45(,`S/&&'^W_V,hURÕNIiR{XXER M^:u°rN(R`ݼ/qdcfHL*Vض.b`8̘:wNtO4~pHjXrK&WZ΀NOBk}*HG~\\yV`ʪխV>)U`YC4Ve|%Vybu޵_9XeZn:XC`lL7d`\Ou*ROZy&{V SsH F^,W;=*VUG kPeVa_qjD*G\|܇\l~;3T3Y}]K`"t:Is%g1,O.}nߚ_/Blj+Х ^5~bBLٍ~ogNc6lj!,[mPؑ*UjR!\pHLiF s`fBF I<N5VVyMBV(fo/ʕ?j~=JBZbNkkĀ/aw9ijR=Zf E]^#X)]$70/7TuQ0eWca*Yъ&|,2w0[1)ᔙZO5TPuM] {H(ev.Zu,i%EX*@n_mjQ̖ɹ7K=}M gdPtT 6مęqJ:PC!umu!-.$:!惤d,i$*E݄.amX2AX":ċaaWPt֋/dwfL;3K<CߊNVzU|asQ5auI'8ƨVY KXpu1US[3BJruaa*Xz.i~&-UJTMݤ%WWy IDATvyjDSfo]PTmvM8XlVFm[<%Δ U~xXR+Lʲ=}ƣRZawW'ԱWwTHEY;*Lg~ΦUVY^UU+ _,Փ wCKKcj 5UǑ\5X%0%)&(C/jƄd4VKbӐUqO\$8[щ% NQ Vuqlct6:6CXo; B9&pܡ]Z7sAnF%? UbJrKpD+Rө  >A\$1u:>].j)S56%xj.V)p@ՋBXqGw՜U8]e4JϔZ)=PNBª&LCe|SmO潬Z=)I@ݢ7^o4eW,DzcR^f5-aְXI9#PVcMv*!ڊQªð^}Ոpª*Y'AjΨj>Uu/UCkػO eٿV 4]ҢNӐ8IHY̷7S_ѵRkNv㑀X,`-UB͖ՕVj=j5'`AR[Z-B! g (ky"T&L N^>4VthZ@vX5f6VO"X=T Zq ,V V߳'JXP,XV*"V[0VMkZjVqs*+V;׬@U4#(r͊JGNG6U=:dR5~/M:$\йեK{ab%jWXe7~Q*U Ϟ9;+1MP(ABJjvÈU[Qa3j]kZͯon5͏`:`3ˮ}+)Vd6˺[$.Ul2`}UV?e.Kg:/&49:99r#zJ%!^bZqq*.) ʭ!-q@ƁnTspkPFzlOGrUڗs~R.) *mT-Wn;|$T(BZ҅ {ZEj{骓q]ZDH-*-{-%I΃P(Uq W;PǏ#S B8 X-.xK,W Cê`bCn$W+*V^ 2H$}y/L$!&*zEg\;K11Zw.xqt-?׊]1e^=bv vڌViQkIU1nȾ*}0re {B]zblw*V]F[^z)DzMզ,~ )*Qg֨U !I%XJr:Pj߫VݍX˔VzӪ]~*-s|撀*H[+&GG'qUЃDT%^ HɪFM_iͭ&?UJUML BV+`?/U_DsԝOP7ueꯥm85P,ZU-/C"V_[|[ ȩzZvVU߽{]Xs*6+yX%wX-`5r"*aU%Vi/.'*ռM\m,ny» uYlUer'*`zWsZŶUVoC5afjw f"p۝nm*;|z}*bjpWxz5&8Tcw~pf*[ZbF[A'ns!0 r =?&+kVOwdW>:VOwQ/ * ͯV UG_P}UrSjHGr$>*.XU߽6//w r+~(SZkIjNLRNNM2TqvJդ[{z֛R\RjUjZ*j+$tlW-Pufx`@@ RA=L|Η<P HӐ*]󰪘+ 쮳e*s*AZLlmj~@z@/**TɪvpU2````^*'8( .o/qƵnmj/ZV'H)䪫VZdV}:IA ު: w3t΀W0_JX w?՝'d)tU1)@>7rՍ*05L$}(X<>V_DS> #~;}DgyKA=m"*~o:~zGj2wqGoT>*ZnJesh/>B%AgY>Zm owI?3CT&>kZ~6fT!VuГENJͽH%5U'GT| P#t\=R5I`1;oJlf~Vڪ]ɼɫYJU׮91&gJ3}Ԫx*j(kLU+j6Mj>8;&, Lv3+n"aP1XHAY8d")j!bqt@Bb􂁔 H;b7s՞_>۷yKiK {݅ UXrL*EVK#Wj SbªXXUU )cK zjaу' WÔX#i U1ZMV_G hڨ*`oYrr7G|Tmi_o3U|"z >p+de}[ %H HO[(JlU~| UU:}ӐD6Yo4 **" :Tpz2X?gEXڑ:WzU zTM(Rj:;;,U3@(r JL@Ho\]\ _ (W3եi"+Z}]`)}VU%~W:N. n=^ω{&[Y*iCR2P \SUMIN^(w۵Z5*]2!w=`JVaLAZuMj 0Vq;V_ۿ׮]gX*N&JV2Vh兀G:FF,DoUҩ)au3ֹ`^m歪YI*5I& о\E5]+ bߚvX<|h1j;Qw\QJT}7j UUN<-yạ;: (TYCsOjbK&OPV*UA*ϧ{qA6ΞwtFhke%'D* R*Tθ^"aVA/FQU7x0e7ym>IvV(aStg^Rcc;-ZnVv,VlUYPRҹ BS`eˉh!0+L>\0}vݣUZVXCru@*Yߢ\M2@V`'$W3KKKZZ2V !jQl[uNvd0.Heb5h1X C W7rO3L%zٯZZJ['XZBiZjZbيk[ZV4d^0hڦJҀvؒFVYyX F5 ͨGH6)|jjއYbcUAUa-٣WK(WIT\rl$ǖS9+U/k RuP&U*՛[cIZ-Q"vkm %Lt`p2V# TUPvS#.Ds* BV:Ōo+gIXm4\MسRۦ?Zu#J&ԪW/f'gO,^ɮ.*t&a^E.PjfJ\LB?:i!bU;j@e[UшչE5`퍱>Ko'U>ЪR W_Ұ[ĪJX +Ҟ[mFS?"-jg~`VL^Qmt v6z5@W?Q%ʫZYyArr FU!V4FM+/`y׺t\ pJZq700Xֹռ/T&c֫r V9Z{x$*j4*E*tbfWAEs&O|UCT"r(H0&bP,Z V z,Sr0S)$jMSqS{1*r[ N-j\Ī;_{qRqiV{S Yjf7ެ VR@&sLb̪k j ߴ \]-fEj^SSDΰrR2}jc_=w㙽b$|sOG_%4[@h_QZA'.X5޵P~V0 S^~ba[e:JG% -*EY6XEq)U*Kr'USqXR՘~ BR*T%Z.wpVIU WiTTJr5#9t,LPg%+*V o Uh8,ͱQUza^5b*m ꗇ&z5ViPyXF8;jMMmHFzjKm W_іRSZKVi9UVWULի@sNQ>v,Y:B3Qoa~*9[ @ {m2M8,`mZZF3~3SAE[6d(,99 ")_X,#HX%.-*G{_}?ɛډz*pӷ}WTy?HE5{O,Te1֎'O'Z'PsPVaUi*3T;G0-yr"UkRSAϙGFeMZN"Tf0 8>$U)@?ڭ,c ԭ*gUUJ|]LX/x!F՗oxXUgIλw |‚X4Gp/rG:]HCp 65lXuN?j XXZ*WPu.{b@:GQ+T5R7xj+V`*]NZ[lf 8sm;j>ۊ;۫D;`Z*bڡKœ'/&g"Ŀ'B=ՇM'F?qUkV,WtU.=&j,֤1TT!U3\'};UizXU`ul+cZe, 84bmo_]Q#J%qXzU=b.n_Ji6+PSj.pTLqS-m?ɕlsɣ<^5Fj#6qy9B)UIU;\3!r2NթVe2ٚEB_PrY%ˈSV/'ZW7jCMUПl@/-?qU ~~cwO1@շ/xx*L2{ 7 7yV&UqSk׊WF3T۩  A*Pг%O*qupuX%:9vqxXj:XE:*s?V IDATnMDINg (XmnV VoU:|֬cUjk^Wb$6UKUZ۾ W#p399kxD՜-eNT'RyXJXwKja)Tg_UU,TAK%튏UT~\c 24Lr3+U%ojbUUG)U;tJ1jTKZG(gI[r~ʯ72Val|QxT{k^-M~jqN=;~Hz* K,UUWVȥ(P*2aEq.eUUUq(Q-Z-Kxղp \*Гcz()XiV-Q,Uͮj3!&iK?B8̶ TaUgTKC` ˣڻ]9uujְ!NRVh+_&-Y[Z]@<ؖxZVQ>HaKuee V{XXe+Mo=R3Ï9=U.oEp`Wn`՝vV9z56L]IrNOh U&jKf*U)TӈF3[j5#H}XjM*r5/ *7ZEB'-BUѪMx4TM۾4O keE=U^P].sSuoIhV76``I,+Yr5 g /C UK|_zL .9ԂTEހ~5h*IWDƊ ̧IfJP3\p9b >_+!*W/V"Uh>V?`UHV[DsTUt;֢_oo;NzBJVb59|`PV]*]gTVe%zBu•U:1{J+Nu)m/H )Wߥj9:UY7ř[\,Lpi| * U|+ef3j|e &=,Vq6R ʽ,*+zT}+ehYŬ޿y'A,{ V:@xbIt+R*k(6cuS:~ T***ლe|_@j4Z!d"uQl.VJKI6mU] ZCBpVQWYX|g'=bqMM(RVs)E, : F.zUuq1Nh.+DU`MTqhՃUd}8xnlԬmu.V{XvZ(jXڽsnLynjR0Ez{ftey(s%lVCTYU"G/Y+*>i3RՊ`Ujbu [j"Xхg{sUt<$WV*t(22zOG5*VE4իPaCJ ~iN*O\*TSrdusPEZ]]ߝ~AfmTuBSTZ PۓTG&ޗVV Mj5*P٠ :jƨU*&r*WSUettj./9L/X*TQo)fV6mW~DLtqw?ww!)%+yU^FU)KV1C,Of Ei3H"ƾ/Z9O6>Te;_@[#9i5h"6 =*Hkz呑,h  ʋ3@UhjV' !&N-q|uyDBFOa*cVj KKK[UaWc>D5)27IT23VK6M%d ?65/ `/)|oFUVҲg 1k"5\pq~F2Z[V)R#bꑵ[>qaWj9DpVXtO긪Q]pb:&ְ0S }eꔌfΜ0XyP@V? 4[XX^fhR^*Ӊ*BUd wp*g](V=.=T]ǢU7UN):0k o ڬJsn_Pj|]bf2?'W-QcJSU'S\qF(nzf v m7=7#5=|O6|8LˈꈯV7Bmktַv\U7T_{0ͩ +Ue\|Y>]6Y-!oUMJLE!zX1=X O*Wmv1?,bIQl ޶DO:!6\j`#ޓ*b9TꌫcAWmkQ9hwnɄ>ֻժn*a5a~B V)"XfS'O*! 癫D?OO_-,G=!WjT%^yUU XSg}3~ UV8D2Vo܀ X=r̠joG5*bj$wbͱr YVu4 ZKP+~sN4);Qʪ׍Q̿ 1Z/*IMwΌ։--^lazrMhjkU)6;/-c L E >U<A5aժկZ51XMx} Lj+iNUGIpu*&V/I8ւ?%NU5.tX=E5ەcH:Ys jWJ`V56vS2V܊aBubuE+I=zSPeY읝+ ^T%P^zU zi;E^#¡ewµA?Zx07L6{Luɞ<,Xi)P?\&EaTTZUWW;[NlQ)T4 rUUNnxQv?Kƺ4rZ5T0IƲݐBw)գ~JV bf,bJj4 G]EbuU 4'V\`=Rڍ lUTwuD\RBf9!B/ c߆V9 .^ZJ`ݘ%5ЧA9ީ=.U#UlTxK VҒv8 2T'R ֆD^ @j)jh˯H5XZ5#9 *zѲcǴU pUU#ZTeLՒj4V PO9Bj~$X}U?LjVYUMXeHEBz< +w|?yԥf7jOȉ\`$^V18Ω{*Bo^M،ag0R:DoY~UĨJ =x#Q^iT+:խ`G AjWȰh\rAvɧ\9& ,""DL&(U  Ϟ|!*R WFL3\682;IDX93; WsGs `H86a mҀC/[Z̸ڙ}z08+O Q4ۈKާ4X[6ppG4:$\Zjf7_"OXŨinq9)aubIQ^"T}幀/pVj=8B*W? Va`"3^gwXyª RHCL_UK弾XmL&N$0:c\_KU*#7^ѥRMeْgVKCTd(dX8*gz?h#+_b0QRXW+$cO\e~fj= HLmڙ#Z%W9 V3VsViso*-W:Xډ@'srar!ԭT5/qj&N[caj IDATzX5q;khSk+\]XU.ӊ,agjH!NǴ E*)IA%4=~Cfy8z oIB~7t ͛|j'~g`[PVMS7jA+Vg+εk*`c˗: FB:bUK1fEjժ9oœT2nab 0IT=t X=&aU]).< B ɤv0'ʂ@X'qrk@Y-\|•'Z>uT8zB*a+_#)^-] uj aGV97on$~ f+"܀MZQje@ M"2exD{\LݿzY~U^OHZ g]XJ'\@\Xe5m U85W *w T PS1Qt|X=>KjV5lW1tjM~;ow-4-PEO\@OlZ_='q>28"Z h}cc]fdDD<n,6E 6MjU[*<֯*ZjʭM0+qilX[GW+pz5}vyܽH2,+#EE'ڗpmDM- uLoqgTE ZWܯ¯bV![o<-eWj8 UP,dl8}S1V2XU1CXMOՉ՞)*V8vZ{"X529U U[ջ++wr-j*5W}TU&b55;]V\p˖Z޴So1`9-ڦk&_CRv !*F`-ZBK_ ӔVZ}/2VF5! +r7oNOONLw ZV,; X]Ye2"G ꪎZŬ !/hN>zɓK,a5&k Vlko ʿN;N1NŌkA<N'&#XkNkk0NZdU5`c`52Ws-`YR=6SAb{:2eW<VWC2~!~Ue bjauvh͚9UP/4V$( ]gu%|NjUPzWj'UtD)e߬dk;_VWOޭ_ gZAKC 1 0.m{w߶Z}p@{| r58J+[Hz>&c!W5W7Z4u)2Ό'lU+C:JV=hN'(VPVߨnP&٬b5[=NAOӆViU3ԕE99cKHrQʀ+@X^xVCI*R5ҪqaζD` UI 4kEj2pEΙ+8<3 i ;R(*{?|sa|jjʅD~܎0ª "XvӆauZ1UJ}>__\DfՍ dN;wXZHr?b8!r!J;NSXܴNr$J;:dhVQZ6cQ5P͆Z 5j:zkjNq*'Xj >Zb$+jEoWkۜs烟8Rj.Vi׋+R1-(/޴/JTx[ISL)ݙ*Lk%rrYJuĪZ `N$k<*RUT1eWO)9D5^m_*zXmXmC~WX:+UқQ5 jR CF]rupe](Vۇ~xueL"XE1bUbǷMe HS'ʞƪL+D`Z VA >Vr3T=ĪUgT.*1t8ibUsCӰrNՒ%W]`Z*RCm`EZIe Ru#5NX5 zPY:&@!_5b9SHHͧ{ۣ6+Ê+SUl(ZURY+[DVh%+=HMM.êEuno*t+`x% <,2T0 W1aJu\+ۚj|Ī=Q0Ijp,j:阫UTSFǡjՖ6V;X- X$q9Y(50ꊰ r\BV8…7LB$?4sxb.:zs*r7W_ f ^j}ʰ2\>@ZU#Ҩ.j  [MAև%Y-qqB⪤ ZujH28 Y \^U$ꌳ|2r\q*G`} jXTeY*խ@CzE\jVeju;(W ;X{gJcNz OZyϪoPTU V:GS ~>kܩK#I;gMgTFiPeZJu_S81Z^_ \5jt(=ʵSSRta5?ZHUVT3UH*3N@dx"XM1H2Hž@WVYPX=]UYeQ+eL,E\]-X h RuIWKB+eiJT,T} GJ`%#jPN|%e USTUX* RCqr^Z ոV]@mR'o6:32<)WQJ d+*.U϶k۵ꮚ̪ʋW5pV? UU+unTAJ->ϑ:W-4_cUJ&%Jj5VbeTUJ{k]a*]xyAEոnUgAZ$ΤШoefX:-rWYF螶 Ihg:-* 'UoN2jo{_gP^ R59VՋH^@yPy;t^\_/_/`n5m !V :O.9EpBU=UVj]ud$,im/%b` FVd]g[Y^uCW"fu4CjoLʹJS%T UeQ|j74TTQTix Y>I?Amytm <=1^#ϗ ,V}< eRӢUI[MR/JuZL^G'9=:#X`7)ަNU&Jaf[jjW[GGD|NO>#S(VyǪ;Y*{(V^ 'WI*͉ܹX`y`b:_Qsuefr+YTX`XMW^[|^I\!YƒVlv;TU*0Cq*D@Hư싱j,yO)qk'u^(YGotoTVC!?ĕ5껏~Rm1Sc)c(;X%:YqZvm"+՚в&WH!gj`Ju'8Jiϊ4y` cջ\ő@Uª*CިlچIQdk 1lRuBY㍴qEVv$4,qn*H5[v'ϾW/7U #V5 NXiuJ(p 0XYkhX#=-TA1p'HX'A*9•րTuᗠ#_$ \gaPeMabUUܓ)xβ(6 /+F2JR;f4\u2JU \}:??/U8'-XE'wk$,wʛ&My1J,Z}jY;ߐ3sFVaYfnU_Z*Ss -J {QL! AETDJ- EG k.vG腝<Vck|6LH,V ֚wli"e#N]L;VIbޕLn%X*Wma^,h½ ,vOjA\:YߞԃEGNS,6:W#X"s:RE&NsKBD%:~Kr*)O!63\VNaUTA!dKJKemW;Y%!\S`qm\z`wUALO\c2VlU$ ַZXROOV\ZYT:]Rb7I`ud_}wbϟcZp*U޳猴5P% gQyav !Âu\oX󸤧X|ӁI2WʕOXiU|l+_*\׬]DӲ9 g'( r\["Vw1N.HmAKZUm*HU0j6`L\T #+, UGv[ZZ8 \%;@}QdVtDL!1-X g6cN/vz5+Ԯ?O\. Uyۍ7˴+Jܠԩ)Uf( K_d9˭W*_sCAy#W|%PȨՐ1ZѨdrǎsU5qZEWvv޾n7)XZGD/$C5/zģokZ%T%4Z M.,ɀլՉ6WȌ¢x$T5,C\UiljܭVO">vLp%5O}*Rn*)VE["eީĄM8[1ݮX5<؞&؋4V[ VkOy*A~LV4bQ%vqJ Y >`*>D}[Wƛ]JҿĪ]mgm7jUA OXP=xL$jĪ*JVWYrE۟+`u;Ϥ)+ 8"[ 49ePUqէJ΍%)oc^-bbޔ@2[FJQyG*:/]u^EjD` RT hw{U?~_k7_} *&x'󘣡CUՃSUYx߆>yȴeZu|tj;$PM/~crbuǹ+_*D˵>ju‚lMZ*+#3,PѓXbYVZkP,X%+`jSe'b6TpJr֨h@E戔"ͮ}VgBsT3NJ84#B$aUXV{IUG+L"*S/`wDU3B:ȝ{$Ja<쭲 @&Bzߝ zQ@cjj:Bʙ}Yŋb Jb.jW?W=gg ~?j|gR{g;;oX[{uUa5\cZ=,V9 "z jun\*IRpVyUIЫTrWhZw2WωU!Uzʂu`$m91P_EZ8|#KŝUcQS"F-++I#&S( }Q6~'څUj7R]LXV٫\"",+DXUƪO;;J>WkU4WՎ#$W/ U8AvCԞ~PUUL: I \:ՈDzv΃A8y8IXqWEU7mz}<'ef++d^"93˼d.ne\E 'VYXx~yO̧ϕ;V)}zγuz|iE5b_̐xa>tՑXA\n N \ZN2Vcj:* ~ Ɣ`"ϛu"l+? yuR*EGI+k%$rtʒܨwNiܚFjRZUjqyUUmC* K|;e8uRۯ|jZV-UZane+%YbR Ղ^*AU2VkKa)ʫb VD$Wۛ`wtAVK@bj}VUUysZqr$`!W 4lm6ZrŇޱaRʽP2Tmoo?.\^U.+5Njo0bQbZ+%QT@@j!v(av[ju.UQi •J`U/;3Y`u˳JWe+aJa+Xuǖ߲qH3ԫek\Wz޽gr۫wV=X` Ww?+Or  tݴj:!\71Qt/MuɨBX+ՔU)a, TW.kUpW[!0Tj5&|ÛPS-_wVvݸXjԣSkc6=rz VsX}C+_N~/k@~X-'Y Uu`]ۊvx WY،|.uU%rXp*UW1Vk \VaI>tT5vY~j"jhYFm`UAI`kOϤ J߱OS?.m@`g7Gvv*`\m^z&<$KƁHD3zu@9R{ LVQe?p<\>VMP tWXܳ *뇪z4˴ 7}wLni<ń=lk`bGDkn$tmQJ]bsI unPC@l.mXM-!xzW {a =ઝA㘭BȉӧQMuK\SU%aj0lԪ1Ulc-T݌U5`2T0UZ%t FX"bBBfD7IZXfUk>s*wtrjJj_a]/]ã_AΣgxU^X(_ik+aVd$=NOөc+FWS9 sؾ ZG]9+,f&kR6+PՈUkZkt2+Ǧ/hd|R11:b*cv$Xb9NOUJ63J`MWQ{yVNieʌZ@V,9sY/fVueUP]ZSXzw Jh1.,$n@-x kl2p)?.*mըd#j '[D0Xـ2hU'Ճ}{?ǶjW-UҌNjnʶږ333=J:_Z5 XF27LAN:Jz52WwZk΃ Ph_ i *`Ӫ!-~@e9m$U&T:[Nk[% Pە'ۀXS7у+kO3`;b j GґT$cSUVqMr$ iZMG$ V(UX U/n\@:sRR_\2E<{y[c1%2f:2b" T4`c!;B5kXBo#ׄ?2SB m6:d6Vf{ly1*YA Z-SU32k:vUquG6غXTx+Ue'_\nuEb:V}.թ-XMW TO`ł@.cD'[@8cUSZj%ez^j~o DSSs1u3\ZNKHdVVgf͜d>,^(XnYytwCWbi'qE{R[8_;Oz_aogAUGJBաawij}f;qJ iV˟e6d&VK7aU}ȚSN@|ZTteec9S_=ݩժ#zS<`8.#'"#FquzThtsohVht VT%VR[թ9B`e"]X1O -5~J5 ;(#HՑU6`6Aê0'V6WB6]٨*6*'ҕ*/٘6\ʷ%E W+@*SZ5pukNn;G Iɺcֲ*XW*A@fk[!XV e}V ,;!)q;V@&(zJo}=U+YˆڷrՁ*j9*_E7 jnQjXQ!?`Ւ*sCn**(ruM5yv;yke& \ӦU/ j_5q+^Ybl *W^W,~#>g-7~Y ծjW׸U;U9m.॥U=^\x_x~SǫRё?y,Sx>~< TWr`5*WNEs_wVcUV¯ANO̚3/U'fa|*LSVijE M*" ;P鮡u Dv*j& ^&ȲB$*%BXp6azVXZٲ_z5VmsMҚ@. T!` tRd V3JּUTZ  ̏(( ,Y5>aULΑPVA.ٌ VgCSE1-i4LZ"m=%ڶ;|"`&/+V U%[saʙ`P+{5mVzWV٪sq/OFtFgW"ȳg*l x?sˎ,Z`ſOk@5,|]zHJ,ѦX xXrbuEl<8re*V?8&Snm1gU+ro;J6[j` j'17P W۩T)6KY]Qî뢒ZEƇ %v&AX*V\M՘]q'`Em ƻ(Xl&>b/oC*ViuB=̺:F*VV)\.7֓9DžSjU5jWV&XW0:KiV:g%}:tQԒdy_l5DdM%8/(U OVSZruӈuznp*kU3:P/oNO]4sBW H<0?;C0pVV)kELDc?ۦfC#VUSޞyZm*&յ WۯYvfj͵(]ENa*+Vk5jS@jfE*F OZ) lUʩ!=ˈSe*XƜ7\UUqE]xTAU UPy3`- /Lpd1OjHÀH:ntD&^GVD?? J޲zդ li9z%򏕫)VO[}x/iIV-Wj>z.\G V [LS@,r{8Nwj\?p?{FV2SQW hK:zz/T$U%VUtZ՟]XU_rQY"`VUs1, B=^K$ZjnWz ubUr牱:ufCȼ%RfZcgݫ;bPLx|v=G#>U+Vu`ʦoln׷s8uq#be*hIVX[mV G Yy'SǢ`}jZ0_]_ ʣam&zdpsd@Q$R8;vZ׫46j1=ߥx$\] 7`uKUVSuzf*:6d߸1klS҂5Qn퍻&.rݰ]}9&6D_>*4UTrBxO$<i3[V >;kJ$5p̼xurUB4UV1mKM[tW%S!ުqu) W{jb{U]iGਾm4jpvV ;jE,r>@aw*T+tMZ U}S=VI1媆"vƬbu;YGq;ǚjVVc ڏ7W-/X<(&Ed|BvK^Ze^# b6.TmOPzPXHYBV*[Wc ˑ9)T>-AV(WGwmkLVr"V\v9XVrUq N=}7k+^Ijr[ZĪQ?ss(W`&VԪUGCzAj¿9ʫv>"ŗXg^W U]j6ZP+_%PE*VLjX]^4T:I+65%f9[ZD Vj\5].VQ#X5΃k 7۪X BPiUZX6Wȣ}NprH.ZEZ`I{\+I#^ߧ'*lYvgʙX jժc@bY"X̴RR0ح[me$ q ׎W5ËUJkZAUëo@M&TȚ)B jPoXgKq|KruUly5JV*Sp|j5i*Ī_seZJk-$^Ք^MV׵O]H⃣kv-{ cu띹05{Tq;|!Q۝.aEH`FKYUy#Ϯ+2i"d5mSVopgG4i'VZ}hnqB:+P O2juJ\K;h@ZA 9bρ\ L-hm ,-UJ_pۦNPUvuuԱՎS-zsՎTrU_+[۶EYR긯'ЩzM 7U:I71V*V-'obv@']SSX1 1ث{Jک'iڈ}ث=IQ𨚋AD{{4\wf*uv6ج^ Wb-o٪*gO֤=e)VUe\J+,Pu:hNͦfDI\TJtj)HXjU*gbCV8,Żi[;NUKDy~_AeW^JHzTW֞%Ί;+U SժP p6?4m82V\Ejs˴EXcuXEvBG씼蠖)p10ꤪULZ9 -Ke-LgUF'6 7 `ITUͻOo=q,4WbG& Zjgb#CajpZ;2P{!Qs9+M0R"l1 @8 E*_{ WvV`6j|9T.CNM["Am3vc8]ŢZ% KXk [l-&kx֫;Xȸ4pj2 ' +p.ҦpI(&6HHUwOq+`E*]D/>Fec6 Gj'S+W\ F+HToa'nfbLYzDlZ3z\<V6jA`+VP1VKտԴF*X6k ի$jO]o9H`e51W11Uj@NMZ p QWFU2'.B\su~*VXu<`U}YIUn?OPU]Ն\j~%S/+h ^uTUVr !ER'yE+,!WQ]b,WWUyh@ "KIuh} #{y9n ,_hzh0<p:~-- 8I14`U4o\u7^dbbG]LRUA IDAThOIcK/f~^##_Z]^>)xApQ]V/u-6sbzq`@bLl\֦QAB]IP]c7ԪHp5ɀz*` ~]`ͺo0v[1IsUv t7Ypj:]xJG:Z C9~*#t8ԄdV h 7ϤM  "UzPۿ`f,7jz  ^{xUVk\D;퐫VUrUWcU$"Ec*C{fj׹ u`V,iA`ro}bWW1 էŞmb c*feU|ׄKj>oT"*xY5գ(ֆ?Ǫ V;|a&b9_zEfNrVw|'pfu'["$JT0};cɹbjX]QS4EbLxUw_a 2Yg$=PYď5^]._au!X.M\K*bїfD\֗` BbհJWɏFVMWdG)Q\upu~yJͪ>Xn~hVrߔbt1hW_Ajs&Z/LՏ`  %bV3zu+ p)Q&տ֞i@,>JTJbL db{Ax>pT6L1V!V$itnI;²l ̬zͮ'^6ax=+,VWW I iG_ɦ<ZJV}\~t|)VCr9ʥ;^j/iCpiŨi @j7^@*GYPTאb;$ՍP\e*0W7b\=إrjDzto@eˤAR+MUn5Uq'*bI#:Շ2*JVToLL|ZEbHXTUg\zAR W}Bv8v |rЊ 8wc ;'N0^@OkCC{}˱RWgZ*O (Xmm@ қտqRaUVSMO xpzc]!^b r5m խ[[[?gN>4K2#sKP]'zR?jNz֒G0j5/j5 +짲 GI4~e{Y,8\Fl,߿ w ȟ.?#&}r8[|e$~h#J*rWĪSV,.fm^:K\tMNj:P3`U*ͬaj\:W*XO{P\V .ـ6v3nmjf;jxjďRT8{X}OW N'>X!RG7nWiX_U j[p|`S㟻U0ؙRHK6e.NAzH\miȜ:٫ZZ;P9eaU(`ТS_6 Ŭ> `u$pP*XժPuAX eLa,E~C=BjJ*Uɿ~+`% N"CJ^h8|%R. E}*ƦvƇ2j=_s{M^ӋbQ32ӳUgC n {?tPTEpW8ASX8vy%Ȧelt5*BMҘ5CK*J[ai;wu x-גqUʁՁW:M6όBR(l6b]k8\Ǻ^#eFjvմ_LRbWSa꣙hֺ} VA+DcǪT,ZgZ1OB\MUpU>Br/jO@hV' P6:,Wz$sZ4{TX:~s+8mŞBjjh3;*M™+xs~aUMeA8>TK UXd7E]=C9?pTUN/cb}2VY0[@ՠѶ8)ɶC"chv5 M]5 U69߸-܈`ۛkOĪDo=3@X1k9SNXT5,O)~gsTtơVKCi՝[`qb 'de^C>ͨibUTpjA!&`!VIUXOV%!GdgSkVխW|_xב}} mY˛Z$tTmz\bS^ϨEoϙrSB0T!~<0j%w1b՜pTM葞[}N*8V$]΢|GwM WO2b@C#"WC jb&s; (j }@;/.cf63Op9ǎqۋP} +k G6pHڣGFGyB!Eh&Wժ﮺W $TsAx +;?AQ5CK-I?JY7z>TUINyM UՕK+bmn 9~OYnU:ifmURuuՕk";[Wb- (UbTųƑt]]ie-KTlK6" ]m#m^BTFXQ 6 %%O|N\~vyêLT=Q~jn9-D3TL JuܜA:zW% pV/\ \VnO< AR7*WzO >} hYTغuë pjUL&e *] =-BXX];%Yj_ vY @C~UCc W\ʷh.5VŴn"KD2ů-țܓـrEU"a8֗Ë\9@}ߐ<*Md2wurudad5Z\?6WVj_&ŀpQҰ:#QCNNjQ KTjڼ=muU|+;Uw̬=o]}-Y+UG[ 2]OV"K6^Q.i4Xٝ;ҖNdՋUo킲}k ٴyIۤR͏$kicPR2Q1z?k_R՚車ԟǦ(XSQ~Xc8-ц}Uê5G}]IdZMx&0퉶~Lj@zJtvt\8vw?wO>JTԣGz$;]ՓDϷaT+OʚLKF\A˛W(W%X{z$~Gճ_~*d*_UբòQ՝uL2=!v V&XU\NJ}'G8ML`?eV-Z %kQ¬m'UHkY6\ZUkU#f-XOC ?ըtD%@M 0?ftŊqjzuwwׁUMq?^dl=JlNe~M lBϖJp).V+fɠ*(JL"7}1rcMpK^Wyr|Q@jLwZF.uWXTe36o5UPuuuu]U6#Zvvhmx**:XO7'Fe!S ˍ^h6$%S>M`t;ժX"f}h؄7W?*HUYbj SPU$Qa NvZQETxgk3"BK.@G4x#+Ī{*XL=kHUbՑ/CUjF.0U3Aq3M?^Q9?Y +VqNTMr])DnnͪlCeOVgXJ]Jp2^ 0ْf<تIola+*T.bʰS1SVwx߳ǥa(XGdC܂9[7jQŪǏ`|~β(*2n&tM#!hE$dL $ fb3( =(bS "˙,z7{WsϹe_͏F{ VSq/j:=jUSJ]_[Y;y:Tܥt $gٝU˓sw ~`N\EU,TUTF,pXՒUa+P5[ \aV]Vdh87fZ[;|>͗z"5x j5r*>=mO@v-TϜ&UbW֫[^^|,b#٢` UphUX~wX5gM\Ujz.9Z/}0uL]_.UA3lVRF xYjԮTTapϕU2R^e aWX}ƪquaKsp؁· jzi7pFվ(;SQzRy:vZ$@;;AJU,fz˩zeDyZe7ptPbӂU;}w-xqZ뜆'W{a]~٦l1V 9|(A6S#nFjQTq6α8BU?-lxtk1S;]UZ`:R{Ђut]a@:1h9*U Mo&=8ЃUǫhΒ%Q׿SJWyO9mㅻjԿSINY/$rT}+ܺO%*&{?|juE*x&0*TKY. Q@SUKP*$TXS\q\r)}=6T.1tcPnGyVhU?W^`rjorz4??==ھZ6I1VZ1-Rc#֭$+d!VSg3iUrկ2%3^ƔjON&bO}C[1*AC"7nj>s3ⶮ5jŪXZXe( kS7o:&X",z J*JVQs3:VٺږXVpnߖH`L4H>@xEZX$ED7rHe[OOS\UR)X|ehT*k#fjUݭuTLZUZժR`PD[+ZK^BZe R[z\X%>56? `O.^VIӼ#*ӕ9U~tiJPrUNY}׾/tN5P|MA]+%{k=pNwͳ|U/oۙJٛBHץbBҲ*VOXݮaN9ZV(Y="W J`k݁Z{ `ͰfUPdAa2Y7A[Zj&?\נ T>@%&[_:_o^XU"䟙]~,c9 .wVX&*rj1VEf;][4Z:C;ʲJA'| IDATؿ&5pf*[RgϪe6U;M~jaو|Il威upoOőռیy#6J ob Z,瘻mU* ה`USY$ @@DZ_k3A:ojJ :T)WR KV?Z}!>S}j>u(\ ֥Z(0U/P]sSZT|z*LMhUl5'jue#u_ꔈ=R¶PRe13]!VR b?aܮrJϮn+Vі yjuoVfeX f.8V|`j.<|(O"ɪZ'Bɻ_FxuSjD=ҬZY )hB܈e?AL֞b>Xۆ_P5CV6C^EbՏ2SUʬ뎎Lۃ-.U}sskábr|`AUvw[qoǪ=ii۪FfTUT}/کVeE]`&X\ZEŊ6f%)Tu8P TlRCG%xܖǢ̙Ϻ_J `Xy);ֺEL@Պ5,26H@FL|[Ιai Zc9ݠX]GV_/.?G5rj VE3i}f:h՛61NwI)$"]]V_&fVK?͕l BmׅKXHhkn\Ge*_:{%x>-QBՒLXlՉU=ڳbU*@!;v/nz}~P7_ -4|EUmBZYQVߍmU |xԪUNt5R-*u_+IFyjTL*8/ު% RgT5LL@lȵ'XEU^nN $UdW$| 7]9UAj pGA,N:q<1؟GEU,.~pݟP]wwv[T?;:}|G<*5WOi.?۳>Elr#ձ*UYGAUGx9u*fs\ŕ-sSUWTLkThoF/ng &V{.b+; rR9j!XNpVzIznj]=} aTڴn9bH9U7yϨ7À 坾Z 4jaUhb%X2oݙg)_+Q(kkkc38I<:,j6^ꖽqZ X=VCYZw\+y0hW6֦$Bj`)!'}Y5ra@yQ& ORұ*``qv6< ĺJP]Zᮚ 8I*ruu*4{">A0Ns돡ѱjm{Vu̲U2K3k>1fhJ!GM.a*~[F:qRU-}znK(d~.(ucXղ.EMy_RojnIWZH67ְ S/5h 5-zUSԿprryuǫp_X_- wy$JUv U8T}Zo6x|?b-Tt*cw￧gl#\t$ *T-3U|Wyu{[ D3#d}c*`+x?^ݪ~84uTk 7Н멀 Nm@a2!}-}zm*2X ÂU3*JAz\8 gt#JkV-u՛^PK3Wm5]FJzЃ ҥsY,K:n)oM*ۿt3W17%X+{X*μ< I&2Fo'~ 0nkW(*d+I$ژ{ӯjԇ䔜ܗ< *n\cUwwY*nm\o\Z #|tEe-> X<PwS5_j kY&VZVn쥲o5#*!&nC)>Hڊ*rVNoTeo﵏UbZTYQgJ*CsJU^+Яu!6U zK\S%먟5zT  Փ.5Jd?Fbz^n&'WV.쩞+K25Ee< pEX=\:|O:*C~_&ˏMx`kXHJҕ:[Ӵb}tMe`0$ꉀ[T,Qۅ55&W9Fbl5+]BWhSEV<>\mx*T-7HTײ28?Z6' dTM5簚Qknn>,-Zj#\  ͺy]MV"˅+\ּ+ezr*&CC{ZB*fH;3F_Z^XHOd9zBS5" z2\,;V]khFR\!**UMޱ$\9j΂d4Hʏi&V-7v:COZy.s y8#Xr*mV1J49[+UfgP ] ?9TQ|VPX-6ۯKaH‚{77le~9Չ|Y=.UCJ EƄY)GMsE.lUY?'%d5),7@U_{+n9Y@aYʄUma>3,]<6 }m[@i7.Fk0hut!UT[&-,ծ'Rc2 dR]]Zr՟`WcU0+r'T/HUdmj]VV۩-ܯ& &V|FREG>JjbUU714%UI LFuZ0/ͪ0*N8uuIê-Xb5}`ĵUYnm,59TN*a=.ᅴV#rPmV-IʞXbiDǠkjauTleQ9hʓo -/`}"^e0DK{JM Xsgo6@A^.K`+Xyy|lVTwuS|g-(VW%|{z+ diO(w}XB+3k\tKV*iu.b]Wo V>2̨< ܓcau``llx-HvSt$蒩p5ڋZUX3qX@UFUקIL|T|q2[HrVj Ub Z-X9R37ZrlKҟay+I봶֐U6^:8suSM@MIX C23itۃU,)6T䩁cSEg!8 M?xð*`?qv~/Q{_Ӵ9tr4tFoj r䔬a +A *^,yU{0A0A.B0yϏkٖ|~AՅ' :gERUX-a]}qXU'ĤQlUŪʇ+VG=V; 2lMNʧ<鋣};Ij'޿ٰ e*j|Ro rb\ +VUQ Q}txE,H*Vl{~G;xH3UlU6 ZBKM[JG"ח\fph1EjЎu]UY9g33LЬ;L-5ճT)F7V?˟_XUP1=gnCĎ]qդyʥJ1'aJyj&R.ijʪj6X4GU˻Vu /HUWrBn zUIjC.Cln3Jd*VaZ_w~1^TR5`p`kX`Բ/ `e&\3R='U sbX\ M橭])W]ӡr-o8+Z}?T7\RH. ʥ΄t7D,(_uo;sImʹS6ŵ~?.mZTKo ~m^om"ՇgMǂZ$_ajX S/u[m=`UFՓ2jyd(r|Z䨪jUj:nq]өۂWc?Xg}08g4a*:gϺPWk/p]\;U5XAA1W!UށrU?z%JV@W.ԎV87+j]VS z.dP/hn{'X3^#on&*bZJOuOvF޳iLHٝ[*Oc^CVC iQ7Z!`ՍWSGt5-OU'$r KAh9vVnnx\;ߨh2ZcZ>kyy7^m[ݞWu*`?VBqzt@HZ(7}-\="Vi X=Xe۟~z=֧ONؒ*4{W X?AnT-u jUR+}vW w(% vv*X fDJV4!XMLjpU4VMG?ѾH1Th5ƪQ`wyz{֪VϔŨj]^/Nܸ ҩj:`L8;bYxQMm VV4 w W++|  xTeΚS-"z.9 Ǯ$ZX,_eyTJZekʺuY*[I9+QxWMFZu/(CHTdSZ׿;}3ά\w,6iq(6X@zꎗ'lx=`^ׯ wZQ[Z7鋭hESV)U\E!Nе,jWxV48cw=S$WU+W+&ҿ`vp)EA6>Ցa|oE2zBj0b© nͶNTت=+NW vF!%ζ7oUr8*Ua{{)$UX=)@B+ e aqXͳcȕmH0s -ԋXj7R -fpê<ɻR+bR!p5nr\5MV#":xNSMruİzniZխ[Y _ׁovڅ\77]WV+rK!6]:,)j-j>1`ܻ5k[rEIvaX}FїM捪[~8"шvEJջс&6RXrzх WVu,/jr"Uű='GhSb1+~t˕**hJPհ8Sw>9ǎ=igS#GǿU-O,k^05a2'6v&#tl l(o0p.v H"! t!ęA/ A0lywv5RYYg?'0kͧ V,x*vZ؀Z OG#?5JͶ6ЇNVU4T~7\ [KUzqULJᴮ7w6j_JlNx`5X])U% ٣ Uӥ6Wyd>(ch>iH<>dkbW_M:fį;MRr T}TUHF2n+5GV#r+gcTvjBVStt3Vq*6 :Sq7UL{Z(V!7%*2 Ǭn@AZ31n, wF-6[^~GX]/$o6I\*NaJf⾤u]Q{gEk0 SPq-{a}Q ^]T$r\m4 LZZqU3]Ӫ;*`Pk8[`q@~v'Vu &֩G*K5nQA8_d# u7R8TիP9O?cAOMj5jǃUMT!1;KBnU)1q!dGwy|65UQjS|@[XVj%UHU^yMBrQ5aU:U\y'2%),ZPqEW{7W#J*@ԫ=]t[_z :5zd=Ϟ:uz#ªJ',P:@4^^U?%Pf!hWZ~mU!UˮۇPͶv>juT5|*7E"UafXr׳BZ5P {cjkA_]mT4A [Nsr~ h[o~689S$F7w^P ȩ`5;pUVwAر{}x{kzGK6`z^%PW?xeN*TelH_YQЪՠOOx\T/TK vPg1U\1/=30ZWjrUA8 V N^ fs~ssCN62DV/!-D'Wol&Wg}q鸚dTVi*^)LmpGZUBjKGxNW}oX@z5\+t:)OÃ06=WcTHhY4:sUOkv@U&5Ua7I]䖏KU*BNG)WE&b1s#Ԍul<jbUD%51FRbbur_2RvhQCW:0:W_3*a)]Nҁ*~%fX}gVUZurRVnNm*;rVVG^U:u0ճ`VM:y:~u՟!uk( ꅥJ Tt[:C, h x:l`njKl܍x^[>dN?Tc`S \jL@@VV%[n:d[*c#h/yw3&R߼j՞^' ^vE[U XV"WEUpl^PV#r2UK2.gWUFʰ`xUOVd @\GйjªpuowI"?G 60?`z*'Xݣ:}^[UH 5\-{JUϟVg~"Z6x^2J^UbM'Cjԙ&m@v*hêB:@$ڌusET*kV;G-u=|'V? UO?ꪶZ۬5NT jq_TUUl@ժUAw?7Y/$&3UbzU ʪZ]"#%sS<[Ø>.ctVuc(^IUx%"FZUèRI d=4JJX`IHWbc{`0: )lQ!cZdx'Y8pX튰=A4buy8׵)$FzQdՖ bԿ{j] Xe6c5Nr5Ѳhƴ Vuֻnfi_krXp>[\ؖT- 6zLQUŪtSX}S7_z:a@k 8f kV ٲjVy2&++XPk!`ڮνKyD s{Ikow~s TnUBxPժWZ@Z=U 8#*g:fX#X)V?Ux-1Wh-`eiх/.x "oeoFX=?׺W|b5SOMn*KgXُ Xm[tZ;7[lK>@Ehl ,WO`U%-āQmU4 *Z64.@׵%Ī.t WMYY*q%PWP,$SltGb`E QG%8eXQ*j/Lt^6 { ڞW>cV5@OjUɱ=- U@U&W(+:.Ea[~j"r$36?QV _EKoŪg7?1bN|ib5"aLN>*6rcfV_d92m|jp]JZ-Z: 䤖FFƆ IUUIêz6s\)jyh I\fzr{"KVU-q',ˁ`-[A@*W3Ō0WUjyP3 j5*J qXji+lX}2_88̎ +΂  I2 q!| _n(!(<"04.{]}wf<خ6?j>s=xEWTվsۑdbJ6$j\ U$oU5pZݶZ+_b?[`)UV6Rݗ|@uNV`#P*VĖ(V`uu@Zb; 5j֡SUn`]{ uXd 0Lc$t?{SQA'],psfgnRXX?٥Z^LjHJݹv?R-WmRժZ+Q@.5rULA%gOo? @u~TZ9l”`;b߀ը[2ՑFtc˜#;A2=Ubvl-Ijŝ/Zh臔šPEʊG2STWZZ|iXMR@tՇpE8'lYKNIu(Q v ҉hmLU[* a՟C:^+~k%#iQU)+i@Dj| Zg}-Ǐ#1zD7OGGWi|u|6N@sUTBFs]\wL֛ï?2mŧ:z9k=wMYgEdrPr_#UPUoYQup&?ryʰ-y YZEW;Vb1YVTEސ€B3{L(r򼐕N+XdM2&YPo{ gW p:m=י^54Jj{Ekjm@yxdϸ=fS hsUy-/ݿiߘ*{Ri&ԪFn2ਚ;j‡)^@٧%Vmt-20cŤ1VV  _5` :\%Vwv똌M*׸j{/ Xݑ*$L۠{\X0C"ݯ%F}?5ut{m*7PXUS4:2`eXZKz&]V*au_*kĪD^p"Q kU(z:XO>~f$hG: @"\8b!foq=U5֞T|*Y᪹z;ANdFh*p[R0'W[$1V0VP@iB lI: gU?b=ŊWXk~IX7*FVC.͚8՟ "^ yT Y۲IU'6XSk(ZmQ%Be6e՚a#V#WE*VKϟ1":} ڱ: { XbViJЖ!Sݕj?7jhUQ&*2Wᬉtc1VaxTUBUU$utK1]ܯt,,(W"W=XćLҵ~90[wYa/O/ /[SϴϪX" ^=?}UZ|o|%VVVY`fu(,aG U_xzvժPJպK CV} bugkXއO?JTVX[}+`n\SbUJPm_ºS)VJө'HMy^EΜ{zzzn]{QݳIcMZ10cj5Uqrgv)X͵UjkUYpZ5GOO)FXm^?E> iU-*XYiB+rtԵk̀ZHT'ݻ<_VZ\nrFXfQ 05ˢY<{&VIbz \]1j8t7@):NբA4~CZ-;$p\t;DSX'J*yi18JeYU&4Nu/Fe_JgZbUa=` V:˾jUm+Tu}mҰHq@-f@mi$P]hQ;D8v~{VwaoM\5PEW(vjuO}u)"SPJUթd ^` h<4:s]ZOT>mutf8MB|SlOsS[GXUWp5ԃi-?|~s rk=%+u`5UIX՟@&X%Nyd-yKmkYmu&T%T)VT;hWm'XuU7"ZcN|S!'Zt~09թ{^U#khYQj [[{{:`:Y)L[Z}U^ui$/NH\&azjL#XÍ>̌rVjru7`5ȁ,SXt]\jTX[KGXtfe⊥|\U%EEx?j)ſfzaXqzteP⦴)XiNy(V}e%:=ؤ&U9?-mRXM!xTJO1De z]@`4֏zőM-\ T [ `UoM| :QʷVIFiNY65TlP@FFX(mƞ4ʗ] hTcw8:P:5Pu7JpTa``X:`(:ool`j#Tu<ۯT"jÞj ju27'TRl'poy13{\ȥ{s2 l (= u)UWٲrj,ʶp!O'BkWGQb{, ]XMx#T&jljpQ3d0dVe#06! IDAT%d, q[0&3{{>ɔd;f|{N cTTtU"l=YA8zJoQևjrUeNcReJI^U^UD`Ut:5)N V9X%X][gјL ʦ;=nXB"vf'5X̖0l" ȸbnQ:uEkOEH7a5kZ+x:JqE]=8R窸v-cEvL5CWZaȺ>9I U,w?}y\UUxWЬ82ZtXjX\ *#6l/`Tu8B&)cV%eVl!x,Bʊ~79WX3V_ufUCUpS5U/Bg=E@/8MWڶZPg*}Gw"]&A]#T׽+JLRո:;IxjXY*F+M!WE>iʺA ~E 9DXjQ5pƪf=bRmSb4@KH6[ڭmtVE;xoDk_T'ib1dVuaĪjhAV8S; fa[ɔ)Tu G}_H\xb`Ֆr[T9(z9VNGR VInh -XCqDm@t{V[]* uXo;ITw{T-+{lzc)39F_*6:} 7RUxXum$N^+y=VxꧪCEP%Pa_{>_UԮb5*mimŪ|#gÑcXgbQbuĭʆuըvj%SADΗ*)jrXm V񔠉XE*@>2=T bۿPX-d@{N@վ*Ib3,vNYUOjunVltMq${SjBU cjurZ*VsU6>J4l|uV5sGS`UUT* [k;nTCWU>:50p87w4ѹk#6׳p|+S~Xv}QԖbPLJz|V*%R[i80݆3Hp5֑^j\jں LZbt2VCI^cU[T=Uk :>ªӪ>կJ 6*_MԠ ͥ[wI5ǝz 'Ǫ{Xm\ED^+_hS&UzX ju''`UGSU<걪a]_F`)0>ZERVաP*} )PgU-jQ$;ªԺQUwVO’Ī T7&* T KV+گU_ u*b>3RS8kU0m;eI*cZa0Ze6Bcuw:ՎSGwZ ϦVXV;ִ5tF9 򨾷e"Uۚ.xLf HNH zŷjFz'lQogm VӴBUչuf_JBV 8| Fkh8@FlPCRz/TG'V1JAky3TZ#)w:F*hc"*#iDVujMy'@Rﹼ5jK?HGo jU"W10ƹX.2l~?iPM^)l+=]jKYZ=`o|PrUݤUu+AI\39f]4`՛1y,:!ޖtVݟHSkfT©_@TOWYIu u`Bhp'FW(H"۔B\#!UN'b|_'OTSk^ŰlPUph*V'!GVp5i ֞sLuUJ.8\^]f0Tq Yqv6<ן~9Z'؍ґ>ՖK-\h iUB(a'L8uH`JAZhrG8P_P,?oڪ:9m0X*J'p~ꦬ~z@'3qbսjfsq*Fx1x5o{&0EzSNVW4DKNlD;s[ J˰4.P#TFU*-v"vɪڈB6WkN6~Hcj6`5mJ}HZQV 9zuj[FWBUDwXm6U6VhZ-'^{+$+GmIQ=Eڰa`\E7DV`sYUU)L.V:,X@2Ku tDVիrP1nnkѥ5E[M-"W;[ZUJqoNv$ewvU7&VSUHӟ ͎*DOϾ%{ {UOQu?]_*+Ī_.xZ]hkw!W'. R[zc zUZ`= )?%Ee e-Wi -Y%}d*Dj<\I I}b._ycX]B5r[m\*M*Ufd2~ӻ ߧsdxj¦:Jf]ú.p(Xi.}Y%X:-)V~yN XzVUpD莰ZRtq1jOzQV̪edo t({ 23%LuXْ+uE@Tj؟DUH,aV!V buͶbP*W!^}uJWK />$#O%lV"V݁vsh9SAhSF6L1%ƪ,QhTm>t\[YU&YϏT=<=8;'U?}Ը*TVZ֒fW TX\u*3)Z(5XX^c=Vb={67^O*jڌXlZyίf^~}Z.ad-`u X5U.^F&X(`]pBǷ:JS;-ONVˊVT W=X ea+_|&^VEU,"`}_G˂4>}w 䪮j-* ~ˈQUֳp Iqe`DS_%SusS*S/ U_v݈UT*Q'}:՜ALZ*É*eVeA[jtܯ2+ml 'PT=NBZ_H=9y OK7s(\E9 HADq|QBT4ZKa^ PUjm*V9e}Uj߰ijZU Ճ u}ʋtm֚bRb%>a_~hM"@v* xYDK/WjUgggXͮ=)hKU^e'%fi.RjVZԔӫz1|9r1gC["Ҳjo5iANbtma"q"FKp-MPhX M7>:wCV) C C& Uܫ%QV6G?7#*ԪyB_nmVe%j%Q4AvZDbX5)Y#d qަ-\f/RU'FU>:Dй,멺Qy$i*W㦕lJqgwwfYVw ,*HvVH&T]ѪSw ~y OtPA$W\*XUĭ$5.lY7 پ PW0W5*ꇅ9mr(K K=Qf`R#zanU|Il2|eV=n.o-l>մN  py\uD%^}J_굀/a5v8ڃU+Ppc VssHИSZ=^Z2N%j5\mv+-L5? Dҍ)XnTGp [5%'$o{ͦ.idz6Wn` of*K ~[rM;`jrVmRV*Ncu|ؽ"fTVSIG}Wx/TX9!TVG՗/_2E 7*RԦVfPUwT=mZuwMi{;dy;Npr`cwx*Rx&NX!ZRXZXYX]Qz0Y LjhXV5T%Wuh4XOb451BϴSP5Z-eVxH>Y}bY*pP.2jfA523~ ur>r U)rLE[~m0FXmPh0t?T-V[ MV5!Zz=XPբruR:*CB**W_neN_6YD jDUQX=x$_tu1 /D4&HE 16-[\,u$sRS"vhUpD!S|^ʳ(Jj'*r(t(AadSZd&j@(A: W4s}lhNbקλ?MpE'4S?} Um"V**R8ti5?R FdI\Td0?վX bU.ȯ߹*VVmMӬcS7*j4'kYzՉ+ bqw I彩Gע̕@i`"VZ-ss#L5uS;ֳ:zbiU\!䓷VoY:O/)?U啙-w'Z8.b?jLmŇi,a,K{eeCRL8 JVl1RpQܪ T̬U;??$~jLZu_e9ZU&e:^>^NYkݑǑou}I5Z jվ8 UUr`USK+}4qn,]t<4]bU*ghƯ_Ydj9'LE) Dԩ&L9KN|(:\G w?ΌV c԰I% IDATȆgGGVX9n萪}₵U"`Q5r\5joa=X$VhDWW`>&1J9$u C @t @L]b'J uRF;V^``jjwgKZg+IINN^Uh%CV%`U9Pu0q PU`<'Q>*6ce±T6W2QBўqu e˳*j Px9XJXKeɊǍxB~yۨZda- PB/) W̓eCa3q!:{S~ bUz%aVKRfc%g j: UW%oXyzvuDz MB2|)L~MVU'X9Vu*VZ}ϊUuޡauMsvacU*{jzDx(k<UJpu&j` `"G_kbɝj\iM'шhחbXSu2&0vr9L 1:dYY]ToVuHiV*}b5V~JSVTZQPVVU"$k0XJRq"'0j(؟duh[`S;yVPr'VYEBZ)`?n9.XjuJǐ :XQ YxR2.:A+qD=bx](nmpQuGF"p5rX=@ӳc\F?+=*{&VN V0U]R,E't(RVTĿcnV+.McVWeUUqPH)I**@Ԡ{Yê Aj|fuz'5IznVԤ`2:ѺjnQbe XgoUCZۈ^XM fڱǪcbê*ɚb @jgBȳ!ȊZ WdhKPԼmx|5``ܥ^ͨ fӿ VY<qCiڭ2EU\"WN4-ZYXNxUqfTov߽պ*h>{Y4n[_F푈LujLJ1īTwN.Ovmu-{0bTsX^^ȥY^E(Wk=JR#+hJPZQZV+Va DXU)UqeC*X$T}ʀU_  Xp56ut0sX+l^ XU9n467M^wO(t:it<~?V_ejuPLM~ssJ _MM/'o{`5hŖUW+Z4#L`Cb4 r#ljZJF[a&9k3<k=Ѯ0'Wi2Uz6g֣E'+y0jFXhcJy+qk.UU?RT"ZMy.N0Cq\]uN u9ݼ׬sojuSu|9 ֭72Tk*2W;Q]SEnTwsܲ]&~c6Eܣ\jFK櫤 XE;F%^*2M*T Xi`SԪӥrB Ua+!pR1VMdUEσA66/7LwFj/8*(QXҀZj5ժPT6VyӚV+cujj"Ϊ?TuR5Xu\]"Ǐ "* )J˰*kdFTL>c5N7ؐUgfRd 4 .Ǫ{1䕷8@'gE\$ZpIJȏ- ]y>rVYUg(Bƍ+_P7[S5H R) WʘtofUqd3:|jM~vTU_hu17e#,H*3XJd'Y \9s|c >5OnRlcuXLd-/xe},ԣ{|0=mh)GXE@nݯPMaV3` +!Vjn Z?WNԑwݩVVq-i7lŰjJx}ʷ*V)TΕK^)O6qvԦiQ%F$F)EP&(ւ]īBi@fٴtޅ:YI"d&@HKpѕe3<{sRޙyO4=5M}~|ln+(]2뱺nͨ+Bl tQz \6fXA?\uWc'Twx`UhQ3?y[Gs:flOJ`QFRҨ*ݮmyiҒX^*Z{E.ټ):ǵph`-T?VtT}Ъ[ oASeKa \mqHVI&yY:atI9m`WnUOF[sUf:m >X0IES=VRC;;&*F4]sAcX Aߕp\ E`ϪVUչV3cu,WrQ^8+w(W -1b5mxzjJZ *޽140@B{+lW6Bޭ`X%XEʿTyu59 -敫. VgJGkqKFRzYMp08E̴߱٢yTLar8hZk*+"8cPut8`Cj_[EmUS#@5NmU:`U`7,b+?ޏ C >yZtQ|.dr`XVwٹށ\-ksk{<8XA&JXB-O43jx+,g\{8uiI,X n\դj%VԾ\-0, (W$cu&ܸq+aig+VquCݵ rb .v`C\s){1Ǻjh|LkZ ^UnRrlQª0@@ 1V}rfwi&n2pԄψln1*ZS6KP5yi(:ȋEV|ewݞypsRjRV R`51wQQNF' :*S߁ê>H(`?*8Z/\e[xOgc)HU9(WEGh$=kI5TUT=jXɯcY.U<̣ + tx"RB`zRzټުfoQVziٙ:OFՒP8 :5$ժfZ4+UV3R2 T իT`Co-tgPmRb2KZ!D/q\K*je74*'u]gej{1e@AU{ĉzy2>\ـ:e%X0JB*<d@}jYYP}T](V?NVr"iVKrPXVUG^ C;z.QTE{b|˓̌TZ5X;5(G?PxP?BHDklkb+UC&v$ljÑ~q5ZD*D H 4Up{fb-fo[ӆULUêcK۝1y*WvquZN`Q#8|4Ʀ>xj< Vk 4fT gƪ.Rml4+XQz>Gx wF9Hz)hv.(ETX V5dfK.eZ'ju.]¾jvN0nE /=bŬLcP,Z!j*k:VEҩZuu%VXT]p ɉ'pҼ|)P:㨚ƅ,MlSsT)\MP=c/IMۄc3}.x.%xZRV5_P|ڏ s^>8yj@ZDeXG& ; P.W5`]ța9Xդ}vj ꩄ w `m rEaˍղͅŹ\]s ok %^a6/a %A?Vz'Rt¤.j":b YjQQtFa7d"Z+M>XVHЊJ&xCB?;y  Ug( ;rXa~_*E5j=T=#X-Z!! ~e1 ej a`榉$\ݎ4uՙ2kLT*UZ zUm:&bNSV,T oSL. ͤ`jV=X8*Uj(}q!նK`wkXjF_ZM99`UP(zVAMZ\]dxAoαV#B4V6.,X&M_vUAQWU@n\W%S:J˩U}ju]Ū0lk4bj\l~ee KW\_F#[JuD58e{8CAAճ3U4cj7^DWTa%96p`mz-`Tn)SH ͦjUpULj Sx dՒP^*J"zz˩V]_&i/\@ׇ4_*mիla-5K=*6U;gUf,Vg3$VH@eچ(S /ҪVG^` WªͲJeW}E޾M.te a"͟+ V++ot'9[n>cUX:M#chR Ssکx+t 8O9g+Ů=^t3Xj^o6r݊^zjk:E`}U?huau(jc5_|y*U͹ȟ4V QЪ|$?P}PZvbu?*SuGBN(Dg{R.L<];-Oʮbr+S-MFzljKRu:﫾R%W#ά^6%Uj B^hZ%H+&V1}E_Ъ`Xu a]K~KȚfZ+j:j3+ QReW]]0jjX6vθa;rb3^I$s _rsL%U9AejP???&(4ꦍJ}KǪ(Q~2Twjn.jQ 1X9B"7&r,J|5r02{s  NQuzP}tݲ}Ғ ssXUAOُ0km 5|p ^0WOF͙ fs_n U=VqQxZӉ[1UqQc ⣭,)DTL;)Un8Bie4h)௳CU ,ʓ IDATKH[3V4VV]|r6/lE6S>0ToUKYgQVxrL[ZXZj~or+2fG{4d7zWeMw.y#;|\eNUW pץz݈Dё!5HVbZ5BUCZX,"3W{K} [Utituvn/xZoc+<~b]y&̹tU4q9HbIՀGsl7&'Ua21Ćkɘ9j<lU92Ud<^{B͝^ڝ&ԍUVۨ*7 oJXLPpX%TTUuMbͧǠj `ZNRM=P6u;)-#3 չgԒD/Ԃ"WWUZ\+7oϳZX*U/U$_hui6?P^8.jOfV,8HQXVm-[}83 NM;2Q9k X[:;`uXu-q/`_*,-\u$V.?p 'W:)qUT*({{0Y5e5}ZcB_Y'P:EV 8PRT|_nYu҉U*D}R^"mVtXAEӤ-'D$zKzZSϒn@'QfS=!)X'Gt_D6E X$hQR4oT-TEUT\?kfqcTȎ<z( uBE\XR!Mx\xE^2/1c˦- m鿳ZS{X? R p+'j;Y_?]>^+ KZqpbUjz %>]#]$6ws,xYv = A5LTd*=Uټ[ZNL˗ۚ9ƪKniD*X\X%UJXJ}J&^$3ڎ`5 KUlXK1Q'XU;ПL@E:j_ӭi}_&XE!ۻV])je&:ۄ-9*y|X%V-WqV?vw/#jJ,ۦM*<73FwU $'' U4H^盰*\ JjJzdjU'WN (pS_*T[rѕڨSjTӓc ΂0xek#VljzzZ]&S82bS%PU:)_-Xd`#~e&ʏ9&j`,S \oչV:auE8LU Pj`h*VLBRkW\ {eNzҧzaa/7 %޺Cwcs (U3*,VQ̧yS\eZMiacgK!Y`h\u.E*7'uhP`[{`V,WBXB83ο`5PFˆ ?(ilFW6`g\ǣ1w݅YLH TDU\7Mh"} itA1f]c~Ue5SZZ}IMT}X>vaκ?Kk).PG _<*U5EK$kH.hbYAe#_b|83w"VQTlPT@Rbgllj1:+*UZ}hZIU.>\uY4Mxc|N4Q[UUiç,:z<1 0v`ib:8;e5>c+PE"`Uj-K٭튜ެ-0:.@Dѩ J©9:g7T5\-wTo}2-AUTuT:HUUB$\mjpu]~=> 5u!cIZ.Cɫ XcjR\,~ORЩv߮f%ru5osRʈI]j\:ȐheYк6n$wۥiS _5Wj4ꞣ| , NIUy,](y5j+J ^l?P+"sapuOObg@o`za_`)9ZP&<6"*+I7⪮Jq\ 4Pm/rK c&]I+ZxCTUzP]}yN | W$P]-hs륌 -3:XVVUTZj|kU0-/;WщwBn|XU.S hn֋AUժA߅ 2+2,'Z=2c%D8/EVU rYzkټX|?rڞ Z)/jJFX"`U[aChUӫU ViY`_mc54t3V*,Mr {R`ljK*Qո8ذ{JGy7D޷Xbӧ V'#ډެxUqVUT=eP vUjVP#)ZF%/ WX5YcRhUdjuuPA} 76XES:ІϼOZ5>Ty+++J=p'`X璎P7VվJ9WilJů @Um/zu9`SX_s⎕8:w90:sj;QQ"u@\Nm鳨^jS5z=Uj e v}9nY͆ˈڿ<׎V!Rrj5jRj&Ū7eg_%V/qr'HU'[f;0PaY)Zb|h_=?CUmZ<*[U: V'NN7x ]כ*6:B[P"V%ykkfW]GzmcuAZJqhjA[+-}<ơ`0+J* pD󍇵FYVw޿^ NJ;';Ngn²~*]RՠNč+YũΘL^#̀)jPξԞ I:XYvk+TqO%jc}u< 65UMߨ]vj^]O>^jzF /r5U-۩b3T^U-R%/.[vjjꇰԖcbUTճm" /b\&XughЫjUW/88{'Y}ӫ'z1*+W 33"JGPX``aUŪ5,ev)VohE!5*Le`eY9kk'~+kk ׿MBHPnpiMW5uz\* @a*zǏ WwU!yJ6y&ɗ;:g}bu\;;&Y\¬jU?+7o@0$B5@**+Q*V_X5?>auES%ɋ|VP*+' j)`-`aC'j4pZ]}/q0(\f&ZauD|&utRTFM'u )Eku Xm0a* U;-+0j?V[bȁsZjAe0ty@Sq523XoTH]_ppдu{RrUt⪣[qͪ B14C V˜XPՊ*V V#]qU`qڵU+iq)C8k.X`]Zҽ3Q;k)Ҫ-/Co\'M)}i_jI=JRe~z.<O6Tl */@bK([=ZsłdVҾy-=j"> J IDAT}Xd:?EhBu5a}!j n*ą5HTԧ+W TRΚߚtz=y;1< y?ɓP(WTȿje*D銫J@=}rE76}K옹VkŪpN"Z.<ۇ/A].E+|oXrĪcմ"աLzV2{67'#UGHΔYuTzިWV ՗,Z_EJusUӟ ֏\LО[Acy#vPa:Ste3r[ˍ|Z* @f=TbڻiCK(3}mjXMj]ϠY:ZUvf#[&S: KHh&$ V7p$!W6䉻( @ .GTdWBf_-CE[ 9z+eISx!uԳmIYK? ťe$@[C0qjrVzi=QUzzX]֤*cD`Tq{C**(iy.&0,7,go/\}M³yH^ X8ZXV񎻤XUU%i)T;-*`gh`SOVWWVC1pUDMlGS5xtO Vx^V>\TR7ռЕl\>j%V *Wm*T79UVm,,x?:C3n/Y:'_[zS!MEi*1U`GPbU1ـZmZ9XXӫϚZ=?;)T3XB)=VzUU5Z.:V]Z_x(Z:@W<}cR9Y w2P+'8S R P󬴦:kXWDރ*]R?k*Uۆ`+v@xUU݆6'8ZDnѝ}Ӱ _>VBU]k\1'!WyQ"X0bˉ/FrPXudXU*θʶկX\ZOUC5 .V͡W/" #}/UxJ[fykŪ6"~=1V[VWVQ( %wk\@γޑ>iRhi/'kW`tsx SNlB~KbB[ &Ze:?Qu ]djv6l@RF2aOgWrXa` j7.:sz *V};,W1k!Ћ(R ;X *l@& [dEfr7- Y]Ԋ\tK!,Ŵ2òW>9o3ok[|<9xd@!jb5" T5D&b^۞$(U ;U6P'VJjlVl`#*jԨ]CUxcIUq[GiofߺU: ә ɓ>z}JLpNZ_XL2SYjjPwW\PTVA՟HCR+Y+S퉛jS\oz(_X,+VO4,`#t-9x-tWm 붵XiDEMy.k$F+xr֙P VPʪFܷU,| $U)C*J"8j2.]tXmbT=>z(zz\'˾UvU.X4puȚWE4W[ۜY.s* }~㺠SYHPD60VWVe4,W?T3]bk꾡袨q"W R!W7; 7j{2܆#mDF>17;W(?VAUyi떼NlPb07\M0BOn.ؾ:xG \^?bhg^*W++N~)`WQ!{v8}so@f+l*Ī +E6lfXݑd^SA{ O8sQ6jYWbz|Tiq jϴ;)S IygPPUj!j'\xV/Pr=a j2J7>p.Vm!<`3UGbCS(lȺcbM;1TӲ/ތXXUik~S`쭥e_uVzts+ jaUTՍҮƀՈեGFr5-Xݡ/3氺N*j9m޼fn'tưTUJ!)VIvWM5^oW*Xչ걺P*VOU5V"XE"j{\ )^.l۳@tϸ~q%~e-ѱBբB!4@x`b&kFb/`[t*|$dR gewbA:|_ ,pdK䨕j@}ZHuUpjt.PMU/A_WU2>10`h(i~U4)le!+55kOyo].*=RqdU6]K2:b)0%4%,pVga*zYɬzUit,ņ>TeN%~*Dѣ?Pň p M*2қQAkk^JRKq@R]s]cQ5&2Kq_kHzDd\eVIJRf[Xcꮈ ގAJU"ȗ|PT+X+H~JLvlHU$@Ro~9(Var-?* Ps)'d݁7m]!bQ-+7a3Unw9e!!Ʋt_\=UvyjC`5vVg;66I6V׬qlL*WY-*\VAy歿Vӧ__5ªUe_Ht!W\=FkEV,uru}LeeըU@]XՌ<: r7ȹ~nx: p h::o-&w|}cQygUu;v] :7Dء]TT=ҎkUJH,TdQuM<[LlN>ZĭweQi$*=&u=UOX6r:0N $DRչ80b2Y jhJT27==OZoγKr ǒm#G(XW6ێ%\/^J-U FJVUrFFXDme^n?` ~#T4*U[-+VjPy*@ݱoMRcrxO:Ŀֶ(Twm}<$*zט(Sx9gϪ̵qNhg>2uLeg )`ӳך/,dsc1,!Vˬ`VVi*5,MjT݁leM ]M?8\u[O7z-Vͯ*j^Etu:9?I*&5Lf]֭\v\ fŭ7{HบhjY1 =-UBUޟrꕙ)t\sV..z U&c~E쪝bJ:'U>̯ n9x!%T {IUU Wp,vUW6J4$3Kժ]*GVŪL= 8r#ҳzYĸjix?]^h*S$qPWej+ Tf]*hdjo=cWͭr;^ DW +B(=\:ztlz:VʭR5H @JVK@ Rv-[ꡂc-N(Bk|{Н UO;A݉nm U5/G#U)W5c $VX݌_``7g2,#$VM _DWAwZEQUJ+cҁp3VYN=XݬU2o+C9VQًFus2RV+ON4OUӮ%0VMJuSI &pfFz1ulmѮaԂO@{\EX3Q@d`*M9wbӧe@ wI'hG7gns5{*Z'Hfyfh1v)@l͂&0Qb`^j$tjyyuyUbռM/O?#5uY9 NMUeup%+kY= Oy'4X?|g#j(y[Ur/ijDzԔwp,*b׮:U?U/U 0 {ub6e_j0JX\g:^-~U(2[[OU>4V1*ZN]V[KVUӚl UjR!RX sa#`G~)[XВcul׹;C!VA.ob+ꀰ:ArUXŹznĪEժ Ej-@ëilPkV%BF{də߯\B gPX6x2@>vekrW\EU`u*(MX%i@CiN0-GHVhw嚯wӪ1ks^&Vճ/goU{~QTtKm姒5[u+PU˒VÿO_Tw]l4K5c~8Zldm;˷R$WVwœ7s#͎/.=S>U x苞azᙺYHkkwsրMa՜ÝmŶdcaRXdXfX-j֫ vBEBVm\X3E^:JxoE䘔s*4. O? A\T̈́VIEUIGIF7@3c X6ER>ņTAGM cB] /C<ԿWT-U䬎!mx(j+ @PXXV`l'\=Yb5J.ߎf6 abloZ?0-zuS6qt\-/ȘhU6k6Y-*lM7v*\IȜX Vڊm X`ϹU֠VHD:7%phf~W=UmPܘT)OD*-BO5ܱQ#:,T@$@x'Ւg`qu,EEj"im5, -A_ͶhP4] T(g5WsԁӓSs> s֏گjy(fޖX}K+5ZBeb=+U܇~0/̉Cs>L/Xqpy2P,6X=xBB*u>Rlڛ8*d+Bo@j1~3-Z^k{Z fbTxB3o]f*jη"z e Ry:g ͪWnMC{}ޑkṦRG :V:ESfܯ1t{**~Pc%A}%gCm.-;pWǟс=R) > ౽=t SB;ؗˋ~ 帿VٰʕJӿN@&Ҍ2v҇lEJTSYINZշ^l)Ojj\ŝݐGG}U`e=\VQ~s(;UFUU\VWe$=S }^r+vgo/ TGUJf&Vw2DK4s>Nij8yeE L O0kC' VJ V{M+ ^G$rr,V6r5"Zcٕ IDAT匎-RbNoCJlϖ \lAhU@̰YRIHM-,t+U]BB6}XMSXW)UV߼(\~iuo⊪UѺ'(h)uC J&ൠkSt3ݬbS^S˽n:NTݟuk9Ae9ym#QB*Ȣw'*[g1dTIVWX= 8\u~lA&[)Wj(BO8P xlL~we[~i#V/7*ͨA'QwFbyS Vj0`2LR\2#/*[)l) z)Zc5B݀ƸoՎLkޑ0yEhJ\u$|bU|JzGT:m_~b*KzH ,&'8*>҉5ZOmvIT+t9,1h=/j-%;^ڎT߯v`Xa0T~N*tU+okZ  }בZt3Z;-yTT?|z{wX|.;EqtRAU+ְ:m"wU1w6=m_iyQhHYA lAH%EJɊt ce uAeD$ZPD+t`v]=<ϽLBR߈`?v\GZ8CD`U$ w*VV6aSy-,ܼ*auBBKHfQOU7@u 6݇aUJU"ތX jX'&| b;OḎ`Y6!V+Bq-7 Mjɍ⵵Eg/û]3qܚ쮀UA?|n٢Q*h >f n4:xB*oWSCW5*d-JQwކծUc6HԖ>|u5sZ8ÍLI1eR ZknJ>-}غ)< ݝLpձi*/xRܮfdS*^X=g/` tBm(ճ9Vʚ6*}ǖĄꓪ,bŌjP|c[K<)cV5tKXecVlHQ<էLlSsФ%bظH5g'_1JgPO6^%Vum恏Fc=mʿ? JlՀC``E~d#U8\ O%^V6r [;?:vVviuE-0 5<(ӹgR[ UKaN՞ӱT63jSىd]߫|Ji&rYJE/30%pyq ej5O.@ͱʝj1uc>vqñ`+::H7EY_4Je> N1CW`^Ѧ zp0z[؊P0 P2'^&X,6|CI꿢\%V)W:Ç{27E ˘Eh=hGF0qj{;⋬_$GX*}z=}62b3M~d>Nӑ3VaC(0q +\}AnEĪߋOZrWq 6Iz'XSV&Z + ›__úy濨*=uT^=VKZH5R8Vսʹic|5ކUT5R3Ew Zu\V,Bkc,{OY`-K0Nv>xnK1*ڐjFA4Ϻ.@z EeUn+G|ct3ڿm7|lKUY,Iuy.:U|hTSY[C@gY*-2HUjVHA{q `"AMTlJLU1}?o[&J,龉Yw2@uw76LjwRQQ=2~UPokˊՇͰܩVȱʻ7ca`, j5{4fUK`mY9wcg}|X mgL{ HVcÊVg2J*uX?s &-F*,x2U SjZVUz[WׯyY!m?I sqӰR_mYi_LM(ie35#Mj T)X 9}- ĬCN/myVog0"/dG"[MU0?5WV 4a=_:MXE _ /ŝӏԪNVV+sT|0Z!5JRa&' 'hUR(UwUS u_uja +HKW|Y}~T=qiJ\3b)ibXU8ʙ.Z]TLbuS)W벭kTm X 6w:Д6N?U.U)/~Ua XǓ6D6/ժ姣J8G>U+yI^_sJ^3S8R5x/SMN#VEhA[3?Mx>\:pZ8@p[{:H_}`5+jh5li lSA^پKƶXËXeeЏet*fv Ԧ/\vGTV=?(V$ۨU4WɯڼU1X]BPH۩gjdmqkuZu^,5Е!+DaJg*)?q_ J?VX/40.Y`Ra@j#fޞ0 twcR.C6.5%qCOwj_1iNTXe7b+կZIXԪ92@w**L^jU١c RT՜G2vV?^KD4;fDQu[u j^bCJ.M?vULž1[և՟#Wb!VxS?yZs< \Y! zma*j!wϧM Y*2YujBnUå8X[XтgY%O5z&;Om]V|b](Q:U$h Wkwݰ:ύvΟR^>!dy8X};][k*ƌiSԫUK[^8 )ak0724Ç3x躍Qw>?MY4**aRapC& aQgS5,nvYL $s)1JHnf2yR]xUBEIyys;ɤ*ʡOqVh>E0#XBb9/mgk3zfƏX !*_XsԨgr"P=`j&ǡ'|kP(]D;lZzj߇\U~a7zJJY "Vjj`ukec#*j3RO˪S֖ލ+ 뽗 +EQԿfs N38rT-Zͩ\ plԤ.%U]*U[P*{NЗ\^ DZVj1e=X4U-n3(Viհ+1Vz:X8%r.2}\DizzJ3ZhQh+V54KV>ToI+kϞX]\T.0$ª"G?XwL2?CzQp jc14CCwY 9To럻U-@_UGAɔCQU3`ņ8UV]Sx-'U%+ztx9+sdE"ބ,߸U-6e*krCTiKUT^WE`mQ@բwDȀ?8(T/i47|LeEe?w>Z_>`AAjbZ* "_\?թ~)lv9^io\𧟣r^XCux&qsȗF)VI<869`s dg^K6l]4mMKXVs쪋ՅF%1W}75[w_ͱ'Tsjj&WYP'kOW_;hT_%|]Z׃= leh+U#>mK]7z`*Ǫ^S=\uD'T4ё.N9t|]5j*xc*[B>M0ҫSSw+o6wd]}J֎8ʬCGWGXu4aUMrZViY$J؇Uy&,Z&U+ K.3z=q/Vej'䕰Y~tjӿ ˤck oXw #w(T+oJu5vհz-VGf_<*q O|Iի zuFCiZm);cSO!Qjul\Eé>gKPfjruh-!AmssUSU"iG6Ww^ȹW ʢjws}e0Յ|TXUN'/ZMXYՁXH,V UR2rUR,8PˡyU33 VV2Cߋ'|˅XT|+B&6(ͅ?ZEQ*OV:]dT+Mv(1;T_._RMZ8 ש4ܴTU]Aqk7(|Z5@@՛81VI%J{񗉟zNrLA5Z1p=le!U%&57헨Ng“TIU(JeKfR[`ݕ\~{&EmLIָjV R,Of+aҎL_=]Zppo#Qw}W`"M,PsUUH?ԪUKUQeJTBzbm[ьՠ5%[e*jfP& VcxyL |TZJ39T,{&\kkZeYn^ðVΒOrXja9AEk]VT'ǐY]Wb543V3]#դe5F}.T-uaXWQ+)H;dDЏ'U[ӸO.qX ʖ,k}3304dBՇXu/汚 W%:VΓ5RYT𴲧5&DzVqCu{~ԒTt]#TZX=*.xR`U.n!~*k#d*I|BLM\2 xn6eV54vkָ[S'@g$3u娺SHjg>$*ȪB7J_W&PaUbY pI X]-dfe1fU Y//\[{y0 P.W$GԷ Vj^=УVYy&$k[%S˔U{ǰ ,+G PW]iʖMý%Y9/nik![k> ST68?lR`uq-I\ Z2Ʋ,$99|޼ Tm kO% ?o]Jx֣RuZ갸BҩPL=T~_\NJ=n],Lp][=zS J`ފqU?&U/Xʰj@TYN݁vZ0@12hê#^^GՕR(?!՛B? X|4_}!ѰyyYü{4SvjռCnJ+ՐRg_1.XP: jo@6?q=G3^#יP|X@2Tbk5\?(OVbjZXYlЏi%V{V߲QVo$ mR}v/xq!WʮVYhE˸/V!S78 IDATO{U!h[[l[MXi7#@ԚjN5դoTr\ET6 EX*<_9ViY\ee [K6Vgk5ߵJvSȐ R`fSu S2XUz bWzQU:>`w =: XM} ޫ:^CJq VVH5aJ*V2ȔT)|d]I`5[MGYDh"VU?rzVg`S1V%|yIM;TN*33)Z5]/ښKxVQ7]hx60g3 )b50"P.ު=r@ )'w/*_q!>[X!:*K;;9Ġ=ϔ!yyc SzBڣjX>}KB7U OaM<0Z S.UjD6V}fUYЖUF;Y,V;/`oŨ;{ԶHC*ΘZz]{$|#jXUVpb@-qF{M3 rsmuW +v-vWEb_   u]}2%U`mNVRZ2Kz\ vcᕁP.p(r.S]{Kl.t/ln"|JhWK'_>;[~ )RZ*NTl[ UX,#e**[ zM-kG],(; y&R˙<*S\^H!#AD?cViڗUr %]PfZ80QVu*9'HH6R۫M|T$:w?q)NR=0r?2:SS.}k4M_^z ڊ]%CG,;畼YX;nг_LWx*4jپ[V^-a:u8DiNYlɁem6k51jTQXAĶؚnq] 6?O2lSQJ5ٲҀ4֣ 50}E-uh$OaX]+_:W`)|4FJ+qE^hx)Jl%HSJ ՘>zqqs/SRq**thly1Jte\tprQ:.[M\2TÕeUVeqJYZ +$&~UԫJ*N&>C@CYmO;S{b@W5n[QsNaMg~*u&5ԫƢu/Lyٚ֊aնx% T=,B`RY:k.Ukc~&\m nk*Ed)UvkI& bhIlJP]7)eۺ/;γ+*+4EXr܊Jw(Vח4}zI` U&~GU'F?P\dJFܨMIQQJH~+VOG򫑧-b4Q5 ~65mչ?Ajz€RmUrݶ}նͬr|* JM`!H!Tg'(|77L᢬IxOOi;V=Ls/ *( .N@ӌM cުzeڪCketjr&`j2(ưVfBUHa̷M {s 2VMZM|"Wk+o)iR@mEXjWثY^lMJ*eᣄ0jQZMj0-I\7ˏ" koƕZkHji X]gҜs*,q!q '{HtY 'ʼP+PPXY9øzx8A}'TR/ռ׽2vz99M=zZB3%"eӬD%S^?3q5^jUbGJC &bkɷRݪN*Yeo_V)/UuX]VPiT-P}Ɔ?:*aAR9jlN(Vwy3\"cTjpg$rv{unNHGrլfq+?t*" gX9]T@sӪ&+^ujjURڍ /j1quVV'cUIS^wp~~mU7f6vvs ,չ2|U^gkV" \w=}BkNSQ@E&Hmݦ>4T8wz\hb%nЙj0uãԽb= ԭ煻_5n=jbQa}2ɿ :GR2E;jƵj@vKm@9+j(%3I!}nÊqhV͵RP͎tS'ٵ`5W{R5t;]L)V=aQ*w{=?<<r?U88͕ŴJR%euW\fU`M,PmUt{8}t`5n/V!C[?&/Η a}aץf]*n.K8,C}蟋NjoWipWȤI#O?˸JdҼ)R-#[b4V$/_c9/a1KՑJh*WGղ3{^C \X:(|^†)YNe} 3b۴M%0$繣Q*HY *2(b-S$^֕*kGjZ5%VLX-U@v?#ykjxRH WVֿU7.:zwXWӄ{m:G@gsc?PMHRC& I5 V^lmvCQ)¨:;[ƪUIҌy6CjȘ7^#B3nNUm8*2! UUm(WQ )ַ`UƲ:л_\ QebopAu*B=vL_HSKAޭ'^$NXK;caj+V9gE WUDɖV>q⮇FUp$'7Jp$A)cGdHVUL6Iڶ16TPė*N_n=lZIU*-3=Pv?8:V/,X]\^2Xt"㾻N/Hbu% $`' W/N#ߕ+n%]Ù%69XSl`Vzэ`#V/K}ѷ׶`UvUE&~+A"MP-k`u7<+OZVjIU%i1 T#VZ_q-XEB6M=jTejr˔jcljmơXcZ]Kj9?"}%$PƩxU8Vv5d{{ |gªNcx`(K4*UUc>0S^~x\?H UP}PjHǟ055װ>.uŽ󤲪VJ6j*` j..t;g°l }>5|VsdW/؇dz~z~&OwGF2MZ^כ; bRϪ=RjϞQLf0VjVc,okfzk?j&yimW,?k8w@jrU^Zr ~GO*}#TNq)T=yzf^HaEvCtM:VHeO9J{iNTU*"0/W`u5'= TýSrvu1ZA.u7 CLXғf:/9ެRp*q/s\o.;v(|bV!1*]J/+ЃzTxB׈76v |[w.}]Ox 귺E[t&=np<*Ƹ.lV,P@w"]zSb} y, n:;uT}OVw,= mN\k&~j nj0 } h=`#|h+v~J,Ţ@ }J)E^7ժ/d 9}W)|4+m)Nd9;=!iHzPY+W'F3ӉB!V͜DpuTU*uS"Uw"[l@uKXXyhJt[r R,Ugڞwq~6QsufTM5֢'cD[~ 0ƪ&g$}.$ڦQXLG[y(VRM'e]@~뗳+=Kj3V=IiIw;ƪHsp򃃽nـU5և`b5PU9%jX"[iuR)،= oZQbիvR%\C ,n5I8V/ }4:jte-F6(b >C8P:_aN42_{UmOܹ4hT? iPX5Ԕ`I}{i(=*+]sOU?O3)#p A^<5=5u? ժQk\lޏ}0OX@hVLEQkXMzVK)X _ _1cPdq[[[L[ vaX-gՖJi(:oj`1 W;a*ijKKOVϩ=UM͡ETOЬ*UJڲdc짋U;Y~X X56TqW۰ƻÓa0,T**9W?-ivJ[Z_۽jeVb>BwŪ.5*aueëj3UUON6SXnԬZ\U5Sz?dW[aE`=TݨU2=nUTP70YfՊPYnQfn۟+T ^ vbpr˵/Ws Ft.jUk*hcu`Ka*OU?XhW-k)!U15+ڣ"@;VlvrF}t4)G ã*9X`U0H;GǪ=)nſ= 8ZlrgklzLG\:Zk}T?Qއc:+ L OJ'i&j Umj2Pe2VV!~VRNsU'撫\uZlW% PAijf+;bUnkSɠZL2f@=CYjK2`[:{i-+u~[J>V}NS!VK`U*N?Vj(*Vya\hjaqXExj,V:rGɬ%jCӳ_SPXmy.!$VRutHFf.?QbF FHD I+LTMx4 ItB*s5б7*˥)ZfTjpHSW], /V@ FI'mʌoLB% lI ``M{z IDATE3Xe9?U%j&{/i`KMժV|ˣWۍf`^JTKdZӱϏbu:< .+ޙCoXV=owjUF8wܪ)#\F#UIV9UXqX`{l/O+ h=gV*;$z GF5p-8XG*WfU)L^߿1J=U߮kɢi@7X)Zl2c] b*ZyU4~.Z)>ʂOV*U0CW{?'穄|`eL]Xp\S4qna1>ukc5~4+5[~9#j9i)t$Tq'ǩʲ>T}'r>(:6ЊƉ]Tvb5? ê ;W` Tڤ|n^ VؚjaϪ OU/P(WwV774cW4.*=n8Q SV=NڪZُsE&yK;-eP2bU6DЩzύo~V*fQ`7sOU$WaX V}zZl?R+_;XPEiՒKB9JnRXL*ձF)|c`9Ny˔"P8:dtgf$ BZ*axvkrXUIY<*9 jX.!TK j -oǹcuD\GQ FjuWqðC jK$LU:TB' \jh[iK4\mQr`w?tZjUy~7~S~s>Q;Y@U)NB4: xPrFd*VeWlT`_~R`oORWXРӱm*QXON* \-bLZ{VR[e.MڑN:"@RIs2PAI+"@8*I VqR7WBb>v9Cbĭy?4bq)NbuZǨUE6*\%zXؾJg6⊀+pqS,LA VG8ڤ)G(s3j[oe2BAv`6pU ( iUU ;L07եX,;0U,W`f۩ 0]o)塊/F:jVM0]ЙQfcP~T2?׎]w,+?;S(wzz6|ijtՂIIR] <RL}LoS{vAc䱺k̷Q 4u>/puׄ0^3BB}M^ZgcwYXJy[j5,}JiTW1pǔlHl}]U:>=X$DX @LPP$#!4eB#Vo_ UnњZ56BU⣧To,j?03aZKU'ܧGV/l\kwq4TOa/ XrΪF ]xǒUח]_T{`R&ՊRzo6|k> ~=֎_R\JAȘ1eT\eciQ@mMŪ,VདྷktLHٜ^U3X^)Wgoмz Xm;J*_̪UT*j!{`Rr ' N(.i,HYUS*Nºe02WE"L7v˫Nj]u`ږx =Y`b -*V0 ]C:t-,:UMhf2JH¶o|Xm~.6QHK4ynKM|W "HXh@~TӜU Yj&lh?+`zf#|ZȲ(n*G i AY o| Bo zW{/f'$ (+yCoz {Ls^$F;t;=xM_V_ ^^0V/eV?˭9j@Zudxu9Vړ h"~HXZAb.2/٘Jϸ[VҷKbE>VjRk * jjjAT?TV>18M+zOGT%2Gl֋T-EݭR\l6ىKO J [-Knr5NH @j{ 'G:>[" V?js]|WXfފd*PVmT]\^Rn V9&~ ȵ՟28>~VE4Sh4*!LhM69"*jVVJثU*>1o> 1v[5UZ3{>HJTE|b`fGh֫ZcIժ1u,JZuk^_"k^+Ui5)RrX%ѹ띾zTy?UjXX ׷ſAbWÕjIU]y w:AKS(72yR}*(BcufMX+9gѯGXӬ%( jd @^}U-zU@vG4#W tPጫՎT19ծA㺁cuXWV{P5)*/H UϹBU-Td"~kjSmpm$Zr (rv̨1PP=-jovm{^ʩT 75|trRdKҮU3J @L0fvqX\U \mjvmx6,U1ϳ NS\G`:ȍ֖XCA}Vm׿e\uX}oZrdӪU:>nhwU3jXz ]o+;C? cFuX UNk}zwwOrOV=O\WrXj9*n#Q@E*#X"=S:::_O7EYܑZ}GR*Ҩ-*wYڠEZ%J71jZN`q1U_Ճ\uZ+~ 7(Z nβр au7Ugzˏ_*!¬(1F%Vn΂@U]ZDvp 7+;Ve 7k`ӻw?c5O՟OۨoZ7JS!Zt_*37AjTU³Yl<%PGVcN2dՐ=r6S w,jVM޷`ITj*W])]FmʬU%?G&AfYl)\slꢩI+;a QJK9gcJcԯZC{U. ` wb=5;”9jRUJ< o\r-DZڍi|-yiҬ$yVFGU۱t6++U\ZuհOma5-s4*\F s9YH"3Q#uy|HVK1qIE^"%*>JFoCnY}j&Yx %Rm^mr*BV 5OIm`lK5VeB,Y}4X3`;vG ZHs[c7WW?Lj84ן'ʁ"Ķ-9dV|=Uh"> WWV_]L* , 8"U] OQ>KlJځXEjRSiXz/T9aJE>}z84B \Ojnw(Yq2P<a)] ʪ.*c'Hθ.֧RwEY:=եdaj9608U~xcjX= g՜My>ɧIIHbyyyU] q7[hX6+_9<\cU-V(!XxX'9vclup =dE\ p-VZcA"}yU pgVLTfWiMxIX]K;U:7o ]*Ҫ|od*GE ~d7XgqX6e>]wU g!=]\XVҜ#F%.BΎ!˙.=e 6\X(Bhn1Y *{"V ZqԱ.uFXXRw,=C՞#^edEMU0UT*EF񻥜bA=oȮEr(3UKsފu{HӍP~YjU"U祯5%ҕŤ'cVQL;KVgZ3(1nuRƫ,/_f ӭO6,V?Xuq+%nZ hr\ê C%j-L#LcwaoE.q>tB||>VWً>FJہG}?V;|1S[mu,հzuS?LŷS׊U4`H*7 iD1VNH4>sPxzX@j*d8Ӯ8~x[IwbNXUi*^ZHhGJvN? UH rTº Rq4w-|k& 1VK;U<*d-,X#`4}ojS* RƢ`JR$$d|fQ*s2=mwkbZ3./C붑%QX@ [P %A&&"|J 6; Ki"À` I7p4O]u{6ٔ IIQ"?=ue^RU5l$UWTe zJYX`qվ?~sB%nT2>Zq5r[;*H^ڨ4?'WesFqTK`@Ӓ,CwieakCz-V[]FkTS*ˮSk~ VyU>eǃډn%=R#pꧏVQSUN*Md lj?Ova'KksiI wTqqV4CqBJozҏ;Ul]wURDjo~ۺwcN$GNSM`ČYX-«JgAu%NUבvF@TaS*n{DU%ԈZ=LgYq -q^uOV{7jjk=Y{9=Xm5U2Z IyUNJU_4ZNG_*q5j5}DjOe12ZbCU ɂ, Y)H7 @`8 q0 QUFD,'A m)bAj׬LL)9ji?.(H6Zѵ/eB>xy(I>r;JU F8_KW;ԑLڙ╪UUb`EA)("Wl\ 2XGJ5kvi~Bj&8ª+AɮjzWD-QUSt#Y~)1PӮqY>\Lq4E\{uQrޛUCr׫25ID*YtLF-P`,6Am:6h?JVy}Z3Ќ7XW<#;MbucM[ZHs-I:ƾY~8")W~ >ѷŘV =OU0XCXq5vKz6<^Ź+o2#P<)-IٯU{_&帺©&iUVZ5vՎ˿Ci=y]Y]<՞)=Uii$Ծ\XN8ﱊ:q;]leeδ=gSX oTF?W|Tz|YFmi.9VZ_B@wrGԯkATVH*ب'ĞpʋAhOmsY؀+OX,|}LT. }2s5ZZ덎]mtoRw\ɠTe^MXueU}tTyҳTHR<+Vi1CCT\U_-XRKe⍕rⰚ\SɁ~+vRy"VS={9VGhiv 4tC>Un7e=uvww:%@e,XtـV3Z/\TObB=UKT`RH k{,(m#֝J.kExpE#cw׹+ ҽ[j"CUUͪAluKNjg%]P99{Pu"}1aU<ס .* [ū0> *zQ{]utVGyZ14hПyvj z'XnڃĖP}ڪ\2X]>\,ơ시?T) ըUF*XZ9':PrK +jVyiK ow(,{yCdD<] ]M+/*U?I #U5Ƴ6k XçjAҀCr4_H2])kVobq Vy_]/X=s?C߄UE'5MǰꟀ`Ϫ$H^9EXU ^惱oZV&2uujlifw"@e(y{ذ1CωOjqv_OĎSR!*!OY@j:T2[,8)T EP_~V}̪nIiTֹ !#a^@(~Y⽜ӣSBն\P8VaӪ%PT$!+oY:;0CFu*gDX,&(LJ%/Yh;I=X7ڢ&*UU*W0Xgg% p+.t u75ģ"TMTλ{/Z Ӊ&֛5Nf>sQ|eej8GY#V6(Cz;YSBU41 KTYDtLlX0^-U1kb)MiZPNe0rgԢH kYuYBj6#8PZaCPXUsS$QzfNWVͲX뇱e{H`u+X|*Ń{xW1b \_I Ps^,A@4Y.*PJZ~b=7`X9Z]IZju AaJdO cBՆ*2EU?gqcá"bDṭJy&k0\U9X)b5RUn?I8, PDj&+1Vo:$ry2ftocSU}b(E|2*ո:ڸv+{tPT賠e Fǣ` ,Y22Q3j0u gm"LcU9/|[kp3jB_Uԝgղ卨^%nGUVҎJ k4wE˰ @1*ڡU3 ,}+%fmUz2BQSNX*LM]Tg'a5ݍN?XOpZO*̪@rϏbu3wm#¸` K4qpzbKB6jNJ5|R.%Ɓ]FAt._p={ޏƎLu֖l`,ʢ`ޅbWv?eښI`UEa!_kcKOewW[JM L`549D9ES*|1V7;ʛyT9}P`uGmT##QȊսڸYl=R]GwŪ]xkUtO&FaLWppUc5r2qX}ŋ.W1Ұ&*T9W EJ-MF*WM 㡪TcvdaA[Yni0@*@W҆ƪ1LT%ڪ oZUR5כ [̀L,$ կT<x+am"k=.SG $ VRtHe[XN\}8qޛH潰JL V5cXUaP*i*WC S*X VwܞmUT^O*L&DuF*@ZTU?,^x].oDju(cHQDi-W9ժy^X1S4VNd}2\Fx~b: jU{2`5[<įB#U/`Wv?as_6o bP-Hn>Vem"jtI;>\dz*JK 3EU֎j[ьI-JaM/sYN8=8+J*lN&g'ss J'!)WdM|F IݑvҳZV W%~ԬZSn-+XQ&[t4KqRHn^ i֦B4PtO찶6s{�d!UЇ@eP{,7IR?~:Qԁ^+>kX:XtZmb߭hVVVC0umJĪڇUiO*jȺ^[JlQ Pu{wz0\վPG%VUq^djY^}0*zyHW(8h$jSXijMUˈ>m$fY0|`;HbmJpZP7ի6ϝ~Z&=+N*B;luUH6UXw/ IMkXM YwyZ,?c\U-lS0i~^sY:&JS+ N$?4U5Oڂ ^Ͻ/w?ޕX}j sڙeՋsYqS5QrXU\vE-Ǘ׋e/tnv:T][~ƪlXS%Ah @KT4D]iK*[[;mUi6Pw.3TI@媪SYK~`-ɊUշW;f{|e}ٚ6;ZʜSG@r*m53`9U-gf>f%ϳ6vL<@{#J?&zS{VY_B Xk!kz/WijF)>APT:RTMY}UX%حiV=jC JfK*M?R\=*V%USPUX}&rud'߃ Pr DyTV`PѵMUS,:jeR{i5V]QUWbU!RoF’|UJuϼտ ]jDT-/^]uN8xzb?l- U(sacz#l\QЁ*0|,Uh OI9*Vgx2QJwJ3?ɹUrN 'cqW>#)ńbJ]4439J!ћVqlWE @wW!WvWyWh2Z)9+X9x4N[,!oWg1llXM)UI#Iant>j$Vs|:]O}j6Qߠ75eT_*>LPŽSVMuhS=PQrEUU:NdPXVT(6Wt-zUj ت:=,PG[;6$ qȉ&LFd2:Qd AJ!+8I`P.p=u!аd"ESUǿWjЍ{|J&YwS `qjjcQ귯cTzVg~*qfw(ZSCCSIyh7Z="5`#_Q‡kl`Ye6yuL@[1\'JVy˯P{}Nj %hpӆFvmKNU&\G~&9kʧPj)d(]Fz&wK-u)Y*{'Ra7iPUn?s`ս|L`jY-JX=k;]R51@c=a8?*X5\4X-vc'۸Ô X\W.kL)(V%^X/.PV=V=W0xE IDAT%^o@V6 ̖_*s/K*;ds"@Z;Eed:)ZQUW8UUju][)bqaNMaUq3%QJ8uZ@_SQN[PR ^=f z 6U@I;(!d0Kjdk#]c:r6iAPMjB"r<!{rժQFOT݅jTY(N$burGaXU- W/6nn>m(Uq?QJXZ%b3|+4Db&NeVMRcjy..rB^[Sj! 8%% `:jn190/h".jje9:$WPwӾX=cjvh$jĊh=wX}ETc5{_ [ŝ \T'I<.!jBk]qA%S)%oVUlavUF#0U\%R΍Z]--^1!W`_bu' V4?iZ]@zw?{Zd9^:@>><<~yxI^"MװbnIOgNCOP^y-KC1ZPU8*wW Bj!FpF[69#Jv)R&O*U&I$@4b :k$E{"@>!?n#BvNa$M|+-CSr=`+6*/^+])7;IQWq{\bfFKNSZ_?A#bkojsv͋5?1/>C"Yr}-d=ABe%>ǮEK2:HO.&"(C1mν!Nj,3tþ 1M̻]7)R6qD.J)$}K*:]^ "N:()ywrj +`midɩvмjOc<ҽVpkgVHD:e6tETyEF9s?pLJCDG6vqYWm0y;yBr IDATxkiL0vF^b#Nx9$iD`1aYEP:hCQj3 B5}:0t`.QnRZWe[V}ۏ~uofR{ g݋RyFؾxѪvxY7:nTjn^qZtF]W꒪R%-h-PvXת{h,w{ϒ V;->ht:zkU*CMksspp勵'OyJ B((8BP*]gxicccjsssmJ¦R]XH䄊~Wb$kdx<FH$8x~+fjNꞈ9adfcC/zsy|ztS%)hETa3Σj8`eWQU~|w$hX=YOYjGÊ%ܳXEIPaVU<+2 P5a5[>:Kdr:`\r#_>e {25';6V'5anD9Sl4*z-qDUQeO]+rrVUy^TVq\L<JVkTFVqTs[ T1b^eX!f\xۚGBގ}\Vʾ%XFZOUY%y|Xr.Hj.XCЈt؅Y}jT9W2b5D&cX]^NR-$Piʢ?)B2eUgj"aV9Ҭ٬0WSkV'4ډD )JvS5X-BD +rjWk>=ѱH2p󂯯@CxL*E-f3b̤\9"/2\ܯX} 'VPM^X 2+d5HqTuooaլ'VO9?kK\=pÅY=ٺ[~X5W.G՟V%W= |-_g6kF+ɊW_JҸ1ȖӽHVFUn\ l+R3m^m&HSKʻU|;$;XF*#ZLzi"ВU3KҪɗ} `j?){*p }1"~15B^[JC[-nت>z > ʢ1*]r!XVjeeeLZVW$N2qH(VXUފՙ K_TuJ?Ψ woɊM9fGɪ&*V1J8^"3Vt/-K\]YCU6䴊jDK5y, ^RVbCuPZ0mW&>Vέ9#  :3a:( n**VWb9\XQ( $EUc!VCAkf`]\+UUQꟈ iUJ9NX|ٻ{hc ¤㌬?pf |$YW7fOULPG4U#SM")WUlZv~*Uf5a*W#X6:%q9GʓfRU!7S*<ʮE a ) -f5_(hdn1W* 7TVĬ4 Ֆ_nhڷo^"W<[PPHUCA~'$*dgMQX}FEnl.,}ᠺ5X*E֕jUJDUVq/UpZMXcYVknI0 nȲw}5ʢ#*<_hjj(C딪kgtPJƸ_%b1O=ue3.WjIլ*)^V+SkXjTo5V*6*^[A`ͪjfrO/j/ ߬+a`XjחM#ReU/Ү C'DW&~^]SDoH-ė[JwHֳĂ+~=c4밪YwŪ}/UȺR*UxWUi+V1>V#$}7Cc|?'<g2̘J%;-fͿLfJUJ:C/oopu4m{u*8ۼ?K~k>%ܞ n%!RڨsV*i>shi(>Vnv5SHTK4J*unD|&wW=quclx~XmwT T):]jCzjT&@S#gVj6ǺgPf$R^4wZn :P '*wZ>YU3O02>e~VYW^aG<5VW]ʰJa~=Y wYGkêynzݻ_O*ygfvBRzQ"3@b5UcW;?|ㆶܗs3l& ',F@@Yanv6~#ԃYvW8ANeD 0!}>?]hYL@Nl-i54iV[iT lWa-[ue/*& PRa,Tiuja#x+ #b( U^07{ssT$M~s̾u;jlo-2UC'Ȃ6$p4*wX*U$2:rTXB(JX վЇ%I^# ibjUF%WQ @IiWXe-JRWyWג%r Di> QP9ʒZMBB*4 sĒ٘형j fԝ@*Mʝ^i0*hAzV V64 U h%S0 1T# **X 9c]aXOEX5Ԫ ?-73L`}C,ߪ KZze7;R%.]~QuhI2MNU'PM/8SQ9p8bwAw~=8O Q7E"U_xMJ_#)HtPN~NݻYVUM=c2X}T*U,أ'YaU$kQaK?mdBQVQv\yZU /P`D9ە!q6nASNTaieO;9&588!Hvv }óWrL_'  v?+fF WŽ}"U1W hג`drXUYBٿ2){Xmv*dZEZUZ HUxUU0 ժU9ժmUWLW+ҸY~:y]@X w}+iܹ|T,~&VAIҐnY\7o+Rx9Ez3Y ǶB&,! "chJ/0nP$t1YLBqT(Z"S=Ь=ۍ(t`a56hn-kjzKoӛM &SBkܺg>W20fU4 J 4S-GHz*60 L dgLljRr*!ƹvvW @4Np>I2VMyTloj0VP}Wu@oWjfcnoqҰJF6j;,)WDU&\͸gV oe鸤U'tҧs3wuXGtYD:m|Sƶ\$0,跡qYn &aH;4Fˍv-qS+:9X+O mmuGkjȝ==˝m` v :ֵHj}k#wKZhC Դ5uWe0lwˀ 4xfez0%[]CɊ_Nަ)wEjxSX*UW`rg)i{VA *?Ub*دzs \22"cVi@j2VǖE6S G6YMteRRf{,DiYsS?=jV=fUH(#@J]ԢFU;wPJTmU`ZW6u^* uR:V@b5ZPֱJQV+ ԪTw+.ׇV˷?4u¾IG#q4@pT}v>7P{BP ~Cǹj+ѵkw)ǚى'8 Uwjdv/4ksۮz/` {,S1 w-#&侯Ĭ֮cmN(xÂ| =l >ybn5 F a_ h< j M Tr[_𓞠~$A5U%=-gUGjVJ0)S0#/3 L\-V[ÜvQXf1+0z (W0)}L:'wɑ~񄀫U wKb2j0Xai$  ]PQRjFhVrUX7; LM!XUjhd^*`FBm*OLT*W5}I /@*!6V7W1Vc+ժUCz<[/ݼ]/.篹\ 7?T P :SՉjL}x~#q`Ź/0x̓o`x8ݣ7>RXGv .q異YD׬ fKqMFa"k;opA1I' ՀP Z+ >GU jUm#U!A&Qmm1L >vڞ7: )=v+'@UQ(x@7cj?.4>덅 sM%811IM*UF5[*R0[r1v!mw'R(rբKvӛBsr_e>ЮHB-AR%.]}qNv[UwiѲV[dX/1ZM~jy?JI^ʙLlgj Eڢ @jQ`UjY'^jid+LPY<FdZ(DX]`R@dDIxH۳ `fdZNP_lS䛚֜*OTwժcV}T!+An# $x:]zj3yޕ*W6ZvVUUkj*AU*"@%M*Czy{eֱT=-*uY dW_}r~|tO{>_l XMzUWxx?FX_1%%'G2*]Q) ^(.T|I"a* H'Jo6h^Þp׸ Po±_="NyĴ PU" \yЫ224 z$`xS;%Ɔ;uc57*8ΏX}֓yvWdl",=WXµP]kIK]*VM)ni*u4bUW?SLUWj} SjʘT^sFowbus-FЛ׫-ϟ;6(R  Vytl VAͮ>bIGSU-'$름SYU [?>YUbj&c*qEbGxS VO42@XM^" ê~`)=qMe&LENUNmOG# W+.V,) z/LPUĴNLmv[\+'\ZTժVa:eST*U=xq( Wju\*djpӮZ~#`v6  keOV}ՉJGRꌷFAĪϵ@ii-\XYZ裍?_FWp@ y-'-GhD)U#V)/`ucG{Neƒwה3D* Ҋa Ue!njg=q3w"F^-r\J:sϗ*wVE:i T\[%l"+dgf3jQUꄏSQJ dR-J_"_qxc69[bv"UگNuPςwuGlՒڙa=aDYC_7AZAa:BghkSVs&gyҪUUUVM۩/s?ckytaotT4l|`UsSJEzͺ,rUdU4x@cUժec Vaeg}%8n$g@s ^@/(f.SƑ i^ C"& G Qck(fP&by~;ͲmbyE?=>T qG&HfZegDܲj𨀏je;$^G8J㐔V_W?&M>{'|׷߼z{ec+vd_۝ݿ|x8V߸;~r?%XMXPɳED 2kדgI G^p:AcV0zVO̰\M0oEѱ(JҝI`&aoPΨ՜Z%da6>U'!rjPZcoQP(kUd.YUS ~a>)GXUTC"dF_jM 5[#Hc@|u*Vxu^%s ᄗ IRMXTʎmEjX\pՠ>*TŸU~+S0XWYښs؂եXNJ*NsÆU85^sPW]`VG:*cIUU Dc|x* z u0u ĪH`U>A1_ 8T3V7oINO^\ޗ U>Kj%eji6g˻/ٸgF'Ʉ{GՋs&uH0ǟ%Z-+rXzimնxdw E3W2F+eZ*&VˊURUP +߸~`1 !TjT$zSL2T.s y"/>f|:ƪf4B}^@lh[gݠZ%#1&䴯 R^2Uc;ZX՞U\M"KGj)6t34k\@̺xjJA;BU*r\gƴ?U[7(ʆV^'Q^}]\S 1{UqWR*#pp^oںzĪT+' Xj RՀ}> Π3TyGU{'YsUbZ_l%XRPV 0 5%ÈZa)jӤjyՓPl뢊(o~YAz)г'E|Pt bX͠DgI5cU><m c'\n 5  !0RVeSPU.$OUV` '(Xv6SJ TE,3桷8"֪T3 Q\ >&S|h*\չ"kLUE y2.\U~KuI+l.'jՒb54 J]W%h#sʶê6H4\%`ge}hOjS=n[~ڤsKrqR"lk(Pb`V^zVPQ*'5s5kU UZt=0VjUkO3UmXR>΃XҠWcrSӰ:^U݆^)ftϴfZ'I!M jUfE U(ɓFUlщR.k&DU&'_QٺJm73P܎UүYB0/'t'^e7qW7G1I֭"FS"Jzb WCnQ;(gk'T2J!T-JN..-%|Zؚ'|j|55ZނZL=S%Z{}Ia0'QTSb/v :"ļ*&+81.%dӂZmkYecVx[}pOwf|rYY9閍%dl-8]uņa_\>z [a u'P1ە`u:*CQ6b=ǖT7ic=8V;:gRETVbrԪ:2` J7`EcSj֮F"\ϵKNtުjQwU#W7TH_/<(NS(G`*Q8{6yc ?g`% _y XǮ:Ƨ{c. >VZX5GPVEc0뼧{1x(7 't?N*SVr+\I(%o{z<1{<^E)"́]&URAKE-?|=.갊2Pg EulGL=VK g*OQ;fVza>~FXzh<#mPXzkJZ̮ʶ۶zQגRWP Twޙk"A#VO|V LոV=޹=v P"檨U 8XMo ш!jqfVRz_MXmC?}QJ@ڰ: =LuPe%Iӕ~j[RjTZʫUYP1f(Uɑυ[w~{cƛ\oLc/LMe)*bu,rYJt^TTBժu2yIa>*SoeLmW]^՘! @cjQj6Oӳ9mklp۳*7jV PTW}j*8VaFU廪'}߭TuqUԪuC4!/Wy$=`q5bs5._a[Q j*@J \ Hْ?އ4Z%V[bZ@5VP7ÈfLs=4)5^A^,&C 2 A5SS8TLU XK1\%|kTmjSHj5"4XE@#X4 c{ѣСjuYJ.\)Ӛ/%90"jk(H}MSj&&9eDk) Ґ(6SOяRX-VZ!W5VUdQIP3jz5$!V ke Գ1*-+d@Û+U\lQ;nDRIFU'T?J_)''1XX/|KILAL8ߋӊŎ踨A8c˦l{1Uru Q&r9y<, 2K VmD_k`[>++ UH ceE <OS2)[ HAʖUDzz[y%VI]e/kFVЎwLX]b*ܒb`yOƯ)Wz52t7*`;ߊUGՙb5êt|:0VVY%j^ZS+`8Z5 VҀ*}$}tdJ:I\-B ScVh ]=],w ưJj5/R6Fo*%ag+ނ{ Pä;9Γ`*7Zv:jTjޙiBq&,F=ְ$LER >jYCSj*ZTEL6Rի2*#^)mn+fV28#d2&~JCx~_Qq ? eULݠ{w\m@ DCRzdQpچy bShcu|dr걷ƾ(woڶ:~-XbxPVcLVWv$~,vn 4) *FVlUy y>WVDY(+}P~c`>Vmq4vSb;Vi'E8V7PI*Ҥp3UgU` fV0L&nuqgҚ?U<*eĆUXj*U.VU䊺$XQv.aeLN(L{~붮˦_EG[_>ņǙ&K^cfɝSuK~pn)i7]!YfX5<25|z^NLKBX+.9!R^uTubJy/7pO 8f`P&n\p*׫׈jVc>pEqwcqa kV q XVwnZAw42pjjVkݜsju.ĪW塶fPu.JQcU_ZU~(VTiT2^<9U`Ucu_,W "| Ga/=zVf,UM3a)]!Yt+Vyv&jqEPDZ-oj: }Z}kTbXߕyx2UV;8Չy@*X]{FZѩp UtcRدH"ѿRʼW!uh^ IRV(#ktpp}joUJhs KIWX5Ec%qa@ XC^ WQHU@ew*-d IDAT\syoowUKLs1⪍/j*틫PM&7^1QEXm,9aJJU*TzDբYڣRqt.[҈auw;V[%e+F*#b M\2iId Z49pQcA11JmWu|mѧ=DuK_K,edxv1-mSPvu"T,yR6 jWk5P')_Oמ.=]PwKg1tY?IYED ږzzRUY+%eHz.]UGgO̲VEpP$p_+pAYI)U^dm=<.ka^QFLXG J12,}uPqO#lW.m/jrim=ݠ.*@yV[-dKȮ#KzDV)8+ z#jGͩwGeg' p5`˫Z]U] pXi8A+M0*j0:Z9%UPU*t$ԫ3ekXbU{# pwlZN(E(2Lkb # 2F002Q(tzd/eq (YI>^0QsR4 v;ԯZ(-Wz/ނ$ z5=V=W_G.*U۲CjP] Z4 Dކ !z6K4ݎ2>ئjUGɀFzj'P VVB"t GU]}9Z`U^*Z{ V0MXۛU7 eiiUWU@u@)ԨaVTzX.rUj5X0:aURWC:f;Ϭ窰rmЫ*W˹1Ūˇe͒V[V3$2 yaz,^Bڈ+|4 FtunaIJ:#7t@i+uK}Z=X L UWQE =H WZ"E8*bW~O`5u%R:M+}俼JlX߆I`t%~*Ʝj` 5Uz%mJZ,mX!P)TFbVYM i&@X#i;睉*\[jr? 6`o/OU֥y8V3|{饀q;7*UuBc*tRV'fO.h{V#V/ چ`uPp`UհoU[ ɡ'W AIUtN3lțbuDm}+ЪZye[T2zиφA +`V[:vY=w>VEȇ9_%|cJF`uȣ!BUS(氺dU*i-6W,jtxCΚX] XΫ]@u7 X= jjnXե%ЗZڪRbGG\U\u*:~*/9Qj&W[տ(Vj5bծz @R`Y@z}{"@av%ܯλ7#'Vi>Va飇eqkDEF-#y"(o[jU#o~gͺ 2)> iHGl9NOa@FdDoSc {ԕ ^0_b5q\xʋU!%@Y Aq峰?Ѹ rX޶s,;i\CusV82^U%j''OaYX٨GTU؏6*f?L;u3ML2SR|#^C*ʻUt3CzUˊZ.j5XF? V{t_9, 䩖8V9 P-n.nIJ[ruԯr4bykEh';/2/ q4ƪ_O^UOєM`>3UoSJ蔗=9`t7WzڳUcK2}ܻ[*%XU QuV^:|\U&{\p{lM: 00cbԪUfue];Ndj9+=D[F&V*z VI䣀HھЮ`u{naZM.gS<9JiOLϠ0Ji v6>%M&-44cTi*SqJܶTԴd)VWE;hn9T[Two`%V.*arG:EIԑgbTethIgUuHy`1[VaFT݇*}`uBj;@Wh>р@z{> Wy%NӶbj]tj V %BU: ]Ѝ6ZjuROkK;N $wX9:6wvj:b2Xuۮajv.|aEEq%.Ӛ/g&XbU"3٫u`zdbucuUGl꫆UٱUUU Qu x{IUbuddY*UySθ*4UNT=9Vu(KKV2@u‘ۮ25r_`DZr\*w]/CJV*WKajj\VnYsD-!#> ѯo6`uX)U_yU3ն|T(j XZu_džU8rrɹtdoP^:ǑqU[Z2u&Ր10{4] &)V0݅?pnfxјlB< }R!vdmn..sh&iqyv~YU{R,n'LwZ״P-=)ޥ9\UP=?85jޯ E`ՑPS"Wu r=!*(@u ?rRbhšX]لf8:~g&W$r9Q9IO!*P(HȈlhZ?XK}TRW"β|@AP%BCu]U+bՠ^%_Vծihbſ\[+V*5>_U"zbQI~W҂ar5\QXm|*TOSgU!U#_sUIubVr: p_;UU wZ(N8ZCVZijt13V Ct`VeOK%xHQ|ӤK=рke?%10)`?,XVm{SW*o\ʄ*OęBnX-)G: 4'*{~XE>8z&r ,LJFDyc,xP-5V׹]VYA'fl&~sUF~$GD}faz0䩐 zHҔpySLYZju5.޶mQY"9_R[o䩦qjR@LVNn^{:SK=MʥǑT*j=۩v"V?ĉ.muQ,k5_,l{82VYn MxLd-bOqLZV#VXkT|G3u νoDP"7|JG|qiV^O"XYjvc (WzaŪSu^{%47`u[%꧿J*," P%;& XMa'i[_.r?)srlm^|[*]7~!V6ܺ`'hҶ]7Ͽ09(سb;k$O+?t<Cv*.'Z'rUVz6q/ڗYYI+P<<&=暼ykN:ONȨc~~8#WըVX7Mwt[Lj5Nm}qLkuRy R*=eEP HpWZ(űZ{ n[` )H~˕-6Z$񂥓Ib-@UD4OLwxZxo.WPN$Ȫ3< QwḻZn1#[LcUY* HVb:Z7@&Vd^DJi?RU:3 %ZJ,;&x <<ԵSʄc9R7U zS)W՘.=W@T"+2XCbiy<@#-W{V9cpu4@N]~JT^}䬂RmF^vLZ LUU#ɬc2VZ$* _75x`J,@GǠS Z`؝bU: ᾸC%j},V>}Y\p O[|p|/4 1YK]rv4̄n&"PsqK*Sߚ((.^UW=K"B<YWc~VzڀUwjYss%l|JzF,|SϹa+&llIb_]YHdEHrĜ33BTQj(-AK>ېjUOJH1`b姏|5B4JMYu{QL/Y{UFXuDfknQbkWv]X-tP&V\ Wem`Dv~ mXUi08ꗭj>t\TŭΜɀ5նlJ[r\䎦tBU9 j upg.#V_u 3V/sm:V-;V F( ]n~ďGig1V7w=XVqޭS.Ψj2ɜ+Wjt]#]t9jxhU,_IڕŪcV*֔/F*>Jkpو*qPUUɌΪur]* YIڄUo.9gI㟖+ka1C%^(V 0(UjNj/VFśb"j_f+VV!VZjT~oj_ewńbgC&bq ٰ۪򾀦YglD1߆NLK/N'롔UqIOUMR ot=Z<r˝ 6^(VA&*67XI3Nf]ߏTm0sX FUѵFKLJlBl@U*[du~˯uGIJvT7պT3պ $YT }ZUbgwZ?Y(|WnRի&M޿ [URK$W[`Tӫ^ET:kEPQUP?ʀ߅?VjuSߔBWTM F"[Sz>;)RUr;"u Xe[ XeʨJaMCIzVb|QW@~('(Rz dfblc|q2 @Gr`5 |&tw{Kn):bAӌI6%1@[gMzDfXZUrٷ{ IDAT>lO~)cTUod7LZ`U}^`ӕ05|lӳ%+ 6}j x ъPm/VCsUj.=6mju~ٰz dEjG\oݒ*#PGmƿLj5jՒf}vd+]a\"DXM)Y}"@\gX?q[X];Vpt*VڢeEz&Ľz+[4fLVX)B/\M >+*Vy)^_H՞*VcIPj:`uxcx99{x,;׺0VR.wkE -G7U8Ii_4 |} o:xP 0"2*LF`XmDLkff]MLI\n݋M*)^UxP?99}~]Vt=sV%_<ע)Y=xgxSԨ#iMj+µZN SU3Wb5!D$ʨŪWWtOCjY47&m2UAuTw=ݜso LڿW/־)xgl*T ;*JEk BUYmo+( `~^ NXuXghp'UM?J `֬jZX JE"5) \6!:kwM  NĆT]߆sLUnf2ZVk r)@~ t=o~t@=?ۿ^X}=jX<\=Ǫ&'s9q.Zͩr`嘛\?8y;W; V=ZAo)5D3`9V`~}A\yW*Jcn(0M,AiUJ}jM(JV)rT=_^J{:_h7Z^hxV%H7Ucu[<ZVveY+:(US@ÀhU:)kJ}J"er_'ei^iX-U$MW*R\(ע{ШhV~h]N<_WneMN|8+ 3 w@pŪ%zCVU8**YJjaՖf&)]V>A\PV Bai{M;`w/ >~?~ϏԪ QUjc$~>(@tYuA &K&iLP?|EP>;+YO664XSF (VY&&1:pVVNj#0s5k}LkNRDNQ@IT*Ht}۬w g Zlj ѯ̟jXT}gT5xn Y YUXOɯHմOP]^6Jjպ7K%^e @^8+U* VR0JJ%2 g}jhqZdX\kաՁRVej-[PUҪ$E 9qX Vb VO V3jL[J/te2bMYDwwЌmׂL X9UQGcjc5ruDvN0Ǫ=j~$~$/5x1zϧQVo]Vj+\?0VsX+Ug$gEOwtu"ش!Wuv4+ٚSTTj&ɀ1?S4%>;#U_*>WT_pus:FdLU|^53@b UN# lOi"~?Oߖ%L8aWF6hDI~wp(SՠD6J4#o%Uvlߓ Oƨ6i!}} mVTvn `]pL}X}$ Iprɸ*r@w_G0T}jfUjY$˚5`o1W{4 OՊ<%N濘̰zX*/֢X aPXT=T{9@2TcM)W8e EblP(S9 VZ|oas }8Vŭ}^{:d gX}(}NwǪ{/WuvwϚtPVuZ 3/kXqj3޾Vu选PҕL5<,LY'Losֹ,/p$ VqS;cP.?))t1WLV^e.j'MAZհ Lߛ5Jp @WUu(U4^yYLȜ}cR U7̊UbuH]?+jTյBUb*6Ke4܅=u}ʖ ^u[d?k/Vu[@jVPHjs椌WƟW[uhy!U%(pQ* Xn+yy4{Jy/NB;MtP;c5*TM&%p߰J*ZZZUuZ%xfVnZdzxgo=ouGն6z-is`E j0cUjCdRrP٠ڿNa+2w՚V2pXL/-Oju2UǬ͈|Sԩ)SsUOtŬba@дt4,+dd$Ⱥ_)P'W5+I K1/h ="Ttk如riK݊U;f;gdhUl{\1cjKf#SKyݢV*k J`U"e9x-(9&&aZZZ "L1WlTL4Zei!lVi"cmUo]c?<~f͓'>DUy-8tJX9TuX$+~VM =@%ɻ Q>V=gT:=a0 _ƘZeu=O0R9ځ[wUp$S'ǧ,Υ D"z2ё3 .~9riF(hi[կHgv}6JYRja &۲.>кi-ZYּ,Y 'eݲ5BF#7Ў(Ur)MᅣKQ@؝D7,:tfKu`cgd3J@;%Ӯ,6( @֛R^ {$4[YzԀ.eeyN4PRQ pq0eZl>S~6qUjUIʊ˪jUuc* Щ%Vy}P&c/2^U VRV%(`VZ-j/ NV {ޮV&|Ľ~ŎLԪC&]}X%o}_8&Q^V5:Hw`)\~xq.U'V cBݷ~{jwщ_ӓTWxV=j5)"Ut?k։k5g&h:w鄀L-f9o1 <-RY@*6h`lu &~dl\Z}cNgwg]K^ն)XPW|^K~_FqH*Gp V:(U Fl_ׯwNViUU ,*͘^UB+ϡj I9_[:E+]$0"ʥ} 8MW(3̹rcuD²(]]1|/#k0rRuTd 2JϦ]?HW Wcuu5 ljU SX+ŢT|t|.NTkBoëXTYpaXZнsU4MHTPҧ^@'dQ[e _Ԩj0ϱZsە 68Ҍi8; Cb z\$XYiTX*#f &`u֤VP%nÜ/aV JbV CJa?z>^weVY$zq5I6Teuyj#v)^?DjX~툄Y.{1f"=:j ?RwW-ht VˀU,TE'+U_Z1Zua{;AX.w"ב~đoG-Ӏ2RTӻ>ӡv%GVzfUmUPH ׍+oj* JbfT`P,3FgűC^65"~TV%|8O juelݾ[SIT(͒ZM,:|y_ jrsC5UI ZMȵJWgln,֤FUS8>glZytj՘*Uڑ/'yVa}99z=$fV[h>C%AkU (GRAS"QR*JA˅{[4 ;~b6RЖOKGX{{~l*`)V[# 29"S vF4 lJnǂF%:4+WU[mؑ}V3\H۠jqI&_V\1kѝNϩ'TNEN D$뗂tʑKKRR1jW1m.p6E-puaNf3^oP%ʷ,bjsUQ~@JBF0+Y94|)^t1~7/ЀKW4eAo4WJUՋ9~y`7nmmm=c`Z%]\M\e7w3SgH:Bj Z&gJ"gh@H:z|o}ϣX1jU:d8ZNGzСTK=N[ 7:mOxqO؅V'#$$C­Voi , SjuH/VumU$JF{ceTL|FUP}԰$@D|):[2Dv͖FF(\jXm] GAN^ྋRaȱ߸煀}n_@*i8uz>)myVhtgQZ\KTz6PmW3ʥq =6-u- UPu^Go VVW". ,\XT)-F Gx+Z_(W۩bu3Ͷx/ٔn^'9{ 2V!WVkX`3U6כ_ tڱ3vbF4{6l?Mxbj? WkH &V7&1I$SJ*7)nJ(mբvKrV^_MjY|w+eCdEʋqNGꛧtR882sy];zoVIM"zWxS?fY(@2zQCtȀX01召J<8e id- 0ؗy0VmXսU9@n( nBuWBY>к(ډ&@.`#Xe /kcUaB``Zuu+"Q>(esh0 k]P NXꕚM V3r0j5r_bc;`juqM:{ZuvhUc`kLH:ZMLAqQA$}x=E!jXϜ4/VnuW^XAS[I>si'VOm{wwmNwj,9H0ߪG|q@@e&bJ &(ԪRXVMvoKTz^fyA?ɽ4,oYCMZT#j0rXAZ@Y!؊LkJYӻ5ey86T^R[^y|5^!jnԼV-?z@xx슢N\ OWZe*U" \DOJ-ܔ(QVkq!k=k|KyifdtUGm$fY0)B@j3^TXV]o"toLz]boz-GssI:o}9ANKBVl~˴+IBf2aXeyX1oɃV5@zd~.X%|2ya3uaGoV$jj&.hSd-ЮZV5U2e`1++'݀*1ϘRU',1>%*-r<<;ߤ\@ ٸ#^gVVdBU&l UMlJf0H=˞Fi7l},YjSHZzc}:2MP E V")+X[5Tɾ#TcuW6C/WOrglj~*=UMW!IIp.ײw*XkBG;c:UJ dgaU*;lz"JccjX$WZ ~ (żb1M!If,ZgZa> )V/Tzc5ՇկTU Xqju[e(`qKujkciH^ ڴb;x8Q+CN~/Zms`J&NUt'XE9(ztou7 Ybl>9%KRdtŽd2Zaʚ a mHӓYGg;sj_p5kwztkމ[t{eDAXغ[־YTή2,D ?W`1zdrm@@퉽rMNY0MPQU : g޷#rX}(Vc_e~*quXbb1:AlO5낡U#3hSӪR}+SZ$fZ!yRcb1Vc2}j\5̰ bkDjA!VWBdުV`w<oC#_o~ oGx^If:J߃NXkFrghјbc#q4 I+VBEꈪ zT|Ҽn0"[ŲW_/V%W?t!FBX]*%j`Ou6> WgRŇnV0;hY\ebf Vr|P$RBzf܆h?T7|&;.:H,Ԁͣ~Tq 맰:6UڒX] V^]qOp:2|-]f+,YOאcq'*q`P=nȔF| Wj>%. [^&Mg}9UjD\!B*RSun.2L`mQtYIfaM Gr%+;K8N/,5Z< 쭨eXB\}u4Q2VwcNl `V .+VdjAեĖ/g6-F ker"NSݗ |Gؗ`FbNt_U+yX*խ%)::O\c,ͨV:V:>]?Xb& U-^U6.~cVYZҀjԫ *չ2km/Tor=۴+fk`q]jVE V|h9M:USFe 3tW\e{s5ᱺ|*uy]V޺sulP!ufeo܈+Id!~d3U)) ;跓Ń/ Vp|/~fɜC_eΊkyuz<#e"qp-<V~Ca 0nob)kGP/5J1u%:hsh ,>\ W X|"fg]թ?kmM7BMFWD[XQOn_?j{Z-0hvm' lyDZ.1Uk5~ d}ꂨEuyyi~>ջz5ҏ [!qAItp^!abX+C,I^5ѪHmEVǬd0bP.X[Ī(Wj 6CUjFߗX*OtVU֓ 0JU.Hê(acfTJ*5ܘpFҸ ?I2U4" [jF([15/UeF'&_>ͤͽpV+&Ī`}]jJѲVIK`:׏ULSڏp!sYpծ<U.ˡ5Ur^]jU-<GG"U+MYW+Myk,b玪fݩ;at ЦB?il`TG+_B ANuyHJ2yy(VI~|a|ߦd,]+J΢w7r5.DcªdKk,]{E; SRF@އ[};Tc %uR1Eu;iVxX*Xm W57Ļ\TouZ0VfҰzjbpz ?EJ8*#nT 'm)HUjx$ D!jD/F򒗬QTs.9 Ub4XPKP]gav8+#Ph(ѕO,M@?ERI]$_ ϵ&ɁbdXWT5jMij C_Usd/e5c\=sv>=QYtfM2m6ԆtE@80¤TqQ$#aJ G?ϼy9E!ՂW=g\}/yWu(V7hiN0 X}ɦPa%QUmT!ŨڎD̙\ xUz}^s*}0 CGtRku-VٮLB*L[x aI,V5fMXX%Vgk0X$|ZfuZڱyB`K1ׯ)+:ΛsI\-foqXH:肫M gmR䗼(g]ocH0QzȦ%lDsn0AJZ5sP}d>$%J uJͱ56'snwɉvQϨW$jrB|o cML1>1As*ruڱONZ7묉9X݆ꏋ/#U-n.ˏ3s٤ τ=V-XD\/#-1`K`‹Ɖͤz?j5OW9V z,V߂dcٯPڴQpiz ͧ Pe}\.WdIVUn!ښ2~+Z[jaTe/j45F©7W hm5F|MG=)W7XW|exb',@ZPYQZfuX Ս^3V؉%X=XWӕL޳Rm]8haU;e -M$ġUVWS5nRSEغrJg`wa''M$-UNwnCWІPՖpuM#U r*UOp#8  UwD23Rjh.XB|!c@3'nT'Ew^=\'ոX@KX~:Ya*AaE!cUSF4_VO CejO~&dP-T>էOճ4oן9M֬pZC+^Zu9YյլV5i{9圐tR . r4):XjjbvdbX^Z藖4k0Rnv_PyV\u"m+!%`*ޱ{{jN݊UقWpզ^8-Wdy@_]Ոa\m6 p#c21 JoιY6iATD`*9Lv\6~]P @\*MJP{6OruU A+*:?Se'$fIHgFTzW& *({a;EQKo]\ʤZbAXhX"g9Ajq=AVV-VeGQgWXZ5+ݬ ~U ϝj{S)T;.,ljUjmԺQEѯOg|Ynj,W9PS$UmԿ\֕)5ߖ&F:~ ?5 DXPjՕZ|TVZ|t/e;7j *Vtƪk|ߩ3\ZVI+x^2Wg l]:̡[f>lW}'hJU-#V9#B >{YN#!3}*\51H{vpTWhnl?$}&aWtw `~RiL9l'>#b4yŠ? *:[6*X=3JߓQ-}VeUʠZTFk(xmu&V<ZꓲU/&6jҫW_Q~pMz *ʹҟ7JjU@-JV*:`u9bwq 뙏WU}~ǛR(DKaӟǽS;~/9w?nn*򗄛X}=*je\L`-UJ)DJC;d A}~U(H ] #"rcsߵjLd a3 ɩ\R#g]x2=Z%OC IN.V`޼!V 0'^j5A3Veʎnp$!\U맏S/(.#Hoy_Pk dZ܅o>A:LFflWq+~4Kӫɶ{ 3 f<}8SMȒOiN:g~:PKՋ7nl>m6v*QiuUU|+3q8-Z-e5+L6N9S GڞKfyU4ԻْUdmMVě+Ukѥ; `Xv*X FFBzZevJj\ t$VܲUh%/VVoju^. SJe\ǹJ:[BB?ϋ^>[ZyJ%r u5LxQj*ӓ@Ӟj5gQtEskY<+^lSwRo|XCrt†p׿hpJ 6gt(R@ ڳy}HuVqu^'(LceODU&Tj@ItF"DQUթaS@#`Ū0|*lLGmaWog>uV/:)êQtvSXW]#mcj84kPiYqb;wڗpC7U9k.RqzK|u5fU`q`U<=2RrڟWoZN?{򙳤NO;U2.X=PwU@J`G:;Q^:!JGaXeFENUy]7:E I9$yAZ;F I`uNkUH9J;l[>t|-h:.kD?vrUE"A{s7ZvJ k X85W?EZ*'WSEJ ZjJ[ͬv>=QYdZ;bرj(#,"`TX@H*PM @Q$¦6.H\n̦|ys?} 4Z,'NU8[b_6ẗHXAnS籺jU[(VFfs5U "0O,Vc3JX4g0E`x߇U/*SWO_8gKX6/+zu()b##͟9 ֩YkVXUQden~*XmVK%UUfuCPU9}Q"q:Eb(]h5|:t~h4ߑA5|eKFjKmk${u X3]/e{eqU*]10X5pA=~GM3yI;U''A^6QՉ5-uY2f h1̵׹*3ǀ#k+mMz1*HZ9>VS QmenDk nMɊwNrNh%WMPJ$Pfl`4%)V;jŧ1/|ͅȨ}N3]j#iB.N+hWOpS=KUXyHQv+n/Y#mAg XM:88 z XOZfEzzUrU.fCձ:΋: VbUbح7 4Z)U\=7Z8Nb1m[Qխˆew;;+#-ժ9CRzmЫ8ypX5qQa`=(XPθ`,5PvXz¡ U(XMS4w IDAThy,Rup`Uj]x@G) ;s2iZ5s_BfxqwzU;UfN60Dkd~j7OEGUGn莖niaBKDg |X-b`Kے"9*,jU&ڵ\BjHتXEm6?"U)uiUZVddTb bWbU**vw{00]OJ,ɮߠ?v{zo;}%mWUO?nVy oKO }*3QƮ)[6SU2I@bW VP6 *u~~,<:U8P׵OIVpFb35j.짰*UMczriBYNI?ԾbbUzm+/1%Wb5x%k G/އH ~(rjUZOJu*F΍Ҝ.J\wtjr\-`-UVOߠKbuHRsGpoKm*l_3hh5bg+ މv:oDzZ\Xyk %V1_Ah[F1PnkKuDHAW %& f3kJҀT&$>lT D,iP%V% ۿbs4fiY^*+j{zFlf([t2!Sv_uB$ 7gZPuYgT3B#ZgP N*NV;BHvJ?"X[_T֧߃T^g[Xi1*FG祶WܭXKSN'&jXݪ476MX{y?:@+$Wqc;;ѷe@<:ځpd!WgPUe񭍀՝M*&:*&N~Bӫmfx}nX-o/ Rnx~Xɘcȩͣ:T"j-jЪLLlJ>AvB^#Df wI,WYULW ͉e-X}#Wޠb0Uљ3],D#.U͠Wa9eX3T( jЪjPKì O- Z0ujoBa`uCJ0;zsѫx۬JcuFe*U.rhU>;"<0(~C XrO')+/*`Zj Wvj2:4 6@nJ\r-+jal>*JbojO6 0TUV&O?Dp1ĺxYYtk,q2#YbUślSl//Q\VY- w=jE?)V3CtB'*JB+ʴijUa^;jw9ZUlI*ȶZ:4F&0|k;)V}`\UFmպQ48Vyj1+V_{ÊަN텫/9/@*f?+T/ZY3juã j՘*D:5R2.4s p[Y\AcZ 5J7}8DR*PgԫTMyGlUGQV/^mĪY WN]0X3G{ Q^Zxcu:*~,`էُ2I0[Qcv M8BsgOstpisĖ<qj[R){68nTPfyc0tj?gX)W\j5U_mF~Jv4&prblu g5WȤ*V p4c~s}_|: Tm,?:S~#UeVD[muET6UrڔXMjDUpZ9|u- ĪӪǭZa ,rJUujVE a@&fYտ~+z,cP @a&V yFëb5`U[m["Z8kW&)$'*+jW7DܪSjGK fsR[bjo\{ Ų/z1TRۢ3\}8<ҟRvX=bF%o?`qN;,pz =U9^au!Wm4w \)cOl:U(.&-9O/1vIw V7 {)V};py>rbwm&C{ƵOB\Aj+US,ܞX+ns7ĕW7/~'5<@֫C~PRuaU>0U2*sSUhtOœb:i* W!Gғ4΅5HVͭ̀BPXa]~oDD܀o_&R͗0PX=8KhVulo+$V~UơU]_)W@ʢV +@wrV+ V5PqUzn`aUwn!FҜ/:'\Վ݌&K k3qOdrTR(yaV[9yU XU~a,3SP"4zn3&WN_@Q !-g35TgN b]lbUlKm r 7M*U$&Y_i31`*ӲveSTP;˛N6 VnXQ4>N}<9B Pô*;a 'ïZRc; jU?1efr,Y*ԪCKLb3hrlazUSVGZFm#Oj+x}K_}aceMb5@4 fU5tN;R\\E6 I-XeīݗijLK:a[NMXm7jڭ*߬sdK` f:c6bU_ a<lY]jλV]wX3g1eX]X[P]#VE./Źr)>s`,K_0 +N6VGtn&ٸd wkc(Yf[jMR}C{C::4ؽ"̭k<5UHU>ɟOܗO "^EjC~hE{SeƢ%:{KX.+~Ɋ(c[+-rՎq7tH*΀̪[ڥOVsj*[oFaX[`U>ٖbut4VV9?;X XyX/PWZX)U=V^-!Z6= j\3XlJY0piT]/^2 ,mUncߐiXjߤlkVCr~T·2$)Aש3-RcBVWYd#8'ꎅYטTjaUIb5/EiUy)ر?RGu%LkV,G_x;^j_QWխR%oruo*c]Y؟/B+!ډmjׯ[E V_VMCZ5 R^V/WgT g VTT/XbX=,ǒoh2!\%4j5*Jaw Z PGX^*WU%2XV[!{nLjΛQgf4N-4}yE!jZ̊mg,lT:v.Nꗩug}V;՛Em(ASi5Y 6Ũ}y5rX9fmQP+`u\aRBF')'30_)?=F9R `Z VWb`1-XܑN%*XQʀ:Ylj=[*y2*2r܈\5z͈X*UbV4dԢMQ˴uɀoEmnj+2/ex5XXXϕT2/ `U4fo/̻*~ϳj;T:;gv-)05>V윅AV՗GavU=*'VSAʳ^-5'$I]og5+5WUe ӭYpEN \#@'UYQo}c_jHV+jg^I#ByuզW$XR4UTf&B԰Px||ljaGqvfV:VK,4^!Bcbxi;*5NADl`YR =Rz/29W<7N8R61xGKQVA KD+RXWkզ*|SW8UoҦg%Z(WӸ'stje^i}?r)VYo:*EʠF`?A1t=!znC[PVybuժj=r͜XkCBhQyGkhVxVvM&jqZ~Odgna ๹DU-cJejMԩ2RW(1ʗec]HGSĝ+S`bU'SE.McOEIKhAVRZ4 GI~**5q*PD cUӬH4Ōl uv ҡ^8UDqNNG[0p"0{sY1VU`u]jWI[x{`~(r!UUvM-720l}%c=aZ=BDqPJW:ݗ4K!VBJkA A=HS+'~yvjj1Yr_M9ziRv`v4˚K΀9]Z t<]J XSAU=o(HHOTn'VmZ_UYpm*d LJG'&`q;RZ}=+-B>j5rNZ}wQҩEFr%*ҔXTՄZַ}2VH* ^uVٕ,ȥ :j(WX9 9Z€$Uê&*Z-WIa'l`>yX עܧa5ΕɈo IDAT}fm 3KZz\Edjcb.,*]2ʓR]lƶ7k`ImXݘI?qKrVGf)=Iќ!SސL@VP`ːeOKj_!Wf5ITzJI(U\ayCUƪXxrD]E~r(V?w5U) A,U?um88Ӓ_Hc74Q]ϢaϽ>?6.REVuJ;ӷvZ@U@U7"Qu^k"~q^]JU L^T%zWԪCL{*rjuS$1՚%&*&]GL@ުQ[k{E;r*Z,jokTkBKJv`;٨UV2ųJG=V/H`V%U,cUIj( W3JZ}^p2 \;{3+ rz!X+4@ǦCpT!:䇇bUB99^+=Ӄ(ڲ^ax^5\=\<FdWW}Xɼ;keSk=mVL@ ;J=1_ i1kMq N䤳TUyMّvұjM`i%Wbyc]s<~2$CH ,ZjJ0U9MNhT}^1U|YyсS DX*KZ Vbkm<&jUJD@}U(ebz, (3QH=z(V߾|۷MC Km\PCuIMb?7-X*C+cMDXؚf뱠o5>>n +,ҘH%CUwaq# t]QSƫSlB0mB#N8N51ڣܐX=npfZH_E>`feJLXḊJ\V'1%n6Pe2.|B7 SJaC_qji OUаZ骾םLPXuX8UV{^} !KBK{Ȋ!5,N~,N,W}Ybr5Z@ E>V `nŹWu*tྮ߉}9B0:5L.F07' ~$[s V>Ze#U5}-aX傫*XPb'-+Y_U? *cU,#U"%])ՔpUiꏟ>XU\7>>k(4qk^*2w|CXx@-h2pK뾍XYWؽ⎸eō}ޚ1j˚'odxBokыU2+gt3>鬴RYY]CnܑpcݠJ -盰@ \e+jGr%sWf'S=3]&o'b@iG,W\Vp7VQ-{ަ{9Hr)Icg/4 $+3g` `k֔-d媖VQxNT"W[`uP劫c[{ :"J,TM݅*=@\V7bn+ٶDWI+`zLUɘLu3 =}5auPsg`R SL )|4KޡUwM~_ ȍ?kiƱ8J٭Vj6ŊXVX\$/쪘UojȂS(-31Uua}~10I>9yꄓUl[`Qb>NL'Oλ{ VW'frV_-͔,Y_XQUVO;ڱ"WȽz5ݙNFQV;m~4}})۪V;ɔCuEڝ*p*`ڴjeGgO{n>dW`#w/].WT,'/3.W8<?r.|X̕˾n\.TG\5rw&*;ޝHMQZ׽J[U\ vuv~[w}3.:a"u|Wcu כ/N! a! ˑZjbFzkҺPZtoz u+E\piҢf(xEw ?d/h_v q*kTڜRN reX>1:z/<U7TU ̉ӻH/̛U{P#5݆pO,Sg! =soߪbf˨z}V[9t(Su=\%EKr-vwv'@&;gGok>&3ȯsQLYa @˔>Y]Ĭ((z֫Y`^~} X'T6_*3׮TR)zQIzyz$U U_]mH0^b$`T{҆my_~pY8]{ ]FZ?.SA\̀y˚O~KXʰ#L,Q܀?/,F ۯY0VaJCR!wշؕjr Qs*&dsYrn݂d AΖ^Bd[x:sU**G&麿0aQTW-iz8z `wvq{{;?5A!~A5cݰ:|أD PZs Zg[ihսU$)_߆,*\2vfNteXS W5P 9baSb}B XqjuJa;=P@YWuYh*TNf>a Tu_NE:a묢i`uxUQʪ|Y5: #VUʃh^-0`UNXA:dVLx^N,-͂86jay94ݿkUֹ U0R]s:݊JW &k \Ie܋\{aUR!_U*&@ԝeU hQb)J>8_%\{^c4*VI>r9MjE j r$QVYZ@% d/*b̺:>b&tUS,T cchyFUWj"uOծG8<}Ѕ֧c߶>;9պڱ#`Pd \٭]jfݫW4^\ T=)q:)^ 7Ndy:Ϡ*E=Kg ⢪:o~S%aOAYœSd'i@\gDc91i%JbX`@5ChiugiiGꜣ*¢b6j"jK(ް񘐸8X;eU3 R .pK,1{١D/U` V%&G2A^BK?"C@rYdH9\~RUr5\\2ҩF|n5LQFVW85-5_׾ޫVL]"ª?e"Jy "jaSZZ+kReW+VPl{Af1X{ԣ>]յ7>.rZOEyաT'cDn^qBɰ>\5ڑwڼ@AW5MPVԪa_z]+џd7{a$*jiqЪI,Ӵ}:||mNSxY G1oT+Qc u#ZKl+s{9VG0nnVFqP*CRc0DD"N r1[P/;iZ͈p}SR)i KjP[q*a5j5o b<&Ϊ=bVEM+TnܪWeU2;TzwK}5^V[G֛WGl(r5mV}ְ* Urh~Q¿/V<GꤩU1$:jRVԪb5$$-koXSXm6?n5?[ʻw?7[WDVu{`ruWݴ%X="] V5cIjQ5DTRh(VIU&rv*jƛ<}9O/xGG[ ͬ^=A0'VWaYHSm}'UP*j*f'WՇ,Vǃ@5U{9ldj_1묶#V UTZvyTvVoJ-_p*ᥧgծ&,ҝh׹ 0a$ FOdJGG_Q# VJOUzjV%ӓb=2҆m\_Gߏ@Oj8ڗ|rAB(rVtrNquOô¼ΡLT8=vVЙU F[i䫨GÚ`bm:CZӖ4;.ZڴC%?Q R΢=< ..7hJr^\D賚}0VMeV*X2X̀PƊ!˚o9#ߤ2bs'mU7 jҋV:0 t3KJժYWڂ1 kvz$<lb ](,FAz|~f%Dm*pm?UMzUj{zJ~V3Rմk Ձ@[]Wh@z*5 Pjqr'EPЉFiJ^OZe79-P*|y#gm蝒e+V9F~`oC~\LȚ!WnJLfURchE*N&C+;,OL\e[AmkyI_gk!Al8)s~~{<#q+; 2u#U Q]EUwW)`+{q#`j.qUz5*ޥ*%J\ ~; I1X%9bUx UPzr*;j`UU0V')|6͍Vc52,GJv#V0FBgaE4IQo*-DnEUSem^@LWW #*;^WɭRϏ<pu&uf-^\RN2٠iElQP9:f}ϐ,ZM`Rxo<:XmU]τ7^lolHUD">H2jMZzGVTZTi:X{EUiU !s@jYQg\qp@X T-PzǼX$sUGZ}aCx/Qt2Z1^wX`uzӿ]g%%#ozݾm)_cLeʥ-U Þw jCw>*T`(֖hV+˷ϟUgJxsQcr3pNسzx ]GM`5մZE$oǷqN9)Aμx4 "q+~UN:`dlZ ` *`uV-1zJ֢*";auJ:`4=TU޸;tvp?CgPpX6D/O`U*~n\5[hwU. }ij9#D[խ:k/( UʔErxjƍd9< +|>\rUV%sU%ʂ &wmiT+u~ހIiCS}ooժW\FY1HwjM~U7 P@DV37Lb<8@x)|ƭouVUj hJg$VT Wb)io5߹~8{~M\˖nB$cH?98' k#8Ov*+83%ԙft}]~>ix:&#Y~vJcFu$ר+&<{ Jđ_FVw\UM\ kaxj Kҭ&iX &TMZEYK4b#SXcmUKV& *X E6ӀPժ`5բ۪US9E%|cTj}<\ٰV};c'[WƺȾ6VEB|굫đo J`q1|UGV)(Gm)r:UJd:j`VR>e]jMS`JUv"|a.W 70nÕ_sYĩj9>wKSnZgzE}oqNk[1ųkc_' qe)Oqhj1W:qF8T sL1FՀ69G7Χ3"Li4(Lתdi"؋XHր@Q a3 KYEBYwo2_b{Ϲϟ:-I{sqXM>\{xM:k{.G *Bg4%|9(^t)Uu0p~v2DqHV^]Xau5Vy w6 _,[ɥVcbh*=0/MByQxi&L>8JO SDt#Um`V]bue7bUz^JVutΪJչ;^e?ŪZʷaL}h|tdc F7`*ϡ4`*)Ʀݵ[ٰ6,a>vc8 VBGkRG/Y1+j>ݣ>qTrteA}"&mZ3#GaU9SYsz ?" _t^ZQDZ_Y>%ֆȴvD jTaк#VP[M晆SOq@cU3mk\v)Vuc =ƳVMʨZ(_s,o*3*eDM0hpUa/߆jU`^UywiU{K.?_=VeZb5V|@UU 47P!Tی:ôZ&ggfJ~jf*$x(UѸZHd\"OZ VA5V|#UG\%\S!`;էd!aQ'- 'LK/bkԨ:,\i-;n%Ϩ0uĪ.P4ϔAWoJ%5j$NVn*gUk$ rs o.m[UA(w`TVkR5+ LJd541]env] Z VU(uR-eErEUߤ\}3u+')\S?g4Sb)u9:ՅsԎIJqb·;jqi6西]nf˜a5DP1^UERKTu]Vu'W*sZuFҤM:bxyu>^JroN0zҳEּ*˘JO p/%U۳i5 g q1Y0LN*Nj(}a ̫C\Z5j|\z_yX98zW`0UsUz>V3Ket^Q6n ~r1^r]PP]խt!VŖMyXoPJaŵn7` Oɷ>-\S:&V` zEӆtfWR.L8e+\aau i!WVO7P0LlX,0ZjkUYW!Vq4"3:=?}>t-K>DX}s謾ՎRUeT{pVviXV>XG V0J8|ne #`* lZu-ejjXX+nk!V "R{VFN Q2wViJ_>)U+N V XVތտ l,cmk4V)=U]jqN\ U=VԖ)9|FU?ޔ\d%=yĥyhpsʬu~/R[r&9wDRU¬ QS5W5I8kVx-X]_ 3heeLtdZ5P9{ ZmBSjb祥70VmO5/'h#Wѥ]:jd2Ղܥx,Cv] StizӪz2Ӫ, X,ZS`9jz`0鿿ou {;6!U_xX0̲Uݫ6w*ӼAѴl?`VmDjA&1V]WciQs[UNXUi5ઍo7Su{w\n+W]nhXM걪|a`}M hO=Wmo ʲtU@G꣔Ţ \UUi^U)>'Q=-rV n]X65=5UVVATNz*{V$Vd*'J@p挏ou9VZU-W޾? 80f5%D:~ſA= GՑmէȊeUUūP"&淕յy5՟,&u\F?>*t#FK_xi;$|jʱG]EcJZ2hG'wy:SJJM_8 .w 7SKT;?==f2V >މfSigjA1D#B}|^Ѭ8%#8Eݝ]Cӌϗ\ڨ}CjW>~};LIUUb~7@TV1jP5pE4:RND~h룵jU5 {t 8RׇGi@ʷDbuM~{<^Q5;ȫ X%4'` M}g+u` Di<{]a/ٖUsńiJU/rep5;:971N~}\"]m|w vQju>)jRXB/|#8Ɖ&ū&!td7%]"V7*d.4E|VGUR5FnT6*QNXz/ Xr,9 >1~3#4 ^+ґ׫ͪ띎$q ƮE^ W "(ʧh~ͷ⋯HNe%TkBzqno=xo.zo'W0T6IVxXz(j磎dzUઓ RubuAU*ej]o,yeDSCQ|b%ح&vv:DoFG$UkNpTPJRe$|: ZGQ3WYBeNc5+^hjU>ilj7Z׫w VT/J>*Wrߔ]J۟DHBAHha ER4R@E'bRT"{fBGL!5GRa -9Y` X]bX}U`jEX-uf6 )eS1N:m>m@M`uXSSQo$l|ֶZŪjU:5%&+師+n @zrgy*WRsXu~gcGlmWfGO-WDjV>1%UHR+2@HVYi%V__[SNVN\] ofyj׌AYVѶjmGGj;5grxh8w>8@2pժJ 4֩~=MhX  0͠z FPCڑTHG ,-q<‡=uuʐ^(k}rC5Bd,:j/.U EBp2ʙhVԌK_*ZU`o 볲$n'>A.vD>3c*"zE Y^'G긏deddJ W.Y\J#E-qZ=2$j=]`F5*]ڧT41k2X]b*>TP,p5bp:Uw8[(PMê5j;fzJ_)RnVX-cu0V^D4Dz2J~)\QGU]+x8`j}HVuYZ @5 z@VrJ>)5ޙ# IDAT/3Dd Er3 VVn{hCܑgF U~~j#F(u2b06B0uj9h*sNXU =UWʅX05թ ͸~բtc ĭJ5N&Y!XRbu|mU ܀Օ1cyAd|5cUja`q:BYˢZTݖdkמV:Q:*9` 2Da\7!on/_<ȑaU"5|g275:Z)/v1e jKn $p5ąh=%@rjLoNݻ4kC:-PS:ST8[C: qu9k;%4yA" F!j¤r5\, WO*-lkE27{.v']Tt%@U V+iS-ReswIŘ$pX Z U'X+ǎ:Vq=+ /rPUWf=CWgkX!ֆf5 U)f~^8X́u\z%HqT 00xN*+jETՌ MIa5նUc~Tʾ^A،XM Tm8[*IqmΒaEE&pJpXgiɸz@kj Qʌz_Ve,WARru% Lݧ8ڃNy/>;?:Ҳ*OD*LM*W!7z)T]]e5*/^*ʂՏwիwWGVԜWc*lVՑ*ONjUէ2cV*Q^JM+rMUUdJ<; W՚a–ז SȸL<d7P)p{l>n}]?ANMZeƨ&@uqN cLC< = X=Vےw΂,8)U%/ڀlnXF Yِ,,/[H*ir7PtX)vc=J3TU)b~6jz=97#zv^-OjYWK`eGW})ԎaJz\UO-oUL|I"WV *b}밺*s.vz~uNc '} q nۦ P1ʺ8χ( j&*:8|ʩոY5V'c5WdV TQnS$fU~L<"p΄Ӱ:E5QTpuw1tmXUmMSxH>~Pkְ! h\|M$/2MW8ə>WN*V+VI$r=RvTӡDjjߩlV W ڦd.MUSˁn%щESOMUU>F*\4++Uטٕ#9#x]~5#vv}CỤ}ۖӌMDMj>^%ڧڛw -xh5#):wK 5dut. l2I˺Vkɘu}_NM(ΔT\]j-\sT1UϞ{Uyu ^ oXjŸlV!VSTGXX]#T7Œ?fg_<9;[O6LOO=tVq Ijગ\`r Y(^ִ4vk4^O]'ˤHMmhgm2bUG-{iՆk.aViliņh@ Yo^Z2`3 V݉ Aku2߲A2ٰJ`~PGJC.MK-j#W&#V`D:.\\ç9U @2\t(.1WTh}wE;ҫNwZZ;`ꆝՋ в\jUJkV_̩V"L2 DRd'A`w~զH>ܣlZ>aui%.1-uJ^6TVZ%WDZ*rUo5f[;7T>V39VS#cPE!BYdwbu]rU *A5ruFq*UrJaW *5ҬA䂯{!Ysfj;2,;6q5VrwOsUyZ1\ALWtVInÊ `:J&~Sa~8WQ})iHKc/ыz+鰺(R/X_DN5w]:J \Yk+咆V+U)])Knjf֡|jKb'S3}zNO8U!b~59bruO JކW~eҪk* 6Ϙ"SVAXuICk:ⵃS;FUY`G*E%2WJp%"nRa5f *V[KQ 3Z]%\eqJb?dmRPAVl:k  \hlGDUsZn-DŽ3@!o",eY,%<`U-Xdjġ&r%G&ՙb*u92V) zGݱRU pFh1TD*%m!Mbqa?a+]*]o [cX+RH4F*X%FQl$;jѦUXU>1j%[Ʊj5V3r)>woL:X%*V=LZ`[jtlo3WG*/{L̳ʅ%K3FP$$rRx̽V @f[JVEj *oV[YVk㧒k1Ѯ_^IjjiP:Ce ZXeOSENn\ȒQOQ5I"V@{c1W%e lyG[//Z=Sŵ*5#+ߌOױGK;3m+23bIk Wi#k#i8ĜS;$W*fH KXeX]Xeb:tXy:'b5*کS`o).6XHau>buqU`bu/]j>k:f5;i8٬ u ĪW,V3m*NmOgw+Hm{V!-lc%^NCyƎLǠ_7Pe$!Zx'UQGpGVLwZ樖P3LkeWrT=niwV]:?ROd^"0ˠPjzC=T.b5hI@Vo \UP3+Q3M:;=oKЫlxFUUxd"@]X},g^gqUMRe42vp M֖hLFF* *)k u׃ʶӂT R0A)^r}}>D0dXUpwRV#ae`r^ ZF-YrU +9ƶXOU0U35Uu9?nXMj9JVgSKEwJY}|]lM ^+:׊"U+$InM8e}@HP:Ћ dOժnrh5`ujSwo :@i`1)'Zeqռ9QXp^ntW "UtD:_7joDFV^@jSX2U,(WԫڿRDz:PG 2f2Kũ.nQ1yiNE3\-KPKeZN*&V1K%N?z^'YjMx/ j 33tFWab@!L..|BYՒ̢l>-gF@j!ďPhLES~T5줍^r-0~䌅x7m_%䦗qbQnh女U5RPO2hYي<#u: H>7Ԫ :+SNnۂVE]!oXu#bFX1@hCU{R"ͮ44huu8p9P]'P`%jUMrVupsmy Vg\V!V_GNL^UH-ê`:vj7]jrGgOzUxL/3nxcH7Dwx jUZQHG ?~cU'բ.YHtAq8Zzlr5Q ̋礝;UB5NlA&*W1 tK:foȎEFs|1+FRPW`9^e41afϰs#\ZבjmVIy{qqJ.8hC 2n$*!W/a}>5t~do#PEl%''2~N {懮?5~a*j[ Y{TĪHե9w/ TX;080Ī? :B P.G{㨗U~mXv^UZ䪀wbr`@_7Z=K*AT:A_+UϤ W4XoDrP9ժ޽p/k3b-yAU/Ym\A)#+ =w IUpU$:}]j RQ $رzFet=+@; ^.AU݉5K%RHT j\=,ukO”V秣D*d]Wo!tcUPI͔hq JV-v3%NahB9j0q*XD@:K68ne ŭ֪vcg#>=VV=W5ѿaLmKV=PU鿠u'قauRjX}7V@+e IDATjsyЩGC\6MP6(ք\Ũʖ+`75-I[@Tv{UXm|Lr|Uk afT>q"X̂~gCRV,;r_suӋ7B>VXռ7Z:rg#.`*@ә"e+̛C= Q!h$Wu^MRJrS,s Wryՠ2>0qa$R<UUEr"?PT[3B ԪQc\]zdb`Wﲌ셾f-aVo,7z Vhŋne"G)8sqjf*6h.k|Uw`"TXUsuO_yJ*Sy5BWTߦƑ&Yw VN?qr.e9:s/!Xջp3[٦Y)J}~4JX}L_ [}=7j!`5D9%!^zßTɎ~m"S- _ɪK Jv WC/6" k=Wѹ9*IZ JEz}q=6US0/Ԓ`]r \E8Gtk `XeR nQ7l'kȄqoYaps xAz(*jF h,ǥ~~wPu~\)WeNNSc'Ycvx `Bܫ`\dwtJ5ppxv XJ'@ =a[녓1Wk+P'ըf5"Y#_9$H2`C;68Ҷ# =Ef3WX:v2Os^v~^uW^նF{+V; WuhpʔØtXX Teزp`-UE>JVB+Vl+P )B V#)>W1u%"s52$BVmyswqUcbʍ,FҁOﶜhK`uQ*DJ1M~[}Njr)MU,.ѫlWdO" [c4HkQޤF,Sze=* Rix"HOʒWĝ6)CpU[Sףz<~QubUՅJ!U|u әkϏԜYUPsVR_bmjzBi YBjX j;Ւ=c4 \VUm)XMަ Vv8y}cTUݛ;־+tUj7+>|^"cLKVBg okܡւ HuiwXehdUGVz@vnխV_zEy)XIUUld hM ~W6zZMHr/Q܂Ǫabua,|>NY2 SVp鱊tYܔV^U|fۆl)@xFJ^wNijhlVߤԪ*7U\"=rU%)WWRWM.T:VVzLƪ5"DXbEFZ($Z0R)լm $UAXNMj^CjbVd RX<3x&V#oudV-iK WVoVVh;u۹oXiHS@nt[j3V}Wzx3O{{۹s,W5ߟPWljV [V/uJ*ߜ :` V~|J$ (Y{Hva9f76+"P!Y&%9RT;{a$+Z t6Xm">㵪hZ jfSzK0WUJO4갚_[Z(Ry3B?Q Hţ'sE y}uV е"6!BKCѷ>[ ӬBZE6ID歺Oo-}l .6|WRʍ*UsVW VSKb:}CnκVِ;X&;^C}׀@r%^4?%?{*jXx#%`JUj?֏ O ѣ--&@V Vougc㫣Gmm9+.Fck}WRl߫5.X06wm~ܫ?+o?oN9y9NY9z(ZZ'8} c5зjTeʘ? Kz8O{W&VHH{~Wb slt@R {3q(H@:2R *Vu`-"+Ľ;D%} VkQJ(Z13ԗjFujq+cH_Rݻ)}5 <ї[-%N^IJWeV*VYydyw|KH:^!uW*/VGu}>juu^@k=V+,Y9 gM4[LAіPRatKqpdU@jUK&V!B*W ֨quh+d(Z&U@}nZɒ'PmQ+hI SEd}|%&*{7R 6fM}<@j7Iةob*&Tj_f.O?Nnl<V_w9ݪzRYyPN~GjZ;; ~@ jhjVW3UU*{\WQv`$`Vf_:"W0.*{G@kP`#wD}33*q>~tdB#n>?$ L UP,L9︺&`%e)fZ=KkUUY,7-S9T3yxo r -+kU>D, lirZr;o\N՗Umɜ2b43"UëUOVUW8v~/mYg^8TK`0UײVb?.*-ڄڛlÔ# ŋ7K67&7kf<ϛ:}_kOy~C67X^2lXՀ/1.Xۉ2s߂*(O0 AUb1}\V9PjIK1v(sݞ#4o=׺z|~(?V[ӪmkO?oy9^{az}X-r%?IھVڷS)~N!<:/Ҫ'>[oFW_ժݱfܪr1^N ܘ1nX *2Bש`rjk1ـ\ղgh*9'+++,:<,I-]*NރUTv\4ttcV7QjNrźNV >=s"̞oab3ŋ0RrU/tShxztrYU RP;;l!UCBNrHŤcG>jj5ݡV-,GzjuT _P{պ(^[}*{p~0 V9#ȓjur:7w\_MдM+TXۇ_m&P:m}p`?CSJ:'[ :u_)5oV+哸:\nI~㥄W*b@!?]RwsӋ߯?|xj6 g@ҡ;>.sL43e;j&aޮ1V nϚ}T0ಚE\UchOjYNK3:b]ZPۮq|zJU-U^N>\X[kf ~T7Bk阄\SUך.njb& 6)`[VRBsLՇ.rZ j?CWOϟhAקlxy}tW|O{5UՑr3^I؆:*"bcSYfGf1DδhSUk {Z3Z=kD̙LX ֡>rmyOeKcŪ4/QPĪY1a)Z^Uct1*NyN|P'*KY[K$OE([ijqud}_U$;7ɪ\U(uޣP/'U`UisTȲ ,s*`uƊn'V/n4 VMXt@gJh+%W,?o9sV:O}Uv_Q\MoYvKw6[f_v<^GiUWAn TX r/Cڲ˚~[j6`~ꡢX]`Xv8_0ȷy=':w֢0.D{Vr++aECemz0oj `7:j6eUIWT*|9*04ݗ1xcʰ5h!W=Ц3UM!*~5w WR1-tM:7] Tvc zxDYV!țM:|p[:CU? (5TWǯ^7.^l:nJyuj7>vCQ@*%XME_-h'4juX;ٺQc5WBj+Wv? ^,BJkj*j=zT-\1x bU h9gVaQ K<#f/tl"xDBJӼ589*::RԺkEK.]UPNl_'V,Mh-aaP;8Uq~`Y*&X&*3'Qcj/UUGӃ3m$YcKy, r=Vɠ'Tw0MaBR;KXp`9 XkXU0?xwu'5eۇWh/kٵj jU|NU ~r+Vw=`U F4/S?_x;TLY6Ba{UT>w]VӝVa#6@u"Js*V7Xn9`VO_`uw? dPS01uVz)!P,wy 2ޝ֖!L 2b9 RŪuhmp2 кUjWN V]sCFg j5sWџongq_w7SGfEp;V-3V(+6{U:U *A5m5Q'b8SVU/:iQ#RVMJOHmOTatau:QEx!ذ*e4y₷rvd=> ӷBڠ"M(z~:-%U S>yd!v-@+W˻QmZ {b.ʅqt3_VVQxT|C"_^=VQ qju埚ΘLٚCfMdܹk;VNnTuEGlԨ[^FXXe];@ǫU]T&bUVrk.?s`>: *q<:ю-2A8s*2/8,$fն;rӼ@##ځX}}9j=0nq+bj6%Qi`4تPlB0 QjĪQDjεVPC!*6 bfe-׮@C‹u.2 99+`YW`_Zo#[P꤬ T;z/&>.ЈEBhrw{iwYf<@~°UXʹڌUJ¨Uju#Rzbet *V[MfT_٢9@Sg>ۺTjz-6gB766:gxgc i{_9أ4ͧy\`!y(˰z}tQlWQ~n⪔hvꍤZRFDzUg1Vo `χ~EuXrONbp|sjCw\o;jU ƏO|+Vݿ_+i[SөtVMc߃'9_b}0 a9֎N靼B5ƨ;wOǤU^U *y 1~66tjG L&~0ͩr^'; 9Us7Xի[ SIUTr(9UWP򶮠N.`5).*W"Pcp4;-<P[*VL(]]jեjoεZThaf`MgϮVT*,^"S %zi b5 \j Ve}  U_aUÅ$H[@P5wfUâmۦReX'; )bU-(ع?x<mdyESZvgyêb-bd@W>O\G:~* \ڭXMUQۗ.W*WloEveaൃ'W>3R* l P{ :B5ÎU*zoWx+yX]9%;+yT<VsjD|`F&#N::=Uњr!rG"fd;_BxdqSIiTLɓhI8)qar&F#^4ҐsZLQ:ɽw'*Zqwihh s]A e]5 xpb(mQxhQfr)V"^wb*jfoӝYHm:`ԭ /^erY)ѫ,{|s]yfnS)XFT.P'*Veܿ0ܗ*zV\˶ɍOsFX[ҲX}:U\A^կOOOۏjnzv4g\ů y^hYxXǔٲJjE?qxZ⪕()VTTf؝ϼL";9vQ,)P|Rq=T(ҫɸ|aOd[1؊j:PUŊYj.u7+n$(/vlx›4C%jɹ:a.m;|Er t-(ʪQJili+U8FX}A9yv⇀ʎ_U@*HКH[RLrG ` 5'6PĂ6lܫ] ѤHNdO `uܸ:Ɖuxy>7*sqcbDK: z7QjYʹߑXRDR 1vbj:pԨ*LbӨz[rżjh9ԸOrQ6 VkZ{E2a{U%Ū|WrG .Vr$"~3[w rs-dT*/rU83J]T UE{W+hmU}O'.)řYॠ`mFfkLZve)rXg@VujUsg0Vzc_1#+XJOUM)mǬ-^1g꩗(?V76Z}-jUBrI{Efz>"V$TmX'X=6s~SCUnHF3,GՒsZِwKzKMUŮýĠ*q.gVEwpbJ^GdejNJcZV3c1]l4jecUzI?2$[X-2[g׹2MZ,vxzIѨJXU>۳Dv[0*ZȪ\wZ%8!NlbKU8Wѳis<'KW3'>/b_:ԿjK$90AVFX/(V VxǪ՚յ!5aUJIUQYUχ /_uJ@n{+wWa5z?Vg~nS2 в{pj-O?J9J&:b5&1|fcT}: V-̜A3KfX d.^i!SQSF5j@]j}gݧ[  UfSWJu`Q jS%뷺=$:q)GUc'Rc9-W"}-t9(UHճ3ӪTZWVV.K^|RJz봻,VUڰZ$ *D5*K8_%53ZEߊJ+խVPzUOz>s}j᳾k3UV'H#J?T8 :4~.z)YMNml[tJy5&u`H Uq굃Yd,֪WEc#9c$uKqnׯ4 i+U,9 =vٰi7u_.[E֭H0MG'GbP&OzvAveX-zQZ' VtRqF구V%6jlZ#*`bJDTIZ_l^:꫕뤢VѶtBߵ+~` VǹK=a EJO ko)ţVgRS s'BʂQ.jPYMaU 5Y_yƪԛګp4OFp?GE<W?~u؟I?8<)XɨŻZV[/&kVo߾e%m"W Nhmw 䴁*,GwTϦOu*&͸stV QOX(GeͯU(=mj%12Vq=p "ՠhXMisU`+WTZmRS9UK/V!NpleZU i_jS_.*(V}u܆E_pH flJκUtݜ^OVWVWX\в¦ Uѫ!cNX`jp _F T=hTׯj`SzU*0U%ߎQXv"V1ȕU*ZgYjE^>Ɵ^0U?iG1UWV$Xs:՟ךVXz$˙MV[eJm+ߢCl񜔜nxV9}4TꖞUnm"v!(xDj Vs DXS{Jklov%WDֿI`U S]սսnRYKJ2@ U2*jUJT3|26 ǂ˂$( Vi@2Wپގ툪Zf:14R*YJ<c.Us{jFAmU_ߊVWmB`=^ v s/2̻Ґ؂VER >W[kbXTeA_̱L9Df:@u6jbI;Y:`UKS T/7K?CXۻ_lxzgWFk6yl!:c9jf'he%ϯRU,M8~=*xqN> J<_Jc$u3ȧd[:>i{j*]y eUFW 46n=*H7a]pzVWMmgбz--+ŽT5WV,(9G}n ^OK6:Ɗ $wK^Z/T!Nu(nYV0W/6ܪ6sJYFy`p:jӸzqjXETfSCS+ա+aw'kpf7gt\Mi~iu7v:mNƀfLl dLk1WRȕ-0 Y^ewQy86Kkd( p &3XMZgct s3Uv)Xשrєm+L9jTX[FՌը Q?(Z5BUVaf9byzvvƑLJʲT$UmQՃɁU0AM`t4 #6b\.\dBZX  Z1+?0Wx${'T}j|ٷ`˰zcuR_,^ ׃U+.nR[%>?}FU*3# \%#wH:r(oԄV+!Xu'tYX}.E7$W)HCU@\;[=ޢd-QBkǧHqvJ|'}W}/VGX!O1%&|i4Z'p5`o`;b#.`4'Su9 nfHՍ]rNjaZՆSU)t; 1zT=sWErOԋt Xmԋ/)*nV 0J P+q$ZF8+"]BVC9W}mnuXjI^ jcUư.*ȹRj5&/kbzzXCX++D.d_j۰^f@&_ IDAT1E5fIWe9's)s85Jf#@-8Rfpud=~a{@ujm56ˤݤ+N2Q5ŝ=w4[Ck(3 }ƶr9 N/\B1Zڪo<-jĪU2Z{a]"iR\YVQ>Z U)h@q:t"VecfGb2 U*UXnҡ\]i.mJZ脍 T`Z |@Ua3"Г9#u?1&EY9_\8g3}&7a[y^V{d9%P9ɷ%Ezu]Ise ^# |35O&66̓RjubjH0:XM_Ub_htD;^ VW݃hj̫+(Me˟MJU+UgBa**>*J(reր"T+DMͺgoZRPj9RbYMٹ>U1Tu)0X P.m)bUil_[ _ևSXU*'\V#VV`WQ)jsw"UnĪV45KL\$}}9N֪ivNˍikJ5)a3,O5]q*Z4w*K5rƾxmX^-G!,o GדĆWɚΫU'cjUVՄί*kV+XmAaG^Ui8 Ԫ#|m*PUe`A jϰZ泤;möۄ .j.W=,qI^ LWaϪ{[Vq(nt`U2PqVORX%~&}QVz1zEQVn7zzxzrYU덧bơU0z٪Wu=}@U:"m]gYI=!wc7"buDW Wb*7V W]XozCa# ֝;h=pnD_|>+e2)[z+her]|;..κu?|~ ?w#W>,<]YwTqj5XU~sZéov#Ȕ!H<9ustՀjՌ `U[_77QCY}^Gf/ o[ U2jWc.&"cC2VuT^'ZNyJvT #=Z]l-412=ZMauӰ*!oQ8 V U*yp֯  sUXQQ*KSիU lRɔf~mWP?Vk5@B?FV?P[ZM,]Vv VS\!X>b̀S螮,5TGN U@#eX%=Qi<Ϥ<X\z8(Ɗ՜!`J5?pe52G*{JmuFʓ#`Z6/l4-r^Y|QIjb|PKX]V`aO*bqN\J';lW[=1gȖ;lI>l"3UDQ2r՗VQAV{*N`q@%cVh+r"k@UZ+Sq:,M*P$ XuDF*eҖiwh:UD=j=tpfiI3,=*իRk[vU$uJ>䪦mȲ%X];2Uo_!z *5lLۮrs>K3V=UYUcTTx V`$:eRy*D'Q]zX7a;b&U$Zi”vXn0.r6i$o{-H1|>X~2Ԟɍ"Qj@UUIoT&Djxc,U[v(i a%)0> WZS^Upu'o`Tņ`͐Hڪa{Qe5VXbI+buU[ʤ{clTrXzVO"m?N] MYYqLZ.%:;;5;\|Z:U&W}^9]ӀJ)ԨSjF=5_g9 w WSV*[S,Vb+UlZuTm=VX~\1W=U jZJ.7a#%jɮtOk+g4L Qa@u_,[xc%1}aBt}R#\Dzm\ee%kdX"_JW~.\2'ELʢdP`zؖU I5@eL-8$r嵮>ZHCŋXr&e*/xsAbԪџTUFn>_YY~ ߛ~޺urODZ[_},s\-;Q?$:YR* opـ9X:*fTp*=^, U׼,\ 0$bTejs!JêUgU5\7PJ(RTO*x}(V[i+Db5Y-GUfӰ7g $*>ODV˥;'JŶH{ft +WK99W\n[Ly5PXIhOV$TGЀ+dLj9V_[B_Ʈ[/jP.C?:63I5d㦡zV[Վ[喚|2l}eT+QtҌh s@V2z`M !TƈU"*soBUXzyM!+9a* nWHz\E %p_Z};2=[i+9"EtJU^hj= iruuU%o3GpS&L%j`5=8X][c]J\* B`r WĢTCQ5.xX P>U)LZU -V)"V Q3uj/M ٺlT :0Uvz;Yx"Xe*@"X% VT Yxѕy2V STN:"C]8>kz k{"Zz38\ 3k UUPj'Hy,FT}Z?"TiY] > ^PJHE޴xuSϰ[2V rUqZTRČUUW;pZi)X:sWjbg VE4 Vo8-+&?aVfd z VzjUxjpbŪ᪏U6R*UjDVZrUjjgɤZ^EvuAғj~Dp3;~<hQNC<)])cB1zZUu^r1V gW@M$Z9?>pw{)#B]%ajk>jV63ǍF㒠=6B;Tan=p{;l7MhaA;::0ޚK쾷fk}`Dvsgk֎= ~J} RCT"H} \2b0\jnYD=Ǟ.J'7bڢR?u[/PԐ +E𜏲iI9XeJ~~a| VUf)/5jj6\Y`Z?BS`:L8˪8/X][]hJKV: W D(gĕ/>ꛋêG9TY %;XuKWW>! d+(S5(Z VH՚*VP^)䏿LUQԧ)UC\ YؑTVq4@EE|<^**$]^AL5^ty\@9"VwψUU fϐO X=;{c\uh=:>7aJXEOIաT% UiELI+XqTB,RUCud`5⭓\8!UY`:Viu_o.N =:$@ ;bU^P$$P)gOESkʚ\b6J#*@iyk, 0XDÑ`w(MHs*`*Tu+VTvx]jEcu XD\GFTخ(VI7C\:3j\uj-{!-XLMA?II|pr5G+rţ  TkZ.<@`Vg4 YRZB_:)ÁBɷ!WX4;S{>!IFH2|AX%T=T52oQtC6ZuNI&aDQ:&0Hj#ojUtvpQuPcTcl T7\5L-TPOڊx~V}ȗ]L~FdrYYH[ӯr]+)/V*U KQ?GJY<|U+V2VW$ׁQCen\&`?=6`50USU PuRJ9@50U33\0XBQB}$ JURY;{ſΒg=|\AwÙymMEz\pŅǎZյʹP"@Y;63 WɄżKyW;Z nTBd&̊Bj2KL G dHǠvˮ=zm߿}V[RK>yV'0~T"T#TTE}A_?*P9ԪTAI~.Xنj5 G៓(S%cRJ(PVFz]t(F+T*Ǫ4]^X X][|omqq5*-Ӻv$fEKԪ*PuSe)ܡECUVw3Ϊ&|xrIᑑ*VS Vj3NyẘAm׫T򰺼|j>B`Bcjh15QDIvTLL wVE]s[X-#7螩 \9Ї>{rW"VE^Q{(~xC1G: >K$HxLU}X>a*ҢX4UX]gb.d"ֈgXjTXU [jPU*y UYD|(U2^uU :LscT:2ꋰjcqw$NzTzё ֹ92Xj:Uջp4_)VQK|I f!B'տ R"Zr!'N?t<5a5AZ5&a5"6K`u||tX5(uNW:TV\d/wծ52j5X<ҕiҘX˜VZmހ5߾IuOzu[.UG~Wn\[g у|bRFLGlr oh[RnOJh rU}Cy62U mX5xLjQsڭ&=V;QVY}Ԫ F'ſM2D)\Rfg@bUT%}kvS3AuWj<$V'\mUp cjoXy+7y`ꯝU#KVP :]n?rM˼s xT-M`H:thV%Iz[ JQU\e#+`|^v*pF~G* Sׂɢ>UGÜiz-À@\zuWR`Q[O `m2ݕ~3 OlmQ;"Sn"\"Qx&IQqTrz\T#-$TJjV*b*j5ds΄w$\Vo*>X)5w,h*rOV$DZ, ld&kީ`N ~nV`z-zz< *nlU G)$kf*u *M˗XW_uuU2} MZx33@X 4k3/Rquy9| V3qY'T=eb.VGGSmԩ VMyJkjI"lǑlk:ҙr4! `+կwF:v/[:bupեiHIQ OshB=T]NlpntwBϽ$U xQ9/&ޘ0+cR Gl6MJr_KL/Ux̨դZMhU%<jtiM ي.}K Jրf%xo]ŷƪVo< Vy+XRrUjHUfQpOBb=YdLj=*VYA!VOF X:lԚUkH&|i;VK}}xL~ɏooG|iؾ/X=1\޾ O`QC惹>U8jh)jRUHVaUwk/_Zá'K<9n(Dc7JJ`;ZVHoLDžhi;hW3`Ȣa]ziC s!e1d eAe'%,8tt%w)wY)VĪa9sUcU8Xb媃U>*IBVRZzŸb0z6yP(Z,m5\5%"QӰZĪJ}ى۫Ib/??JR[W]U-*Z/m{MU=u`Czз> U^o?]nK{齞wPD:]iV}rҝO?5q"aaemv>w3X}c}ab_ b5Y@DTX݄: Avf+ªX8R,4k0M V0$Y#ꊲSWp➨UI:5T4Ƹt5&25k4@ z`eM`d9n jM=@TU@՞=8o7|UMV>-?V镱\X *X"ة)#Q2XEibWJ{ \~Q(՛]wW[/XUո -oظxj*cj%FbKaUV۰VV!fa9/NWA#\ he#VB!Vm"V!Zȓ9y4FZJj(SpZ`5FGU+몳3V3UG VCV UMPK]F+dݸQ:U]uL[Ʀ9{JINjWIT.Uż*j.l2j2 buڒXEd\*UL"&hTUX{VVa_o@ظ'k"*bQ(XX!}FmbuO vv;`CX_}Q꾮oj T=/LxVwصU ePphY{wEXQOKKw+vVG^5^;;slp/+,c8+Xj׫ j?z|da%ʢ \>kZb5FT5j#*"-SUqu@NcYL TXM*. ? 4x$J]Q*bO2jƎܑrAhkmjjՈSruUS9j^Z) L6@zd^ǨhKT ][T@*U"WL`-@=XU _]o{Fw;\ 4WE42YIݧR~RJ|zjĔ1S]mIz7cn屪W(VU? Tz SA?L(VAeaRXH֞? Qam)!RK(S[)]:2u6LzYbyV?XXHjUYE*&~<ZU l闫!T"fG U)H]bS\x4e WGNV"*VJvzMr5Gc9lՓ+GWF(d)̶PI/beo.|̇UrV\gg/T7CER7|RݪaQ +F9Xk\U𜋪3Um"Ua5CruȎU Ru~V#W?8:-zwï+1>XHϯ4bO0\HRv3W E*W-j *)RQQK É3ÍU y5q V WUkV)z5fPc;f*PS XhGC h׿ ,UF=[K*byVx`^?yXUL-E15]z _۰atP 尊jUa5?YB 43 %9~p|VCeCeڢST*Q* ʹUZZg^fy>V=A^W0 vp( 30 68Rj f$X3IX@V!TZƥ+wD^y#%7î,*N,VOPΣe K,1VEp N@|%[4ZI?+atcQ:Gu ٹSQ*QUsWdGZ[V-g ϕ{"SG~FV1PETUeVq"z ZW7PvΫE8c5h b5_Ԁ[ƪ]Zr%{ iN l  j^Dž[SD^xZkcu; v8c=0; `Dَ˪x!ݍ4ݲ)>:Es~cvLV7V3Vg7ظՅoRvT_k{*K m<, pUv*lG޵P,AUӵږ аU0e*4<l%Q\f(t\#Wp_iWZ9-+֡\PR0zTj/$W?}faE`jFW'jEU2b)׉E+ $(5P-`bª8U"4k2Ye5K'V3h8Ie`+TF }sp迴h`NH`xdqk..?$h,`J[^6 3'P]u \3ǎRw]3ZuQ<=gx4Poj͕k"q6%(j 4ddjE{T0ijZUUW0m4j5UrպƠn~2V5 cc 8 kj#Hgh6Hf%Sʨ͍~ 6bmU-?E`ܤʳrv5,_S-+1[@ EM#]/ BY{g^C?Z< ,b,Lz՗Ŋj5>?Đ Vnrlzx-UVxVzNUkZFXr\JX:kU*/UUb*&cUb5-(M.U/GHEUHUۊU=|J19VpD vu+?=󫫿Yo{P5+՚䍞XmjW'޻V^ah;`Zj*Sthb U{Z}* W䪢+ ʪ>U#>W ʒAZ-R\Y^r'cu x 1)NuXJ=kAҬɭ WߑOI2WO٫}٪¢5We9_xuMO!j[ZJ*/~I \hzYM]HSn)g¸-Z{`e~nw{oڜ!j}Ub^|ӾZªJ+U5ꤴQ ІjdIXVYSPHԖVV)tjO YmX]m8W5$Wa5+V +]].`P;VS>< jz `(GX ղ?`n<þCВUԗU@媨kBz5`F .M &݀&jEP#p ^Q/q_m$ Bsatpx/&dhuSW Vªf3|}jǎ=Uhހ$XkkX5T͚`e\5+U73j؝)5!HHBiў%$]BK%O`q|6 Ws%`wUȾ> \Yzw.'ƛ/ 9 ɰZZ|5᤹Ԫj8V+I@)ma԰0Πwn;{LFztbZ*iV@zYU4\5ʢc ڀEx`5`y 3_jMZy!/yybW'$;O]+^]*WPjT}\݀iKX _Xӊ=&U [ R35T=xם W3U8TZBb}Fu8\E\㩡V=gڭQFfG"FTQ3$_ꔿJF&PGXjjdŹ%aϸ,0`gmUŭ/; y J W&sNΫ+_jO&|c7k ;~HK<ʭ<4qN'X:? VӬWZ'orPnsoIU_UnOIo5@pb:Uq R.uɊմꋠ(i3^b'f@\Wdž [vuIQ Xc:T5zTqu `ϕ}|Mgwnb~+:k?5Ob/چO]ߺIj{m\:Barr˕pU$X׵ UaYtC! աރtq.6&(h%@ RǶ-R}X}G&>c Tz Wn=\*cªQbe#@`E}U YquQLQl` IDAT:`ujikZE:xvz fQ:2Lg6'_}˽E~SŰxfUUy:kDHj(JL't)aLkX&UQosuҶ>k.Ҍ*-pUJJ!{<[XEڄ]VvIVٹfhujNPe>RW"R-Sۚ;y'f{jɢT۷ N7~*k)+냥5 -cpQk%}%5/kG{\j)qUY|~g} Vͷ\ʥejdj+5U`7Nil66 0774T8njQT`ֳdױ):q`Us^!.jգ2Z Fp=dەZ]&u]v2Zfmf:-W; y\!hQl8˟XVPxU}:F!TABXOoax=o}&:곀V̡oH᭑ <}~UUGW*L_ {XE) V^E9+pϪY;ɵ7Z*CW|' ,VKQuUD.@岳 6Fc]}'\}z;'C"*`7 /O!Vr_eө+>TGj0ՄjRb,]l5T٢g UWjT}5]r~c-UbTUOzP%a5p^#f*RUZV%qՒj>jcVVYt;nk`sT£*ɊNu&E#bv;vadoE@|z)vetT;:5p͊ac>VOCW{j܌p:uh ,pX2W;m&N'U6]ez*|iW S]dĕq45ZUf: 2]$+X%즐 4 s H`QF(nzQ"lJ?E!W|3љ3}B&_X-`;@ά:&NcդL-W@*WBWU%ԪSaZK.mͰZLIE}VyV{Xu U\lR\{U*]"t;FxȐzB՛TEjudZ/ϒ`]st%?A9R\m<(CtՅiO14u79e)PqY=V\{U*?YZ%b2f[qhD^>|jW>xut ~ԫ(WU:>z*wbM]i)>S6|N_X.!u;0_PbF ̥;s;頻ʲvSYb3@l~6cjZ j)-X4+a2 )V?ML{B .jXUW!u긠ӨU_xJUjKZs _LU_3JsVdBةXUj[;<բ[[Yq[T\XهWms]癫Ӹ`6OO3?'%P;"@(X?foNFU*?<w`TWl' Z4pũU;M6j5~ͅy2øQ |?p?B֭;:᱊r^ Vtg~x~TSՔ6Xc醫IfjNN!իgZVJ Hnu6mqYm N?>@ ^=,pS-rwE9H4 >2uzA_mլ#,XZ*bImV^ 4|Ta()yާqcU*qCS-=G%:0 Ke#B^!t@VQ@q([G~c ^ZlW;XKV  PbRI_ebhe5 ` Z]2ZlX-NKE/XM˨X-VeZ")Xb;V\z]{j_EXE`j- 3Baڛv2J.@m Bk`JJF1[MVUMCCV.feP`VVyx3NhFR"U#*V9S \p`*ZYV_k}f|MڲVD\*vnٛJ篨_W%jbtB:VSS<ߐ0P*Pѱ Tj!֩L9**Z+~mUzqL>0jRJ?vs+jV ieb% sp^U 8O`޽vXy^qU2U*OXZjyNUU4l?7X}VO>}gV5ZNk*TAU<@E pkcVGRIc]TSWY;FJ=nkڬU^K!~GՒl#;aj!]G|pG>~HwOm(!zZw/P9] oub e*9S jUز2VRPAɪ2yMV=,Uy׫JU+UY݁= q17l*ٹUM"Ջ50*^9MΝb L)^` ޺;(B5G pi 鋌U(\êZ]uz *Hj8^hX奫 mm ft Tl||Bt͍5ap^s6j6SvjUVVTZw~ HSV=eX, g}B`Dkt-hJƬqubbD_xoxL}+jU26.qIj,V*ٶ+63WmRU+WqA@`4`eyfeۙXmgE:ᴻ%?{wkFU_^qoCn/zAe4 rԈ߾_z-OWWJ _=jn1rJe+zƩ,mMl-Reaɯ8 -mPfQ(T}>zMȜNRrz9ATc/VXu8iHkR9 3K::.,]@iR VIU*-Jc }Nh I[UWQ?evݐhP;2Ve GQjk Z]9 *{Vcc0vvMytvxrhsQGNXd"6P'"Qp(,[a(Jk vS,J0wR {sx8{yIIONe2[bvMSs4h"KYXqW'%WWO\Hΰϱq+N#YlޟPcro8u@as*n*G0VUw^EƲMU)e)ǜ)*03hIb*2xPVl Yڀ9{q~2*Lȴﭵwk#BBEb}QfNx'=vBMה~-a X%v`Ufet+d2toYjUBANW jXWm8uU, Q$LQ|HcuhIjamUkM}]X k G4T97IBugᇨ!c2ZbUX;[.σGb"jHr \wXPb!^.<ᖲz lJPZцN)]\B4*Qbdn殲Z}?^}Z_LT8jUn+UWƲ*ԟgYQ*;R-0Xg%U\{y5gd.`]0I(/\EzP&ז^hP6#p$,5RM0VA:x;w18};VRE|6:|\1t.n&2q!^ukOUxNyYg!QJF6ds<He㫤qNӤk֗-D(U=qHƉ>%iʯU;/fXUBJ@/z|BJͪ l8`Sj'XF}MTT#W V6 ג׮75e kR ejO_zSbxj:*D{5VCΞh~İURSu([@%tlܒ2RKʼ~T4{xA>X!Vp]"S[Z8W+?&+Qira(`fAV> R2kfRf\*jT΋ZFAW4An %47Z!VA˨TڛݝMplgvh2a<Ȁ_LWh} OGHR/;}/$=X]gp3{b-hytg#lqWxik=jk-]l/zKi`kMË-j3\M#ԽhUj20FX;lj:<M.SŰnSJ-C<_1JTMcRWS 7nQT8h"Wh *R6VOp J& A@*Rz9V U?կllAUXXSRj?|TTMcVmU߮jfX5. 6P(mU)Vg}@{"rQKjf d|bBeK\VWT5{*o.@ݏ[lǗP{Tedz_y/Y(#b<DH/g'1.ߡتVK_XV ›ݕjڅjuVTĪ JJlF[7o%q jjfT -.Fx*k X~:8}U²W\VI(WVqo0j5uQS7ar TS`MW%b jV6M\FZ§u VZXjPr9WGB ٩/0TH,BU%z$W%U^pghVRvF-X;Es1W`09:Fkh"bquu˪cɪmOa@ȪTWP}AZuf.8%8Y^e2,dx{wЙϟo}ԐEZ݄|wYr,;\c*ÛM֫FVOW4>uh㌈5";x}=%VaQZXM ]k]Z/*eUn4z"TV dVVI"XAreGo[շUDWH$z@̅F}H:}9 o{p_B ǁT(dC_7Pn*HX j _P~*fC,z?h1V/TNa&tI lTuj+P#HY#*aJ\)+AxjpѱzX  :\vVV`l  8ZݓE SJO:*Pգ[J\/ Xa* ^j=SJ?.V˝qgc|VQAQ:ju*Fs @5ah0Utd:uwlJŏٮ u_Įjgʆ\ACG.rZ׉bZ.Z 2X,OL-Uj@2[$ Z ojxUXxR#no[ vE5R]UX } ձnl~'Elklr u^S_ƯIl_wyjO`֋?VX5ә*!I^,7r*D +)"ܳW:vwz{Ҕ / OdXpY{GR[U:cYUI2XEބ8GY?VϺT4T[ Q.M*f԰+նP%:TF *tK+D-qȵŪR\ W}bj[BڷB/HݟG=J8UNG&W Ołҗ1Xd+gu6K{}JUNZǮOROuPv2E1bPU"Y6|=yf +K[HDW*rq jq{_*jUիX2!"W\`ݯ8}/P&"h\_B '/5j%>OH;:4HFX{TX5IM`I #V_t֭dR"q s5/-*feAJQ[`e85׸.xuGpo _Nu{={Ҫ/hPCX:;rժU >:V"ipq:1\lXyjgR5LUӶc۪Z[ ]i;@W\:Dru_))`32T"xV-U@U2AG@nfh 5S(W/@ Xj 8X%b /[ZzLUDTk)%065&ռ;gH~YP:5J8#3x0膣'0XXY'jUͼ0Z-TŝRiZg\eZd P H_k &F4 NŘ;!L Fb݄/j:QδB1J)9CUtx>W]j U}r.zUvx5FA&/U.Yl񁖕UZ )̀n-|eU7/Xl|r0UmjIǁu70Vm_U`4j +'|X W3?j*jOW0فp!Vs##\"c9\kݣheZOOsB /LC*^'If痫{*d]CZgFQBU .V?^A5h,&/j Hk&*s`VQjRŹ~tv_*r2`:ZDKUtnQ?L P%$Xw߷- "V#z "jz2̷w[p9Pm0mލ5}rnzM?`5!kXu!Gjh[j^hŪeL\X}@ޏX`&Λ7j[ea@BOww7quBbW(X5V@U?|*+;.V^*WԦǵZ>!z!Ҷ39d׳~X%LЃ$&; lǾU+کxωP{+Vg\;M>XT\^ezX*UdJYgJ"X] "Տ?LU~YmdBUV-R*Gg:7?g:nAW@\-UW)jsqcSzuyeZ*U`JVR XHT#S#//hid7#f2“Fp<;KeݑіXM*ed8 678s ]x3D|A{ X'TyvvKU#T @f81.SW_樀/feRUG|rbu`7٪V79 V!?_.jZOVczTՠG* Փ_)Q] `*OZIz, SyR.+ϔUsU}XeMb4\EA*V) *50k)Ϙ@)feUW !V+aׇכU?(Coi\K,xXxpu)eM-mi>Uv nWAgۺ.٩CZƗ>NROqQR `w`Wٚ1%PZ%V;Gj2)DMa&1,_ujpƜ7|RUOM~}j4**-<4;½*Uyl8CUg6Vr7K)n v1VeAWaKzB:Y Vv,Oj iR[c>A+>ĺ+ϜX=3Vzbu*SL8pL$I*{ ˑ[n&ʏvv/miGEVhMflO%׊+k-u`-mz"t {x O4QRa--R=(%gg=ZaNzy7xW5Z+?+JތR{9VypoI#%VۀUʏaRJ!^:j&%+ZtؤUݽuU vntiVҮ+kfzRXQd`-T[tWtEojZ}hR]az B |Sr )^UְeN#?=(WU* F_o-{UShGW-aAIĪ9ӵc?xS0oe% J6HVQ~9pz˖/~_L;a5KyP6+njJ5S**L9,UڪB ۩[$of5#T{&'B `jb__a9@Uyl@FV6V ŵ7e?^ncQbgY/D˚nqc(6 h6WU]e J/}Qc |LdeVu%:Ƣ:xU^a *yA No;Ԭ29OiXE/&E3>TA% T8,lω*U5Vnz|XHK|"-'j*S\`?A4<@wWctFj}~AW]iUjv|h*VnžQ͵rՖPuӪZҟH-Vj{ qVI,޻kng&<_D<ރ2LXLӰj ;@WX_7JBUSƥܷ*ZbA(wPmoloF Pf΅V``5Jzzz -*Tp1t8,Z5FP+KiCVc59J[RFU{ޙo㪚p*W*.PJgƃ#`<[T4MVV=XE )V pAj4XX]ߒTki Owƪ N*UUN*RuƨZ+ו5bu&V3^}ϴ-8@l te7}XS:+>ҫ ,=꨺4.\5V+z ˻x$WYbt eZpvJ%W:anV2Xy`>;<2EbGASEUUJ>'Tz#%-<5'Yb6jSUߗ+-bkX8 Xm>BA4bmy*^W?4URb*jbU1?j`*.,TMʮ]`J*hfWTՎFQ@󩊟"UՍ_&UQp |b:HB Xsj* UgE/Z[R"7_+aKr5}#V2X5bUFR'(UVܸXTm T P̀S{Ei,h__K@WV-WwwUJ@Su q{v6jL%Z-A*U@jqVXĀ́Z%\E O+k1Vh>%=ىU^PW5[-b*J5Xn5i/o)*TU\ߔb-ZN25\%JVMPͣOJr 8.ļ `uWW:rU_òEZMj#SoYXbXU+W@~ `:Xy@$`za5wUSt`!b Qc4 e;՛!jV#q?VX tWĪ0RD @52uoa@j !Ad3 BU^ b[VZT c[;(XL'u4:?aDEҫVL2z2X}r* P%p"dM-[SIz`N0[G'Ju}yJ0NgqXbS9,Pt'⠕E ,ʫ;h V{z|WUXr%'=1*QkWIzVዥULeg"7WT$`4J{ZL^ՠZ5vƁWqvcǪ9m/zd4i64mX gp;+ӓ `XW3h`Q@W[2X\FZ`2"S+Km @D+\fZh6}vTw1T! PtN*S0 2lU؊/f``>~T}\hoR^I|kV\潛oIuFqs n!z R4_׋wR`v8J\%f[V'pԄP;V0]:U;3Y#h\ F-X'FŨ/^0kJEbި^jŪʟ>3E,&U2dOK`+\UgaWCD*\ۢ%YC @b~>G/NqP叭Uɤ JXpFW UH3F\2`jv5إb 9Vz'k6qSXsP NN !U_/w'edK SBUo׊-\X>yAeUjZ$>5qO% * YYǬk>~\=(p `Erme~(nLeح9C$AJzUV] %v\xmkUzEuTxVF`^*8vU $4W'Zu!sJqKb%j(JWAjjT`߲\jT ̏/U}ES. V f+ܝR{uҖ"uWWUXFx Tւ|d`M;L_0SCܳZ\uuLU~ϞTxU[bX\PHUSUC U?DXE> +:H=C ҆-' X7ۀQvUrX&(m}c^+VQ2@z\=tx);UreFUmJ*&umT LU~uxW\*@Z5[\E<ȪRߌLRgA@kl?/$0&Nq M5"ICj[bXJaQWjdeM( s[crHgϏ瞇9|ߟ=(mWIr.&Hiڄ^N96SMrr5@Ah:PVȻs08`F#b,%%XXsHH^tWjUU*KIfNS2)Zj`ը-ִuU![!/3X'*-ϯds˳+K 8_ZFXcAUzPT%\ jhUsU+PNЪI=yNp rv6)pu[^MjX}[?@&a=}j~ ݕj[2W)$7V%Xѫ=}W7mL]??"TvO%laI V|zujtjhZ~`}K76)9hp j-9k o3_U3f(VP͵dSz-XqjX]cFkFwNX,UCJs*:JŪmq Q y`;&UtQpUVX:ʇVUtjxcU\oa@ژv>Ku Ls:/zv+lά +{~偈*תlPjZUS& X jLbUiRw=ݘX; \uj\;UXՊ꫆K9Z5^/M\t1OHƒYiOyģ ]WcVWmA4ZiY͎>lFVAUWV[ |KOoX+JSo>V&+Zep"rD$i ,&'_RU0U< puO 'M dEC|G\窹U l*0W*TK%a[*-ӊBy`|N"UU V/V@71Ch #wibhTrY2:XUm=b  #'*fkxMn5ٱ/;aq므jUZvP~\j7V%X=@=@X逕jkJ9r8XT-Sg@Nݬt݁oir('ikV5XX5-WSUE՘lLo7CC qfժ}>ܓr_[ݜݢ*KbUUv8ڒLChuRJ-FFl ol^ĪV\( YUb5eQ'lj3,j3`̪wMP-Nԫ^#To=@B&+ 5|n%^ӜGl XE xvW?FI{VRח0 H? 8kej3Yu0U%%Xh# 09AU|UV!Í_&L^O!SI_|}_}?-rp|N c3+ g Y{,Zf}VZ*|:5s*WUz  V+x_*X!V6{Q g2A*c*k<T~V+2dERkeO 0x$+Չs]<9jUCշvɝ_|I_߻rgV'Z+æȊ)fFv Zį|'Wf^`aUaWse)!z-{.xL*qj^t ;p @p՟8Jtuku}TIpp\W% V*LNa+v/l:=ջ* \ѣ}tpO7ŘJMGWVJkc Qq 0xePVG.bp _geáOZ䕸:Ɂ3WgFG <;'Mi*X5@uXVjdrP \-͠e7yU[Kg9UD_ZUD^`U-XVmW %*7Z] !+Z=)@Y"HT;gE\rUb>adJe*E&P5r5*=T]~w# ^9h5V`uav~>094ycG;ۮjHʹ6\|;?eQUO-#XU8#:Zh`ZrՑ*( @a Nl+j UOhG(JS1.UhaMU/ Tbg,6lj}:|Up5VO1B6+)WMV_=}pb6 ]/k /l}V{׋ռ~˿vCvH~:2WhV>7<Z-v1Qg/$ȶkA2SV7LnF]C &M!$R&4 B*4X-*Tft}v MzfvaA8` ZezՕ'u_}X]CÞ p-媇c,plw쏀 Փ\Z*+ XO3Ȩ,eC nhIԪ؈)to`zq'61:\,j3ڗؒƖjZTUnMpUa%5JyJ.{_3UZӠmrMd\ ~WXugyi~}}gZM\/O5?Agpy`#FGE, 8OY=S*.+d<U5|\Q5OiߨuOX}SDb*[9ƚ GJGT#k0b5T*T ʏXUx'zg|OOtgWn{j oZiX ձo=wQb!ǤlQ?S+(G0:ބUb*PcyuuXCx K@BVp >3Vp۔ZܪlԪyI@",aa\,+լoEB53 jPfg+ʧ_kqgT&Dzb+wI[ eJN7X=4[kjT C `ڎ~c՝}yycAYej,|'h\A!L.VU}2g\||8ھ8ZէKCw_ߞÞ0o23j]z}B5SOaO3jŽ}mck7+w*q 2R SDO@kj%z]2ZKj@ǩ܉vU5U( ݴ_j^xbz"#*PʃJZٯ=b_Tk(⌖ά>y|kvuf/Cʰz _ʔzsQ\ЬZJҔ>W-VGUo^[?"|N:p7"!qav2s :UԶXX|Ԫ.VVU\EFqbV\=ûV #Y nho\J@VF^jRZO:4j YkkDuW@.-A*&Q\i`4ra{ƶնQ-ZZQ .@N"ΐX5.VMO&<͙oW7G|S/W ^@j¸3(*a;<;3b聑^\4QI15oΠuV@3U sGf#?1XVc5`5XISo7bUWbZa~wSkU*PUYU0XMH$aZرJ,ZV߻5>Y]EΌQqJZjq74qT`ձܴXXbuÚaÚX%nƬ ׺UvO.xXT;ӴUMzd%]$Ur*V~dPVeTgTe7W1MJsSaG*n?ijk V(3r _,!/:eTJtcH>hoswzVT5bM~޿/٬J${Fm71:$B۪JY~& 0lyU^IUեbI&Hs`P,fȝGjɞdrf` |1Q;lLо\/6Mi w%C)Fv}U\DájiO kq6RTXy(ͳmBaj!UVsQ|IɗԲZR%/r8ܪT{*\Nmݩ ZK"7cuMLC?;ut@u b5\3-7VsձUÃ8 ߮[/5  :lܸf/,`Z.Qp'gԪ pKϝvCU6oW_TUe:CX-C2W$k**aÕЪII$_y y?p_סXUޘC*GwSw nMoaE\UbUx\ [VRZ5y蔨hȽdX\k4WE.RD* kCP(XժYP{{.HBhGؓQAZG11XKf fuL,Zi"WSbuFm^rU(V 5ZF-}QQ/ƊX5 B;\bq FULsnu,*DEvTK9` A櫰N.V==z}]]ݗ8.յ`U1֡{a;G<`:쉸p=%uU(lY[Ցi*}б*@@ 7b' W;^`NS>U1p KPHD71VeW*T@EU<rLR![ZMUD+ X:CddVFNccc8U}}0+eB>;s#HX|j9/FX՚" @Uѣ}wv~L':;/aD D6q|`ۡJ `mD CZzJM3 a`_l2j0 0A. jzP5aթaubA媽\0XOJ+z;auA*<^#hPnfֿ7ILzLj< v%*WB ĪHNKUK X)+pEau#V7ܞ_v֐;1>48sy?K]],pA`\\X~8+-F*) MPZbɾXh mGꗦVTUbjq)?{uU*7V!TRXUUtWȚ' -%VZ{vռ$9Z99Zz۩bULqX5)JVUR~$kfTU^^+Vn1~K~]orQOeAd*Th%v5zǻ} UZ5Y1LvCu r|J`ÄUdEiv0OJ>ν';i.4ժn ]7&U)_Pe(j<Юk!Jvf[XMX%c*@] @dwc" ?/ZUjp4UW{8=e}T/N&t@qpʊ@dT4ՒD4D#W`,${ӦBYL?}<D)`b b_3/mN5P/ _y~5Z:pM(k@^q}o?ձOW]jҷ9U\ PW=)5TEb_0`WkV*+%+VQ|*V6p=hV OUTa&-  \9T17@i+2@nBV7V_7tdqܧ[`<$ wRZjb\l¬MXE[[j2tJJjP8XVy`LQC +g@C]]T!TT5U jϰ:Ţ9k";kقSPvשderU!+j ^@`quHRUdLU`?\]^]|YX\Z\%g={4?*Mﲪ`չ*[*VKG23USV1 &B*\˫fתY VV'TngdZ4z ۪š+\9Uq \{,P gh Z b*N0997ˠ1%@;3SX&dIvB%"79Q XG€.H{bJR5|cFbz2M_h,ra'`u@|zV|*qu@/Q>섾ug NKr$<'QUzHTwUޮ(A'k\4R&p:뜎:ԙeHXjU"Jr㾩[ɡQ?.Sj*WahUe(V i^œ ]T`R)2x*ޖ_W9VUUre,GG}K/ 0bskks;P:㳗i^V&{rRSY@J2T-VVŔ'jmZePVܯ FrOҔ}0Z."1Dz5y1x^6S!UX U͝0ʰJLToqzOB1rNNVS+S$ .aɂ<:Vl/G \& D,'fgW5-2ӫGyP^fj9suŀ6VQ)ӫf}V/R׮/(CV̙Ud(M.餫UOTZgɪ3P^* G@6eAjy]"ԄUJu\TV?X` W]ZTTn8%+܁b LfזXb~O5ÇBF嫧CLbnq&QYS*e޽\\m.EX`lϞjV~iVgUZ\ +XZEsvj;8vdi V&b3c|NVB񟡴~=q 7*?xTIi"%X%'NOGS?&o߄ʦ hRIX3~!X151?0={[r8*S)Wh8}JC!%{WVQ[U&OL=^!W͙AT=ԓb, jX5UUKژ<dǏf~U.[X+$VE 4EV:UVztPeX_hTN^L9VɸIz˦(V>b K"q}T~ V7ҷW/^*ˏzIt4aunUUEy)U!Uww@mX`*}^P?{c5 u<=gBphz3vo.6iTf ]LSiCID *bl/Fvt34ѬxW AM Ԧ%V:(X8T.^4x2?w߻W{vhdX%{Fu=~<8g҈ [u]5[Tl͐X(NBCLvFl1ծvDPMTg#~aJ`6 )S n(G~ AUdT5䉪bnW5`u+[TE)ܺS[xA6֪*wN @+PUbcտq.UrU )P%ZE@ktoH(iKӯcOӷjj?ŭ#J::Vs}IaQ@簚ZUlL`Vw=l"mÙUY+ -V{AiCDrBX@Wp9*JӋ1f!|TfO\QTBŠb>'vmcv1P_5alg<ŶlYR *V)0WqBw|_{LJpT+FЛ?%tl)izY>:: u~z~~v: G O |}GV/ۏ-W%n]QP=demy3ewvETM1\5c(d϶/(@fX]+*sUjUmU:i`+CW)HĚXN[,[5PZLGUUN^mUIU⪡WZ|Xuokcׄ`5oPRe'\hl*c8*Z}(QTޤ둪G Wfㅰ*aj]&m9-U77u+ DU>\JV+~bTN=FzRzQoXAVLSjЫlzZ9*zSSUhJ0600aa}p64T|W+ɰ&b`Uc\EN~WW'vvvoj $k(utT>WP WKV:EGYVu0UF hwXUɾI`ORzY+L,Umnb04#[*q,X*V 6P1;sx6Li)AU*bU@UTTjX wf,:U9XT˱*rQǎE`? ɃUu.+9mL_$cסݖ6žCVhe(6m·2Av-fS2)/`] qai-5SQU,pb4>bh_X5J 6WLT}%2z7VZ Rf*ߋbìh9eP mွr TL&JU*nG R<#   UVeP7CXz?|_JPl) z%QSz>kI, uuufY8&'Sr`':OoΓ1qZU9Ȑ*aiqirY#[UHFTvߓۆ*_w>~TAE¾|b|XPN*TL`){),uK6*P|SV#ee}S:TE `aJvqBBJRpU\b9.x@]ToړG.VSI\JVґ#RPvV?4Y?...$7\H-.JxUX`}H\ DWE8]_︐jsfR̜M0^ ?ga-b- -tԪ+҆C]j8wTRVvn]^Ғ^éu9֞▢ñTov98:Z]w5h dX+ 8rc(VYʝr_fT ߋWCrWeQ(@uH̶"}XfVq.Oq V/FRT'*|0<0V7xD, ;DzCRᲛ @ήLC##qZ+n4{_< )K(iuOzbq`%pbZŪC̑SO" dvs:sVXHUԪB<*q\UQ )+@4=eaX{=V sVZΙr# VlXUճpXeJ@ޣU6V7 :JSFX&q!YnvoۮXhkF[ܙ";b1K-r: $T[qit Q Ԓc vWll*fСZTTIjIWvriXJ.h3p{]K£hÍD/QTV伊PncԮ ZF_x٪h^N>JV aVz/F"ײ̥+jT?6(]+'wHWXjxd (j_p_%cJǙv cG|DѸbB.^=Lc1T&?S'f^:=b ruijh9\E5j?_}.]NVD֊ r6}{ǟ,2<3E:~pM]WUyHUrΥt((*# |t5;\TtQ#]WO4b5#jcww ЫӌUOAjU8|I?j`* UWYv¥:31 \][Hq݌UՒZeUX-S7k6Zk(N[Gm7@] Y eRT.5dp Z@]KDuڞndxZRRDX- &˅l녻z!dYQ|vHFuVl( 5Xwv1MfiOifeDt̪K,HX'z1 ¬1&D!cBF'  @4PKPvٴpAzad8qܽBKR9ڮ1WYZq튖Xq~y+UkĪ5$YYk@|V"=VeU4Ve7o)0pU]d8b{GM-~RxrNU&S7HTnaqa77 'R>VW'aOo0u:>_pjc1>4>eۧ z\eb- wC N[-Sx<_\2!VAc#\zUKԪ̤jz177_QTFJߧ$`ݪ6SX=ӃU dK//QuT}V^zEU11VYfa jPm S*VP{?tj^'`^~}~ մ8V AKjjUbU*HmHZdxhnoiW/0V,Y$*@3gUTl^l~`>6(a)DսeZUz7qhn2sdXU*55`U j5SRvPv*` {|3O^Z`+L_󯍰V9]RhttutTtՋU菡?.ZpJw>GEP]]kX (~T5mj^Ո+RFrrIտ} uJx`=#J'&ĨSSD5S;Y@!jOauoyh=7~hw[W\{Alܺ}hъS UtNFHW"q9l6uteeQkWbRr(J VX,dyJʽ;~לՊ)Vw&bugވ`5Op]mw qb VOKUTKrs'Oru/Lq*VM #8@\ªҌ\ `V Vɀ1ټ0ZU+ZmXU6W:I=NҫN\E"pJÉ)T `gKKg a츌I4UPձ9OȵX9yWl))Z {`7 OڅAfMdmՌ< #jp* c$Wucm#VJTm W9JR6bJ*!sjGq*@F\=2o*w%Uh+ֈU%STLPW.ҜԾ>ifz^;e}t {)0GZZU)jru 5 N>/9*:IjY b$`\`jnpG(\Ҧ 7:;I\sLrZV9jn6jOΝ[aU=PVl4 '93S*Uslq0+ xה.+WDDeSJXj"Zf*CԫJʺ)*QI_NV+bM6y 炁;^ pʀlG¯jCXzahaROK%-'5ˬ7颮Y',3m0h՛zxjՐw6#83̙fh6ddv{";J#r5ْ ̩ZBCNg4tE3[t]~%ұ*j*BU0@f(]i%YtV _N(Ǖ*+U Vg(iָ =w}%L UZAJ>]q518xДfSeEբꂜqbiXu/lCv+1䙋19Wq4X3_dV#C::f;zϷ'gF;b٘kcVau\=U-ըZmj%bbl`w%VUʙB *bw jU2aAzik8XPU{ gJE XeM_p6別U1%@{zAKGVKԹZX5X Vl*'UZuY߻Ö+%Pk8 *f(֫Tq%ék5W4 ŽVpS L%rr+]7nx(3~*OMNUn9Smu|& Q53\OD"Tn`hkc7 Ţ$:&h7B+f\ny$uԩUz/+R+ߧtމ+_bpd)Qɘ@*&V1! nVb\&VoPRtX+_fyDj5X}4/ϜXyB`Wz2ӈaV?PwUO2Df)"UVT7$UKUe@U)B Jzb Ф󵵵&3Lo*>T_eDtp `U**r{WvC"U]Z_sUv]>Ģ$@kq 74R(Sl{@0bk=jS V!WANb~|{>6_Ш\)],|3ZRe;lHb_͇LXgV7o jUЙz6AuU3g [Xƨfj=F)@f>r=bVpbJi|.1 :V`J0RH-541ٳ5Vr\U׍IXݧ/ v$?~Vː@j^<ׯ VRN kTZuVV?M̷ӽ\Ck8uU.V577QSJF@P r O@X'q?9>5 Pn~x(\C`lpR`*g`Ml=M@UT]Dݖʱ +]k* z=XVssSф//H|QT ;X+^}.44-VK[$B\7$wBz[RΒ罍P}92/G]z]tnuGYcN=#T+_*`OMd偋  R gjՌUbj@h=iE*Of5a$kE*Xkz+UߞfP(."I: U:z怏ު;Rduկ9EÌ+|DH4qc$WFmU@ zU q5xx~b瀡=vV%I&u* $UV?Ul&ҵZܧb˪Se^E `Z^H,-Mm7ekMH6Z[3(4xan,Rh,[\}VRb{ͮcՖb Jj ZZe&Ac^ XoW_,@IiiIZ*1*W%W\êঌ#ʂkaZK6Ү^2(jrU-llm=SE`H-T^GJ}ڪH~a+XR;\Ԭ.Zݺj! `:d \,PII:ڠV @\\.x_5⴪djJx *{]gَl|>Du8m9 @WQ:ë5@,5>GGb@P@/Pu,VStqϴrj8XfBDywW҆+j?|(̮RȪb~ -J-OS{W?:*qQ3TG^>}Xp Pg V;U/&ŪIV _~a%WAb߬VXCG=U*aՠX {U$JA`wLՋwtW"kQKx-jkɊjgo"?e~-{. XȺ%$E T?9j,%ja$V"gq V#¹P(p4LR:|p8L3VW=VRbYV4U:b.OWO 8,XMRkOME0*}^x3X ,Fr}tb9H{bլVOlcձ߁Z[.o{2|10Sҫ^LBf3 '\G4Ր8~Btmc*KAq8sm4zU`*1HbNx~D* פ*3*rɨ%b1V fC]1EGU{b.ߍz+b!RuoGxroM_?M>/|)(xn.sVVV+Rbә]PV,Qh]XMkžrGΟ?O7mg?5 XCe[V@[aUQU_JʊUn40VKIi,V+@սĪ aubX7yXM²2"\͔oX X0YjWLUUW}ΩrUlUQƸaqjT^F] Ȣ_㹗J\E"+Rb擣X5+_)2q5W Rc55W` phydSMu.1?U̍ 2 JNG$'NM8VQdM>S[luUϞ<%zV++Zѓ֌o)xؚo+WTiRIqFVI~u sѭA 6XҬZb5ֲ >fP*V!{auuʮ7`TiYv7UKvf4]-^SͮURXOjT*oX``b ~ZZ5/zH:)**vLP9`uT/fY%W'# p 6P(+slW/KօU>[Ttjc¦V??VVՂ!'ը*x磤byr%w*bw \=ɹgYvWVEV"buhSrª[eT*zbZiRFԪێUXս: 52c%@T BhPuuJ|5OɆTUS΂UJuf1U\*y%E^'ު_ ֓\=jUzTyZ{x0H!+q50 bfjI ۈ*W`5Nf+* \-.="êq]T+DI* zʷZ#wá؀kdhd"7:}삔L `eku* 'b%<`*XE4+W̰M;ƺQZqFz(8OWݲiKRS|u5 jR\>TP+drKy٦JT#;ErTGޯW9y/=MCJ|$LT_&UkUUQ,4 o (8F'H"26m^*`a*5_%oƟ.$EI^t|*>!]ra*=b>0ހJ0TE괋5L0m ]Q]D( LFAX)Re>dDeyXWU5e[\Lv?!v {mlJɯ99}rfXV/_G6ձZrթ-;Ѫneu|Qvw5l+zu~tKoB?C3PTS@_dչ#Ui0cUf@-j].U3\=O>) +r TNUUvms;@𗑫jm9cuY>Khm*նa`W]wr_b5Hw<q1UFI\(YPUtFU*U^FuXK=z QHZ72*1UUI<kcecl%əi&OXӾdjLMqJV1kB(H |W@]Ē u'Hp?xҫ)Z\GӗjUyʿk9b?c맡r;^Ւ-VU/uJo _W)(`xz:)\E~* Ν=zՄmMjJ!.X WU% zSC1eanjUD ӼTjR-Q -,:WXg*JYj\-ȉt**(JjU*Ϯ:%*%I)tnJ%F].AG[(Y#hjQvQ*;^PJEpDڭL*42ƪ@u:0ت*T'Y$X\ ` LkO< d8>2՗%u,M| xdlaSB DQ_#`aV`;֟1TkW3A?464fXS>Z5*z! `VHߔcq|س}Ī/gmq!OzjT9;,-&`n.q^GG}q j*Kb+j*Pm*UJ.Sfu9jHM6VV6 !X3䱷VT=P_լTdu`[ W-~R޲ )`5oWubYXժU)QZۛ֌!T,O!ZJXy+2J_ߕs8 -\ IDATCPpծct:ٰGY1pyI 'Xmh y+Ƭs_JfjE*LjQfê4gGiPc^m\TӹI^U_WZUyl SeN{tINt+j5U bfUbr-ZWZu) 8v1YW%9dWX6r׋ܛRP]jjz'W[Z+8bqPdnnVt:OMd,1R# ЬXZjbc XY9 Z_I-8 T@v^]@֠|kuF.$E&Z%Ë٭N3 ö=U*;X-S;fpp-XB֟>eS8gj5s_VN*ZLN Xe9KGŧšg%;AVK3~>z* V3VmW3Te|!V0α_mӽUZlo7M\|YQ"5YJjuluQJ,=O]VY5lLVS{[u] PWw+;ޥWW? +_V}#xp*< H&pz>wx<XǺUzúU"!JLnpOUHc*\믱, bԊ*Zn+y=Z^+[,  `e3@V؜[͆:mž_2j5Z. $_ۊL&V6UVWb ,HئH0 ~F|yx"~p[14?{\z(BUVXZcu<\x[cbd7s׸JMVUJrz"Zʒc_$/*~ T2HNje\Lo-2}n)ZJVn7XXrZG\ձ33V[e[U`cª2:UOzq*2`njU/lk)SP)N_`}~|ӧ}Kiiy>&[Dlq-@-Ը唔VyURjtք0*a6@ b7Vru*Wr\@֭;H÷H"whj6[ٛL6-lfCZgc_S T[W~*X7=p@]wmwU p۬Tԯp'SU7 V@Ph9P7FC~6@6Xa nsinijjy9~c@T׸zTjVqUb2%&^<æ_LRSG; 56HfkzU^j+~Qt TrՕ*q@Wk- y'5, Xbu֬d HUg37ͦ"\5 * ǚɹ5jƀUyV A'WYJ3pU&jWю+x^adjT]$*+TT݉XeVjƁU ]kV)V\kZ]{3RԭUWfITW"k'+4jW1N Jm+&ŢX˖3l6[aVTTpFԪ8SP@=zj{k1;wCTk^_zӪfe M1Ǫ+4zF_gCP"8ptk.XVںjbz)k5捨{Xոر\U<R&OfȦH:O4s-ro/msUKk]Vߐ^Eeij5/giPUɱXMestņa\ձ*LVOL՟''Džm]h=U̙oG`u@XնZϜ z^_K`VWjXTXo;|GCW CJYp`5"\!U?T\&U#6[Vm%`&WYzvw+puwmA^”Kbe[ kgd ,X*0G̀NEZ@lY]}=KAZ@+.h j]]m}=V@AM0g2LE?M>*頨Yu\ Rq1¥vAzYho@SXe*+VҩʍEj(j&~֔I}Ui˒^ s5*TY©,Vw{G,ޒ*UR:^kJԴڀUPjfysx_ f݄XxJH;;c`_%.*#@c:Sed bFN#ou5 [r\Qz!kw>bVWq0 'd0v*xfԃg)0x'a*=n +MX=,UV%"(W]*X}h%CYqT+rkv]ᰬ[M_9s[ rAs^XŞKSGT_LOOk'Jn8!SdNr5UѼj$j9}' X]ȇ]|!VMvռ d X-ލ99Us!J\-[Qb@U~5k]0tJt>u/)m"I^92maR;47#0Í:UH/ENʼ* :3"k0M~6nj<0uݿKVDbHu҇&VwX-֭aXYxq ZqlpPņ7QO 4p7_ى4ovaZ@Xj,TXd>^Wx@bAVW@*VkW|譶Ҕ>lV= v<hPWթu\U*JR|\զѪT"ÔyJBs*z*]*O[\Y1>Lq^]k:yt4Ą Q2LuOJuJse`>CT`W_Ccbud[[Z](Ī J4;T°Z(Xoe  |ñ*G`U;"dU\Xzbc;^t̘|8oޜ˃ɫauVHNiҬ?jjBB~xa;@tleWoB\|aJo0mp{He[W2?}*teUo?sox㮫60؏ubJ4@r9cdlWc߇yZp1x` j YZ-+c *WG$Ci+ `ecUj3%O)%WkHE@RxfGy <6+ -.p\~@x̸v+>>=X8TTx"VM67ۇ E7:xC A@*.ZTնnT_rR-=T l0/kZUˏB]y)B-'O}[ͬZ";^ɰS-DJ~J ǘW_eЪ"hչ cv})V7kj5:V%VDl[D?7Uӂ,XMUgVtj8V*-,{_<}wps:3xɮkxHoͻ]W]h93ғt/ v}v_932;m5/{½;gFt7 zyXRo`RAU[w BhMPVUπYECA~L%骦Uy҄pLҶyy ^A ;-dXWjzQߑmGTV` ::{[|t]h W{;1 t`+[Wg|"'-XSIi6;O,V[%beAѱhlr7=lee9YYmCTJoXX\5vôA[:F#pSLoF-g6Tƅ`-tsȌ ^GL0)7/Pw4n__A1~e M6]94=VfPZ`5y=VF( VEMlJ۽vU*+W`*@ Zֵ63~F"lٵou55,mz9 p%} +/e^ Q]WyXM]ij} XwaC濯&&lG&VC+-ҌUt@n1ЅzZXEaf?Ccugx_Y..f&CDꬖU*UHY#'*@**{H@d ߵJ\EvQ.)U50YkբVEiZiW; yy @նz`+:p 'GAZmV )t|܏1$`J&<?@;z:z']a9=&"X!z,}W`/Ũoj ! ࣢ 94mɀ*|xPY(YVkJe<GZUec5T)%k1zHnUz2kL2"T,3Xv hqS<1yTfeT?>TXEd`j>2yZ?B>@t>Fډ(V˸K ñV[ϪmQ&ka5^Y-W Dhݺu=o4_B.O\}}ًR%1q;b5 ~JkZsYU_ e VGK_갚@XU.Uk8%B *]4zPbeߊ?t~_eh!{ OX~^XUGT*ݪJrjU!(kT㎃zQEIJ'S3W0 {킩܍ke<@SUKaŖ-B)ҵzzjrs D@~*G1tzԔcy.ӆURiXhyS׉VM^oc%\E"RյH܊N?&*b_ ^*755;1:P?ml,W&%V7PN7tVk͵ nT߬0m7Zڌ2:X%Sਠ*9<y~z@ՅQi:89Nf=ξV^t9{p* Y7jm*lX]\\XE2aubE-ڊlV䑷 q;s p!EcP+oTj|\-IP8 h*4*jfL.PiVҫXV`ZD+TMўTa**ҭFV*hq̙VWAߜ8X}$/b_Ls:oPժ`b,C-{Rղt]˕l|{8 c thjUjnqX}{:o'8 VxX~9W~7t@bǂ̡7_jRͅMեǠ8*8.҉ͱ1[K& iP8<\ ?XՋ[׶l:UW9UEo"+zWټ ?+;85cHULMr8eܕe&VLTeYuωU*e zLQEm۪M4]\Tii'j*j\{-븰UOk*Uъ | tV[e]=CQ8kBW$V;bUUPtGX=Z()H-7ZSXڼMdê {۳z;kmS{_.^yl+?W;Gҝ;U m|v|/^'M[<\ܰ[糏OK"Vv{[nF^CA V^X6JU96V}aGx &Wmyծ&YaĪ}`źT|+rH"ES`ŇM= B>s8w{.nf+K(4H&FF`7o&o FԀU)ªk`̺qT WDVѫGFG[HGXK1J ?^ XPm̹w'Iu %E+mtTׁo謆ЪСEJĪ)/"U=&YV=Ѻj*U)hu8Q<*sULbQ1ƪf[)):[Xm9CRUἎL[=O [5al 4e7ZIݔ`iڴ*x f+!P¶6 Ih|uVx {qًIoŌ׆|0RmYq,UB@ b4;`^ken6W&0+HAV}Zm~,]YV)kFj})뺏%_.HX+UZ~: ) U-q?tTW52SJ=̲5=!Fں=T C%3VnMV+8duJDT@q~HTXea]JRlki:į*vͦV?"V [OAoa'v[{S@ӂi)neR;ŵ W#Hb˶dR^Z4E]_+XIxê X4 _i͉f+*aB7ipSZ"~'.\h:F]l%nb2+W*`e] (@;)tƿ.d11?^W{VTY?'T^|Pudp=<ݞ:j<]p5€+W8V1<*Q V`آ uU@իD;bim}M1g1 ^Dd937V0EPegզ2L͖TլY)3OV *W= W(jUfqȪn"e,Vdl=^VU3XUNjfoAVgKZ a![*nzw7*L_X,/S_-.y`zRz Mt:fR~?/tN%VM\U V[WTXJU?UFѴ?nvMzSYp}BQSZKRkdXU5MWv>TV*lE0,̕#[F[(+Rq*eT"UlΖa{r'[8tS`;q0ֵZr"9 `޸`FQTsëz_Gj4zb?*Cߌ ŬUuhC`ftr#{gƪR<`gqeb*( ޶Z3 Z=z A$]qzwIې1VQe3ua>?PE|YasHՆzT<ٱpEĪ2٪Z '9X}Na64 X,/ޱTJS6`UCiU6#Vs,c7k/}bpUXŮrFUÌ}aUSAyZʪt.8Xs.kGHaWE8T@Tozƪ[qU,iaX.ZDXh/ >[j/NaU7>d4Si r`΂or"[P:@ N&'GXȌ]_Kē @յGe @^%<& "1Ջ7Ll܂`CGszX rc`e4ƬJ8uSbXjqn*pś23TV !frLғVf[UU9cGp׏q^Vi}J&Z%'"nSo!UelT5bxU*rL Vltzx"UDX]IなfK (k*ZJRo^XjnOm;'L,mk!)*"@1`b\ dSZV.jevJS˴U6sꔰU'+U{H>Jcu [%%$kP*Z]c>8~"L( `l4wUlC6$m8fͲ3J3sGLč|T VщjLLXruxWB 6PM5ԛ¡?B E+Em>аƮC D" WFX%f1 W)g 'W\EJȡW>UqTPE5:9zq Ub\ZsꂉU)UWJ\tX%7d}U<*[J%SW(m*h(ZeV XUJ٪U^)9LרAm9G櫾NC5/VYV:uۅ:*mڔZ=Ua ;l=wx~ >jy-%T+VXGẐ|Z lV\ REUUW[,UJf{'rR.SK}o*UjQsTgA;p}yd'*VT.&[jfy}PPޚ#Et"1lL h5majğ%F_Mdq/NP*bx~߂\QqD/;(_[]4x3ΒR[NUU@,rJU! BUUn_ *Z^H9rJպY)GޢYsԪVWU*,Rs~Zpxkmd*gR'1* UW;%=Z7)2Ui)`$ 4Uv<=cYgOm &`j) ,֪\VtZ|gcis29T-.j,k~e=" ` ]5~ )`Xdl:"s9V'lEVd===" O)P|bU VTdRΩk`QׂV&; xD<YzW ^ .D"h:3>'fRɤHX!3VxR!>jN(VAN3ؚ yp Ng5D:+VQ+S^jBU.K"u5Yh/vi)Z evhLEO^eEr{y W]R*:-UgTI` X/kKJUeZDX9Ѻ)Wu"VN R&V4]m;uYj^B'c[]gol5'[\9y}Um_5OC;yGIvξfh71?EUW?=ף/(ʲ**ģ?ГEU*CiHzyP 7&Vd`jwOjUvN_JcII ށ@ *ԪذasaAM8RU0u3NPe[+`ڍMסI"l1.]iV@2&Ԫг~cK)Χd:Spn* `)* ;U\5" Ѕd4z <\qTzj5rV9P0a=ˊU78=@peP?nJyU~FTUu=jLweY9j;]I$|ώE.+;+AڃkӥۜZN Prp| eV{l\x;Z?\鬒0`J*lVEq]װ*^Tmnnf5K`+s*/W3:{5v65@QյT}к1WvtXm;o\jW~^fV%U^qKbfLSq&M[Z-)`-)U%E7,Z""nvʷd zϻU,О=?'UѱԪ*^+jUZԪGY,V9a s"WkJ݌JvmBZIT?<660.-S* og " eB 4x'M/Va#N/lS3aNS*:XRsiM Qa9,tJTzK\%TodbaKzZ|cc8<\E &6Ф)9c&\43[#1N?` 0b?[ۧU:&IsWAK[ж"+vc 뭛/wVmZ~쎅 XVR~!ҠL&L=*%JX-jjtUR/mr6ʈp3Td(1eJ}П=xIƪdUe2&~3F UiT\<0)ζ4ZUY_Wz?EMo(TGt6l%C?"0'gy(N?,~1E/jM+s,V[AD뛉19(J#9[SS#9cwH"Nw0̈́Otb1|VC j|rB='AFR`A5<9JLhw9!o8u !Zn*%oEz0ln'bF)V \W WcU2aR UjjŪq;h_ChUDTe U{[QO?%z;]Ւ%j+Ep50d((V}\*pt`N1U5@M9"Z-Lffƙg%j>X@eC]H 1Jz,Gtt<*nSDnU..Y jh& EX:DY*epZ$Vq_ڇPj\ }YLd&!a_ X{/oƪmUalbҬeE T+6cZ"TP[ \yV[*3{Yb5蟝£={5ɫrk<~IӮP'Jy$ʭ}T-J{$P7 X %(HC&kRw)9 '_L#r8IX %zZPq20ߔdܓCrINZIL9#͐719]/W.s~ juo_YntjC*\Eա#j\WT @<ݴK]h;V U#.A*@*<*쓡Ztu +T䫪^uۏ, @e߰#@ٶ2n~Ӈ-p)ROmVx1p+bǀi@t<i 꿪顪4JHoN +vCDqpE=eã[SC@kB^^tS-a+@j"P[Uj kPQUz1Sb}cꬖ9Wd[JVTq^lLWTntDX-*. *U*'Q'> IDAT"XZiA7՚TgUê} P ۢ 3Y_)׊ϙXkU P|44jlhv?a{(&@N4}e~8+Pz( yjjN V Pe*;o?_Ch~; %,wr:x V ps?%GV PQQ!,04Mj MjNxOI.#G#&&M J7-q2Wjލ )ښ&Zq:=hJ Uzj!:rUͨO*X*ZƐY#VZ׭ki9uUWev}ӄ`ӫ-.0UWVA`.@vmmW m$Ld*tǧ}cqX 3wU@@g~G* @F)6vX-f'gQ7x? h'kG~7%"B~Ԏ;qiCr;|܌3g*ކSZ7caԵum~mz)jѫUvT<\_%fL+2HRJ@UTҍjJ;*QsU +PE#W5jrխUcijmQc*zF )igN)]4T[ê:!^ "d5|%٥;].۶n sU@vQ}';;;}cSqa@ꎵg 4䏛آzڵe#Z@G{^}|U,sv,}6|+rVi2?=![ڼ]_ =-ǀ3h08JtQ ,dv\LbXHs0s^8 *; T$1\\.1{{J߷[J}yjJz|->JQmA de1߻8q yj KUUkا7H ʹU9oakr_,;%7o 1? xYl 3>4+UGGN{:~^n~aZpըՌ=GO<"t2 OU+$֐#\~ũXJS3 |Z"-+`~-OweXՖ>YZ/DnIlYjUB*NzMg j Z j V e4yҰ!UT}aYUI \O^?WjRbWK V`ykꃮ 9!^0CCOFӶ<Ѱ5+;HOFA*Xs|rL>5Cުg3%+=ƿvMw 3q:V7nܸ=tTj$~g;U׆R U sHXLK޽+ \5 U\XTu_auzD+k'vb6yS@3Pjl;䠻4`1+~9܀݇jq bk_x摒V UB%何rPIHI)è @gy!D|`a (KcbŠtoao>yYHRxY:dfXrd Z Y)*Cj ) K*W| vTwf VϹH`UX%@cC,[ VI^-q[jڟ X% -YP5Iշ5`?YwmVL/.*QW8p]Ec/x3lWhrÓ=ѧ:CV1=L<|TM3%:]TJ>apd` g^e-a]yC+z>%a5ɞuʡ _5 LQ3>ՙh*g2a!bB{`%'2z}zz@XJka~a^!αza@բ"G$/aRg3f@HOљ>:tMk"\q}V'|R4 45uzoVk|-#Ng_s[6UTu*ZN#V,Aq(K JA,a1 ſc!@|V3h2YNl*X5]? 4ryU^^yKλ}V?wtk`2PzP  hX%8 fjZOahJqc X5#P%ʹcUϰRLZERAjD@*PRUo5N, *KX}s^mjj ,VŒj͊ EnڗۤVPq ٮJi Tam(}.JgmX,QїX"-Ȋ9@8]Ŧ8L 1TφuFxR%[!n*0r5+8B7FQQzCT(ԗҢ7DW0Z)pXk ]6"V3˔T~UL3]w''V= X|%P*{Pg1bЋ |z*i"սDKd0JP^VluhWO;4{O"W1e)nvXOAWVJov2sP{jN0i4:]Fn]ET##ޯ CW! 4f\]U} $g0u`KR<PV z)|0!Z=0 G@2ayElR{嘊ե$b5 z^X<5WxUMabդ.\^#&͝B RmDbjLTe`ERCUXU!ʤ*iU`E\$sb2bIk`շ7()L (ﴄҀH5[Y&mB* ~,LUEW?R;ZGlkX͐e30Zj4Faf:"Xe*`լVXe)v?7 UK++O8QYY>UXMShPHr5="̩I/Vbk`51ZW^:)Ud'N6: vGez$VK~9U9Lr W j9v@zZj\N \st7T]sa'WwM še*yZWVnt^s:.EU #s׶WJ(_x*i 1%jWp_gpH>z:^eo--ϲSxs`_& 2k fpBٿb`զVkP*`ɵzVcCQVX+J L $_a5I1itew`5@nǬ`ja|w?5򧪎WY0W[&6iXE`+?bƲJ.ɁVҰdjZULOrr DʨΪqս'{wU#Y\gϞٵ*e4`V@&W2_+FU*N{ fE.On 6[+`5zEEڪl[;ԫZjTЫ%rvq&V;;U}k3]ޖU^TXSijr^+ ܿW606 YVع]5,"D T‚4U-4M[Jcl5[,+ E,9$% 0LuPD2hGB$RIVy9s5$ڹ~67?Q&իTuXx" 93 3 U"=̲#c+_=">tg~ϻ_ɝݷ=^[X{ݥ=Y;g _=n`5o|wOv9UZ+xtV= NV-bby҂JD"XŮV/3Z V9fX^1j/XG:buL>Ə^^@XݱcΝvܵ/Eqmr97#)Q:@i5P5Īiʗ^-qm3cTXQT,Wb^%ӫnZ?+t$\!+^'GI8ѯf.RillATWAj*L^  ՓAWْX}Ի8AƬIpZ}}j8\BPɆYAuztpr]cՋ{p2ll>8@k5ñ:?A!+qR\_H'A?@4ԣpjAW\U"XeV+*0. NVNT;*W=VwCJ\BKXRmgTulVv]m([x"{zUPUŪo?X≫ l˹ƹHH$2RTeX=CJ-j}c TE?΂TT܅Y&aqzXEr2cUT$#AV0mEblVgAZ] R$U`d2:W8ΞNFczLFdx,)4iKkOWPX8Hx.v9=GSYa!XQVVz{+\RU,Kc55]RKoc/1Wʾ.l@Xi*@m/#؇ *U{k&BYZB 0chT5U;WY*T4x=6jaUcPՄjVZ-V*-HY*6Ask+Vm!VVuI*0zO=Um\[dVhM(UEbncuhjH*'P1^%Z*`CDVT-jmH3&(bh|ta`Ъu+2Z64$j^YoӇUFjL(TU+^bjV3$VSAX-cIv p@:H:FuJ85 v@(jUa\N n$ fEguf&?_ Wq4pf[3E>:{ rKYG\@jv Pjb-kKMZԪ@E]6T FBtT'd%En twV}@VĪ5J8i:ŘEe?#j_LT]KXYV ,+u8YiσEQ\uDΪVDnQNT{vFW` Y Mj$w jӹ1VK+~ (YwXC̮fIuUp*`bX- DpeV[׵&/h)TްHS- c 5LzVyn]ff#MM~jcO&6d*3W'3 3p@"L%|"Xu_pZ;߻z"kup:Op ʧX2r3 WTIlwT`_uoUĪ_4VhzUnYiV# m'S)Le¼Ujs&Ox:5og{ĒLeDR*mY3V46*Lf8TluVCJ?;V-W,+J9`u+mχUDmVD=,A$+YyJz^}j\aDUR!U<6~jZU@*Ƭؙ1InP!Ivt9>JL <='NXO#V[ZZ+´c'e" ;UBuJ b5˦&x˖FVI>1++Z^Ú \yS\*=0ܟN'9Tz"s{߬-_ŰJAVb*Jd> c殓?xp7U(?E+:6ĪOCU?ΰ&&zy{?S@@\\VRZ yd'aŶUժVNe^xFP}|nNafbű bM&&Q4I+PV_}^S ue{X[R=1J4,ZE쟞ZN+ZjwU Xeܪ_EjMiO +GLVXGS+HQ[r؄]R)U7U\9P a5l`"iUP #V[yZPda,9{WEeY}۟Ӻr&5*(Υ/ lO[1'N6OJJ/)a]e>:dinRcIOұ$W# ˱/_\Gb5Wɧ1j>QFVJR\x5ZO(2ѫ::'8o׶ۯ?o|+*c1j$%'0W@T$JI35 ,ClrUUoU[Vum I=hvan.T6M\jmL?~ƪcCR_DPxs{TVDu&˔9FX*NFrqwAUAUh8wTaUt*b5HV:lq/$?qY4Wѵ ={VLVSʩńU2Q:#ξV V0ǯ`h 5'x<98<TG@lmHI+;U90< ee~*Ƴn1VqRG/d1WS /c\x7O e@jWăQ譗FcYP çXi*.́U<ȈV=EUV 1XPX=CX57|R* T6?,.몵RX-:kzfg=A|V%b4 XB;zIGB:z\UٻJHjjVr5pX=KN0: x_. rػ}zQub*KUU||^%t\WEu+N Dߪ\qi}=S"k=rA'`uQګ(]V (ZWKɵZu_GV^+V1ÊJLmR8$$'&cX!LJhr```83غCݫaL&1U0e-"5YؠI qd FbFԊI@oJ7-ңbTʣu nM͊=5,WL0V`;kF }eBpj)7U:RmjZuU~j5JܙPqUaUa{o:Z9Ab`ncESjs~U5P[?ma9 [Q7G*Eɫ(Tm$WjpZ-s@5ZjX2зЊjfZeߺ?9}N*,L"FEJmW\\}ׁ:^EbbuvW;vgVꭹiIبV}UpyX}NX})85[XM EjVw2յwkkbsV &qjlrYpU.VVVX}dffz֣>*GIz]DPmCt//k[8MrYf;c)BGY.(+3NϪZ/0ٮjVU2~\f:*Wqb`iӰS-ŪP̪m]]]d ZubIUojkk6X}F?mKmtX7*bS)ZjXWu(gduT}>Q|T0 *br-* %VZ"U?~v]^ub ;T5jWo@mH}Vz .V紐njQFʊ3NweU-δ8Gr V x.˫ cHؿJ3Y&VW Xg(YdM3kkPv& 4I3o6ҩx<2ȍMN$b4 UƖZ^M 2^|x*Pg/~X[ #qUd-Cūj;\uEu.>~j=l:v# hXN_)5ZP@ZE \ȁUjjl p<:>gR=)h[CjM߻yjTg\o`+ J'V2<@UVchqnf)jU@A6ja꟰{uXsC/^|SªjJWU*_5V;z5XItkJVDEvY l1- URk%"jJL"@qK[#sd`,V=`+`%Ӭct<2bh2 X]Gv `z7 Qha0Np0+(Uߦ4wVb΋}+Ν\}giTbTFj9^`4//G񦪓?VkYG XhZU ^5*Zł<4īVcoQU.'OK#U 7{%6~cw8TVm'_W)6 ~M_m[XmT !VEZ-*gjW2u*6p i+M8vI+6WΤԖ4UeF7إfi0&;bMd&I0 ̈́D9{gHaX ^-<'[M5')՟|L=,U+~?Wsss'k"Rj.[VτMzq.~_KW mzqcVM-Q?OoyMsM\KPIgeML<0hñ ֓>hqK~xIN  nVӆҪZqշ@t?gfP.-I@./ZjÏ?74_%Vt**NV%%"o(I4h4\mJsV+I* j Y+K`?HT5~vC-2sbe"V UEE EwPio2ryg">g> GGv}>zB\i?+Uo8 XW&t(3#VXXe jծKZڨV *'KqҰJZ aſUZ r!$ծ&,aJ\KY7ˋjy# ݔաq+XeP%ePu"2^Xz0uE ^A'kY `:?v㟅xA_ ]fZ&Qb0{ ofEz!U/n|h *h^/ 0UL0R*,@Vd8Z(d0YZU Vk/=tG/^DwwC[[X n +ŢBݫP\dfNӣVy>=U;iԛgEStQt&Q4&޻Ԫ֯L\vVEeu@xZVVblY'C XjUhնժ,jU~wy_׫|̽cn%V/UKzDrHN#UWV;T҇MƙʄIjXrE^%Yw?ƿ%zNaG[Ub\ժV/n}V"@߲ǣcշHXߟ;5bRu~^*㪋ҵ*6JUa%ZU`K ,ڋ 3`0aU1fnc33 nX!;ՖUxoZuXiS͈˩Ƒ*SZq:VאoU}IjUNUiC5aqUYP}@w2(AZpk4 WɆU )/:Ɠ|U PUB HAQu*0ny>Xp8*;>'cT%`c-{`(R-6$*) k |YBQ _4 @unu=ŖP&p X^xO1cՊrWlN=Mj,+t*얓IͰjyVyUfPAU 3 Xd\$?S*F'SkkSB_*^?_J4XITw~ב͹#r`pF|k;y @q5XqX`#>k%U3"V549XvvvR.2v"puTEFհ/\t*Ƨc5/0.60z4 WQxa=]Y,㱚[t3R}Ufm0歹K[b5cw>J-/#$z,Vw1O6^פihs^O{:;8v7|ܵǍ~zx)Vئ;BRC`g&} XEF?r.gq|CU*AUwK`u}}e)}`}跹M/ qϊeaX- *wPRw=I$77%U22{O\Vi"7eX-\$`-㰚4tl$ȚeEVS jZ0".pT5 œ hŨUx=csݫصozjd`+U U#>PY^yҼdD&)*gEx ng1'mcW^+T^,0 jcTMfn-`*&]߮k4S3͍1rݞ)LJ*wxn߼{>ѫݵFP#\&?*v_K0;R5#H9WT^=l]M*'VwV4_PFlP2; -X=X}cE!V%E3/)DDRUcKUc+VUV*V ODՕ |G/=dXVxĘRƒ}kxTAyW\@(WX0$rHVU_Ԟ}y?wkп[K[#Vc _vL˭-?#幵E2ᢳlwJjH UUc+(H IDAT*$j$WY3)Iݬi[*hESTe~' Ū&X}BMLV@:BbU?=bU;Z<95/P2jļ\E$+Bjժ7 j7pAtN k@4 ahj__U7UxF[݄bFcFϦiA,ؿMÑۨq֘YA3oK4c6j)qZJ ءjWj@kUX2j )`[z|X}mR$ Vq=_~ wV !F"8n?-V+Ǽ7YXR{ޘX 6gD[jR DҵZՐI[Bz80kjӧqlTp+|@S0VPՠ,SAS$Usro =,]]3Zx|4ٴDKC/O-:5ν*FU:QgJЖ$jzMՒږY`c d9.DO8"?WrUlw&4É%`LUʪZXiUFЫ嵹EYr EU!M ՆMJ%iYԌg*jBZGQZ 3يI3_q*Ղӧܢ \@UC8с/_V`S;P]rVZ$\y쌌Ru5P,iefq ͸R5cH>SYnSuM}MM=25k}_WU}=QVvahw ze91gz{F q%De V1p.p!q*pi'}NV`&r>ZݡJpR9|xKx-С@,xR׋/ Vjv1hQTo*rOKPT^}6lYS*+*ꋥ/W\{V'Z[WfO-n=zft +jo `k~lm٩@,jJ41as{ߥRl|ϰ:?`-XU[ TVVsеyt<͆?,MA:Afּ*"}`!R9P>'yVMU$4d8}4 WX\UA:?u7.0j:?.zwd2yf9qL35LEwF[{VU `ъ1*=ԯe.{- r-#lcJ8$j" _hHb֖,kYҬqe4ݙ`L@1`NBȠh Mkd0A:8;#JLt2_<"|}.[ ss:pZ,בt2VV+|8>o|U+K$Xݶ87l b5 j1oʡbmɰ*Ts%bH`X;ɏAW}fQVn.緆As7JXv:;i2M?3*\vϧ*d: sATE j.!)9 )I"bܨXd]73͒g [- 9/:+bU AXݺ̻`8UPƚjcMժ-x:dmw*n"N1@_L?|ؒvcuׯ**au@=@rPW[7mVCX}Y;v…Y푹JZiT4KUHTmZkR^,J9V^@٫%'΄#.wٖs3"W :\!L4u R};N]akpﱭQ/ZH0%fg+/Mr>~ yR>n{u|@bXZWTYmj }Z-K.C)*䲲Q .U9 ~,UTKTRW'p<}8%a(WLd#n/\~g$  1aAXXr+ HX5P x6XNsZ>^O2,Uk~[סؚ57@xXbf/nnڅ* w\eḚ oȊjVa5vzS1Ԣڢ"VE SqgT-g}YOMheЯ>VTUO~1 | %.HXu WJjf_V+Za+5V/@+0 rY)@/϶tə盛OZ)XA0zu6ł~%,䪮4Gg'կ֯ro\3^/U_U+*@5_iW-@&QVVjr-R`-(Xd8!y$`uʱ*H]_Th*(- <#Vs(gt.ލ 8]VyP>jv%VejJZUmNY,F*NSNGJh"U k@Uxf`j!jAVW1~72?e,(3_= ՝5v/ XY,/U(V?(GM~؊Yj[Z~DR  7egK~J 5VjTdjj*ki"@f @٫.Щ|ԶcՅogrSW&Pý>?[]Xd| ej4 Z+qt:KX]%PuIN]y@VD? "nwah$xY'dYIj5)Xx>k`,j"n]Wb)KfjU *KTJ r~,|FG÷(+9V#  HWKTEޕ;WX= ViW#]LV4s%@"Mӿ^bSUUii V- JTX;͹$n;8גV)IKZv.ê#V+j)l}bx?*GǏgmfQƫ,jjQ,jcՆ*Ljm-)A;Jv0VVfCQ Փ6r&e+{XS}?:mF-Ucu-VPV -:YB0.W0.v5u\8X=«n"0<ﷴ4u4VGqa6Kj[O!fdj: ]O,89Zu>"hk%I.  { mƄ ytܕ~5ZmML$|>oub5}P`33++ktPȰ}Ar];{Ti3_ {BtaE kbKߛjOɰz@c2xKK_踠S֣.$Wf:.j`|)iZ#XJJJW%\MQ7o1#tgO(a5LM1闍 PZ(0(V2kG _GZ,?_~g8Gzi#U{gtkj6@,%".ҫ$WC6iVEZ""m.x.(pf!~ a.@!Tu;Sd)*SUnGZV󧼦9WjS1XU@ 5Jl44TeUU 氖^s^rԏcZ5IG {~'Xν6`c #j&(r^eʡ?@X0,]Zz16hZmkX`UY?e⋐ѪiV*ZW)m#Ы'P!Olnr6a0r U.ֹMCFYՏONCNZT\G[[([E3Y ˤR;U#؈(t)ƎU*NEܢǻ{K5\hcV\ZbaJxchǖ^Hj*XT㒫@bԪ.jYŀU LW]ZH(t?_NW6ű3#tHI*SXQދ\,m.ϣ`&*իӫ3ֽezGF20 ݣj[Afs^{opj7Z]`u[zW gZ bU2XU[h5r#Sj\U6GZ0-gr`\(u&VT5=ߪUՆ:X~nZe[{B;*wXV%UC: )+|@*,3+xkX!z;Y}j7uh[S9]ZxZ(U1Mo }Z Glg57A0v `4JvS'$VV\Tױ~Y$ZTp@*s_w^uӭt< (lV+{DWںܕנVeڤдVWRtbM$54Zep´ ϥXeȱ"̢6K[]F\;+Oo\=o|.V$@W!UPYg`iN`9;{l5HF~}pEœµZ:*+[j Z1 PQZZ RjeVS)_FrT΢hu Wϒ\.PÂXTu! p?8Bu[JK}{P Qe|?*@Z%tBV0Kc\G c*UUV`*B!r$Y5R2ZA䫗d7Q@jΖ6/'HlJm{+ր缓 `3"[4X̃gqJӹ\:#6 ianc8";G46Ga_jikLsZTa3Qm6VEKV(pIVXu jjz TQj4Uᙸ<XxXXtYIt$XuՍrՀU#XClLUU2G u*@Zr !6KDK?'\W#sl8]U.sl\)†`NT ꒤]0npj:6FRum;T*=u*xK/\/RzjJ>zx:/R$@Pelv|#]LfX>UCs V%X~iZGV6XUiRV>CtYڍjjn^?`CQժIeB8N )^$ďVYECvLZvn]UHPZ-˚^t"a?Y& .H*%`LBs,VWY228oƯUͫ3Rm4Q5we_>ĒUV-..TAwʌl \1?u:އW-*vfj5VyEiF}ş"dr-V:w0VAxoJ鉶 Xϵk8!VtVAU\jUa-Y5IA y7sh+@Jic Κj662m\xk(MW,㾉i340`YG(cu2]Wfy"33S]E~V8*Շ&d?'w>mɇV]}j黊՟]szh\uRÓB‚~ܣ,kRi8xU *0.9Nb(ZP}SWߊՋ'*׮VLa&UX =4R5Gʰ(h 8AU)TGZ$i퉼?;|Xrx:P~>xd.-NWfr+c:{vknnMj9Ûp̹cb7gI9N?mSPZKhN$-ۡt[O4J-8i9֓|bkWo"fZ7U;o 7{k1^ի$VVR4Pª4(eNj0jՑVX:XՁ\e~kT]缭a+Nha aV"lk˘ڦ˘o'jTX{ 8KVe*|VUjO+) klEъdM/ kgZf}n0(aTZ`'Kz&3w(9aGb OCnד'ULV-6+bA;!Ձ~֮ӾiT"A{5T+q jD=8 ~jA?߯Vjp 7q#99O;XRC(Ua5$9YY[\qp7>a`@ڈEځPUZfj fz3蛚EQ92b }#wj{bAW`:0]!?e51 $7p0+5, ƭ%dU*b @EBR@,;: Ur 55PU8].28H:p\E {RMtb10c^r:i+_xsm}!vEVb,Њ|(WARe7HX,%"/, D<(*WWE{+._%7h,6a`a" MMo3a`HCAţv}H' dohI>LE">b*_TgCXLIᡇ:Nfr ds$Po(V%ZJC0Xp?,Kņ*j5jEE6׹VR>Ze >V,VLl0nx)nS⊎?\%k󻕍T&a~"J@T&}2MBjYNʲ̧)ɷ]X`7#Vif^By> 98!WMdv-ZEWp8*,!caȗ9ceZeU:`8B*~UvPq\>踳p;*RyIv_+]v}hM$: ^*Xٲ2+Upav'{[C \*Ƹ<`"VXSjwkLC+ۺCQNW)^3=$ׂ N#X 0=~-F(9ȾJUP0ό#(&xMEʪW*X>/ƪ(X}3*`RE[0d,bU5՘҂ESt:Uk Xx겙]ʞXݓyU6Sǐ*XxVmjZ\oKڌ> ;TUªm@2bnʬVƅTVkѹiCWKe_4 Bl3_fҋ=#$i[`j|yIϷע+,VAZ)[KaJ-*ЩnGجPXلB4ݪtҦ+ \aEN-XugHӻ4nܸwg+ +ԎX`X_Z/ـV\*V*+*Ot@Gr(_",YNj`5ma 1@^dOiI)5chb5AX,G!~o7?&wV m fV8[[n͛** Y U Vk)jsZpȷ{q ZD `,.k"^V+\IEir=P qh/ }]L` Ú3tμZKay4-VQ„~z >OV++ۙey;j 3Qp+Ud&))h _ak*x#YjYͯXU^uw6**XؚN!N;ɶJؾZ_-"^|(o5n-IՊo Z;X1`F2U X=ہq[*7XEWA^ bXEvLksw*BU9ijCؐ}*%jy\EU\4@2"bC8A f:CvJ+CzjaX-D_@ *TV+dHC)m{P89w/.反.T5ȀVŪS>TwrWT)U6xS > tv; gog˾V&VSj#egkF*|?3M~MB}+ڙ"nT}0 KR U Wǿw:Cljf%YB1rY]<`eS.L&ka_EՄTo-bZ 8:PޛB 772VS Z /QGDz~{Q[*m\j}ާVb5E]sVjKT!K&\UW;^iVN '\m(x@XE[@V\54{_;FhX] |[Yh2J|gp|Ԝi.NuXŲb 3Z yսo5X=ZCl6VZ+ ~s,L3b{n`ɮSCVk\E  jI[BFK~io;n8~餌q*^tβ 8qgN?ei*^5lUj =vT{$8閍^f7WRwn2՝&TW9,*REps*YkUsu 5:-ms6Vw2˜ p,%U)i+O>!aUj{v9]mSr!V׺"TS ;* 8֫rVo2;Wc]+U<Vܼ^_TEW(R) aA,U.hJڀqTQT@Q)P0zF{ cDPhb !32YQ< jvehX=Z-#cW+YqdQ*V7TmVjC85ۏ2S)5ru S:pmPĪBgEI\N$:)_. +=8Q%LGfrh' ۡ8 i3%CL@) 8r@ 8) C"RPfCWXM{޷wm!7%wws̿$'sug}G[VǪz4->o[pb\?^'!VvnҀyy[W)bCI8lL\]p +3Vp?s$WycdT%l*XK۷%@o `ΰ `*YG|F ɶદo^!R^=W+XUjF+5ON}ĬmŞڠ [=8|p[i,eoN%J8iUT Xbi-pZfU6ՆC60jbo={P/3?~TO?V5ZP'*Hi7-W=WQUva S<)fy%7b))Ԥc*c73VwiUb8`ՇU!Vx:W6{lzID[.=bju񀼉%E̳ZVXik67_"_en1 %px4zO媖h V֘7KK&mX]$:H{bX.zGk]srIԨ➏BCM 5U TŜZ<5ꇨZ9I*VUe+Z*u WPʧj5tmIfC.,<Ҩ c'O}HxXW$VW]qLrgD?7*5Kj USi}<bt1\}P2gzٳ[Pȼ^O9:ߗ( 0*2VR}ZܥUӒU΂0=;.s] XE.W"531Oʦm[pV[JQҙVN@VHRjT P Su`ukե*כld>^H2S8U{VUUF{OWp#UVyjz:, rcwb{`FAH7aNx~`{ r55X-1V*`lms-Vل(:\upl qª@N } V WVQx)$b^ @`JzrW@3g1fʙҀEվXWުw#kչ_UIibH+7]* z;|oĪbث* y*k_ֵVOj\e`YlgYw*o=B-k*MC~E6j;ZX@mR"pl~Ƶ南wPœEJujtd5eHwM>Vgzuż\~V=/FRX7k]X%QMS*2Vh$Xz!'B@MD`'Lמ͌K%xQ V1 XQgVXsUj*9bUXlUpljuT,sU4WfN !x&Rt/ ,VoU JyCNp IDAT%HzV}{8_^_W'- V3—@Xꍅ; ̐Z]C#"Vm&+ՈVd miZr ۫920u\qT7C'*VU Wf)ݲ=Ց/BLUV¶b;c͚jZB#2zUм xV% VMܡZ />0P4fep9N7ȽПsV.Wjjwn`;:BոboT g~Nq#^`5VWcWg;wM &TP HV U}~F8ݎ`{gyZiT5hr:ft 5σZ cuAy>ZI9 XJ$.m5hܧWUUF~s4t6yފ@UUV]l2෢z3"5\j=U& UVOET{O0UW,\ U=UXW*]Xc*adZ1ԚJq^y0WCR:22Kx`j,'sYJ@"TաXI2UkV V`d(ziV?8sA!PqV.EB+Jmj.Wbm IMWaI?V;/7kKd*j]R_ TpSW/qk -1)+Bs)ik!>Ek$4&jU9.ԸWE֖ʏuo6oDwV#U*UWI"ce Jhl`-KeVH@V?$XXEz]6V%ejvoIFTVT%[܁gj\EAU ZX5TEAQv:m ;)UXmNvKҗ/''Jj@E :`UVVNg_9>k`}VVBѥO O&X8Z iWccXgwOWH Ī*e͐ZW+T %HBުRF:JzR:`a8!2zJ__O>)md ШU*ĬhEUuyrF\y*o h O$L@O ɻyGɕFRI@d_s⫉[˛L| yiUF14X_r ,`XY*b+uh%rW ɋ'bcZ RukQ2G֦UVTR<%p=V;bPRګ,VTtpUr+Y4NՐ .DP՚Y1U5*Q#dk!{jB:XԗWs`"Tu,PThIpË7`d1dg͢jOdV Uiy 'U*C[g=9;Ui}m~ׄUV!$EEVueG~O`t$!T9C5D)'SֆRț0K*GqՁyDIMj4z%BoZU 6 Jkvqp2)PU6<1WsukɟYDz8O-V#_dkklz׸՜X/9^/{&]a.^ ĕ qTιV*1QCnWi\]fXfhJǏVmѱQ $5QHW!_YS|UuhU*ެkW+ƪR!2Uu*WIvUZbDVjmXS GJ`S"T=B I(+O,} ԾH+& gvKl2J=(E]nJ1ЦcuXEN=t#vU^|j|:=8=^My`k^+{džn\MP\*% X~jn^T""X˕ qST%O*dVrHY[LKUU!!o-{jS{5Vn NӰ/ݝn,Vk"VϸujRN]jZtNW5b`kSwv[A}KYy>O/4?-JTG}$pn8r5t\%K(p:6T SvX%O`~[Y .;hp&k+MZY-V^ mg@1jJ3Dj0($T.Zͯj>dlWT aCUPգ@Vk^*S)|"G]w+yzelժiCju24kգ(+ [h)mԈGK*Wu5V4O|ԭωW&;yK)qN;u V;th5Z>s#xVi[ /k\n >:Yze qSK?&#qV*lM'f T#ZYԏ 'E *R^MɁ@U?{?܂ͭ,Vͮ USUsL`j:WGV3Yj5 SΝB5 y/Dq$*?"Wt?)ѧ X.yjH.wq,Lcue~UOCjը*jX^sT}nOC*Vټ:i&jGXԺLC Xnx%kWUFE VXVV-En\M"+~hO#U8OUڊBg|Kk2Apsi-+aTE.oz?|P+Eֽ+=T?"ܻ2FǍY~Ҟx8hϼ2nP3E*J74[_/ϸ6Ao[Ԫ#ܔtk:|^)2^0SK8\TX`]jv@M;M3lrT3SXUwGG)Xk+W!W)uI TZ *H~tj-s:"o_*iw>)6K_u*ÕhC+ UZڗTOOvd*j;*jo$ړLe6٣ʭ}(@MNzvnDq9'FZ-Bi`XƩƌf咝aN-qo=~p1j43u8w3%\M\뺕n侟7}j8i_?!HU~G\ҋ[Š~!ӿ,k"f:uҼc-0/0W׺HˁTToK)|Ed%oG5촶P%2F~j̢vtHp}֫aմDYVN/U*6VrԪ4ί*eSk#7TXE1XCo8 ¨UYg[XxՆZUTpDu@JDkw[($DU֚Tq޶2t\VnځX$jrs4j2S)/*f+B5 `NJ~܀U;|!rPe kѠX4sq.FC[VKE@ͱ\m#PsA6M*Xu @VG!yU{db%S4_W.ZRDpM*,WY"+cyw;;ǮV߃0)UNh$ԺѦ'PJ[xtĴҥ95)ɺC;(.`ej[ j; LiNՄUnX ~BB Tm,Z+*RA>,ʳ::HZ%N&RSqXJIUW S5T$W%@,cG]irE0-lfdz! bpqֶ˗ݎy*E$돧Vlv?X^_XVXdhWoH-L#+<2V1 pM(OX5qDհz: 5ʦәޑxQX={nZZv2TuLUx dYW1k~׻"h͟v!Uw}2X }%X݂Xie;v>ݹ X}o5zܹX=W]G}R^$%{"S[\XֿN{̤*WsR5UjLaԽXV@kJM&<lT)sW  u\u M:*W8Tw!V1X}TV;[Uq @+VU԰ [1dCn,l-X ->7fe|t^` =tjjx:]Yn^yRƋOwJ]|12|yxfpb{;4y!kV;w|Tj+Q,iUJS,O0hQ\K܍զf{k{PN Vaug'WukŒGR1bB٩5GUm:Nn *J^fW&˷H /VMAuOg/U`$ZB`A:긊T]\eE$_Iy~w{|jgS \|&4oAtmTPBjMkYU^嬀XŖl0Q >o>ȻN$L:7J:?سhpkAj)kywŧz˫_8&q{ FEσ-r sծfşeN{*/}͏̖̥?NQ奫ryaEvhrtaT : IDAT@*EtO)ԗ}%&VUDEpXp&ͬzAńb{S#TZXE"V% DɌ*֠$r*.*xRox1uU|\%5<, % [9vBUl>VQJOuGU>?7--4Tugyr5/=Ԥ5US?AnT(8#v%F 0>dˏG]G j#*U*?2UqywJ_<5vJ&}/_}ZX} hQcu˄3>U~D4Օ7vyscio,=+׫/|sǹ5o"~ܣK&|ewtS>uYOԓ4檫I8/f]*nS%.}S5J钱j;߻wCs_[fɓ='{z(kUSU>%5%k~݀@G%*Z=[X"*Ja+Rۡ^Uri|AjR Q 4l歚LV0gڲ6qFL56 &Ź,hKz :6xu/[3&xES\n* pz;+P]&ՍcY*L-]-U6:1 W}?X%NZmTTՈN`1 cjiCX?Lwцj]T@8穈\%[t/$)JuL/}#<*–r٭鞗]Zۗ'5+.|Pmǟ\|&䭖o)]@{<ura|z]s}̝[LiS i{FAWI v$B]&$fmn dg2f&bf4&%PƱvnpJFG1XC(/;=|MlעL>X?=5橭jVV6ٷF_ sTO|~{yTѫUn#nWV&HX=)i%*Uq`}R-\ _}Z-цWCF۬Am?XTmhh(gR)NFHTS~zII>v .Lj65u"9yěqՏ3֕(6fj\W#d%v~q#,3j1Ug/9)V-d-X}f"d_g6*Krmv2 t\SU /5ZA[#cTQM jq>kV- ,z";lzUm;Ӆ]Qc̟PgTϟ]_R3iZ?#ʺ#DJT<臿K#t x uO/ R vZO_5UkW5Vssٻ^wZޔޘXq`H/! )LU*cH,!sՏ)U Z$.[\ZF <%jUlS*knBJ#AՏR#RأБg^f:a,qHEW\oWfΑqź `Mw[9YcZor1/oŁ/Uc/n^Y\z5TŰsZϹ O%6[#[ZXj\ eJMJ*dK~*sZNXYԪVNǣ.PWn,CMD[bЩS^Of0Et.R2 X6Xע<\>)cy="X,o5 Ճbu`]'k BZj_)UDLKU+UeSd~bzVwD7jPXa3PtܓfD bߨJҗPS -&RV-m^3ﭺvZIUH| pt{6 5=VOU!Ջx *Q3ߗxb2?)Uƪdۃ !ժSg'>:6ώMVm:ָ7cߍݻ!ZY X]s Ī onpjvSYJq,*?,kUVX%jU?YTծbժpd@©J @> V4lcZF* Kdw\]W7kpu * UMU Ӈ9U@d$}FOT{޺j1Ks K+_Vk`m[ɛ3"htgN婴kg/%` ^XK`Rj_=@ R(^VfqWK3UjOV 2Pe ‚SPQU_ZUZ)a^8N7YMj*3v6FŦTfv7KC"mI &F)Jbb .?y9/歖i>=y䩼@-ҫ]m]z⊱TFk>! XX}սWv6X|'jGMӔ|>Ͽ>];<]]7\ /ԝ7:N 誽lVIvotwo,olk??<ޭnX@D-J/ 7fSy8ʎU @U So5V-*q~+UP8(mIjTbz @`C`Pj @@,zU(R,^ lZRU֛\jO>*QTӔ'Д,Q'+\~P]SJ|yEI{C"Uo35Ub`R&S]}T6- &:{x`}UVs^%MJ *h߈]/OߥRXe%<|![$5d+~>iZYJ3Ԑj)Fn=UQV':z;jgkk}}wW\w ل09+͵pW*##Pc!7pJwiP+p(_c&WcTBiz_=VRD9GY>LF*piGdӥ1.*؇:O]SRvp| OX#ݠa( ^===h0 cA-Dx\x"\V!V4:V'ޟfT&Wͭ7?/J)7dtqh.)3VRͮeUpM۸*^LO)Uwπ^T?fQң1HTʽj*=!X[JXύԼ sUE:t)ږk!sbNg*~j\ "+QbGO37uj/2Gڈ˪B NBTogྟЊtUjլjjl.V\m_(Km/sK++;}S6L> UR,",{`=OXM H*Ֆ{:WgLK'uhq&Uuw[>Z}f;"|wPCCYRSg(#I~OU{ T"Q` b:VSOÊԖ }!L pb$u8 Sbj+,4aœժR!ގolB_0 'sU2\솁XU5No&ոI FY#'bm7$Vm+FjR:@w՚eQ/VXZj SQn"hHoDPL5Md$6W( V2L;ZQіOHA fo+ $L6Uf5Ubv0mLRuV'"[%Ki2*h!9WjDz?HMkiVi*K $Vq < W:5KUOZ IӦ]V٣jq1/旮V7ֶOuݕ<}XmN*Qum̀:{=' (VkPGZ ժn^Wj6 xPYվ|rSXmJ,aaJoy_hahwG`*mniA50*ԝmhIyRӅ4L1#/eN77 MFXL SQa)YK,{2#J\ 1$}0)s>zq^ vH~Grى2Mq=d=KBv8T~gdʮ+Z{jk,7%XEUZ=X,UՆ}V*bU,Q,nfߵŲQ(H!*EB hX3& `2HB[+Djᨅ# j"h\`ulv(|~۩;k}; m|blhja2cVd{jRYκJȃUΚX3]bN( `+!XDJʭ6^%]ZU*3+lk3Ĥj*k@02\5Iyg/԰ݒ ' hc5“#Uq5bu` IDATjv- Z uo`kx0̚\mX}¨5CuK UmOBU+W&(5VPGZUi EbVhr~P~zR9-&k ֪Vnv ZZr{-R"`]b ΏccћH|OޤWX5Ld++CRE֕{w6}I`w&GQU{cxYvS@uxU6VX70vrGԪƪEVCE[4{cP-Lx~yЪje(^A Ue)׃*Òmʈ( V3(A7!`Ś/rVJ\\W!iM;V\Klm۪/K. *b!5ԪO7ot+~i@EzCU<;zYT ΃UP2+0N$ ZU/~o<buyåѱx4Kp@t,>* pTZ\*?K+ g}͂u)vRCtT:7K|:;o;N>V³]l(+.-{3= u)~, XZazFE+@S[GcD g{"$X +PQխ mXx ILvxb?\eXwN= ^+U+R16SkJ)G=)DU񰨸TG$XՔU2!bp^ֹeW.ajreLG\bM՚,5YMΙhvwI$}]iRJRV Ĩ`߯#] gTӫaTPC9+Y_١U {IZCxlH,N]J\Jsc?)]A&N?xxW3ͅKg|8e2wزoy?;9[rjvf}!8N_'3%Nl*=N~ ՓB|̶t@`SqŜGPHG}}oi[, V-X-k?T)@h ܾbfE\@@tŇCӂZMޙKά;PN՜U\z2h8 ]Q毿Il*L5~τBo6ָdU|';^8mUju*>|,; ;źV=[ۣJ@V.r9 J'YmZFC"R.:\.b ûʝT+SzOq%TNQUc5hĝ̲WCD*b,X:] 6~.U Ʊ,UHJWe}ks5w]O`vY&i`U<> I1吇! 0Zu;C"\n7 R}jըZp1׵i$ 6Oy*XGҔ`x(SыgNG]8_.4%C`ID24=AZNI&EF\XoNmAHE-2MYuJ3̲Bbj܋Bd^ɾqNnue=MsR}?\Ͽ{Y XnUcB ݺjYv@#0nL jo+q>[}\*XPcrp*`50ey v\ .;VׇӖVm`PvEU\&jxnݚ8$V'jZ Ôjsz~!Vo%N ٵ( !V5UEv*Z4VN=3JՂ ;8ԫ6*WXr׺T[>V}?V]&;VO ovj*Jo/W_H)Z%J+jVǨ @5NthZvv:`*P7n\25{2ݼ>;55u5; _;~*jח~"5c^V!4uu|]OPzBѺ}bl#.ZX^>A OoZ678cU|Q'V5bBRV xJ~S8H@fH!$Y3Jr0FWYe8LꨉUqZJ.#FKl"舑"(?5r]t]`ՙX&CU쯢,?U+Wb,X:ҷo1ιZnpDo7Ur8o9dBJ&% 0BUVҚp8(\)UU V*SX 1(PS;V; ^,zs;)KNVl{ c5|q6g XV~*kG_Zݸǜ!^U-?_~}e1Vy(pXm9_^o頃M]s]>VVg\E!@+Y9Ԫf&ͅUb5.? l[VXm*m\8tC.Q=cE⑆d!F9g_j:w{0j,-,ݪ4W@@izuƊJ"j}hHkagmj pI$q <`*cOd0`V\y>cVnFzS %Q^4z-Vw/^ّMZ=(^LꢁZj13 ^u) Vb]~X뀗3 :!cYaUZݗR.]V-q॥µ}x8reeu%8RY]}_ڸjO}ub V8 ZrP P5j J(8:1 TDawIs,@]'Ԃ>+ )ͩch-cP>fhZ:b3sjPػw/٧R50᫶U$B[뙥 kê!W[E qAUTЪe Tw*aP7Bˬhj 6FtmV3*X?buo?e?+kUMծA\F3Raª*hWٵѦq-.F*>&ObyLin-P0GQÜdda?P?uL{D#/ :/٘U 旱kW2DABIG;}Jjl"Q5NXMVA6aoOkgh*-~Ty ,PMedMrLoP:+pʃD.w>TkUFUf-!YTۦuANvBXZ;mv'g1SdlEv=SPs0@p78umjzUʦ*]ԉڊ IVGBZ{E7Xo"_,V8D%GJ`>k=BUjܥKs!u*R $_*-'y:#H&[. 6҂Q\JXᘽU2WÊjvWpUjjXs :o@1t*t:UsQLU*u`tVZѬF4F U AWX}ͦB57u%Qb48eTݿw!V^V_sH5a *#ꩆD=l X!6"75i8z.?Yۄ9ڌZ$^hi0% Yվ& kUNVvI4t6YCU XN՜v\5azQJm<+*z?ͲcC<)mLUZP @J5|,YfEf~CU:#lMYNeVkS)ײŧ`ǢWV];J2~Ԙ:ZggR?ê*Ze`O\R4dMћVe@} 4fYX})xp3 ݙp .@k2Z%OJ᫭Jp XF(DUb=UzL\p:yN+ p2j]bp'|"-ܷ0ɥbU,F8 uY6]YI]t hxk~x"vD\9>—𵛻[MXd{MsJ6VY+nN^G:jsKb5Gk;"} #XmGj[UU&1*'<\[C7JA%"r0N{EdOHE-T ޭZu-7$UJ9@vC2ufI5eC;-ju1 7w4Lj4/z&TGly[T-S{[ǚU#W벨kVin@{^#Y:1 L]csuhbuV\ 5\lԪUn:OH~MhDjuWꭲZ0V>HA+$ SPcJĘ'D"KZ{yoϻpbgZϻרK*T\0z!* YZdG ?!S^"0or1U }\1X+jB]{jfk16MEӊEdȠ)"Xub7ULPT:3a"=̈e3h1u`CG,f}Tb0@n-J3Y~N [Dg;CD^E'(z~(fUQXîO \gHjE >zU>F:nDXZ! T8}ٛ+n.`-Vz V'CGʂUۻVQ *2("u%R|VumB~noq;9`<4K.SZ,jbeY;U" 9,NY^;EVM򖽒@/Y1Tr iZN^Ur0}w4~f'Ep \qfT-4Nokg߿Pq,gJUWo1l83ʫS'rȪ7xXZڂEi{rSQ?(WYAiS =B+>:βW1[^n3y*@y#qXYYBgeՆhP9뮫fև믹ͦ/RթաP_L/XuXX%s2YUzyi#xqʿtUxgeՊRH>LӰ6=%Ma5?+.7eE ;jpmN}uV+{4FU a RU<ge"VwUcZ=? :tXy8}:([VU"WIV7;BVO'`u9BS&\K#e1X2>sT}1*۫C* j9ViUGV ɭVX\U59ƩCVɮgj#hU tm_m{ @X]W[bճڣj5,+:僌0  Jզq<ցxU=ٜ X Fh> S*`U'U QcrԮ5DBSIBfǺ4Jʬ]XJ,ՋUrjdQJucC1X>'pXX|~w*o X*Rܵ,t/Pv]ڐ*3uQUY\jU@* U֪Uݔ{ҟc0]v)V)VMEH"rtUªlG>J@恸y)TK:Y `!̴U!VXߛ5aؼ}^jZ)ygS`FQ BUu}!+.O(X {J6(AWGU%1cӚےFS(wV'E"Tl'5~5* %UQ=TPBMILt*鑮مx:}jU $U^51 }j"IVHlV:ѵtUnsU"[;b' k2Q*8cţ$ dʹiXE?? X}jSP<V:1KL"o ,lC;R%'z+@ՀgFaRQ'3>1mڍ*mĭ*b\:ʿ)興:qQU5թ'poʕ\4JT)?v%]ERuVnY-e@ h\ŠԪʨZV38DFeՌKTy[*jU/PWP,VIt5T%*U9ƞS4xXDŽUyLVRMK'SںOe>M"(,PZj9pd.2,ZHnմ)~: v,TvՔj2fF\j&FǬW(,x 5ij㩿@V]iVUĪgj U̗֚J6GHzju7l!_UK#V_Ьrh5SqB RիɀիF+cb M(헀/UѪs yMIFmH@}G)*qZjK"thiɓob"U)Q6Xހ+]'@_ E[})Ol/qͤ@%u< 1(W>ZoDu 1K:Ij!b1%k5tJp ``T)TuEǧ'-8ՀX6:*S `;z 5T>| SU~H X]yqѸ4^ V++ `U,j4ŷV4 A39-jU*?w(.Zm\4Z]5s JxEn[wI)~| qV Ps C;'d= iTY"m܇[#6s-kϩm"RzUWD`+6OzP(E7zBJ*a0"k_GqiT5d:9 F҆!RA!.ZV=Qu5\*KLZ ȖŬ K5CJU@Tjx@zu f+.ju@TU+@wTbIE=(W3BB5v8<ׇ=]Xm˨z.PDVWA'SFc)/"F3UBfe=EVaZBWTSwmw3RudBbbUp^mk=`w\ߙ:-6rJ, zX-hzY|ZmyJ *$ZvO yڰ+s&ȸ?m* *| y]së]=z3+TI\Ŵ2i4]耢ђBvBɯ~˹`UVJH㪙FT쥢}?qU'pBʙ "ێTuu[_RI]TfA*'OLUgD.ʢ` LU)幺FfuBpfYEXRiZV`M|Śz8U>nMP!/SA u9PTt_T!@J#Lv4z,d:遫UņMhؠ ո A'`߿VZb/V+n:ux!`? FXžU(XeT VZ߼E]繟7 |^;_2VX^ѳꐩ'CR[HSnO!N $c|{+Efdoc0:}O%g>E5l[h:eG#Ԣ!׷:5GUq E#{7\aȂ\\\j'ew7&d_֩ADq<$a:?0n3̨ݺLBIj6MIjL vӠڦ]k6,Y$3TtA{Ͻ3 s煹3;?/yWo;S/3YE\*PNP#%S cUTlaIvY%MvqJ$&SYMyhSYQ Rm3;̱~sNU[KKZ`] - Ukt+Vi.UUEԪL#Zb.[U`pU#{>4ߛug*49L;ew^k '0,I` U kkk;:Nr)b*t'KCN)W-YUwm/ Us;T3Z귍W AQEa<#P<nX|>ŽSTsqp_DYiE#L*rNhxو߻7ß+ßRGU[Sk a|"gA$^"Qy":4#t2-Ϋ#EHޒ)Vg8wJqUa"g)pe tq?]իR4[ۥTUZR<\ ϊ:"!a(Fr`-5ے&mcǺ2ꭸ<>ĪּٱbY\j5KU?擄ݡO*ycyږ~a)! ۱jpBz C=N\XEOXC@zXX}HF<UXsD$kZլIl"W \^pU*%YjG&'h-J`V0imU.lqբ}X/ǺuOpģԏX}¾`O,'*["+VvRAu8'j-ߔ d*f\mfVݷmQJQ:tkVxJscXTy~=1]O|H[m.ɸTEzuΩꋕ <9\-XM\_VaUb^ S#^ŕn#Ś}x?(tC/.ޛ1شp##Kee#u&ha՚d3Dk 9sV)`^!~hR$.QU6 *kE`6rUժ",cXb=+o޳ zVSGH~fi9X]5LJfyAM# TaQ]<U}dMb:{j[i{△\>EYZ%j́'@/>G^C1ӟZ {z3qjg&TW3,ARə?I_X2+VRUV|4>ʳuڿ/EÕɡ W7}2bKPiOLuԨُb UjsVEqAԩp P_U IVP V3~;ɤ)u'^=ek}ŪʵWF(U2faUgM[hauyhzاLU\?%kP ^?T:3-@PX  TDu/UO"V~MX-NX- ^lo|*Xu5GM6Ub XMx@-&X)@i,RV@7r"ю[ CwLwz㷽_OVtbM\uqpk 44@j+ b*IZuU';`\%H2L NbZ05˯P *Y3iyS[D*quթj_ W{߿I>֙VnZ% E5Oۡj@(p,*Eٸ%KEm9 ZUkhYxҕԇs.~:8b/60_OMmcSxFV⹩G3W]i_ UD[L}TNXUnQZ콟q,7+:-EG]EN"N`쇫{ n[OX*z~0:Z U!U~9ƖN*UAv`*k@+juXbUNUG*+ScYT3~B +%yU7FcUuy1t9Z%r1LaVMJ%Hg,԰Pb+NjKa0UQVyĪTUm5PUVP=ӏ_L7lH<OOƻG˿8{&㮁~JчGVTܥ_ot4`aM[SOOtLmSޞk*Kr)OVX*ZBo|Oύ7 XiT.*NwwU fLV ' r*eW ewJxrzo IDAT`*JttMNb8pm,V+,{ǮԈ5Ǫ'VUWᮌKbu9|#koao*ZFNS#8`,O*%P1jm/Ui?PsԜ X:<||8܆VVCYXԪ:wa*U&ʡWU=V=vdY)qS(_ 9*`ճ@v" YmR"lju,JxCM"Z- wrĸUK5WcifҴZj3ռX˗ Wm/UVN?.ylM|s|FVə'oNŀj9cgjTN5Xu8ӎXz}<=@C/n h/oaN2WYjnXM:#L*mUj/Z[)_]P bakoGJm03SDwOd@fRs3,:!r-0||s,)5_fY 6+ʉX&;_qRn첫 ,-*Ūb5 ȥ;Ш;fLl`&,n(8N 6a 8@DS"}҇niKЗa[h*>&J5ku9{N2?wFWg'φv~?j8K/6Z8#oxr_K|P__% df_@dPa?uÅհR槗f?bݘM>}rÇժGw|͈ VgΒju0}½oWV\NVIfz$T3g@W\zAbjUaR;MZMQ+;a&@up`XP l%Ձ8^A y<BfATU7׻W~W]ZEVs6GuFTݷ7V\ @8Te kSwI.ªNe*p+zrKW?=XMSX]|xjzJW˰jנNoci,XMp>^WVբFdh nRU)3*-R8Nl*Z*ol)$)d)S rPK!:080j7`*)?U7{<=C=_?V+3+Vo7Vzff%j. sI<4p' փ\MBUǐ ]$nv)ZZ}U j~ X},M ^e@oW悌~q`A\8)h%;W' "e棣{$V=VVȋ8h\ V(U ׶|Rn|d;Z-ĵ4<9 Zl@t*])[5kE!.oV{q*ZA=#E@+I1T`Chhnu›~wgUupPuj5!*'B}J6"6!h\Av^ץ!WB+vJDC-~9ה.~*67s7V`*ֱc5pϾͭcF{[>v?4E?&ce.%=G~2('`E :UVσБѴ\K*OnTv@"Bia v!WUjWĪ8ZjҞZH'Uj c\v` 73&NakYOW]-PZuN Je` u6IX #jR}k"^7 WՐ?ZaНZfQ:PV@j]eՁUMJIvQ@\6UT*~ߐtnX~H*(*S.4wmNo\ŝեc-o.N 4ٶc?/~w+xoz>ҝ@Ջg3wg集2`52841sӋX5͏)y)|z4,y~tu%ҌcҊs2U8B05h(EEXUTbKWUЫ\ 8XU8`5@5B:Z989F>œl%Uc:%eFi _Qk{]02uaU[k*kam7UOj'Y"zSc ٬JvY^ƃ2C%Vl*3RuVN1@aX{)]XeZXW|ْ(NWZJܿC]qpU CatFXMe (Yf'&5l( E[[45aeSZ!V%F}bC*Vy*Q,I&`WP ,T|JTm/nSQR.:V},VSZXЉPվ)G>:|Rj;6Uk#4:ԣ|5~ UXloѕɳ SHVvu"NkDŽ˞ TP@9 GUG&2XiN R&>u^ru 4G Յ XB8}J׹BC:XAjbXx6UXUKzM7Q|2_e:G?j:=9 R*֪lX+)Kg[! +@K$Ǣ@T!JF5R.]%_U n*#[ fV+5Z[Z?:el"VaUfbujY]?MX%oTVU[E*B5KeYMF#@YZer y7:P{zRPV?a*;deEPUcTc@︱jO y31#f**GTDUªxœ+ kQUZ!*O&si*_UaM'%VQ Z[(ZC0{U(VX)@{AhYjd⪍Uj^ݷ]WEy~:U R{c.@5|nMX ݳO PMhjBW%΢-ͶYϢ\}2T~tQBW[H^\u|l[,;蓪-vUVY 4W$-n V#MUDz[^_\5Ab)VIfxؔtQ+hb ڬ RXPdN5d%B"«J03IXB #MWNh&ɀ;{]C]YJi SjOrªV`XkJ@҅(KBbCo ?s(X9U\@ W X [>}ITȖCiٔ=85f$BU-e>W~YFͫVj^jV7=%[j͒0A LVUBujץAUe_wC{{һ`cWy|"i /UjW%Gqj0b*J U(jČxSrL?3XvFXQrUyZI ֞2`V̪8Ɗ X/Zx"*͛&Sj.Wi8L"T@UӝCKT&W0kcu=g*s0.錺` Rb# "TV+6UAPM{UBWBwU TMeS%ȳcW:Vq/Vm^]V@g4{Uj2Vx٪CՋreeXtdzڱK^Qj*xZ,!*+%Wr7M˛⥏/"TO#Ty?FSŞ%z8{K۹Deqau_8TU޵0&FR15kaiu3wU;s).nkXӨߪV0uN(` V)UC4Uh~VQ%*\o]aa돦_W,J@ЉQ9Lψ .[jQҎϜRέ4V[~mPޤIѷ¡Mף*X%ď/=N'ezmdFb*o3˃bU lj* M TUQ&1Y8P_uêU]ZDմρsX4[0s ձlqtФ-VePRbpJfmѱrca$@ĪZ@*ViS9XqzAp5 %qs W`b1$Rej *3Vƞ澡X%U4iFѷ'fe<|bޖX&aѷX5 M;`ܽB]ǸK嘘ZeE;ļu$c,VWF6تD=Txa:z5\R5]6 $‡ \h)5ug*JVԭ0.-q[XuF`qU#Kӂ(Xnu9QtkٙG?2T im jja65!Vah&uN]sݲYw}b5CfIwE, K7u',3Oii{nn;*s(K˒` r;f^u]spg{'Yʄ|Kw{{X֓N>,ÑˍI7l4O?m>B.IBL>?s^-#~`^aUs+Sw tC !M8:Xai"ݐwy-cA5ےEJ첀$Sl~9$*˂4q`q2X-kWKݱI]rz,޵{z0< |ՐRI:UgXTA1r 0LQRVb`0x7tR0Tt%wq밴RN?0x 8ҹ]csmem7ĝ6T^oJI9D=dy;Rz;maWN܇u )*UH(E`%rDUE*jpZU PAUT S>tx,Ubt@V|okʫ;vHXQ 0qJ'\PDv?*K37@lDƁkͣf*N,t`g.&an(͆G]@dqHh`q5װsÆֆQc,v}MZy^{G̐Aor}"點'Svr`:6%(K"FO]s-zA;Nc!-k]N10-Ae͎ٺ!aRkWZ V0JU8_!'倃zc }W} Ph"V[6]u)(%A̸kZ#Dol5~/g}^Yhf/.Z/]pa`x ޭձk3ӱ^0mM qӑ"FAHi,jR ,O=Zzxk5( =DR3@$Zv`%?RfɁVj5a՚wBժXgvS!  *T ј`vȣ`ƂdbYyXEzA@S3+9ү<پj֯l>Hn_|`t՜\1~Z_ 0TI{ɜJ̒Nt u6[:7:W|$y\{=Epet.; 2*3& U[)XiUC;hX5D`$G2hwTT;2ſqߏ=K(S㩜T{vpW.l-oBkHgf UY TԲgjjiJjޞ sH7̏feo4/1y61q-F*3xD6W$` C?H&=$z${$j1p6T/ 8 ) 'IMCw&v1Q]i=2$L$k)et,fEL!MV,IWTLbM6IfkCMt[v0,çs?fi%3sϽf~9y9Zvo_[ 1.|cLKDPʙP_H=E.K7L9? azlT^܃'տ78pviti=Xǰ/-xms*ŭ~\mF@ (bUSFbQZMWU۶nRmJu=Tj5\ȐD6 *]zV\ճC~:fY3c N˖۟g53t ttm2]`xgFjkU( ouFoU\ł񀦽?!`x( oYqVq^+$XS\֔"5z\S~{1Pz]M3MbމOL ko/L֍ɃZP 1ܜ\DsQщҢMEV JMTЊU(/Xhv4Fwa*]?>9?NnG} IDAT"VԈޞ>U) Ah Kljiusħ/&ct˳8h=9Z 8ȍ5 WfM '9BlN#p-R*4d,cN,"ĆϧlH j@ NȆX]2=+ucדּEr@+bcV![ {)j6Tk"'!/ם9_{*سyR8*hZuEj PS媩TlZJb ,QZhSɓEUbK\OjzzUy% K,8ؕ/( ׏*s^YD^a5S >חBbʹ$zOV(c")+ASz /P%3RJ¡{u\S(dUZUE>L֧bծ[ @ z6jj k'mj[n\(sKmn&X|J6 zM&wEElXet4OW1'O+Jo]޾co_'NB,('}`ojHs߮:=@%Vmڂ-K_}oJ֋ `D(UO2V9hׁTKZM@6mL4{("Z*J NU*N2]@cn-̳;*LX6vbXUXERn3M9?zhKR _#E{ Tjyj?SV?x~P&B+q<RZbs\sb?92@}Ԫjժu{]LUX%*:(C'aUU#2<g&\ٺ {!Hjff37XZV'U SS`?Umz.ফtuEDj_XekbcU$VZes]:F}9ժ`5v:t.bPZX-/]Sc@$bh:&kX5} \J{*(ژUVA^c6r\&VO cQrZ؋W>i)z%iV F -ݦpu&>i#VPzP˛ j)0V1\9GXkfZSo V5Z5oG%%V]H:ר).GrmV-I7:Xj 黐ݶW W@OlMe"j(7[T̰<5H@uZګa;lcYBEJ6TlD1&5c,UnS^e"%+JO7-\LkSX}&EIg.Ymε;;#ZwF(mg䭦eIf@(jEXͿ?C? eZU :Nжd9,#j -+J`ݵVUV4VK-kU +U. -g9 jjn[BB/bJX+͵QPWǟX^Oի5OUq:|I&pd8M@bhw͢]&V-Ux3_|;[.dD5U!9'cuqV 6# g"xgVkbSqf@YU @b(y2t"N/gU*X% :g/ X2Vsr'YUfuʃ=dBR#9j0 xU$Gj-c#MO$VQN%BFC9FI|ս`s˥GZE㓱@~ZșmXTM{@`ʯ{2VԙsFߙy}41m=R-A5KG_I[{dU4鴌Q-@Ӧ'cdy;6;c=\|`ʞf^Vȭ,91"t}elȳ]M] x[߬,Qȶnx%kR6ɆJVX(!~瞓N{dNGߟ?yŪ#XU޼~n74UծǗxL$Mk@S8eu:Ȝe饕?eV52j9xŭG:C0$U`^iK7A^#_jt~ }*#xlo5,Ckm_2V1Jj^kG)M[ YVYQZ5C<)+ =oҘUS]@W-2WՎrURe*Z ϮUNAEͽPKSKKg7> `[ئ-AgB HM_\ 2n#" n2Z|juV^e4j pLJVVǵQe?(WEfЮت!K@PG֕#Vwȶj `׉N܀YUnllKѯ-@Ò"[ fP)J%/XY+ЬC<'Jb25E&(ʪ*'յ:%1`*ŖhMQP-bjJA^C Vi6rTX]ۥVg$ hVXa]EP* K?wQqbW,NN"t^[ IT8p׀2 @fajbnC .y7c~olxh'^mNӳ e\ԩVЕOMG}~RAO52vk姦ߖ+[,pzq7}o++8ez#m\@%^+ď?@l0+4v{>?RHo"5NC\EIҭUU 9QCsQnV&ԨU\234NXW"VXm4V =?tUTE`W+f:&T;|,bzHUczUNaݟիcR$𿷷WVE\5"su\Q@a-SU4U*NeVa#;`H:PGd,Si): 3O_ )uT[ L>9KʰOw4%NRIW 2^l eT&@_@*DͬTVvDEHHp2DRƄK8XuYjɬbp :7fglVLMp(S-߱R gDMg|+:¾4wj՝;cܩ+ldw*ŒSj5UFl1X4WCR1~:NZ! b꤄CSV=*<%eq:زzlK^;c[K+U6cdT IϷ>Vcu [L`*UjkpJ*ա0mU JkTR)](W%g0!`vE"si;bzV1ƪ ܿSjUj$ cUR ԫUϪEj?U-Q}*GԪwTo* 䪂K=1V5le"X6VYZ0`2hThQXJލCCd^ҕ~3ԥY 'T+J@U}ȯ.D.>5: /;(Qiz|_C .:%<_e6ٍ`Q\xn{nRjSpyb"j<`ҰF Q(kXX`L75m;ӚMㅗ@Vj6C * |*F֬XG.V;y U1٭G WW\$¨jjNV7'uQC",?4\MVjݮf)Vj(PGzMSn@ɳ3[ 놰TUOSuSXmha.k F3GjVVl KTF'*з#["VU̓$V9 Vm+Fmk!T4=:Utaz|pv&30Zy { 2QTE TO|`kԪU٬Ѷ]r[ i>]90yAnB/U[^ :l0V* `j :767ffBJmq5+5Ȁ,\l&kEu@k1U7ʊڬ1\MUb5QdA5Kꄸ)!W++  NS^X tiSyX%*ʹRUBEĪj(XZIm[2ju gPS5S:XlvbWי.Bu֩U6Ѡg,^BUZdn=U?pdg7xV~J@" `qzC/58~՞x`/$S|\z]͟ɬrDcR!Uh`fzbSVUjU'5TJVšAIxih=[UST1&yѓ*B]uꝱ*hhas`ʮRV*j/l?u0=VUvj>vqU^۟3e 8 T[߇7t:mXc՜_Z[?YE6ӑUau~1l- W eī8\.ʮ=0U8Gk#W7 b*[ IWrRm5dΏM鹚4V]A)UXZ*r5qrTw4\߫5+V+*6b̓UTE:u|8M!X:H*bq t=1d u^W3GOgu Ujj[nn V7/|,wiMX] yrSbZXiTrRcE+*brQOfTʙ~Z@2v^riʣU˶b5 @r`5T"XufJhij<,Ta55nƪPa@ `l-67VmsH~zIQU*5tiᔮV;5~ӏi)[v44Xwn߯LHL0R5V` '7`6 -ѕリHƹ/&8!Jpw$ೋen{)O`ɿ᱿Fش]wFӱ~>7+\heO8Ra|Hie'1ibHM2zkW%i%wy)=!0$wS|/H=]K*{quBf) bLW}0tEsT=&Y <-<ݧ D}Yq+LAEUVtBa jMC5CHu 0=TOԘ#PJ\*3V IDATJk1R˽JWIgzcS#,q`ug_CdY:TaIgVK@?yjVMyګ\En3i*.VjqQ:*Ys-@!PѲBsw/9V䫮VIvˬYxM!XuJh] XwSBKeFaFgZ># =ME?zg]̝\$*:emoDJʦ]OŞ;gxJ*qj*oݧjdIE(bx;)K~ynן%îLaU& ,^l"y(Z6X?}66r? @'lXŏ@s4@XbYpc|lxEւ^ӫR V.F*n@R]ZeT#S= Q@U1~1Uʷ U?[U%*kU U-s@Un\-*ɃUZY8KWTMZ꼺[XIz5QVEEAq}U@V {{qY**8>d0VQR˭\ v̬5M5S@lƥjW@O?7xhCC'432˜qBbԡ(C&l@o'DJj_Xe^GWy)zQe]k+N9|S]Xs>6駗T a4*y('<+:sdUԱ2eLՅƩg6l0SՄs&hSPaZ/UHVjt*WqUN31P OJ&m~6rG L]XC)b51z:jb3cUЋ@Ca|5PwUþkU^fc'V.RUgOXU NTj':~Rs5#PI u*A5P& ' Q (c5X!vq)VuLetoI'dBêOE?VcQ>cԧVܛi?y?z6>#X]auV( CZ%6ƪ$H̓j]'oXu~2\9#bHU_O,TIZJW_ժ[R%ћաj-A-6[;8Y.Uf}*T-&gA+xJWcb8.sehstX< ɚ*Q4, H``хnܤCIWTYH Y"D\UCCiU^v~ι\_v' OK=oVzv #XGÉ~ˆ(BUT7Ъio* JjuU ՚n\v*LYZ'Vr BXWYUj""Q-TTJ:b7 ,cWljHO"ׇiY[%ȨDEMNìVh/8INY(U6(pjEHTuXݫ" *3K"žUȹ: z]qO5Uw{,]sջ؋mc+`:}nȯ|tT?n{W(ã/_ǝ┞dgǸ~c[|j\C(r5S?UV?@Uj.VK!P%nL%}gQw]S[zd{Zܙ_GݧiXd1\רVnzpU t(Qg4>%zk}\Bɾ3r̃cʢebw8^Jlω*H38,9X{ׯ\x2Ew,JH7Mo?VԳ (S*,rݸULh@O`9dՔLs?Χ|>b+kª簂XUL`U$+e?r)*Tje*WQQ'zhpRr%4UVsPʞ+\VEAQH%`LW) 4lH3Z-_$V0AԋX)K*J`^bE")rIzcK}lA{HҊOI_~(|W>ސlTmf+9X"gU JF<qt$k¯=`jzjJb1_yo, NJF"]jdŠ@5TUh* ZݿJ|Zh͏sK{FҲRzqu-Mt3ï]t&)WS U]2R\ j5ue, }WBXu]ѠVUǪX *KY)R@3#j5 >V::JPRpUiX=;T9{[rWaL5z1@53Vaؘ[gŠx@*iejeTR;UR_m8Y~zKIŊ SJ7T| OcuP!sP"'N!'q-[jc/Qn@ՏccRV㮮}ժNzЦ QV9]ɅMaUծ* `W  &L ꬠ*N_ܫ4pXzE}XP\Ux oV-N+lhbu Qv+PelZIj Z`w ۦ5tS;t@xE1OrI y:HUڤ Խqܷsg F]7 ^V1j5.kITmRQ&VC6:%!`*}9 *WP~:VY(hU[8(W {Wa @ppر$PN(X##Ś0B-b` ,vUI UX9VPP!ᴤS+j @ێSL\+Xmsu?oAʔ`~H\Ծ)9lZSU)`Zpo4.rAw#3HES6P݄Ux&o* D@*NM:v%v6P!U`{Ftc7 Vxd*ɻD6UɆe*W~VժUd5-DXE>p: \9f~ *_67{@LuV}Ҧ(Wd6֚v56pUG6W02Ube5\m: xٹ ~SUXss͂[ҎPbz2 U谭FJJ0;>CZXbzu&rN{~fOQD-+r` ֣#.]+Щ[U_x=sڳc`ZoZjUjAReeV`kЫ1٪C>^[V|Vsu"媺{@+I-]V>_u6#"\bV@X U&jqpus;WjN{jOV E\e%귀^+&hio`a*˕kj.Ūj,,.,(H-iWEARUW3_~HUDk'Wq8ׅV QOH \sUE(fd@Q6&ydԻ|(6zhX&UTNWV7DS3ɌUHNFo%ҟ":Y"Ks.'޾X.^ibmWv|j1 JTj\ZUFtSj `Ud jPuzEjV*ծگ3T Ӟ`*}nSYT5X5:1.a++w>ժ Rj*УxXzzXoZm+82U2u,V,U)5D Vùq h j`<͋  Vܡ+z<`tPG@jb՗`gIU\JLVA\M`uBVIv,Vǘ=+h(s1xC{U\}> qNb@c^ua[heP%YkDV3WXЧ/_@TT&a95Saj`՞RTcR^s,V9 "kgk5mhF<%/VfVVg&:#Wi%/I( `?nO>tV?]QғebztĪ+50 _.k.7b%oh,^mH2Df TV@mVUúٕj5r^jF\,.Vy[Q`Z-7VoOթ Ռ*'qV#?;&)Uh'j ?RrtnZ6V ՅVeVYU`ELk @U.5nMQ`T>c5&;b +#+jJ1C T]99BUCwWեr1%HYDj9 Z*{V'eqHP5*`5W+,/ԛ?[OY-qo NXř`\-+ t2Z$X _)*Elz.RtX.>Spe\u'nCkBF2\XP7Y1:+Uds} VVr&*?}tY:Y?^}uzNUVU1UTW8DjZӯ UwgZ$+٫&cf5ojr3Z?U?*VQZKfiJYSیW׬D RYqwZT,RRWbB##6ֿqr.'ê3;jV{Y%4xQQF~ˏ&Z.W]w_߭6"V[r5rJ`@ Y,VINff殴^U*^mZ݆~kzMTJkwTS PEgww?T@\#V5QY XY~)j5d&uW`|-,| 0K^cVzU=DUYx8j +4eaRj2xڱz:ųV(ХNJyUhZu*>EqLҿ.VS ~MWDN9VenZݎ@KTJ"Vm\т{];VZU5ywoP#@KI^dT͘EqiXZ~2*:! IDAT!%c eDªՍpڴJ L%ҶREu>ДT`J aH2V:dlyʤ^mRС,U0d,~O7rz ;%LXjs, )^@{Vvv~ݪֿHEYEcUcWP{'1CWI3PExתV'cBf}b5?>Q*yk7Q/߬~=4ґVfL le-B|&Eyk\IR:A78\,ۣj}}+--Z-r@^DBd'`#&ڲpT\VZEU-aJkυ;;V%b& L P=Uwݸ*v:yՕen2L 5Iͨ6P6nf28)}kJX3*6aIfViBE}-:DRaL| /s3ps/r=s;Q)WS辫F2k9_l&cL/>`n 6 XAM`5/|/xUrG5(<JZdU|%\Tf>O<F@AOгWQ`o;vok|bZZX:u0CQ~;{)uCz JU Cj4:BNT3E2UBɼUU8 UΟ?;@a$Ba?3ǁwSU*JՋD91USMM==Jov^ftX6D^0*uY/1-*a,9FHD/`uR"'9|\@/ E)HaXwKoܰsܱjFJ)NBXeŪa $},_@pUR|*>ѫVٲJXF V1*djGCPjX%PbM,bpbڤU `@UUXl%hy18=dczd X fĪ9h:j0Tn{o;{J<)0y%ȗGs-\% Z׽A6@kX'7` aPdN1Gjo1UtpѯZ̈́Ri,vr5ZU'$VH,`pk1KS Ъ: U՚*Ah<jӀ<_%tJjp]l?>_֓]A>|nssj՜ ׹T ZRK1T%EsJsy!p'S1H(Z*|TO=HUUM*35=CGr&Ng* *fF-(^`mGNWjuz~\5fLs,X"ĪN<ӢViRg8ZݚuXZݫcpY+TZE"E%Y1JU 6R1AUl,TqkoE#@U4ʉODKWZVUMmZ`M3wq \eaꆚZ[4CÛOt8S;P Qmyn8K'SXժXU\6WڭwXVDž'[WSf>]{$W憹'9Qk2XA"NweJĊtݵo9^6SJ4@xt%/(S(ںTUvLkȌW0x 9Lr5Vb5*b3U-jկ5jrZZjUVR/Lw鋱Zn0UX5UآV+@4 E"z') mh hǂH&jIWru@WӰ? όZT\=S}åuYGMDQiT/MO}%9fjzƌӏPx ZipZ֯U|(D RAj߰Pn<Ϗ\ VZ7_gx.^x8cC=eL @RelRRprJ:duUAxBdS? Tp7!X6jnԷFXR5gZOs "U*V=U(nVUi VIVIĚVղw ޾VAX] yRPUD [J!X;8LVF(Փ ;{rj쀂Ij͘jkK(M掠auVtԖC;Tz;^O5IrRrz³:cd=xx~g\1۰,ܻ}pٽ38cZs>םʇ^zzpd|\΅XnI8F}+IhNhJ%xZ1hqgrsp0OV@ pgxxAx |s;x#y6G&0ybmw8~?MO_nyf/ݖYxs/̌Fqp+33lhɃe}ȚN0jci&F !%l2lb݄ %*[ U-B Siܰ !F_~ܹGޙ3{~T̆'W _ݸ42uSڃOfw'n ܥ~gs3i'Oo䛇]x%\*\]ͱzVwHj6N%.cU{*!0Tj̡0aءu̺r\UR^-%lG-^DnjQȺV\5XRnMVT-Ūz58H5UU¥FEZ൰ӭ `jQ.ȅ\EV?NDܿ]v3b{[wmo?a V̤ UCUY=t}hxG1`TQ>Uޯjj%:&*cΣwAVg'ε[%X ʧ'ݢ9vp/Z|3(W5-VX]`%`kGxAzʝ3nMץ l[3"oubu?)Qhz"oWĢ`3o\gјXe,*&j0䞸?}iiN{4uXbGЃ#5h&;1ND_1b@:_!&;xts0'|::0?b$)^?$<*,×s, ]lY#.o >>~v1ytW0ڪZ5FU VU L.QiHA'^jJ7jorV(jU#mn"TEZP}{{EYU\5jXJ=ݴۈ}VEeXMkVk}Vª@Ur`5*,bLnեi'Sz@j{pV 귇eu+Z_K3OR`#}ǍXN%S Uu2֧G_JR;[& 5?EF9ʍF ♴eKOPqfhIo$~w[uSU4NRB&< Vl"A̹nK |U=' ?g`\uVkͱ"_ՐYzӇjUzTfX,ߵZU[{] ~U2+ stzX`5oR)NcMJ>*`5ej YiVRO!Ƭėj埌5Lm񗑑3eX`5{fb``l?V[ckrw <:VIIC+W䀆Qa VϞ LDhsx4ѢW+?ĞșEKAzT{4[[ayA7kts(z0X hj zȆH53g# V(_̊`{I,rgr7G$p*|A _V+Φ9 VO:]հO<^ ]Z=&cXXX]]='G޹|FR @E'kJLL[(RZMN7NgLPUVw/ONꎸ躬}g)$VЩV VyAjdNePUՅb V}X?)t)cFcD[Uɇ}K~z@EKUںL>^\_?ShCqJY w-j` UC^z/lU565mn}V5ઁgUZUj]UbU_erʌB X\9hnZr*NW}3[p/ɈWyɒJՌN@ *cxy}T=].UK5ew-/x>.m-7sDZ'ڳBV̠K~ZEsz 2/m~:0*W.`SNf~&V +.<LU'N`!mzUŨT [van Q!n@tS8y/u3>AܹyՖtGg1%  m8q+'[|^/ܦ#a5eCY^ĴaZ Jv&h}1$JG?ʝ6=yx-'XͼpopY+VUjӣb{ISܱ9+sNJlT9EՋd0R"TBHQUjibbM W |_\{(_n-_ب?7Mc(ɫUanV j/m.c+'.56w>&*q;:'[dzVǰΆV~"X|Y9+m` ]pJkW_!x?0 UF^uX߹l5vwVy f~o:?6Ї\ ԨpwW|ܨaug=^{ ֮8VIV7a/xnU/? `63aRRWb3U)̜zq)ߨ VV VWD-{Xj(2Y}`XH 8S8CVخXccƱ Z&ʲZ(Zb$=RnUVjUH U'g`#'TBG :ş0 (4bVa鰡Sg`e$jw6rcĢ5i:HjX VVTUbu#FXxEfêVxaVIMg6x_Cz)_Fn2?i ,,7Xj,-EAV~ '0@bl6Z j mSG:3^gZh\ݤw5HKN>N|Ni7`U~&n&u歕,-6oqզ:e  J~'^~f%}(ӝs'ީ^SSq ^h%۪l?J2u'0n࿪Pj/a/HW0;)I [9 L1KcUx:1SfP [W{`uZc* l/`NjW%Škw'2vFOhnZWl\%UV$%mjyQ*ca'O(bȏ:\D7@*ڀ;V jrQ*@Uj}aY]5[iTLp(vh04hٶ;gYqۢ+&;bXUB@Y j\fJ=^+tb=|5*6PGE`t XUb.*0UJSS ğ>+Su^XJe!YS ?݉TgH\!vVU UH`R~[jViHH@ hV,| R*@f} {2Z+Pg4bkLS$DaU(W~>ZB*Ou?STV)FQ{ro]km[uieRJ;ŬwջJfXJ*UN 3,E秎SuUǘrz5xJ:RE."z@|)tߑ?>JEVՎ!ѫ|9V:ne2XkxT$#N%԰9'δa5֫cU|?#DJdMXLZ:Rz`ݶ*]T(G4Rj^#Ug`utXiX$a\ jUcKG SZ^c l,f~U)(!WbbNw$^V_Xu60]^n&-@ؚ) OdGm<㸒lb9hZf"*WSmi4J̚iR$㢻ƢHacڄM7z7J)\cRPҦB2luqHB} 0 *Tc {XݭLT9-!W3RUidRU&W;EЫ) j1Py/TKՠiO9վ``%j9mYUT5aLX?[jZ?AU0k 4,gvVbjUa jV{0se˯ HڊT V*BtG 5?KU RUJaX;WgτW׈յ4VUN]+ScS%9 $BiIO%S_Wj=P*|f:1ywUbZ3"@7EydJUxuȓ墸 UW+QJJ~*Sn)TpUJ֕U[/ ĶKFqu!W[: ɖefUka3% V.wfRW6VsM*}NPX~*Z$VQb= T(iu OBQjuT͒#h[cGavjr]%VC[q6)HTZU ^ܲ1IÔ[M1.XMdjZę>rTVq8rVqJXϑX}=HJ/eHԑ %:x!G.aQP۳S|ߢi\ӟ0VXm[Zզqu zo|!W#LvcΤe2@g9( ɪJVQXM,aUK:@XH~W7#5Tk GQUPg^UkAɪ:RijjVϮ8ds횸!䶦oѩx- (p0b1q@n|̝?&/UQ'ƪ:He2JUBS>G_**gTeUu}X VKz72 6֎ RTURV}LU^S"2Rɲ*.,W_%CյbSPXAk%W-hBRZY]jg XT4څLEc5bJT UUHX=C\p*V͆^UMqAT_Z<:DR1WQƾ?]׮4fܹۓn= Y>Oso.N}"dM]81.N׸S';,)}И1Iӹ;4ԅ?ݾ<'0x \ cV%9Yrƨ+"aJVꁧj.՜ЩW[gUVKX-RKj4M1VBj5]RPO PpWqU1wzmKV7CVtIY?-bjI-UX⪍UN\5vrICFc뫝4j5VsPТhն6*RL)WXEj%% Ʃ0v#PN[V`@<3#&U'3yTuٹņ9:2C/UĽ6uZd_9]ff] ºfq%֢@@Sݦ0(Ep:?`xgC1sva^R+NwSEg}LGn^Lqż>|NЋ6bUAk@86{#=U٨*"[I*ýj NٵU/ZU\TAV/K URq|*\]%XqREb?TW#:D|)*WCR9U2+GQ{T5T7Rh@T,SVӰ%V[XIL&8d=*S6XE# Ī!,TAEVZ#j6X!p]"\ P5ő$*p=:`J˯Aju3ShHnXٗ0cՁQrTIZ\Vy  >%Vf~,̿ԍsؚOm$Vpԍ>_I|g1DW=ìAVSXT7'Ueh58@4i;d{òc+T.-Uk Y ? `bUPPU^?íײO5+smlݗ[}F]:za׫* :ok)ZͫVVjZQ5Ъtf0U0nhU;*@U U_ô j!`=jڱ]`s~ӏpАTQS4Qy}?T;_FtQAٞXQµݲZ%޼'!dTV.5?١' hia/g]Ī"0rpTj"V!VT &U* V0qCZK`_.&PC15!> eADs۴ė*O.mZNͰ;t)aOlP͐Zmu{7e]jՇAS-j*TZ4h* J)\ULm>7 y5`*B3m`D^6XM- fjkZMe L!ӥWAGrXustvIExf o \E UW xp) )g-D1~7ѢVQm\u7j=T'q?+xrjbL>(T*h!l< AJ,n]wb b!yY6j\DՍ*O*RYMQ|;Q}V_cqv w&EZ&* (}RS5U\u=T!QECU\Zݽ]SjɷjdumUeD"m+QêcZa5rcըWZڣ TU&vْ}!bUb5je0~={ JU8 ##I"Xq| TxpjMRI;jtCfRU>G+eAK(yz;63㈱Fе3>{]JY:>YSQZJK^J/ijJBBChIBKJM] {IB.}_$JQb)z rD?7>qӏ?zO{'Cr_ɿ`,7>  )PQL= (C±9mY DINd [zX%qѦ9*OtBiX+cUHP;v@*7x+Ub*G\Y;*ruEcǫrgHEִ4/ T%Se:Y2p%t d\[͚͒ ֗j ˫h\5ߴ^-4ji:p[_ۢà:VV!WɑfBNX+eCX8L5]bHSw]BG Cǘ)"j5:_<H5rVOǕ@3??2Oxro?PQo͈aH+W>UP8j-=:Z"gVq)*@fE+W<lyUXN/:Ӥy1*i}TMZVe%;A՘\GZ5(=j U)ZVW-IJŽt T&-j{xDdվDksUҊX(Ū2Y 4YX5Ze`5QXm:hrG뚪V#e Uf ;VF+.ڇ[}//i)Q{¶;J}!E\!Y* `elAUQ+f{xijAO"HR*҄{yJ*{S-|VqFU~ u> TxV<&_FV3g L͑ЫOȐ C'qVk@,UVMj׌LTHiO{QdžդeɊUձZ-nթUF+qbVm *u`7P3bi/U)&tpUA:}\kE[kL*{Y :jQlMje0^* @` U`vVF"V7hluSwjHϾ*tRWml fX^$:P\2Pc⭌QT0S~D3oK?PQ%.;%?~Gc5þikK,ϖ G Df2N0 :reM.v쀕 '`H#jR`V`VEGvXu<%Z`W<0 DIۨST_IN_zmcy08u(V'OTCKx6V0`AeIQ9UVjuj/ Vg8`j}T +5: ,Vݮ*VUkb Uw)K@U~ReR/]oXQ:,x :VU;KyјK_\GW~M"#RmAXSj+W(Yb5QQ%NX7CYUr5W L`|ʥZ9Sr=Rm8*SL];{?d%Tjh߲<19&ĪW̕epQ5>=LoZV-`0U;~ej3=ihTHPjXvV>V[˴cjۮͥ6[xJbUԪU\**ogyE*#RÕڜuOK 骽MJ \V Uo VɱawAd@[@U͙_ҿ5ƪ7J{`;YJCs+Z=io^XS0UIwB`$n*XN*ߡϞ**ZN´*ܣK`.#LUUj^Z" GP_^PVUH`!V\=+պI {ہ(HʚQЈSi֧r |2uþ|!_1f$u.-!lzȲ0,єTH#[ ;MhEjㅍ4m+DVlMQȮ,j}de3|s/lj'8GDo`Uꗭ#$PR}T"P/޺/o/VTΦ]X pG)TPe _@l/ ߖakb5k!Zf zUb :ȅ,M-`E8)S<` kU0j[{m?\_VQގ:9z m.`UbSTתH_&@: (ujRyvRDr&LJ`5Xd UȰ=а@g˪V"S8Z2,AK>`{[!+.@)@R_۲hEW۽$b`?J*PCWI&g{E%rU \i?VhZkiYg/NKnԀRՒ X5_(^) VbulvT"8zUu Tuqu0yQ*xeyNX/[j?c JA _ſc[;mF/(V]pGV. 8Z`I_dP k[q+\5My'"2_-j;"-۞~ u/S{UVQ rY*Z r/(QmҫXW`ULV1X_ |i"[\ P5TJ[1.L~kQR)Zݙn-O*U2᷁`0w[erurիW8z7duL\uj]` RGT`%d:Qz GtgߕGSMXN~KbIɺ飼hrI&[-D>v-:ѫJtjj4k8vw)|Fk7ֹ<5ӻk5\Ve^U塀TaЊlEr2*\l#GKI(&T嵄 q cm7Ū- ZV}`UBTX #*>JVZ VڽXVa YV城{jխNzx:B)$͑"0ugZ pȑƭx< j> ^l>X*Gս6#,N>wjXLM @ pH*L1cH+(E1j=4+$Zim"?BQZU"ߩC_"v@:6jx|!Y6ǭg"5e-B2ck1Qf\P}d3RZ^t*VUh+ ]´K)U+b!7y4SdK\VЃ@4l>X@W*F ^)h6Th{+QY35j{JKR`Z*~ZA_bauAZUU/ak8z9R,0qJ1cՁK8mWp?lBU-$`UA] L VߌUUZELZD*\2%SZSP+T(.k<5ckS]Ҹ god -!+yzs?s5Oz;2@6]`\*U SjM'~l[bYS/:V)D~%X;W"II &UxC T>IڌF@3H ګ2 dor陥ywjB(X P{UdO'w56GAH䡻P *MUTt!l]o^gQXQXH-y3غjWLC{c\~J@vս;_ pvh?yynqT<7OO^j6sV7ۈn(P^9Uʻ5~=<ޥL  0 oh+Ȧ%N׭Eo]ڊ*dʒ@ UTZPkJ e^eUB3pɦcf(qjc5:9: Zf>yaD|5`GlVەX3_V"R3Y"4#Y6X՚k] q $HS3Ue*c V,IBD LmVݛj|ª*78_ImUMbM{S5-ܿu?jhVcuc-\&l,JV<|+ d@?Ƚ.+j ;]*^l0+_aJRz4h|Viж+3mƵR|ҝC~TW*kA6u*6Jt "U^pDñinc=RҰ<gLZmӭaʏ|jk \aPXǀUO>|pFfm6RV}FU[\ }5VA*B㱣*Rgb9B%a*!F/Y$VץJQ2,`7jWIoƕXK,V+Wњ0֚43La )XUBpfD~+CAܧ V%P\jM HŁUUq`#Q1”QJC|nԞQIzf^,XCVj$AqDFU~57Ap"R?AhZċVwD:By$dzeXm gϚ泃*lO''JU-jP88;"Xaq:ñ32]UߢZ$0 6e|^ʮ8%Gy#klIGPL{Qh#Z<trL2D]ث<.`L " ì:RAKig9΢?{~{{9Nl={!2KH-[^vmVlu5YW#Ջ\XUul ~OmUrbJtRV. Ttd'f!NQRT:% h'^X }z4C)PV{b5Б}HUfU@:+YZ.MꪣVB2YƪoBB V͆ Uh3Vg3C*}XV[~xw*0U*V!ǪVuX=T;8W+V5:"p#T@H~Zկ,DaknuP?92?ziu/Qg~5A.Q|x:oϟ>_7s{H7f{&WXGWRi-a!rޔ\UxOXEj#T][K uu*ԗ*xe4 +J7+<%۴j-j5XHFٸZu@p䧅jwR b:.WYu#f蜘Sm Ƕ=Kt-VHh@pMi0\Sͬ_JJ/ܬa@qS*'+JJlLRnYV*Tj\U ԲϹTg% ɇEtbmkT+& ^la997#~@kT URE/D*IZ5b UT!jjZC>v 0m6:tD!X*"t @MAhUm,d AA&89|jD\/b5V$4/cêZJJ(psbOl5%rQL-Um !H7ﴶ&X -kBâZ u,vq UŬp"V]{"A`b9FU@՜!`U(Btu4YidU3aTuP݇g VTDGO|Vn[ $@\"XCxhgВLf5^*^? /,jެbic=GM{lսK?b'3[m3".AʟMOQ1o~'X]ښV2 SKAE\ɪXH>&*g-P7Hdx*նb.rca!鯚;ۀUó3 V^@UĪQ0dS]mtZ;:VB V|bxZz'P`bG `T:F3Yxx_a5/SsŪT 93l2~6k*2jijR G2&1ҙ25՜jU8Ū^rN 7V#nl`t rLR!v"8 0W:Z3"֬@>Vg懸 Xzұ1"+U* veiI{ B(9PLU v# - #3 g촻 toULBMMY1ٓ\<׺jRU:Åz R˵;J<[Ij:h xJ(UI~)})V)U~"BuA:-[kF{mV(UZ/^@XaޅXX+nUkUCu2l<AחUȪ nӨ7X}BCQVR TBSvbu` Xs٥jOJԎbѷ߃W#$ P9ŸQYzT]U³VfU;w%V~X˒ nYS+TqY8e9Å+vEϴEϷZf:J*.? KXU15Bv5#X@Uޣ@ \=(@v{zʾ=8meIak-'U4'WB`6DJ" aF-RLujxˬrP0w Sz^)S}Yt~0exp B:OTŪOU4zCWl-Xeo,buX-iMeAS*:窡j)PVa/bZ4䪡.:*DbTk+zb[L"M;EVkQuth8J?_-V8C/׋/opy!'LJ&vǠ>U^*XjTjVڒ+0](*K[~9rjűZrF[VA~aS7k;+f8 IDATml]I k+ V0]rnLYUj 0j݉d7XUcO,\`8U~믟~qX18&Z@ Ş?` } 6i.5˱:G^5[>Uw C\n¯䬏 VO7՟Ϟ?,r_j Z?xo-R67}Bh١ R*ͦRvm J56AKM <杨* JfĪA8UsUcb0 JPKd(c$Tf%q.atMIKR FHv:7ؖ4zJtkw`B\)+nmz{d8a]~>l1=T^v͑6VuZlrĪ Мf1Uuj]mmm^*Ոƪ `z(zڬX@"TX%v%VF##jT8?c[g&aL `)t@uQ6>3=ovPUq(uv ήdJ{"E@_ 2`N. :OSJFRa-@Z߯Ԫq 7Xq~,RUM|dU.X5ܜah1XߌUUs/U0l>w (Lf\jˠU("Vȁѡn^J4 e hRTRU f'7JoӪz.62V~a)zpl2uՉv=}W&ᇿ Nf=qu^j@4Ūؑjէ,YNWhVwQԦZEƐRdYTՀ@Bj=ªGپܚ V>i_?$W$SUb5' Vmj:L4fR*PU h^(MN*0@J3qU(Vp`調[e)65b k~81l[},V%Qy FV;G0FtSRnMU[oijUr_N'{2:;z;G?tՍG_NdWg.ɳwLީ~4vI\Kkc΁Tsf>_%dS/ZTSc(HqRަX-m.UOV5.ʹTե*U#kj!Ajն^"TRuޖ|XUbUEeU+HFbxqZ)V`ru,RҵjŞUyo)Rd6 1oRerj4ʊRÊ4n֎.kfO[$V;F@ #S,eh* eY~hq&3SC⏇3V&v=χjE`p5 *tnV78yk^:2'`gжT{M~xRfЏbj'RZ5So:ZXm.`uӌYs&X]?k|NZŪY0B\@@9?UUjq *SX R P3VU+KJJ6 FLZVe`>Ξ} W+r;ZGX:/粢HfX~jW BYy d6.Sf&֙")0ī=[}Xu!^V v~"ЉX~95$>g8msXjm$VOU)u}ʝy `TMVK6VZ6A)1ljHE*C!*4 (?MYsl!lٲ>'b5UvXZ{5VWXUW(M`UP.e VUVwSu7iܙURʊsiEh$V辑}#GbnU fD֢9_rS %АԪlڪgN&N;bU{_ 7͎fՅκٿڪjĉ=@fU:c-EAU3F˙'>U>}5܊\ c,Ǩ6.֪Z^@S p PDnMa,Xҫ6V^Qͬ־d鲂0ڀWU3Ws {-UՊFINЪ*R0U<`2XPkE[4Vic@@o=T݀vu:*L5q=zZF+kAoތUgA-ZcG?$0`a5VxN:sW,t؂g/jUcℑl]ju?N[՛8R:>wR5UyU1V؞0jmf ~Onlgܘ몆jPC5auB}|@2 xzPI pu^2f=/*wcus4Q^@3#UUMVBPv)ToQժV$YYnRFUiº*c)xNϛ}bƶVo%Q7qp𕛋S+]aOdryԡɉC 2?0=X;/^RFgQZ͢o5nՓT$ss kO!1*UcWXiO+J671`%A!UȤ*n0\"u&"V!Ae7۫IyM}=]2<x_9PU^EZ=Mᄮ`m]ޘ1V3=\D֙糏 VC_|ht9X]!B~pֵo\-jᅵN8wŅ!jerLTܨ0O`m6S:r*qgV7hoKV]* DX)|ٸD]m6wQ͡"蚗czf#5`p5%@@<؊m-.-[[.q UQB +"P]:JIPmԫ(weUXE-(U 2ԥ>;U׼W,*ƍr5F@15\NgE%SeVX c/$BgBp/~og/)yk4)=L&?fv?p>z N=8Uc_1ֳ+w~D8*Qh~q-#N > ,3b%Y%rJ,8hLS`*`VVYbURrl Mi6V ha=վZD;\*%W (0ZlƮuVrvnJ^*[RܹHy/HymEp1uhaT\6mB!YPjE(!՜ 9ӥŀ Uf**TlLUU㫛L\y[śB*D'@(1D VyC*lvYKmd kqU?Q)UZU|u6`UW~@Sb8-<}~ʠ|XE;4+`\;v‚J ylKEz*&A  f5 ,FTfa!UٙEEVfMMgSe[辻q=/`f"×>*0UXM[bV!:Wbz( ڥ,R-b[A&ު|HHU7z!l2B35S`9,OZ\@8i ʎĢ dl"XUh0c\ VB!,pX51G9`-V{;~\[`i{VS9:0Nb`%J%~!4*pOSʂi)Ie`:Vc*ȹ>K~6%P$jFT8ZiY`h;"u,uffa}u xUvJ"@XhUT\&;g^93i~Wլy{òX%~\2擛 7  @wQQ/Zi@mgBkXq늬फªQO=>?wpQa@cU'k*^Sx5+ rR R궃U mwuVU ^V+pY%j"7No,J\-VV R][y,8 Ȅ ~PªWV]!WU_V`?VDt1^-UZ`&`HH )*VEʮ\]E?n(-S|J(] )sSb^QpU5Vq~GV }Щ6/Kn]]yAULKmUk:‚eVJ=t/չ*_d6- Vtsٍ $z@]ˈZ 1Y +v@ F#$XM8\=5:ITWTBJ(e TZՓ4Ff:UB=F҉Jx\",Vu,ZUJ-V=~+#V%뾲t\%fBXټ5DUUY7Uucc1R}n3fm)Z$ ԙUNZ>VUZZW1l&"]5nA!d5 ׺xF0UV/a5"c~ xKb?L>Uvڰ)YQZ*Luu$]Kъ׭@ Ռ[Ī|J:uIt+eZչ-`4lL ϤOմqb5‛pJuӇV 4VXHf_|*bzpճrU *8=,HڸAA4hݕ{]4+Zau|k2@ehկ?->M FSVݔ=ilYPFIS|WUr8ujZ^#΁U}UEC%yUnhlJhƸtХ+DcUkm:a"Yr I 4qL-xX5˹ VcG9<@fZ:>!;(P+]qgy%zM*+u9wxŢ0+yjwUK9ͣTiκSնJ؋R!*Vu#yjc%{Xoc{*3P&nL)^7YhWBƙZ6%,j6c+]ӾV۫''IbsU&*PFF_yYPP~"УbSs+g0W _ *Ѐ}]OYᗿYO */Ո( *DF,jd%6)ƪGU"eaj%^{xX͸>VRZJ^!+8-U WeW1 &Wۗ@z<9ރܦ^ ][Ud-pXhUXV`ϯ~Ok\xUA3VLXlzp +FHՋ'O? )ej[ö\bLjh* ڍV[!$WzsՊi*ٛWŒIz[̥,Hc UY^{ٗU.L)kG|ǶjئMr8>j&aռYK77YOuxpV*c54ME,e$)u[K\GKߐ Z RzQ-X lEQuTCeUaմ]iu{ ?QK@%h} - @7qyh̎97Y~ְb?Q* u BQ&9pweh[Gh7 :p8 FbU]VW_񭌲h՚H}x7@W(r B Ԁ֑wiuPU*s fb? (M/@m2MJ{V} .`_10NBeyyQNqUr>-O?l}'j+k#:hh ">^sF҉'J~n MaS_lh[bNYNX Qk)!(Z՘ EHU!V{@xx|L8;p [X Vf3-+LsbX=+0V/L[Y*1u0P`Z^=jY\YNC% UQ:Wfa .O:?tʩyV.ճ(vHTB>@ 8r\=&jUTV?lo֐y>}V!V}ؤ=*XmX .,yZ _m'Pn5 UW#{Ղ=9B]KnPbǝj:e[m k%TҾz euLVGףk5XJ }e+:s 5R@73XmFn`Dߍ Ûr QژOGhJsj# $Ǹ| #+݌l8_w*]mSPMLByC S` )i錘z? <:& Qur?!~^z*ju&?ջS**N_eTe+A\7:DρIj]4Zz3WY UK;$.ˊUGVgCwO[M5*U fd9Jdț|\Sap?s.aF2V/[=`:8 bթUtbu`FHeju\Z ? KC~LVkU0Z*/o} VRHڄXbcU:ԹEXyXr)_/Uցj GՕNPW2O36V|KC\;{sP]4P2_Պ êkҝv D^λHO @· \ǃ*U=X*q ZƦ%*oݶ-2G2W->+lЪ5.-Ze妩ҍwPjd\U8j)ZW`[oK/: ~-g ՎWkX@~,+XUþ1@W7U:aXE겊MTNJ9XD KաNѫD*k<\0~*gP3bL njTҽ#UC3GO~4UV+PnEժ WI^ܣUBd\v*T=JCjO g^\9L힉z'qz625cysRULj"DU[=``(bUM=:jJZR%zu;}nn|?n%Q&͝ŠpuYĦWyPe?KD暫2'hY *S:'<:jHH,p*MXM ,: "_5)`e1V >j6/0y:Ƽށ9 ~<? \U-"b<}&Zv$a>"Z8W'ن"sۉ1Xmv#4BE$N[G@ ?yjZ@Ԏ5~5qzk1-zʣɄʎt&#U85jGbŌC&ǪP : Ԧ|j;8b8d `¾*Ej"j58e`[&+1TV|d JTX\$WoUz^JԪ`uVѥuPUWe]A:V׏Mc:Hz٠($~qsQyʎ0a*U2}jWTE+@( V˚oM ӪIGFkV]@_ kcc}CjOj TH\Ǧe|Z)+O;EoA`?gzq~asg(4cͰ}f 6,ŪW\}#d•R+m{UiO VV`U2+*W?VGuO]N+TטKA*,ѹW]3UR?uK;JcM|@ VqtXb@2X FZ5r5_٫斖LJ+?j!UM_]˨,0CZ"YPZ*6_lnm>)5WOt~qo-Ή`ەg^v]4uaar?#@Vֿj˒?tdb\ktXզ^?Ο, Cg @‘` i5VXJl_ "(PrqVZ'a($ڲxVh_!U{;VU{ z*C5uWCb>ULX^z>25ϔ??ӳ)ʭ~$Z:sK_꨺uup!lޟv6\W8'9Q*Od4?Xi*^f8nc-5`[wԪqut 7&j.LW34ލUMʻPMօAqKjZUO0bßgRJZ5@ )r˴6/X]T 0܃c.&X-޾B=_C:wݍ5HU[1#Hi#w/c_4gb "z>_Wʢ/ <%}}6өjU"#08JXlVc-j[UQb~Ou8+uTEU]zϟsN9m3S]Nkq gUX XXe:NwzN}AVPi';*UK>$K`g!>_=9 07NeU(mZ]'@?wSmӺeznl3L:=RO"l}_swc5ՉUP~<(p\^*Lثtr pXT`sj* fF@}oߚ\+s0ANZCa|eS3CWZEgqqjOYZdwkbHm^j'L,&(zM%p^FnC}Zh6M()G*VE6'9e䭶Y*T=8}*V:9qXPFX}$]m#wRLj x)Gd@^=sZ烲zܛ ?4wuZ?3WuQjc$AT%lh_լiZJ1QVaEDew֬f[3mEy L)Cg\k6'SVz̦:e5iKMsPυJ0 !xwsnr88EXuϒr4u{PU~ !R[^~k+2*E4lo{նc]]/ *z[op` Hx++CRݸPm U2VLq}XVXX-`T-KU6{R6PjEiaնUb{K%:w]ݫZH QmIj]pľ2H\SաTj4Vt:Gl4WQ׼+RlJoXJ_) ,)Raed=4Wz 맨Q[. C_jc~.L `!N} ;Vh߁7*W?՜r55M\HX]]XWWVbU+YCql4 7rg-w}vr[Vu4*Xol*j\Sũ|MՓp4uU*Bkzb5r!VXXnf`JtdGZV[0V-kٱ +6?\P+78Y}j!0Xm*Xؔ#u|;U8>\:|NgNc|aMcU+ !BU;/F M꿄+`*e+sV܈ 0ಟ fUrgX;F*BJnm =5N8hU*Vf08g;LJEW>CFl^ʘc* ڠ+o*iUT=A2R^U'ʑURmoG`Pl ]rU:\)XՀձ1XձVcS4meRGbul!Ұ^HۘmoULTwݪ'VOv:߈GdzyVV!ڀ#5(Z2 \Ʋ[cX4;MD;<E&sq'%WyyW*cUR5Z4OGl5%B:\pym-X%RӹsMS+`'[ }X^!17ƤjPtG%"hUBQUGXr- Zm$ b=z꣪VAU$UNpЕ)ն lwC~P*z(QQZuX=:D3SU*jzհj6X־ Y9A_ e,ᔤc$!'~b3<ӿI^fJ\}p=A\9ūcUk.zSu u V$T1+y0/NV2ͧE* k VlX@UZsUjA+r<[φUs VbDZ?Z&t]hKTJT_^ fkT5fp?Q{M*]S Xt3b)ه{ˣ v:~0j^deսo6ՆUONdj*%ZH;{,c0 )Pnak)H!F\ ye YRD]+ Zf(i/<}{/,dF~<9yNCCUU/Q qX8> _11X5 ~a0Ly pgٔsV4=W"]^~^TŮja *XʩFU>T_/?Ts8ZSjP!dЫNצU#Rd`yMM eɅCU*KDTPxnڵPT%Ο1_ַV:J((UjW=,p.kYzC7YѴX  [ YFP*:h4cjDNSw2gҰ|8l "TEQ~ VX?0@zaT* ~fݤJq{`]hPrszg5w?+^R~!iS2|eցgAZʰe(>- 㝪zzH\l֣j>9TjRTyxjb.EP{eմEђyYlk)5#:)Vp oď5.JߨW^Xg XM m. dX|-З2}/9 'As֡қ89S=N!)`JT;TJ@7z8C$>ɹ+=U䥢Ҷzq{ʯhW6E݄WPyn,U^*WoFV.Tӌo27%#W9aB60Hw3zGr5$3&iTXu&zUjWW]Q ` IDATUrUUymq*UL"*`PV:rZ5 !Q{A\Mn& c UOu_´~L@nU* ̡*,]M`Oޱ: ׶P~TqWOjkX =zDP*VMn.6ljU R[gXm5 4 EU<6k*3=ض U֦oڗZx͵U@ۘ㥃&ڑ*%)V]T%r iNXeVjX廜{Vi<@1,USgݳ2"V6I*Umpq*Ͻ\żĪa*1tJDe~ꪨV%}5.!êtFRxFNf:n!XLoY2TJ:^UeX#e @~(L9?*mWACy^ZCx} {YR\%婂SYR}T-vokzG[1^wZZ?S;0.ҋg')u'XQ)t7@zX!kV/jW`~Gw)Re5#OF ?JarqHed%_\__a+qL͔*:V:rET=(cƼ^E&@U* $WfU\ZY N1WǦU b+UR> c NRP̫Ud8.B^ӵzX]]awooںnӖ eS1V+weXtVV 'cb/J* U/*jд *ց_Ʀ->+*Ubur^__4K>O*"xTz4;ĄV2z@"nhTŜ)LYqSAjšVEBR! е|S>pt?;%pjժp\ MU[&dGޕ1LnH耓}Jm7f_3VPͣ"]Je}iss\XY}+:.[AIrYj~T4*VvIjU (jj1Jzޯ|*Vb'c5+buJgm*-:oVAS@nS(FfqՑ=N @ #@³c^K>>0.,K^Xm鉫)dB!/V3V)夦^&,,`Mm\Vuƶ*';FH HME>wE1Ɋ~a:#սp=XU#`TuPOGZ\њX s<b"*ZY_KlWOqr*|z)Щq.`0uJ :WhYPq$AM&n>}Jz=PVkUJ%j.?CFqX]X ZX1 pG)q U ^Q-Ĭ,Vzqᡚ塚*ڬYV=4GMђ =q@eVt?ʪ`5+UsfIVV-|sh"Y̭>UvoR7HeP@ }lmy,U' j{s 8e#AD룰Z 6UZI`e$kʱZ)Ū.`U]mԀbawFX}!RBK<;֙jƮMl9;bΩJF(q&AE0^c>x[;^w웙{KI$i϶*U'&iw5M+d ^UOX+k 1UӮv=4ۖl ޴ @_K?7[10XXGlX2@>S|-.j$I#-phUf甩zKik;VþNU Zj(9NJMZ Re BX\]pTzJ:*:d Vti$-J+X U`{.3ڄiUU򕭏ĪP,j|~kX=~!a6bj>ȏ0J"%vUF/jK5qk[Y9cKd|ױ\^Ac*1z M-n?wg]>ëUrabWJKO[Y\# te`Hfd J-ETa'q <5#eP?"$Q>{9ڼ*u@Xw҈¼ӺiUJXel#3drn["'|bo;IcOxWj?9~^\Vr~ >SԚ zP[2UY<"njI4S-/CGqW-ˉ\nt<ө폟OTmȓ~00b]`'"R9X@$ԪZ%>/jd2{hM|EPU\pUܗ  K}c] SXUPT#oN`qK)2Tܨ8Ye3M6㠿T`hT8 +xJ5j^433_êq`X|4[UX'T׵HbSuZy3Y ZV}vx7 UW@F}랪 mR`[fۧ X%oR'E*U2W+Jqh>[TQOrCvf@w[@ieꍤU@|mWNӭRWGז؉oѯX33 B=^Z[ǦTgoW=-BRƳl,VUܪ?):i-WI=?Ud Luz-d{"m(-?j Jbu-P>zcsUqÝ?ְP2]۲q"@hoJk0S,vAqz.~%a[V[O |Cvto)jAYUi-z~UT~" Vz _ W`5GkSښr\>+U:w7\BUwyLLFzU<b@24MoZ*hg t_A>+c_?VRFt 9. 4 LSN ; ~sS֗8?PN'ORg /$T<U} @Ǯ[*@vѪAl%X^牣4TE!_BBXVU^<'űUzѰZNTeCX=}7@ 3K^b5aAfpzgKݱjtyu>{W"z\4**;TWJp*~(Ui }h2 Nݎ `E?kBN"SUwe%Y+PiJVÏ hKZ;@bP UWVCRe\3IT~*?5:r8}9оV.aKc"#|u+W RI8p\T<+,Z%hW6P]_/ /bObΩ7E8/Ѫf_:mճuGUl+?գHfݱU۵U7THm"U3YvPeZ\ek.Z%Y7ɾîb-Orxv>me[Q0 rS xO K">"  -w~]B5кz K Yk}ι&<Iȇ!X>k`Vr⫖6KUlL&m].2r)X=6vlUm< 1{F,5 mlDT̀.**ju~^{6vuJKYX)%.67ls[_ l9fzrGk㮕FjY WC+Uɖ;;xSLUl7;_z۽ \q-sfSĴjr F_i5ԪNaӽQUlR G1oQR9Ih'GB*'&H1K 3U>ͤMWǪM?5YWoX S5ln1Y-\`!Z_UۏUVsǍhZbujcWU&tAm|$Td6U}V!88XrGLCދ_VXU? Vl Z|LSMBXSmI9S@8:v%K*ĪhKr@lԲx%kE*{dzBQSCU{?̊'4FVخB3iuRO[u8ygԤa7MPE_GP aۜ-0+զ{)oܷ{o:nRW:Z=y\~vl-8rʨPCX撉bs~N3%TM`Dgjӟ`ؑ9vZ& ٪nX j֬ΕG6.c1kU Xm'm&V=U~X$[EYU?H;d>NXg1l>>3se`)*ѷjYmuAl+zuT]hrJEhP]MVB2S,]߻PYVJ]6ؖ'8fL:DX5?@mt/Eq ͒Չآ\ 0fCT UM(hUGYO~v^gplLP:]5~U*yHЛ_ qO:zTZNUb{ڪgKauȯ]j-GcG'pmSXcŠ{?9ju:We~hw- gYi963Y͑fϪjVR Ym4O1)jjj6'su20PBCO ~`~`!UYgIk fJ.Wwv81D{p7T6 -h-G18GpNޥ[0"z͖lkVlA$EFUzURͪĊHpl-Vz\ V^;U8@) IDAT3W+V>(\uzҵzE7k\ +>BUuOd,KEP1dӰj5XI]?Ъw+sa_ VOI@UTM`rUԣ6;QKr\5S+3zW^Jcj UV$׸ gGyj^:SwЗK<{lwMB*cX*Z6=5|kʶ)Q`U/ 4ƪcu_Vj5#7*V=V 2]mWU4ª̆8-aN,V5"aPދŐwk6E*TOʪhIjuoU#7UV$CS뛬Z]l d6v^te4 X0=l\}!zõO-Xn^2p !ë)LTc#mW%t!΃maP*~pџZ.יHU׼~-2۾V_\: */hUUon8A{Zm3YZq}6F~U9 ̐z=(V$kr<]RU92:Ma07}YԪ+et]o&rū[ XH@-N͸bj j1p?24V +M@ %ի$k^ auauaKT!PU1[f?8,53~v3_AJuV{w]jʨuuz*Ru;Uu-^$+K[m/!.۰,8y ӶJ*nq{a5Ŋ}Q嗈_U\"[E9*Fv-`]Cꁍ9z^,ˇcs;pjzw80,GhwaF!+hU~\e$XKá`ѤU]`gToZ7NUkҡb*RK%oe5TV7V-s5W:t$l|\ HZ|HyGZ`*DmfAnVm6*r5ϬAr^6J@j? Qhu#XMB?&:5K =$$5`JU#X5b5G UPJ|w;f]a})Q$!XekR>N%)6^^~U[*s1xe/xDx-z'ny:xU ֥pI08Z3*e|l>) gՅ cSFs8&}cɵ?djIbk 6{ 6Gqq]o;҇.+r?&UUt5T*OX:to8MVR88:V=Tq^!NZW_u'ٹ$sުjmؒRJGs1 T# \~ql]G1*)hԫlWU{W [U`Kj7ozLX% : H*V*oG-⚳*p`uu7l3r]Q> %Uq4zjU"wBX W͒+D(**WX*Mw.~mEU!juW/҃X=#VЛB lO[gPЫ)^e,0Z<2fQ%9v*IlKі(Z1!b)hPaZ [>1OxUNXZǗ4Vs:Z@nUgD}bp˶!NNߤK@w㪸uYK{>mXCMݟ8 ݃:0vEQyR$+_9iW80C].-]62#fW¹hUUNaЛY1*S +\yF\}Piߪ= ZϪDN[ 9@ZDQ0%[BH4[ WBX+HL^f<2*\?tdm`:BrU5@Z=c2ci +w>J澴6'DҊ Zv%,$gU|LU1U/ *SRQPU 1:N VaJ73 WNbr:HTGCDUu~ǰWg/-Nh֮3H!y+B5[[1 /EX QBpESİI46lbYT'3MʞgʽxLX`(NhUod]6rRU,-jM V>y` bCsBfZUWq>7ЖB׵;rA6,VcHVAi/w ^WXN>(UK ` BK#{AuPT',UoTz6Ԭ.f*Cլ%ǍİZ-T%tKYۈq\ؿ9j<+~a!N 2]><9xqEh9P`AjX<;p&RdYbl@r.Lja۰Y }Práee*M< iqkOVLnq*s[\E#`7Gk`~Y[W/II.ޚf`3vUVͲۛ[n|kk0JPY>UPu};DтXAT=~MѬـ`57U^%NVZ74eèS*a]t',MMq2NA>JFD/gDX=^@tuY!J,pX;U:2\SkxBD洪Ɖ]#A3e:mq|bX?b)[Nk&%rNR#^5ݚ`?I4eЮt TM]OteFލa($Xa-#Z ݥ*W}27"KC_êW,ejՖChEMz*Z}"+Z \&YFP$ VWbz8gcA:j_Y N',_g13OָbC^օ Wj% %n5-:uKZZPAT ªZlf(u*}`iRcumMe@jݢ^r[iFU<ˊUĢ;tMǏƱa8:&\\p>.\eV; cU3YD[wQHh/XiPF0Ama,EUU0Jnăm>TQJt*[djnkgGwZ.:[7,wM ^zx#=9ijC˪N~ ^--GU7Ze~Y$B*@*ѓO8X&SzR x$V7M :jTg9 UD1&)`JW>suꐓTaj/LZQUV8K5j (2\N7Rʶ:Ӯ}rU/.ԯO1g`uc;HêX8%je ib.Kw25Exm$<*SWߢ'JBꏹSe34@lwsbWͲѠU:*Z܊ {3͉XDz^m=50 Ǫr5B2\"cnqUmjOb\]ejx R XC gXml}yS(Vt]iUp48y-W(:kbw#\u ':8byK ]QIa8gYefdx ?+I^qz|0a:5(9Gk+$WM*uJ$ ?wAMSE3TLt@1u@T SQ;#>O3A5tթR[Jkg$H'ԖW=E2X%GUI%/ٟOUb_ TaU&H` R+/$cݠ-"!J%퀣/`QU1F,[_JWa++UVU}XuPQf.Vݙh!>5sp,G6˲3bU̯lm3VUvW`Pů/,CV[Ҙ۠}vv'qk!w`tꕠژA @EKET*Q>v}iפM1W7yg*gjY9g젚C&pq_4`,؊Ǻ׫gUNxxZ_[g-UddjO?<| 6][WU31"3F^BF"Tmœ+d d;kRI +Nݝkǵ;oo *~űӉJK)jB~R>)*r`bUZHZUU:X.ܢF&րEb@W܁5viePϔip#UbUũ)v#g)v ӯQ::{Tt=tuHU7sUyW(Bn"V$Vk՟3d˘Ijv xO66Y|]}nfq"*f dlko%uhŭqgsBJe_:RQYIA2hzgrVÁ4'H7*oأ LM ތY_w]K [V!wjU\:\cNPU߽{7\kh$ݰ~IH+,kSMzMOnπ4yhng4~T*'.kNM'x3;~l#*@gjIL걚~2V1j x,!k^5%eޞ͆ګZ X'Xt7t+Ug(Uwl0걺:q*kSE`8 `6K`!Z{h hX-ޔ {ٶ! KVk; yems:T};䯳] =VaDŠZWMjX,:]Z9Uni x::ZCkGfPԂMUߟпT}пˤAʰ T}EMO~imzV^7ڗb8d`8('Wb3`ǏpAމl(XU2-j:4o7 :҈\u(Ki*CS0HV4B ^.uHBky]m껸Vjѣl峱xK`uE~aA&H s ߮F Mi`[30T=SE[gPͤy:sT-ZG[9u[crl@dXa}Wn)SLj'[R9ν\j{*UWvEc*V'Zʊ"UQvejUFe3J-Q)?mV=$X},U:WaF\ IDAT{UѫULk"`YU:һཛྷrCjPDW~ˑX LV'UcGՏuDBN٬*.Ag:kwl_`JȨPHT˓w I6'W.4U&.#,HdSr=6hz''Kj;j*tp~kHj"TdHZ!WoZM6 @JK4uAU:@);z`@4U+X}}տ9zOGb)j&@etH5.NuXEۓ(PǭJL얆 jU|#ꖺ*].ԛ[[cAr53Wpe=暻 j?P:{X}U>+@KVl@y]'X+9oXѷ VVBY@֖{?;ѣyѢЪ;įZ Up`|u*<Tt0}60߷t5m*UJꇎ i(@^=-*D"XU_ V`*\:ea| k1׀*Gq$$4c˒lR@5aEC* Ҹ@P* nO{sνOby=c>yTtZa@-ZcelXZc&+: 5&mU6zCRODQzj"9u6pVY7,u$pM@N{8{݈lu@@}.@_MݸY. zejYԃz3:8CFxR,jHD~yC=Fmz#gjy[`Pnwi+XM>1VW[U 07q crUV'="V?s1Vi^%u<1Xs] .79¨.hSFt+o ZƮl#Qx]m$‘?"Y+oZ}=3pxҵדX_b{ߘޟGZNEXՅWލ=Σ6F)>kLΰ`8YOn_f070RvHStjsM˃uAe} BFRT Yjj$ *۬4 sAG!6?l"mCĀZfb ^{rxPg}BVU`Zu_XM߰:Y:'mM|2XBf*Jh4:3`3X/U;꧊şOl0b;bjR1Hfi^JHO"kwV} 7*5S *9&XTv@Ez*=Rt\˹9{X]{Z2ݬV8 EÉa{G.߀x @BEʟV˥~q/#vl |?Ngd'<P`2ǃ kP1 KUy蕔2]~[ZkcZ*u1<(KZ,U@U_Rxx23Gj}v%,W!޶UeWA3mlJ.+ 뤪V @%.V*Z*VW XkZ*ʿ#TՅnUY˱ ꅛP`SZ&˱𱺷OamX8ɭdQ韟x8\;;rFXh\P)XO~F~CTw6Tzb2MoR@ Nh$:-auѼpKG{t/}VZi'^ug'Oj*"]^$MiOu8rcuPT_\W=.USop-Q  x@7e @^JJڝi&g@g&gf#kָJ׌6]U^G VylIێWYYXU |GrpUvS}X XEXMk**Oi"Uj ?γõW|M0\)7^ZFV/jg~tU6Zq;BVp5iUj45w靝Q\?+-*7l.mkbʙ*/{>qѝ#+[HBji?ğ"XPXյ 5b ĸNGm)lE[ DJ NZ1@Ẍ 0 e&muQ{ P"Ma_U:_?Nqi8)Go ܽ#+*.[hVXP'nWX,-ZIEq38*U:tyrU*Ǡ{XMt&G♌*QZ7O=4+&{}D1paqqi`M8aT~"\UfZU3Nx\po B*UuV( 4k{ET j[*"-3a14XꄪU!98ծ۷+6Z ^>nnpX8И?7v齰:;S=VD_n=o\ j xa)v[2q?>̎BOf1myhc G T]UUoOe}"W⍫}PUpAŖQٲV|F^RYWf ǂ_%+*7kEʻnW-v\=SUV^_\2 2>(UkQu;)x#Pp>/G wű[@@2;X=D f6FdPC+2[fk`U}3{cؿR29d)j^l6(q@NZmiK?39?{N0V2(ҧa[<`k<: Ԑp|5XMM&䪟 /auj}~^6VaUz-dy*Īab@{R3Nњ7j{fY(FTT^ **NRBJSBa6].i`NM:iu/$Uj!>yG΋#GOJ_?܋+-E1HFlMYwvPZ\w}Z'UG8j7BEж j\>lH}Y=a+-;ZQZC+Gj+*-SVMV !TgLԵTyU$AXfU &LOiVaur|/n ν|YȪ)S_{+6L͓YTtX85U\6XZ]%iG wd}#i})C꾃ChVV\W)J{J%)'p55b#%V;('a1*cܭvҞ<ʑV`꩚*OB6EP5oN'. peq*oEPWl'UV\#.m[=  %WvzsuՁbUyBEcpήrCƐ9GBÒj(2U3W{~ͺb֓\()b86[l`}ڮ P%`H>:-TA~G7憇0XM˫ZjxLS0jPxHaWg5͉X]UӚ0UqAWŞ)Ybȭc<=*T-Iռ RABUNuUֿ"W7_X?Y+S1+, U CPDY(j5g8EvDR}IBq-IjU :VQ>iWV% XQY>du9FcX"A:;r25Jbq_M 4šBU ay$5A`aVau![['n0X0NK<Yh;$Uy8WenJRZw?~ a"VS_ b7X]_Oi2vGifM;n|C*XZZ |hjc-*oD`kXUS!q*qX' .LZav[GG^s[ّj\y)I`k/9)=V* ;ӡU녍B}l&Pkӈ\ݯYkXM|'3^1Sz3]VLx/OS=D*2CjQ2VuJǼQ% cTLƐӂmR*ZWWy˲3XLeSRgxʃ}cu*CSύ9T|%Q=a8^jU} ӉUAy5AT|~n36Ww?i sp Mho>+E?AC1v[&OT?!@Ъek?V -d\ TGߜ*ӭZk֕ 3]I[?I1+U&XÜ6jZsPncx#g}a6!Wi jwNՁ1P{D K @ Px'EePI0\mWvw<Tg~=c'FE6Vc@L:+)Ԋ^+krl x+<伪ؒpj ܲbaz:SST5M[uUTZZ%V_mbե*X% z2bTX^ V@{;\^nqu^\Uk~l-7꽠5IՈzpC6[]FGɣUU穾5Gǫz0`kj$BSZk@rm6hx6筐w~RMx77"VV7k};ŝJv=_!20૏g;} Hj>jxY z1z2 ̕ U-!Cڲ\em`{VkOkaʬgʆIrִP{UO 8Ь|8UGg)ѫ) ULX~ʼnѫ1{S@%h96)"V? l/LQհ%nfΨVbbJ;x΂;u dj)u7);+(OLͤ$&AgjRT2[ nR5qNubZ-5QIAKfV `5)V}JA{MLJɚ\Q=C$hr +)p5ESQU`jIJG= bjJ'y}vթw4>mJJ.Ζ0JX@A*`R k*/*i*XUЏ*d*VǍ|޻jXA 󣑱°g`$TA2rU^0QuaO~X}r']>젾C51XEfAI5:ǕVM^Bj DǯĪ>ȽXEl X]-më*4%1Y^Z Oa`5ضJ*q@zkПf秧3y54j%W{br\-Fh5&X;u'c5VVU(X PmMTѸu!G]/, #РztjC+BSpbEҫޅ'*_16O}X}ddYzfv1@%>0 8! TuMٿ9kS:ų*<ƅ Jc SiCNO):۴4)W~ /ZZthg*ZdgOĪpez:]O.j_JqI b\8]I\?d3&V{ٷA+N"Vt5T]D)٬Uz}Y.k@xVlON{X#g) ǂ{5ѮM)Wg5ЀU~IɊ@+G*UU?ZUBQUU] Tw84eKmyv|>^`Q{T=E0` ).kEJڗAtWQu}kL*':VwOy U sRJЕWC1T}&$\UT@N3^~b3NFH;DRV yVA0yOeKҫ7#y9^*l|:VSg\g>LplGL\b҆LLdJ~Rp4b՝ (^^w2ϓZJz@ ,W39"WeNdzWIm_ ހYge<#k-a',\]kNj% ϴ 6Ҟ ۖW_jTRTmtpj1kfg/P5U ߰:h[U[hJgB]a V?J~<Xu} GU8?5|WXE`}n<Ŭt}jjըXs+ `J_jAUbڧ$#ŋCU=ٞPFe!,KݜꯣS*^zᲘXLUH*~9.W+1ZMa/eϐifv.;mfYB|`C[iQQ7Mt*-QU{LGo1aFL1!zLm>SJ~S؉1>ٗJcUQ:5GL} 2AoՅ:F-nq–QcKeLCW-X> {) taf5 U*T떫`:yKUP9WsY^Z?f{OHUh;pJʎz{MwL`U AR Kʭ,z2jUꅬYߣaEjqUGV?imZ} XmaܱRRcR+`V 7m4%a`fH@CpuT:?ZukLc0U1SJ9KߞNo~$LS̓w֓@Z63׍UbBULM990> IOZ棱;)VhZ:\z@iy{rPT_~(xOETӫJ$V1̉X^LdzZVc1*`7`!z +ޢ]5UXŗπ*WVWEg\V}JjVWjw>]ǷYgI/Tj(JNY=\X SEWKгRkW1{y'S%j2: '"(V$#u4~ČZ$VAHլ=*~+iU%V< oON:֒\bVT"aU~Hϧ 8-(`-+rONCd 恨pj˶Wtm:&rU: 8~PX]0zw!`cXsbsK]dY*wL\*Sx';OS\6Z8:a XmBu"Pze8zQ5Jfy*y:\ ~J :V늉I§* T8;JCZ W(ANKFϞ='2%Q|TL$sY4Q0aT,+C nRLƑs"Jm*@Uq ~Utդ5#B{g ?g+qiT:U_6Q w?VR $OWְ^V:Z)D\hM-㚫<]kYĪbuśPŀյ5X$KcgK/8?z;S {\*JpwJFޥx6}}N2oUdAWO3ͤZ=os3&RLUIƩ~J/8/Տ+l [%VtW 5?m@BzD2Uz1:r*?6.|QNd"W%(C 0^U U+M؛g꫷ߋGT%{ikVխ\ bUncZI֑:pA*ubb3CدQ0)fªPuQWeZQJoR!.W3BUT CrO\B*rakyfT_pƈ"EWjfт~u/hAU _0`N|eUԨ%Q93H,56aynS\ZU ~E40 N oRBGEYX8pM&6R0ɭ4*TZi*Sy-{fbQV} K}pTQlrn_b>DIC (M8BAmJ6ץi6H`uݨ2_9jg78#ms"T_iNᥑZjvH6UQ:zYyêtlgV9.uZTJ)aUPC2`58u1$*VVL:Mjn> )+ݞR-otUGjdm\!D/.`PzjoF>t :}?Q7FJtr5`u \̫\M\kp2FmBjs6/9 ލiɔfVVUJ)[5;Gcu$O0>=lU{\ZbO uvW^TJ;E\Ů"JkTUYX<saE*t,+2!Tk8nܩkFzݵ6й|F:洂µh/y^}&+G+_L]ߥRzUQ=Y#6hrS:☯<&U/oΠU%E*F+7ըjUա50*X=^UG0B5[5X2uY"NʽQQt{]xmW+TcuLj TىUD+ ZTy~7p*MuJz|l=YXbð|OBw`>a Q}mr6`5V_TRp9Y_*&NNԪ1gl~ں(GvlؐP*RE8/S a*Ao~DVR(UP|֪>k svPp5C$>PRj`>yvd "WZ-5)s'Maj0`nZX]Э}j3stsS $A@󿮬7%ܴ]$Ť#}hHWWf\ Ypj|[sǨ:nUXUĢ,s`*ZU^ͨOzJg&::8 TZѝ_~՝^=9,ySeGNy-i༘4:$+묪FSdTF )ZbVzb=`v V,MoZgc5};Vcrx&RF%V/ (WњzX:s;ɺ6 l&Ek9CSU7WM S!婪X}JnpۋU벏JF`r^eS]r?Peζԙy0+**~ʢV 6UF UԹDW^^Uʪ@7Va0a%~aeTu~RœVSZ$VmpEXn5U titJLY5XKAvXeVObumVR\c*WASjbGzXjXQ%lI>TdbA{|Pڿ1/jKchur`&< &HLZLX9R-ڙ_$($~~U4C;f.-.JDUGi&Qrrx+LloUƏaVvx+zcuDfTdyaꈘªzmBUw"1(ta]K}ot~8@'IH;*U VD!wfRҔGr1WMZn@V`Ī_뫩ڥQu"JeuȢ\eoeW26Ucs}7Ցw#yy3,"jWM`l8]fac5?E ԻeJZ#ͪb4~!~6pF (WU'B}Y>.<ƦEzpU]XΕ]%03UeCc s{W5mPOk)jjbŪz~MUK_=(iQZBDVKr ;Otu&*>{AYPSca1xAkEP2tr~ U2C`뗖VQQ V͑e2>t?pOv`T᪩0^5쫅m&V'BCvOZ%ЯNVRLêl3),Vim?ۿY":bUUݣnp-/ܢ"|V/zpVFRs[_3y DYJJ/`)dJ*j5A a~Hr8PCԊRQU/|+EݟVmT_kGykjmŋhLW$V#k, V hL GwMFOs~.M $Ց(V%(+Ke<KGXϙ@(g2O7FEPŧJIOJ}˟ҟVvN&jpTN"vY#LX2RX%S% ^$'E-W-Jy~8VfIo@Yj*ǮLMG:JY::-U9jJ0 U00LKB>d(`3oD*Tо1fDw:家V48AAE]|ɭڮA9WVe2wgaXMAb68r Tu٨UNΓ놾EgwЕ)ke#OĬz?`թUک8=33YiH4_p}WSTU թUW;!ɞVf*P3Rk,]0| -St#r]⪻:#TeXaMhg)'Sachws*Q!!vl0+U^BeZKiew n +p>Ե̋Y gi"FT K㰪Q* 4GVkieQԟ9xN p& x$CSɈive{V[?b P>1W)m;P!CISS,4W65{+Y\xz`?ѵm P%HYC' TsQ@g*0*~pjx}TtDcYyՁ@u(wUT8^cTuXwZ}zԪ8O+Rte8z0)Xd]sUJ,WU4^9kz| '/*XGk\`C v0;tJD˜bYŦ5E)t5_Z]VdM(. r#;_G,yKX-eX-n[JhŪ,.\U_Ӏqmj gu+VpNmm25aUH]n6}t~ƕfNG4"6 ?V%(Q:l: 4H}kq$XŹRuhe*f 4z~ERUF5 SN?9RŪӪW"MrF*c*zu`@QԨw2$KSKS7SSD*UDvF]\ Bӗ*X")8b <d<8 5 m #ԓ^myM1p]a}V'IUO߃rNz+VYGQ^֥ /W*@ 6PevjrM%ʼnog~fTjYdTe{Ī ok h7? e~q0_{4ZNqZuA Iͩ%[`+4d# 9U/_\\9<ڌ!#8{r"SNNDG“ȟA5^R].0Maka;;2F&*lj3k'jPl]HP٢JMOΕtu;Xp7+0Xa.WY5tTbUO7bYXVH~,+J@<7dX-&{^Ӈ3Z,nVz}!;r\5jy+ NzOXOZM(V53R(jUqͥy@[U rDb*/_f>l.-syiާq@JTt_iru ĵm ڟsH3wJʾeXh !MpFm2QjՂ8zL'5&U/"'TXE7kpVsBjV.rŶj)ѬA@1VO_*bBKv\ձ}Zc K=kH}^U=;`c-V*KUSfj6ON}ʌXI݀*32J.[Vu)a -/IJIϏT5>p^`4h5Ojo٪ArCj\"mZ4?  ~MS_>wdu-X6@LaܫB0*|`oȇDkk{k=dVPu}'+L5ţ!Ã1L)V|(G7"UU(T/`EOUT5juꁟBj԰_Eϩ1eJbXUiꉂdeǞ7$D~+ 1ٕr$DxK乺{MYu= d%9.Xh$+JJy0rIJwN^WJ}3V25\=:XMn6c"[isMb6M羂C J  SU\]⫿&sPAx^y[Uk̪1V=U20U&MXB1V`uXV٪$k5WoleO\|n\ _aRaz+ɞ\=m"T/Fk_:N'J 5e^ Ѯ2bƪN-WV庪Urÿ́UR֫O^R XMXfUQlU(r51`61nӤU1+퐬[ބUյ5ؿZ4YrZi%bW{0T FV:v`We}Lnpu>Nɍ[GTY݄(+ ~wŸji٠V_J puT|.Oum~^qUUg+(O;op8OK0个E;>FPቍrE*5YRٔ|kO_..폍:ikN.`/kN̠۟#dIߤ*Moz>j cC"\=Gʑ^gXL"Uya \$XV/JNr+둉Gզf;U=B jtXx.zD'+ w:_%?!Z| ݟ]0>Z;)Y?E6PUf %]ΗBj>oXPI]\e"a[p~Q۱Z6Vjrn?o*> iʝŸYg[7Ě:83;ֽw{TEubP=LYRUZU v4U\:REbT{VE)U95*Te`@/a^_bQ]Yt[VZ3l5c0,!*ٳM@YŶr,CaU5jtOl|U UU*ֿ"Xh,ZbAT,R,[ ªtQrUN;mn{+Ѫ".f"9cx(3dxJSz9q`Ԕ*9-# zgM*ߊnZPmWT dx6` 7/DDE$: L{6XT ?ޏN>QU-`?+VLJTuEONsVl}D(V;dsC\ ûCԤ!#˩YU~y_VTe9V~V!fq:!k{Ft.rl3($Ne `p Wg2|5FX%\7M8oaó+V!U~?s?tY]7 S)wK -܋ IDAT2/'r:ąuXǿUfr*C;WjUw&j[QjF_. 5ԟ)^}dU>:xqBJ*qX*,IQD7LCSHFajeՒO w 7W>qn[94as')z7YRWWy XCWǭJyHÖLllw X}\{JLUI(²o:'OhYX-TX-͈u.OWb`;ѫ;%ۘE &TvǡL`9Hw&sݫX +T4=%Tm+Xe XZ57f MO~DJɎi뾾+iX`i +5p:heiӰF"TIz؁S@TCu6EIR0BZEYRJSW_&R~ b5;Q6 Y VI+RS=s@(v;O*WWS,TY%!9 3s,FXqRQqE) cjg.-cq.Uuʦ_U+E^7]s~\j ӂ=+ 'c EXMJ ̶jƀZca,iO5LT,A*+aZ.畚\\yW}Jm5ڰ ;j3ǚRZ= Ԫ=A륔 DF_#XVT%g iQe7dIժfWLJ.D Gc_QH!PZhUKa1 DiSuIc|"3@3CW$ cjbmU]T*GB!tӭ„Vs*[Vb&z*ZVOV-|`:"b~ʷ+:`-UZ!U~LC" ҇bua+:۾zX- L BuVK?A:τz #%rڷ]8N+OҷS׳RY* 2bQUH69Y\8;Y۴ϑ})ƱK5O?KD_W=VU1;Jf1|**T?JUZS]@S!~nL(jNoB8FE6=6|,"j5LR%U5;JUbLV<@553&1VK`U*S{mIȪzgHͽ'{&„4yU| K FʡJrVj֪UkI*hÅ}V1#9޾}(Vư.jXqz(V u"|aJ8p0|2PatBl?1^E~0ClBy'x +LVְfP_hh:ZJIg/,=v]sQTƵF{pDbi@Dx=?'ʬY`Rob5+uQ^ZT8Ӫz? *W9B;XIZXi\j=)Jzu|HG#=r>rh3ULr!ij]j&f:'!+XjϬm:D`^b}Ɵ VX\͐bt`55(=Qߗ+*P9W7kfau&K͒VǪUSq>$Y_lj&VjP9kDնRbULu-$~|_@zFrUZWF:JQb (S jվ jUUh/3= 3;hԊ^MhrUkV\u ZV*V%W]U=*y`]VV * ~q|jJ{U~VUIPʅ AK-3abŬy2 9V+yUZ-qVY3gIZ auj^|d|g)-MZzyxOWq;ªX1J9=`UrEwUSλE}v&F c#CA-bO=8Iؓ.@>SsҌ"Q?Q`%z+Y>k+H,r/ T^y&VAC.WkV_TcN%$qj#XSDZTGn0j-+mR>RZʮ*$< Xj=v<=jq.ix(ƕ|YjRYUSv o`5V :奐 0-`uXuM_]O͗[lDzk\-t0ԜSf\JQv5ի+-Lnsh_MI3VcUӪ pT=/ 5 S WGPNDjq1WE^pNjG)|*KԉPCS5M7N(VkjUʿ$tww zP[jU#>{ߐVhpD;9C*Nt|] +ՋU{LUr&Nu:vSr!3$QSv&AR&~G05Pz0hQ'5U5-׼:-,vIsX5Ҿ0uTZh\\*fXzX] GG2' =}wziuga9Y߽ #hJtM!\SKU_rKFMI T5Ҧp*#3Tju,ˊgЦ?#jƠ΂@/3[Tca[{jbG,G\!e,@C`w}mHdi2WLՒ{mv7wJ)4E =[7GcCavnQtW{US8яWuLqE]`-4e k*TjF?M;8<3`?r6ӵ$1wqKƾ{lk׵-G* NjU`e XȬ_?*ўT=jQKU H#1US5lׂ$I"G2zL;RGKY5ؒtr.@E½ry6`2YCËjwWU2YqǴjjˤȔ#YH:U$kmI M=<P=z Pz7*LJiRbVϻSpo#uW/ !G*n5{kKuK$jEhd'LU 7 ^I!'#Ѫpfjbe*3!/eV?ؽѓv.b4^X̼S_:+7HYUT'rjLmZசEXU4)T)kYrlu_;d4ZXlUj_J d VÏ^zIh†;zIuۛU:TH1tjS5־*aV {< ,Si֐ `Rdhѭ_qrUjHPQ܊]UۅX!p[J$(j+P_ >^") 9[i_ V'uZi,5) #ig8쐒Q\OG> o;VR @T3WH}OϫUo:EX%?rjOj0&$8(1) ;\"#/_~@8*XڥD*t,fRod?8ة~P4rY Q7V{VF+9\MT@kWdvb]V۲(%R>~T𪎻:VZ7 ਈz;aZa[]a5<VÆiv^ XUI v*%ɮQH22Y|=ym;OQ}9G`ps^tj.橈ק[1d:i|TůstT=NpIѼ? m?⟤*AR{C+]b^W:qιOz!rF-D*KumWa@U=Zbj01Cհʬ+VCޟuGAY5] Z9#XdUAWNsUW*w>{OXEQc;D9V㽪Bʃaoj%\57jsXêmJrBzF3ʟBWh5Z_0 wyu56 ּVUF\5^"V`ջVVTMwwuD1:eJqׇ|b+XݗU;-^:&M;bO(ʁ茦7AXnPSI#JPІ@f*j@SK6R9ߗLj BY$GCz"JFq:# ^7w hR*3שRU dh-Dk/յP9κ0 W*2+]ui4K?-}֞9bb5>Az/jojU&`uν-ժ5QPzTE:\=9mMI[@Q[xSWdz/^] \ҁ4rtE]j ,h+\RAjxԣZXy|*+l@J<?hN3U5Zi:,HX/cL71ξOejZ/CJ#(jTsҿT[Ȃ@&7:ݎs\mPF?oVפf6PY}ro_7- !:X9Џ4'hyihЉD( ?m(R SW4XyH+Opg3kHDzk}.1Ƹ}_Za?|,̜U\E3X?{W |gڸ@oZcW:vT+C*/?M"<m D2O9y{и:=FnU[L;:bQ:sN NJ (zmʮSvpO@}z\V#ց kH}LWcӑ8-1)ѯ\7ݘ2OK 7D,a /V_Q FZS f2HsZ9lP[ X6#vN : :}wlMn ho.OHHWk77,V? `q ]\v:%6:X7,vګUIXm`| ]|(Z@(lx0սg+`o2$lWqS*RsHA&wJF>(Sl\ntXMiß/HU`*6Z/צ\x]*fKSYjs|]=&*U;LZ,|UoUHT-B,Plu\50'y~PJ?bz' `jl 8:ڏJlU!S MUPbF4x4-Lxl%Q}B%O$ $w;V;4 8T Xw%?\w}Ub KI+\_϶,4 *4&’e*3MRc{-JTnZ' ϱ$X5 T}Z`9Uq Wgq@դEk_>|[DbuQ1VBhˢHrx!Ik~2Vza,V] IDATzoj&@(cj*d*իL+UVzu =qDcMTr/#*?%}phR{:]~jRm5߭@RUH놘s-TX|h_ִZ V--Mk2=6`xUbmdxӪSVP?2FeQC5V,hLU/RFCGVBkĢ)AQI5 hWpk$YfUҦ-U_A+'cky/-gfӟyl`58U)d`*ȿp`uhE&߸zyy>WW `EzLϩwbrK_)R4ݒKK9U\JSXM^6*L . PV,ݘSDk_V&@΍<Jt/@{j/-L: cVK^tbLa kZ*Rlv0jmTHHE*Bw{t0m_CᰆոJ;WQϮM>ocX='jk9yseܠ*BVa\f}oq6&+~m hA;QaZ -7 2+il$VFL v**P8iRn oZ% vTa VH R>X'h=BK̈́ZNZ ez *}`1~1V P#ӛaժVwXz*W"JV9YViYTR dz?solU/Kʡ؅iQUW!VU&GiQՇZ|jjPbE)*\EЂV7hV~`ʎu Ma{n?@UX?VWSq !,X|ZXe2Cљ}Qm ,#VԻO!_Y`6H]+̉OeY7e_fꓦd4pgY8<`)k:dF~|%QJתZ_ʪ/X0ճcOn63Z`5k]˟r"Vy3f:c  >@N ^aUJ\Wߋ?}j~u\]>@85k؀*wߠV,>At+Vm)U_ @խtWpNIRy+PMT ~Ê?~o䪰}4s6VM* VZz4̘-+cyVZ < >.}Uwń8%",c{vWkKLTi]_RE*[`͊W+x]Z@=0aQ_ekU^NêW5oծՠՊ"[E2sHܝn '@ R"n)EObIg.S=]?_WU?KN4MP_-q/.Thf(*TEճxS q/P UxRڕ*\ z(]<Zn'_fPuRٹs գ0OeVŪ3Vr:z>^j>ec-x7 (F)Wm((ŘV<3c^k,]]@scuXUhMXWP<~JtUPffsΈd?zj2l1:KC_PgɪUzyRGČ |]e:ؐZBB Eՠ WVe28?PYT0bUkIVՖ%vM ,V6/kȝIj4exh#*wMRJtX S57vJg^ EN)%/E"9jC`FXā hmt=j!>1m0FsM,tgZ݌~\f pX܀~\=ieƪ]KE)\3XK՞Yp1ar:ܫ ޾n`P+T$܁Gήz]R3+P!X =p]CQ./X]ag&UU>ƺ3p l+rq>ܩX n8-'-A~T3`:rSaur^*f]puVV1YT9۪W^u'UOU~M TR #O'.Wy؏w/.@N*`'{5V7S%XS1%8\hbyr2.Qݐ_NT pZJ|*SNW] _Zh4/J[[ϼ-cz3.V;uyZtR*@SCUW<w2U^mDPX1T3l7tڹZz՘nbAM:qQ Y>p6U4BڛJ`9` qVYbp5VI ]T.O[s[J4U2u*HժTZCUBIRU)%i )\0ڹ}%*ViN%UXxt9%/2Z5JZ&VEǤ:Wg,݊U[uME陵 aHdцU7c!rUF hR8"c~=^9xv-ĪffaiL\sX=$Jww-_К*KUVM"6tUޖ*,=%U*2//0$. VxerK*wݸ?R>Oi*YZiVŪBvZśqrK߂\5΍&B~ic2VjVJ"@\Y3"V]܎UI&_@j$&XX ɢX&X-U* OaAV{BO"jռBYjK+[ਆ8F\Q W@ZTrx\*W3ိUnV o~R{M`, oBՈbwoJXRSU,V7EV qepOQ%;;2HT|uH~6MNیw[>Fcf`.u(fl%0@N"]1}n/a5Zz`u X`5W`UUG'i2ϹJl׻n챚7סeOWq<%rRVOzPVoTLfPP{2#̚&KRJ_t2)%<j]}5*[)U3 P=y}zjuר`ZuvUKnME+geJ< TiՈ3Wj|DR5]54ٟlVv3W\XZH-PUIXF$/6K .jOdU[Z*au֯꤉W-ۊ:Xª* #)?jŀrphϐ2KTH_eU%Ii ՎT;;WXk?^{ J"^ò8($wOP3e8mV壣R QEn+A꼦MPcpK)_V]M2TgZkPKB2a(auڹmK5= =52엊9 lPo}imV1oljzbՍΗ̭X6iZ:vr9jI 9 GHթsn ^?|0To #~PY <7^-hc4mr53)4J9`ljΞ@ mXݢ5=tPY)"U `ŅTVCi*?M[ $XJ7a87:i Vij:x4mZvOVDTYTU7Ul"HW"k~;B+E6EՒJ`h@l+朸WMTy1Y ]* ZcX6U`q-FcZ\U)~ Mnjj(Tu=k,M]ՇX@oj(F[UVt@g!{pr:j\c@UZtKk:nê=?U|>vKbJ=\:bʨQO56e 9^E*knR 9XH9CU[E#J*UmRrR@X,ru9㊫X]&^M۰J+;{6+ hiriL1S_ MҘ)Z@`ouck6r?O#ږ-(i|}Sxț ?L*ϋju{hb}(0{ZXE+` 'U,nk"&'8s ^ȍg݄A9gl`GA%43>i՞,V_艵IG-Wb ɫF˩]M+UMeʂ*>~"R J["Wbx"\mZl6Axu(Z(WMð)zgHkX]wUAcUOVu]6~oVA'mGc5Xdj4E {roƚzxch-UC5e{ÓxL-fޠg*ru73:-Aܱz{i8ꛫ!Vuj>U}Z"K*޶%cUUSgZ1[سb@ƪ&*K:SEg꺒zӚ@f-ֲQSRuek PŋA*kCqn4iezj<\ƌժ<5CezߴԱ?\`a*jAmUͪ4Uy]Aij,~eGM9aTHSu*dZ_t@b /+ Ջ\<_Š /\1Vuv/g.j\!3Uw,_ZB(AqsӸPzorU2;U&\9J5Л/VdnW ޲IOrnvu_N^S^t0U E6T;mUhvP-VIXԒƨ]<5+CZ@mQ-FU{@B35Ϻ;[xTtp-.NDOz䩿kP9rT.U0c.zj`>?"r/A{uu}m՘^NЖ _LD2UޖշdT-**Tjhym)xo_7wP|to mp5d>;v@7 :V`R& 0Q?ZPvݓA0f*oExtԽĞLUhto\fWC2MI;.LeE VTk2qM+ >e\m~nb{"w)/A{3o// h:aT_QH*>KRM U' F4V15o#e)+өGmV5ʂԤe4-irVUr:hlf5rbRx7*UX%f2f:źՂղ;X+vs8Kbu]4|8e(U RKX!U'bq4U@T'PK5{ S2աX-W# h]Lq|!& TVoC OD|"QjKX9*eӵbj#V,ۜm rL<* n[S*~h5JU;̧s{b4 Xۀ%NӨ9OmJ1YU5}n kZ4P̰|ww/&` _:i mv`CUb5T] s3Jñ+hVb~;%XM,4`Bo}'v r*(|+8'(܄Zs-doeQϜZ}X9lj*w׊țIUbn5Uauk UEJijTPhEc kf,Y@FtNUrᑳX!Ъ '^.(V? 1Cj$ .mU$u/Uv9ZMXM1NN*8YcKtdM@Um1Ke5kGOGXb zޑ 1[JBjvT_dEX\w5ҘMeS[B8i̴h.^7Mfs"M@-X RA|SV ZOj`N:cu`u@we +JW6,QvjJeP\)T~23J.R@ߺeΛ:F1 sA&y}LwD1bnE{*w&ڛzHRUO"|.<ς+Kv?żD*,P=(]U²(ڳz\=25jՅ;=XvXVBߡ\&We*7/zfa[q.ukT}+(nUպ"RBXS[~%i%[[bJժN3-z3Pk׿KUzSaǪE|Ui* ўG=E_b U:7eι{ ウGgiǡLVǦ=w#0? :.A:b5 q֦^bUdgVU孪ϟ>[j;p1Yo+ֵG!w·_kҬs†MѲZjlnV2-՚怍¸)6ejNjF>JJæunj7 1眩x(=hj֡j{#~1O7 ӻGbb5 ))̽ cut.~C@ˋXjU"S_eH^7yAX W)F!(h* -S?}5ܒT&P$w?A"VVZwzj}[k,/!Tɇjު*6\F(?_boVSպ.۪qX*;<YF>) +YPp}m^]o}; ѻ?{c}DPՇA=d2p @fpXŽ)9L=Wj@aÌ ?47h-U?P*fuVi $.'@VT:RzU=5X~Pֺ:os?IO;tR*8U7k6QeFB@LL/[봅D.!A:P(nA/<y+ f(~O+×w,ejW|d?Oj4BON QURܑ;^_Y-Qo2 ~=qyˮX苌TO5#7Ϯ)w %UJ@fhjtj(sE~VBծC^DV[ߏq4Pcuxlͽϟ|X>iVSٙ܃ըSNa/HF 6 htj=jXx)iwg]qC2 VT=̴Ghi58Z%o GV~ERpT/}u*e;jkF25wVvqzM`|M;Iy4`h vѷy< 00@fL@5De6MIH2F2ER% oOҳ-ĖeIlsZWpKU\W%ZĿ5J9 ?EUȮU4lM{UXkbڝ+@KPRQC7*D,%ր %3FP5p%j`R֥On5*d$[n&W ȿ̎UG ]fs( Dwd*ș5UX=oV?'g?-鱷839g&- :"/uĪnx޺ҟ^p#ƖV|G}C* e7@l4,˲(T-SdRĪ8S"+8ϩzf.T*~jڪmT`eU\.Pq՚{(W+(KkjU(*n] jFJ\2)D&4d_UkUk:aH#e ΚF-:NLaYYֹRt,%HٻD83ZՀ/iw"[IؿU\o=A8َKU}U4^wYnw^ow>ۃ՝Km]X3OO6eѤkPO9W/`%Opg&VR^fZ P lz)[Wuӱ݀1 iFVl~V |@TRbU *+է6Z^Ax4jz &;]UyvZn1et g*&Vjqu\^=>V)JrZ%XNO _}p}{Uuw8;{8]oǿ>: WC1 '@`ilQSeTfs?ѳzښ[+q5BKNR;ni`IULÅ'\v=Ԝ:i˗i5 gBiH)qe(0Ow6@ 4+2;mfM'B@h֓P(gtܚyEՊsq)4*8Ѫ]{9ǔYÎjEi<{VOL[})jW^wzcornWwegc` ||@ VժUb3zrUaUXbn/.^[POq, X;,Sa @-VAy*FBlL ~ T9 ;BJ4OBJ^SWRVILH`nT73$nKЙVO3wMc5&ӪV@cU]6^s| Ui`5lewQ{uXfFwwVK67זlwvmmpcǛFVq6?X0~&0jTU .CËQUh jnQ.-Va`2Uv5 Ψ:/UӁ(#ֈz;<%>ViUց^`*fn&J}K?9+\(`)**ꋕ, d@ Ž+#OVۥ?˒}+UwRXŗկzeV[e@y_ކ V9LE^5Xe6ve}/nVIcJrI #$hV,g]WAYTUjDi"+0 XE3bɓ,c\Oآ$>Vp2u'Rhd)3C%V함H)UjUnrL*X'&'si4Vz {rPyuRaԡ^Uc\8jާ,Jx*&ކ^w3fڃz&FZUÁhw70ZW@XSZʴZ*^lw1|= VGM6b2~lcbsՅ^57b'#8WVD㿌RUEJ5Q@BQ}Hm"RIkj RڡM듆RPU\i5K ]%j^ݑ$i7N ~BV+[MPq#*A(=D>$ɠq Tu}.Sk2xfQ pm_?V#UTWw|v`/t/>6 VZ.s>8ezDjYK]3Lv`G0VVW-hʗ: )"KжPPmHTՙ&E+Fe"8m2BUB{V^ɊHhnD6M=Seyb"IYlMH44NUHXu$#cժF !:*.`u#b{R5uԢIa5PbE8VgJ2V7ppYjÕ_p lU~jfxRQO0TоޥL S--ջ(XQVAoΊ^V7;TXU=S\B]#V])`\*ROW٥%+ T/RV^<0JZw\Hr6[z}+_ *9 1R'>XMq4Vf֓ L&UNծUO FA5{||[ aKzWք~[~fÐE?ZaW f =B1U*CzMURLgsnAbjUPU E*aR5B4.RvX [X 6˹?3 W2Cvٲ??ah* |VW% + ?)w"(ijV"~*!WqտwUz\8pE5+ ns*&7 Lj*d됒V?~-__3_:CՊ~1^W,6N:ٵ5^Wpo}o_OO\=ɺ=cjzB-ة/Im)seJe1:+ȫԧ[)YWUE.p: t` T)vY4:^`Cqh-iş 㨌mVh*rُ=uL;vZh)./V$er_֭z*}lqaVςՂ_ⱪ鰺qGz+U>=dCu9ڃr&G~dFNMeH;өCrY}Oا-Zҕ03+kv^˶]()3fXuےMRdי@?PK@"=c O%K4 j])!=EA.S2~c$y准!5=u!Y|hh4(QV{P=UUR8j"U~ sW7MwW֎PV Nju|VIaD^Gީa3|Xk£zܜZ0ʆխ(0F,jej{* ++^-kUKZ&`oAƤM7r/]L\cľb:ol"uƷbUߚ{uo-TENZ[1XO0Q7l= aimV!VVIP5/DUjKETEU7óC 2Z5,g0"Fc*uR.ύՊK^G-JX+ +EXU-T`u Mt/k,Tщ SicT?rvGPvxةs&߄$S&(<%IDATU_c0Ԫi *8:naA%J**xg2*WcCbYFYr][yJd_'*X%#9EΕ\ V ,e+o WLIkKA0pcg$}* ZqF̵:_uɻ|>/A~)?G6#ߟX9_9w!yn'2[p]UnEzev vV9DbE5l혰OSeZYPXe@!V)w~RW'Ǐzor!z*7:sa*Vk8CN&|%үBPz>X5UKeFU.P,AOzj/oIe,ِbw jCV\tQ;l:BBIENDB`mirage-0.7.2/docs/screenshots/04-create-room.png000066400000000000000000013050251407747233600214430ustar00rootroot00000000000000PNG  IHDRVMK&BPLTE(<\  /   !  #+,). '  %1 & "!3'3#/ %    '  "$= '&?2 ) $ %%9 , ;)6 "7-L*H"@&E++D!),7/%,2%5*9*A3#C  )1N +& 7*6,</E(H.A/,P(<-H4G !0T'C5Q7M/A1\< K_4K F[74X'M:T2C2NfSgJ(A#Cw!`!/35?-!'CEh.d%\yBNPBE&e%>1m.+683tыCK66!$TNU]8@FʱaEr$P+#xyZbjP>B32;ͯ *`X@4O}K.h;'>lβx@śDNy̯i̘\35wCDWsvy6_|ahp HŝhrMN~Huh_BIDx0avlMYh.qr9H"Y-uFȂ_( IDATxK#ip_[;(D'frh4ɴa]L! Ò[Cd T,HB*/GM!f=mo{yު۳_jWU*_>~LR͎(j&&VVVac'<{,Z٪{=u rZnݶ,xvݚQJZ6U[V*7Uxyn5~}}^o˓T<[4 S qP( 9t;rt.W(t*䬶[i9{K]=| _f\).BiYV6[Ӈ!߈e9X:p7{ͥn)PEQ`uFÇg0T6UIۦ{ť?x^۩xu*z??{j{{5H`nn׬PTb'777|\]|0֓Ex CPQ. p8{̕L&q؅ > UTכIU33TcUՅ7T:;*萧?&ap-Ym UA e2x38Bk2RhVUjNªY"H`iRT!k_n*HͶf[U 0,ъgDMVNяgg@~"C(Vʍmd]T.**` ϞUT\wv~bu ;̪B~H$#WSj<>ĪYX&U{G:%֝ +R'jr+rXeTˏHV՚PTͮU]?B%ǛMtk jjTU*ҳ˰RlDYnXJV/ X{*j^UN)%ԮH=)X-002êIemdZq`0Vvj@~ڎ&a 7E66PdNS 10CŬ"K1BܕE'TfU Y%!L1T*ɮF͸:&;V ebca'. Z4u7u 2^VuUTeWʪ9Kk4GىUV {ˮDkWUxn,ku| *3/\m6j\ z(Nz7J8'n r9/3 %AVeCi1SUUwdT?XA0]hfD%t W/Xi.81Oj̍f5c5|#>Ve"fNeWV>Va$TU*L{Je&p+}X+VWwB/U\5⪐:6<ZI8kI91bZ.?#|dS*skxح5ӦzT 6*%H*TJXujVSp_Rclm r vdZz::Vu`uR:|%T;3ܹUr@Zن.35`uMFHV#,ʫìb}iP}h:VQd XݧW VʴcU ]FZ7V֛SWo_Ŵ_~) Υ#U`8uZU~Y9ՍdY+Uv1{]剫F&U)zYMhg2ִJ[ňZ4ceLYiLx/\ldΫHg b>T 'eywDZeWQ/j]qzrr\eTeZRj3̹1'X洊N1j7LSG0nj7,H V>4XfbBUdUZ_&:MauaܴE ;Dw~L^?10+|#g)  Vu\87VUVNFUbUNW`U!1i%f9!ɪbƬJ}]v^ld\m9nTDJYyqTURmbWբ(vv@Q!)uv9B7tj4Vբ\P4y^ 'l&.u8g\EVA)SNOs?d7KMV9zD$W7VBeæY]*Xy5$YM"A*<Vؖwư' [֛~= u7'V̋\3>~jk~Cn™MC>wo:u~9M<@Kl> t!6vKV?k48w{ō+^5VUfI(:Bn~aaę_PȏjXwۛ?aOƛndmԂ.O3Ѩ\aKW50W T4ޅ9+AiIڨrTSuj`56Wu>TvEMh2}@ܱ\…A#SFT`8fmJwBViGցj2噧:|B,qB*{!;ELVy)V*fՁ]( aT'STG5S>VAo$GcS1`/7Z~dUOk3IՌ)U`U zX;]竫?r~Wg Uۭxp O7uÎcnGGUTlCR8g*Ng𛄈-/(`䃯<@BU3m#erKC1ΨHf>\ZfSަAA VeL'=&n2e "?>?k;j<3?׼])fZV,WM;Tah V U6X(1x.&ZRq1eꘪVD?*eiU(m}G1 LŌ⯒f; ʝv4uljpJշJ'R9RXmVgggѩeVw}hEA VKIVYju?;Z b U6V]/VLU*\lw$JB\hwLm>ͅjq^+zm~fs"{&los{:tYXer/RA~~!;awr|^\8'0۪`GU^ٟ.Lnt 7Ej00;vysnZXtG# 2(]\*N2Y*6 ) r:\$1R0}RR\Djjo/T$ª+݋aFN:iՊmA(W J3>Ix 7*m7nq--!)IoYMu&4)j,^V&qR)X}ZQfjx>*6K[!,jA< Xk5V Ui Wsٳ99jnJ]V zM*a5/Ϗ\=T3bH:qgq6ldGҠ.D):= }x;鹩 gQVd[]{^DLf>2KɆ*(|٣LǍ5]9]`%9멸VU;W4??ookqG'qN@С{OVŮ`I= Jʴ$'8 _cjg\uF+Ԫg^Qqz ,X𞤦$j7nЄR*rUEW1*}`P #6Z%ͰdR]N3$ji[r-_="+z\D/X=f:H.fx *~, :rx"+PH$!l2dnT- tBIN?3[wVN4r4/4Ut;"!EBiYYְx\pرS<׿q]Wj(]V~+ Teđ$y)O[oWruNrU嚨|f bIao\X2YRS*Gb" 85-DxVꋍq WJ t^%a6F-_g Tu9~D5/˛՘^ )"BORWm5JR5%H(>@SSZ ,cV åEtVZz*oԢ}>jP(Tr@aUUezЙUJl#a/X Vp㾧Uk3^~XX&KՂhVշz9ݾ~'%hy:n5,n?,?U`pnW!uҫ;φl,YR6UFPq̢̨wZ۩6Xj"@XfPZyVr1H[baUJN2x[cv,Q䗟>O%׻WVtӇ1qX[XWbYW+V̤LԐE3k,U}uPVfus+_z3yP7xԪ`]_hjJwc`U6VE+j7`^`5XE1ӫP!%k5-h"}^W⃮{]ٖhRhhMru|H4<& 6NM8\ǶqS#C$e Ry/6rhG73'*ӵR\k{%3bEjRU}un^%TŴ BJlJu഍y*XUdžzU5r*wX k=a:vbuO&i]iël`lV.LOs }=W''I5(x<$]f`Yרn8t!`qz??ZSXjt;OY^Teo\uV*r)6P dQU%JbՒZewȺgχ?cp LVV/W1h=j0X%Pv{SSVVFgbe":/j^o4[2J$bTM2v3S; o`E+kֳ,6DE *sStcV POoI|)GUE߭sj@E/p]Su%7V`aʞ'LavjE{!A]"x#U]9]:dJeG9C`[R_Ë\JrR$ZqvxPUF]<[s-$UiN9\䲲Sɧ^rU_j-=H;-dz!j`UY[jU!+WeSaúE[կc׸ZWJLfT Ujg|j^&εU嬫r2:OO)5?0B2 E!TEHe/mV7]_BwCVZ?8¿+ fT.U(ʏ6L=_,P#RJӼ ~} &4[Icj5K&vB,՞h:{ IDAT0m`˗/|ȮF ݤZ i`5X&VCWp—{TN2GG#4[!V͙кPcU`QV LUx%ms`4ԩ lK` wc:=9'fp/kGN]PT(VӮpj?aѫFIHKB>%kKӲ.Zwej!jP.EkhY`*n 0Sw7&k 6k5XF*'C,<f:תiaQZVU7AeډRzuWR\E۵) UӫB- ܄ZVY.50VUAյU}鈽9Xju(U^iQZa~ YvcX=rԸ갺a5/Xe Wl*;հVzb| ` &{sxyU(]X}{mo:bX`=oon" :/m( Vu,/6@]JQBYfξGw\5x[ְ*_$ZZ^M)2UqoW66q05>^Y9 vr=, ƪ?P^vXՀj0kw#2mۥחkAAtSU,T`"\WRsX U.JSUOh`} ƸikKSd%m U4TUߚWa\G+MO^:::6f$C,@LFT{q*a#D#+lUW Ѿ7@Nz'#ϥ:GV|$}Ϸ萟7Xm4ZmOөATJD栊Z_#Mkf\QXMF?Va>OV|XeBA)mGJ4U-[UZV)RTm4 t-q$oz=xހ2V(zkXem;V9<i/U.Km:|%_չ"W%Pu>;+*VsZMFMvWY [IvI{E)B]꺬EXeW)ئ=| kj6TE~dlLOCja2*MV?*Up|UuTI2U3<\*"ɓ#'zjX; 7:vaOՃ 1V3\: ]XMX=$WU2Z[J s*Rvv wƱdF6-m,-e`UTϼ @2R.]_`bu,p]cU%U_YtBzQjwZqy+ҩ3oէHg,P𸟕X5 -'+Y[cBN0^UkV2!TFͅȌK67&6H2L"d)Hz !BH&ԄrQfS %^T jWj\P/V*a{z7}y9Gc$yU҂0o75͠:vwzh3k" ^p^W?PS>-NF6@+vW!VWVQ2m!U:=1UÕՏ~1: jLYTXZ ڦ`:~v^jTJ**]֫TVVs+ߋAgj__Oi h~G"gW%#Pl8Y/#k7nml(?wX.s;_P'gѦzu T_gu@3X-HFU6r;HTh^@U<(̼:=A`VnŋSrj50W'0ǿ̾jUҨR WgoW#?"j):AX=eWoT=@ XV[ۛP4DId&@L\%G#TZuXu`=7V6w6RGPMZ5nV_?/~O=[7ۏ/>[ݗ50ڿӅ_@ENme=:ɔLFJPj5`eSUs-<'oB Pϲ=QijUՋ:1:IjRA(sx]VG 4PG@YIvmk6jjߺZVEVIFw"is.iWJ~D ^ L1X+VSnd BР?+TuU2ؓV~UUUJ݆?e*yR?4z.jUOޕgu O 8$[fsaBEKU*8q'xFvW  Iܡ_~:[M,hCڋU(&*h)ʜ/󼵈r]wk}ô U.5$VC\e_1uU[WS UHX-y} [j,--+ɹ7"VK%XFypp~I&%rs{M򔦧:P 'ɯfh%i͝aĔf~FbEc}2aI@|,גk֧0*a6 \ͪjA)ܯ+ t )NdY VUXswE>*\ma Wg\Ð">u,0.[Ы^ VWd+cxjĜYu q <f:b… jF_aUwU=clRX=jpU՜|;5+ytjL=ժPzv*GvjΪ= RwY*tZ"ɝ):R&f.p3a`QZ jȭ#-GY!øcU˪|2r{e[mqVj"VV7O8Aocкq"(OoOU9{K ($ ZjGjYW5n}b`k Wu(V.DWT\=Fu<*QaZ;Қ,WQJ.ҳRJ71XѺVnT{;WjJtm/HTF&3,V$~ ժNXTs%RE!J  Ug*6b:;~<ûL(!Tu##=(jk(0DRAw3q'[({ йY/Ԋ+),@BX3VV(TupQO VV`SJ?8\ϻ's_J& }'q-ձ#V@T_eTZ`oRe5bjFN'90͔aMX-MzHN ӧ|^]`[bW5 LՕ*a:[{&2V)>%Kvop.WА\/-[ _2U]/U_O{>MϬI#XkUrX1TT"kի4'\]j'W20M_UJ:쳆J(VU)TM|%V4@hТ"V5*y*ju]4::}\3ZD?=xX߲:O@j++Ma&_ "FW/ԒmU~aQ5F"msI650a R VXtF0?;j*#q[̰r^gMۦ6%,2QUm-²* v2-[1SղV˕x{9fԵH)s)[\0@EKg {XzOZ9ֶd5 oC~u{s0/o iRRJѴ )1 Հ opAQ.бt0PamaEVbuO(jzґ#f g"h ?g2a$pnI+q]=Z/4wI}Up>KXD+usFx4KNI"6+mYd{!C럡DTCZl$)` *|-$2d3_ IDATB2+y;ڻY^ }<+`iZ:u P_%^Кɧ ѩ2ٶ5cf*2WP7cw?62U_^l_\X UIѪ~x[l*]YMNٳlg (k~FZLTUgǟU p֧^ }}tQ"UZl&*s.0!2WEh*Y% ,XMUss} J$ϡUU_^2ؙb2%GU*:riu7ڔrMώΤA"Ԥ}RE0UpF*kr5'W|BW23 mk H j]ER(,=9%2-+tZ<$M)7PXl$Ϟy7oNOGRRN\okt i͠1ZV.֎pfg*,bƿ'Mג Z:E*rޏP&aj\mZe'Z FU'*--@U7 + VTz 5ZKA\mҤU_:.Υi&b V-ro]U[=﬩-$j/3inp·JHh%fU+AqAT&T̵z&OjU*UGӟQWP9`águ,!Ò]B; ܶ +mgEqIJic" QkDԨ 1*Bq|&\&*^MI@ߚZ{9eR߷}Y rU L"ڲk0QNQqa喀cu*VQtXTЪlvxfXWEF56INb[jUDcU7EXQn bud|L"XjՏ\(Vީz.''K8†Tjc9{:%K%2]c3`5p3p ፁo'{]L|Rhoshv<;)F ɴQI!s>p9.FajUƁW(XU{?VwTKU eZ{ 7JUV7*Vڱ0'y;t&!& z*?oML%'&.pO׌&CyVIT͝~թ]㒌n`w]F @*l$T.^ u./PkќUgf+%,XAgU%U4XZ kjyJ}WH?ͧX3Q)КQLk,gU*V+VVGT+?g5,ZcsձQ`Uؙ˹ZSp8W`Ujf\պ\m8,U.|:|Cڡ8nujb U!ó8WqY|NX W3X|TչUT`ƨpu>Q1G*B9ތ Vv6-+`5v-`]-k~2|q ˄5^S ?[ɌxP E*nLēp`ճ|;Xl9׷4]_T N8}9BUN_ꪢu{ձ:X=VׅkǙK1IZjUoμc5l\+mV-wEJX`MSv:Z5|q£ի@T`Vf)obթvg7:.Tͬ9*VU ]pUz9Vs9jU;Uj{]FpT.J]D,W?Z^>gxXUZ}$7Z95tʜaP[ kފCXM.ѯju /_\Bn<Bzpx88.0y0 ֞9n,V\tH*[OEm|@귤:[''fZO]vVWzV-PEi\k5#mIYU!9Xa>n<13~H`_gUE#mUf0VJH"rw;txF3IUzV+})_pubTө`nU%&Ya MSQe8̕rk?8p5v/VdcWWVU%f֞vmPz LsIC~Xq-Yښ?U4\$l%d֪5br"@aj^h*WŹ:r5bW(cj|zfѪ]Ri6z %_w^(DsViM ܱRlnոZAU=ccn[*j%RZzUD0*ϲl8P2^yCKD"dh5)r;G1<ט <>yil-jzwK WiHPL}Uk 5K&\ި??ruXj8 cnum}5ɓ|OAUz) 4UѪI]e,V#[ PWUE5Y*T Up[)|ryɷOmjY*@MYEϓBrby(WmU츾r_:+x@E}gyA+@٘vJk}j\g }|<fTKF}fa,ꔒFa$R0͠F$K2ĩdIes1`%DM@R/;s+ٚ m?yBc##XiǪXZjBVzEOs'NVD2>vQ KL {XVIpHT-><<\[=<=&TP1Ú06Pc$(=48cI化ڻGb2o:)X-Gl%F:e{qܞq `R|cU{ڀZӢȂe_ jVp*Td;ߚ;]VWj+j6B{B"X b*]AWNӬaXnj9w@UU8Z]K7VV L"D&Y\+("}leЛ㚧k PY7 iy6aō$ZO&VRU TU*a⬷ ^ ?ZiJ"kդDau HNϰJRT/Q"F M֚.>?ϷjV`8Y" Ur1y*$aUBVϾM,xNgXD^,..<,]NjwzcXz+jiՒi%ӫ O=4Uev_[:缜[6;|߆2\§md"ƏJb\R |BG (=`JAuJ2lIZQUg:hbTU_FPU*)SBF|WFN3106Ɨ"t?Frd2<I&9ePV'[{^['r PZ?9EhipPc2ˉS)| \9*yB(Eܛ3)}qUbj ՈmX5TX3}"h10 X}DU x9CZ\lCүFT%$DRu/`Ze0>=wp5TUFó<;Vm=oXX-(=L.x *_dd(,MзտVD5U,WC#5ui+e WF`y9Q[lwg,ܔΞ-@t2ʼn.cM@>#7heӪA 1JHJ':<+^4@puS֥r6pX \F7 :R$5gԟ-(gG8T jTaCjU#OrͧڧV"]aGUXxV'dYՐk*T-WVUe *!ǷY+^ |W $p6Ȳz?:0 XZ]ڇVXjeﺉ'j= H7l!Vȕo7`*^%T j U AAVE:x&U}xON~\*r=*X KVU`YUoazsnxn,?ա\Bԇ4ӗaU_5U=OvU5JVoav::8ǵb$]LXM S}mgOZStDH1VYW}ɗS /Y7Zksd*/)7 *VMKX uG }Ӹ~{\MY4o}"tR^~i7-y+]86Ks>vl٬3'z}̖*jժnpJ1 WX=W'W^<0OUU7b5~k"Z=wTp+6WCJsV[a5sTMwP-ejVCTkUd+ U3Ȁcׯ:ڎ~ _W`Շ qYREXӅ9d=&vVo8Hɕm/$W Sg?|lk.UudpUcRK dJĪ=P ԳHqr5REBNoJg`V,EjJvGR;5? ,։da. UZuy~=f_ꧩWmTwVҔi5/(ѕᅕ~ݸubz'aFPR`U6t{{Nd뽷F-⚯*SZq}@[.t_)T!8שaUkёb ^XpggJO+WU~ ճ\j6IŪrqDZJ\VsUtjX \bqxGX.v%r:du* IDAT,HůVPLaUTH4[u0k_CڥbUJ pjX=._)$V%FY5dx8Uܟ*˗1|YV)*uWs@e=I"jaB劆MOryDS\#1ka^.cI?\eN/r, R$kCf-j=U%?%2+]ښrxxO?tnPE1)s]ZC-‘ _ޯb4f7m*XURY#H5*=]*{$GVE5b/,`0 0AA/T:*m>aULVյJsr?'fWtLlq~#ܢV+ў~~df zI2FUPʉ/ЪxVV DT7Vwgl8qde*%HJ|ȶ`eH,aH;HL(ƐMGj&BRذq ] BM_|90l">=Q*TI~x?r26~`b,`\eը=#"v>Ϫ U2Jd\ll.IeV/;50 pTZ_gVX_|}mmu boVɚΙyߪ[V6<=CÔѪh< @O6ޣhM+~{r;KF =7lڊ?C,yz,^aT(4lFuzE-g2TMLy ꨺4V6i{Z:|%Ww/7R[(Voie&P^~jU^>>ΥKSgk;ۿZ)i)/b5XJ Q5[ +c 7B nhJq`\j3y:=jU}_QmD^"mi??AQc^H?@7IfCVџ5'~ [Y*n;T|lchq t1 BdTe%av BWZ`omu\xn3XYNo7VY h/! 0i»iy*!l>AP!SA$P(ZSq)K-KO79bK$)"(䄪0ocjG3 u4'B'GK3ŪbU+sP$JV.!yոh*cU(*g(ÙLF^sXpwTQtoocwjc1<1[أF1# f׈B i!xаiVvw_@[Zܽrk"fnԶeߋVd!]<ʬC1$#-KA\6:*5'聺F=}Y+P5vӞM* X0%@jUuQ)g \zOgV-V'6XPtG..dByZU=/JlGo+Xy?;=ҮEZD 7|omJ:H}wEBT**jP"4)fe2]k\g“ ,~M' VsҰڋUW&+_i5>“ZyJD3fYIi5lau^lZqHdq/ʪU l-TU*[Ej?"W^IjJVZ1դ{@jFB8FI&Tb'Ut0+zw`թշTWnjb\EQzzDF֫qc걚Pm&jc7ʤlf_~oՉ;_{j0d1sLdD\~S@4$ { U2jNXd`pGj!r{U#jϾ̫\$;DZhJS,VҸZZʡ]Mz\ G|nRƸn1!`%2%{ѵ}++w]ُDshL@NhVTZ"ҔyNzXAUMX).Ɏ8x7⪒ \jM9|p^fپʉ18FaZj-X ƅ 1gVCY7KVȹJ{%/8TY+"Y!.JoV UdKrwjcl XCfHΏidiUw|RcYC~YBj>ݸ.>%Yz*g@m]鉫ha5 'сM\ha K*V@~LWN'Ct5nΗ jMѹVIwt Xu,X%AK@_ L Ju?U0Iԭ?=jqj1WDՓs3 juFXe*8^ESSVWՊOy}\%zZ"\ X h=r2j((dՖ8V twBT6$uV7()U&i՛6B@RS%\M] P'*uJdF.]~c+GFZ}r:i'*%^4eëu`ȧxP*7m)}V/V~gu)kkcv.7"H ^nPI*-h9āu{1 =8?X1!9⇥X]FF,*=b +٪ Um߇uT]ʳY].SdwdS .ʈ8~"gբvh_ $L >Wd+erwxlBlN@LxnwK iL*q1_E- ^QjEBWƘ2feB(Xcb / {*msk<[WSeꉪ*} Vno<_~;CX~)TZe=\5.pY XMOUӟ!le_ξ, ؖ iv ÈMMÔYǐma Ja:8qAwq\픎d  [s_9ϓ'KX_~.[ݼ>"gV#j$"@SGb5٩V.BUnp bLLNFk`F!W H4r$CgYTHD-{ͅkR3LWMY]*XX4h4>UT*q*[MW_ sp%U_hƭp9W qx}D'fUyb"̱)~T+yM*"j) ֭-߽@JRu[[XXՉ/JZkM{|{T_Xkż (PPdwe='r]艹 ú㪒!q5U֫ pzV[Y3^*C#TנJr\MMQ^0 N&Z|־zT[ɨZtIS}~> SZ}I`O"j{$CobXe1[`W*<VY>ṷ[+)j¡3w+j/o*W߰VhMu[RK1m 7we+Eru*U\i7 ZU)vBը@3~)wc׉ڨE3 :buV-g{oSѧկ1* PT'@4Ğne1fV_B۝֖W*L*KWo7_>ZCSAV U*0VEPϋ @t!U"bU,x[XoERVSU[%f-`֢PHZhy`&&5IQ5OkG:;BJr V~:*.J8y;UO$WϾ >U'*x:}I3LFxـbGJ?DZs jVe>B$YROT:UEṶWdLpU Xo VD7JUV#&dMt{gN@̤ˆtN;S{nwv}墨ՏV3UjgkX%l$KjNĪPU2fe``uhlT+YXWshP-hYUĪe+Ȇfӫm~o& tprw 2Y5*ب}iYQmE"Ijm޲\Uk.O$@W9ʁ/ uQ+:PՆյ-4kfiu M%Ƥ!m)Z% NMJZ3/*V;]~nWGsXX Y]=Q @X=aTAHޅjިJbհ>z1]JE ,g_s\區V9 yP!UWCLNVwNUwpyU\+me;V{j\$k%cɨa\ *].!`@竸yLc ^.T2^*%*"%-FkZdf©G@`SLyj=2b)UǰzF:ZDp,I`Whv'kLϾ L tL&Z QZ"ʁ\?yP=k7@.nKՔM+2ѽsC+Z.dJeQTA`8V}<*歮S*kڥ U!*iAdkU:Ub5cլMFʋZahH+XV(T5GUUz<,kc[)Iau<`% 08Vu;jC)0t+XbWDc0U5VTTSr!w>%du&z*VO|Hxp)dceUgH'\E[,qO1GM'V+|ɵ)|H'зfn{V:_*yD*U+(U5yfԪ!Wi Ǐ%kZj='>rBooדEpX[HL2ȕ{MCX /"DϘhQ@'ev@:sBI̭̥@S*VSX{==!VX^U /fe~>@G;R?/ZB U2MUd3(z5RU*#Ɏ(Wͨ*-9)3f3? J2avyDV-+0p5_͕5BgzUQ#]T~,V}8la[v! \W(փ_:VW5![:ijrx.KqZRezD@!ά$q QQXIi# R !fBpCv'9(,;}yw4Č5o.֛a}Cp4ӓ:h`vXX bUa5q*ϰύy1yZ-? }mNJV5\M-#;~1? _5T53ac ;2l .rDHOB/XQ :@T @,VZպZ8$?&Objv(VUœ5_\u˘b%Z*VW$jXU>\W){oOzWcW$TV{nߏ&+kjU8Ѕi?SʫI5^0a[q^o(W?ՏN-oZHwPv6if>c`5ӄՎv‰ ^Jk%Ͱ`V J jXj4û߈)@CjWҪVMw k5^fGXekPvt\{]LX=OϭYGC*F_{;"LwN/^.is!R"êcJq5r,mj/7i+ZjcqHFEj1 dU`YRTiJkfZbkkˢVQ@VF\ڢ 1Z,,b`{.MIFai]/ J7}wZ>T5Sܢ?U:sœz) 4'#\{''Ơ5 V}7MV`x0 1CŤ:3Y4!` %Էl|UxX>j\͒b Wr:gs%@7%usc_;:~; `UW߭imj۟E(V!A҉+(bWV#;r=0`kO׌Z5L$p*b,+jlc}6UNXe})1rj *&Sh'{Q`5"]?sJv]jkkVuxUz=7t*VـU;jW4p Y[My՗A57&Fj~@'+T+-zܗ옋bu&L[ 6j!N)aoڸ UFfL}]JG56AI%f=!HYҽhc2\p6妭Wq]{R]y@[_,ZWPߡ`{\oMbJ*{q PՌ~0zѪߪTij:תOUBc_A&N6c1WrbWWjjuP*V˝ʫV}&anb6`wT\+#V𺴐7[BN-{6ns"U0QQ$VzLp0sڪX}Sj;6*<\  E7x6t\ЕP\Z ի_^Mdxy{旱 G\׃{q Q-/c`ovĪ:V 2`uM"*W,)nd[UkJUˈ́&xQ'ni-ѳp"k0Ŗ QhIE@GqTxy^\kۼЕ\tz]0;OPC?ESh9JciDUUtҬݴ:EqU>M+R9@롴 tu*X=V7C K U[q C8QG GVظs玂Jژjk+~ CZZ~Ъ-Y ~jvvIXrUhmM"U{`PZe_?`kQkˆ S"4kSm*7+g`#`UXݷ:u GfxjwxXjL:?kG$({:(<5>VcvFcG5Md,n~dEDJ0RY5Vh尉r+duՅW"JT SQ}ߺndmb l%ZVUJoj|ռQսjKIvS.B)\**Soߺªe MڍXj,0E2pXDr"@3VZ-&:i3mV%N*:o^nOU[US=!U,4AeQ;`MX`_jՇ"Z81MX̋žF2?PֶAn3k}j 1MM>]gז=r5eS’.~_xZHճՁ3X /0|*k,SSqԉJՂ%uhߠ/<={@Y.) ,zcU(s4MMe!)O#E]@@@޼dj ƧH,qB+Uୗ/`=-3 HgO)E ׁG8ǷP\֙{A2dbb;;YmGê#_~ `>.X+Pl~)}x+E=+ .p}=XX`X,_)GKd PE}5] f$W_,L-X͡PZuIr]!0vqU^3آ*ˮE=2EGsܡ@y-wOê8 l(WK&׃CjzE.իHlռ>X @LbߍI:N&V\>pV/ykYհzv εYvŻZT[]WHiڹDOwso*:\FkͯU嘆%QW*^hZf0y ن>H?tBU}Pee 5%v.լR"Ǚhc5o۲D봐.Tg\5[^~ ߐm? v>y)d*^щN'X&䩺=f@5PxZVښUVxN!'b  z^b!В$T=R1)֍zUr_UXFxGӬV6ŀU[U1W2L%w@J*W_,(}S?"Eqԧ#d2EbꮈVWdFXEmi[PVǧǂ?*Q7:}д" e#ػ*䮸/ywsC"iWcCTФ_@#?WVtX*pwR>V}n%zJm f`u5$zznj`Q^3JX,_ *YwDj _ ;pͱt` mJR[TjʩYX Z5jêVV\M]䪼H*(Y@SUՅU]CuaI> {cӊդ ruTY8iwq"ªj~j)CfO*Q6aeU9saiuʯ<VSS?j^D`T5j S1L1uG8:Pi^ ќ5_+Qќ0S*(Vk+r6=1+BlhW\9K[1k(i}MpB`4Cid8X _5鮄A+bP< |e3Ru ˝X`wdVƶ>֎B# %nA'_/EU"X}p.L¶?sؚ5zuЯ L=T=:¼/.4Lg!{"WTOWJH+諍l&.fS%8v:z/?nvMSVU*w*΢ \}pv\fwUwAf3ޏbnA5.x=V0-Bfu봖_Z?Vbҧfa}iʎ@$X9@>Tƍ9X =+s]ۭ0+FՔbauDS% =UD8J WwQ` kVV (V`I먎{6U/jUVȪ{gmF| OaN~ XTܲMmi%#R n+{zl"AmXL|%XK38KW7)W2+r3eL+& VE7 4꺙{ dڪf+tJzDJ:QV # X*Va -BNuzwAJz `4RIX;ZQM#I UO0*"@/[av4]tDl ~vwӚ Zuy!WѴw5`4fFky4jruۚV^:ܣò!bUZ4T6ػ&[&#|GH3H-ꉨUUSmjJU<~uX"T}42 wUm4R%~yͪ__D TI3gr֌U@UUvs93g|3V+:-KV+@pLs j՗cvWǦ,+W:kz XLn"j (XMH۳5z=bMyցިC+ucU^1VՕHuv߅X\y3wX QEX|@ՙݙ 䪆&'ng5;ܘ&),jj5O~RsPX}`.%GO(#є1s#UMxi,O &[<*EݤWڜSh bUWs6TnvRVOUeXLȑ!Ui:L֗#pcE#"WZ鶭zr9V'(f۔/5në[*VV۫ۧъjlX5B,cUWWLssR"R76X,lpuY:b\TMSPTX$+Uu%V3ǁ3krC皧y`7mz V VS,H`zy:h5XMWYnzZuPRגTEu[ _Qs`ެ~b#Y)X)P@7XuJke77?j{fIN  D*P% S yݡZ M.=[–UᦉU:XËs@ N:4\NrU;O Dc \X=lP6g-R?H1UV#l]SXqHN%[ӬXMBUs1V W"UcFeDJҪ2edV{j j@q@a*z͊#24 rdoaU:mU"ͨ-F"Fr}FvĦSRhNVWY(j:a.C\UknЃ)1˖ЮVV;n| گEWy=;GǨ;w(UvX PFo ^z Y5zrr P>՛ :ZtpH^'ye%"D+H* 5CkoP ( RCWcDSqDƋAᯜ99ZV+pmfI>v1I7WӘF'6rUNs(V sּڰ6#s9y\uJS PܷN'ЪsW>; N('*ص/jPN&zd#VP'9#6peU6;޲K}~O\zZN&=>60UVЩR_$’P˄Z .*T&ƜbV5w9.TQ>}'OR!k:sr;h'2YҴbC ~{++rUpx_SLҫBƳI^F $+{zbv߅X\"rv[Z%g?jqrwmհ*cVRYj~ER[\S2JNGi҅\ϣ#i3H[}ut~*V%us-qF+XIy4N@!ڐP ZfmR#zUZz*eq%mPR-AE˅Zg:Uq1,wWTI$r&+X}(לjXS7퍍hBGh0**@''X `5*E 3qu:#>عFSMj|CU4VNr_4dŋ`UTU.,̯W꾶jXJj-ñ%`L@ZօmEgZ$V'hVܭԹ%8\ױU*n!zJ * xwې02\VϥU(T+Vh  TfuT]%V+D+&Wi599BXJgU\Xeq5VV3z\ DMm“Y09JMwO>{pfjRՖ: `)낧΢5zGeؑƫc%긻UXpӠVV-`(kjhREW3aHZy\{1V)W5Y-Dh&\lU-޶9WXiqUmmjCЩ*j k:S nN̕ASV6w?T%9g^OBj}E*V%+XiUeV>9,;$oK',->:jY-!"kAE“.Xݘmd̊թ) W 2VIzXg@zKj;bTf+eՑr"GX.]u8*o^^H5 M5K b _省< 0)[:-:^; ii(ֈ?Bt %!^oFYʪjs!E<~O鞈V9ySՁ>4Jlqm g"N]RXPps6f4ytB&9]Uj娺# _1OP* 7+džXiўt$`YM+f*GvʪU[5'+ˀ74*9̋`U$+T16Vh Fvr=Hmع3BbBo@ ͪXׄtE*tcyzyy~޸ "Z&)yViu` ZU\V_ĪX^虮s;{ 8JXTbnDlF}ac/Z"%yϛ8˛Xa0}k=i]!jGU63g>5\U]jR|j՝կy/gJV՚pU#}ը Ha4b6欒31'%rJJpUzfzVJV)ʦ#ZƋ!&pUz.R:$Z;lU`qսTUK<J͘&jLժa {(zvjU);[>VKQ;鷰VXon{3c[=sj "sb5_cuP*렲ɨ\=lX=+0:-8U+fc=;ܥrZs=/"eK_U]*'TQ+Ҥi~Ʋ‚L,dX{?[B[ X][#V %*<`8Q׎T-Dhsv;e}LUCh"M>^nɐX=Y?+58OQ^!IXosjn׊PY]mU{LJ`UVnJq\\AMfrJ4N,/dト1ojUđi7F LhG|z&Ԃơ3dC;"qIܑR2xeeŝe4с /* X^)7sBu NZj>j@3قUp'Vk 8&F]L@Y؊j@*YECU4x._H/V88ƊW^O|ZMOMZJ[VzڨXԪ PD(VUZP[zdjeպI5;0U4ZZ~32kZQc{|'V+a| \uXFj5`5P˶wnŪ|"@lr%bkQ5J hUf_0L61n&Ҭ{[rUw2A*V{4NUg%5(Ek4$7Cv*syćiޑ:%XUթ>V2:|- T9K3fAwg >}txX`9A⅀͈Zں ̤Qx+,24 mw+vp? CP7zWo``07 Xm"%uU+$Z!m~пQUa]mj^sY(@-/WVPy8ѴMXWWn̚R7V@#UOOkE*zPYQ uX}܊G ڴvyWedJH^CxGVW}cR}UQAvttVe,TW VU՞/aWX}lp q%Fh*?F喕4Wz"]rUZV0X5o;VO +TpݬږSj"\:Z,:uB A?S5x*"ևфjT0{o;QRN>W|u7/`.Uê^ E+>dVWpT.*Ǯ{D>W_Ł [sJ'qM~+JbrՖZ[^5\Tgo z$%nhޟޑ7;Jխ'jJ' ]v5l~.@Q(KK~mu˯fl,/ǵO9@xDv:_De.͉\ml jr ~Y-9N:  3deilCW;EİU)ذײ׬Fuęp(? փauޏX{өUeԧxwO*DaXed{T9@Z=WJD6>XHsq+Q Pքq$*U3JJ.M#[Qj*UVm0$Y^X/4 `8jܟbLşbC!0EV; aՇUՁV{_韋h/A4D]zUUr7c׮y=jԺ OC{ժը\iPm V\mju{lY}۫b!U$Υ>oU: uG6A⼏ĐIᬩLQ'v^XeUy0<1>&58IZsMĩbT>=sZVU[qʬ1?ec샵=U㞌\ i7\E9֞|*b{'V+~鰛W?*0pz啴\}3T2~m4`oP(T]D75 r?t0Kǁնau+iXxD"VuDz侪e>YdUc!W*_ՀDBՙ H+>lX8眊[UT$*SCjk*f\@$๚X _V4lCʙRG^yګm67 U8>mF IDATj\ hHj}tY;U,()Jh= ,r0UتV ju>Zu$3Dل.>>7jiU*BܭǪ\mhO*.rn8M#hUz_V b5O^dU㥬rsZ׆Ut ޼E@-[zaP͂ K:QηZ P`uu5/XPg搫0D*^ȗm 4m&:GnLn;cjc5<,*y)ѫS Pʸ̄-P:C`aj)ZwD#Roo-}dW5Vv(KgЄY) j lR ViM֐R@jRm[Q*18=LXwWOU\*ڲ4}?@Uˮ04|U[ /K1uVy㫨Jnw\c,`[&􎑬"fdLnl.sei*.t buӰ5ߏjX/\+p ;T* NV+7YFP},ظZ/<}/cuWzT/j&>Xɸ>1 m&UM U0:2S*ubJzXRiwEb7"6UD]~s`g0H ZEƚq''Q@C;V}@.AZ]'- XZL&`Uwʭi!T*qвމ 2_~)ԗ>U-;PqeU|frջLamuOd[\'%X5@r 'a@u?v(ph,VX,@Ve[(UNPjrIA*VWt[z,koo_^BSvu9 Tl0]*U z:YV\-p~XHW߫<:î `XKC9Nէz2I$W{z~ %U \$z=4  6**y*V׋7a1V1N>!\c8\[Tx B YeZjDZ/P: zQga|4d_ )O{:mgleeBl%'U&2X%X$ fu^+zU WK6NU%@́JccSY'V;>I{@\Eg= 8+H1^2bA+I9THZ؅q,d-7&(+4kXnzN0d<+tWWPi)S9:=PrH]\ԢoY}eZ5*4&Uhn}=iKA_j6矛&UsDbOV0 D[&i6ӫU4VGRbUHޗ6"$b%!**A6$Rz۷o?w#oJP?cB4W{2:XXjsW\]]NUCXdEVyI*F OК<Vr{gꪎw i]|S *FN:ʒ?qsi4@9I[*WS VK"d9gU{{VKۋSFi[=Tv}zԁUPAr\-r<<ݤc\$jeIk- b,*1-e2_jlǭ˘P rzkd lij ';iGz*uW[. dV rժNi U,!8 V{n9=B*`fM}&juvXˈ&|V@0w)QO>Jjd1@[ k<̂_1V3#=X獌]?Ab(U|VyL_4Xcˍ  7•s gL@};,cwZNq m/6f q. 5X$f (e"*]SJ߹ @oIJž<&qgΏ5>}V5*ĪglWdveXqlbl'\pe,!! אD}kCc 4--X4|0τcF9KFài6N<6-#mr[+ V׃Y)WlX- 6z|j'WI B2Wck vj)۱kg{H&RfhquViu}95gX%呌ӃV)I顃UWj̐5NՃz=0h=kѻV`ӕriys\F&$E; )EtǪjAc)yV1r^JX_YV1GGf\Cb0{cq׼6 ƬRe@%~:ԫ}3*WttldD>'ۢ}_cLPƁjom%5GRImپ6Vb5SY0%$ p#9pUٵ'azup~pī$ TQj˱F C5HX=r lQj['>G֧4cU *L@q*YZVi+ C6O-UjUT5$*}`TE4p5tNQ\E>bU -0EEVꩯV}hs/:Y8Y` զO?7\=*߃G2V\UFƬz{0~]s#Ux`U/UcfSpⴜ*|\. jWZ3UAJ-/e ?Fb4lI-L)fWLʫU5ǠCZaJ1 NQU9ON%b)鮄ǴUjz'Pbit\]XxD-gA])F`IRc5"<V͡.a]>4ga҂`7 - ۳P`6;DZANjZUUmWݯz-Y1t n2ΪbCAJwlܵ(CX],V▭4`\-tg,V=V)pתXVX%ӱZ_j{M)mvlե7?瀩K *ut*p٨xVV=W9TՎFԕTU^\=GhZWS&rٙ^g4EՉ{ҔIZW>XЖ9ii‚~ =Y&^·XdI[ xۧ] + Bf1J`ew}gkvM& VV Kg8 5@K=%Dѕ}>|>Uʼ(Ui>y<3 ezRIm +R"(NWZU,jV(96΀ֲZCUv6'(EUvSڪckMOHX%`U5J"Ze-͂&pSO}(U'Z +U_Q Xj?Aɚy׬Y_UG|E`JDOGkz_;:.At{U,zgSj})Z#bPPw6U檃jqRdٸ]UyN% jřiQ^93v~GD#:m8&sC)V5{no5eN㮺 `ӬoUnRt5ظWfͿ*NquK,RBi e_ VmUĄpJ{O/B'r޵/.>yˁ T a+U-Wi\bFggJUsU׸Xu@իL R+FU969d Vb:Xp! jswcAl`mU4u46azmU(!*Z\8 f-U@qs "Umƶ_*ZHRCVlY}FnzX`kƵn+2mUJsFK;_{}IQ'MQW?}s>Rb]eݲ+Ws)b0ڹs ]}/,Vp m{%bWQgUkX}Ƭ޹TrRXYRySegɻ^pm|uWADZ\j%Ś0Fex, 0u!߭D$Vm&5JUtdJ7?L޷:-wO J?; ×Mb)}XMr55Q*Vq9y`lVe |Zwl1I-C!V@̴<*Z]מM_tk)X%/Z|&"G;>@ó/x:m2F {0j &ۗX/ {R IZΟJ◓0B$9rB;,^6)oΦ瞪L-2|y$OIb=+jp§Jb,VV; ]ۚZ;K$UVf5ջܡ`թ´}X)̾YJuV%BL;ԳzCV&J5.ǜ,U9@ Rh . VzLXQuEVN Uru Bթ%BUVJhL[y͠:m]"*4G| d.NVw)74Xݐt.Xѷzձ?Pp,n><:s9Տa59z|WtW :g*a}C\}"@}EZa.ɭc IDATiUDy`ĄUU%D<oUSv]ʌr~^\5bX?zuQ kIY!S5x;D[fKW]h쐖[[Pz#TƿʚVCk *Ok :m.Qiˠ}j´_/_>,tզu_K-鳰t;P anY=w9Ѣj_0[E 'U  WZe*oVzS'S h4̶u^^l2k-ӣ"-.Z܅HUz&'''26:q]k꼧j.\&)Yjꈔ~b"@ZPuLU.WB6 hTFSVEV^!+Xދz?S{FWOYb7^p!Q*VIIE\G"ڇU8Bj)Kh2cvS%SW9ԟԩ :0lM;M+DjRyd<8r \:i-V&^H-xWnbB*  U׫YF!^)o{/ kْ% ]2,H7*VۆU^nK}LIFeB0ɱ1ΰX+sPjoj=RUW"pp[|ϊK1/u=p M+MI{ie4>\+*VXUHGiBn\=u?lzcVgjUXD8 BwZWTX^TVq~P`**ϏV^ՁڢiS$(iLD 0UY&ȫS{bpEbUiY%:b{J n[ X~?s}XSYp0::[YZV(+Ws%U"a9,W-8Z_ʸCWG4g*76e@+[ڿ_iͳFJo{Y)+xOYw7!D,?PF+j Y3mDDnKC}m;]=вrѴ̙5N:d=7FHcc%:VVB]jXEzjj;F*2mE̺ύĪβZbJE !XF[o 6jAժbu-TeJ-"Jޙ21HlQt)UqNajVzbsj*iRqUW.QP[+Fye|yW@UKw_zݟDCM`uAãԧX`=Xmwe3=AU^:פ%;adմvc\gh\^) Z j5F&*˭U5`5p}ZMj5`U3ّD; b%$˫W^UR Nbduӝ#F2VCE/Sr^Nbɪv[MS9Va0ur=vZ0V+)jn,$4/2 pg_>? z% Vb@e:h"^ -dՔnkC j*ս);OθogX-:2vo 7»ܱZ(1Gtج20}44ʚRf*I&N:Iԁѫ*/ O;c)ą:5' :`5Os4ydC p(eճx#q☿U[H5&Me\\Ɍx'}*[ } եI~U'USRZ{VY=P3XαZ+#hq8ɴDi gl`> HyQU*c|sf]'qi);NfvT)^֒٣-;garg⥷SVAeWwW!χW|ATX} $<=g8]XۜdmBMY|2㸅cg"z( T;aP,65U\OTgYV)v 1{ _O? &r*EqLZZV5 -gj}ԟ=遑ưU0觥FY%Zr!vg#` UbeV!X'k{2+(3};b* ڐ U%RB\]<|U+nY#:ʫ.ܸqX;Ja?_w _%݆r$+{Ut?X5V}XBkَ(@M)zui :Mv"U35p8?>xiic.'SG"uaWM]{cU,GtDG8FJ"u n4ʳ _HbU?lhExaK+q{U\=̦Q*UC+յ5/ViT+!V4S<4bXܚ⭨vXM V{;:_k;HoͺD;Y9@V5KhVr8T@/VIJO=?8YiB;UBՆւ’?%k-eY}L $~9?\LjFOj7k{$Y:d<*Qm5$hCXFH]=c:u:AփA!ݩut43uUlub]~Xl9cwDa/8R+Ee,-QS0ۻZ$VxlPLhXauV;1ҫ}VwGW!xfV78XAҩ6NrCթq 5PGzgշja5kXVaz͇=rMÙI\uP`'T})Z=QjĪ͘*ԪP_a*tzo;!zQ5=^eFP*lJZ{ͨ?}Hi# U*JDI5 V²b5 5pj.ϣ\H\*V/e> BըcպX'/NNOլWEqJ*VRŕDuՆ R@Z:qu*--)3AVĠ*C-8B55)~)#'zq&I9G!#=2Vh;? +t|d*íঊق^ V'b`RUx-MXn鄦L+ꕅXջV2VS .گV 'rZvDj1Ϊw4>9 0֗>Rm5NfQ4@iC9̰6w`830a @V-m:hx2K5)zSFV?~4d}>uzX5__B@W#(Iz9Vk!G0BSb{Zj%WW)VUo kpC]ꄃXFu4gW_%xLryN'/1(|kS?i{Okߦ"y*ijo֖e\ݴfN VqةхWZ1k0s n Vk`͕T FCp- 1cIBS'}ՓV팖sGU:̙,^_/*ޚxPIT*rg#ĤQfQ{|%XExըv\Մ 8}UBJ#GSOjK_;##HfDU4ߜ*cr5`}}t@Lմj} ;V*'LY ;8MʺА<"[, $W@t 0ᥭ|ss *#ZMGf;?XV+kkYXP`]JA7 8>j J-~%kUX %j6c1>=?(3,(%J? 7Yիݦnt4˷w]siXƗV&88!Sw 6QW= P[Y-xq.QTf{™ʝ]wbSVaVP׿`jih%jvb%zRz)k>1PjIm{K״:2Y*۬l@jsuvҽRZmRhT[I~#k -ܴޓ>Q.-NbU>/#V0!:Dm#&W9L> Iς S݂b (+VecZ7|k)UMPki/፻wlP c?4;cEv9 !b ! HNuOW&r=j+xa5+O'{fCXf{o[ռVj]59_q,+z:6J!dܝƆ%>,ύ~9|ejuyyUm:sn9M9gqP/YK:AWo,_0JK6ɖ=VXw)՘bjlYVuju~g~L7/|1pQn#jU Ajok.8:>.Tq`(q.{b^kXJ*QKV{}quV/CjFVjd#̷B? V>0ؾKnj~9bz |TZ}}iŪkK+Qa)搭dDPWBX:XQ>Ч≉'M\RwP( VV!:nDbUUU-ǪYͫzk@Z?[Y#c //U\'\UU+}bu~>j^=tiIbxЊ+պ"#ԪLbDJr 4mO7F$lU*feJbutDX\0ZxnMԪ`FJLY'<@muKJ˱^THFoVPU]հqU\5VQ5͊`n+|zJ&6"ҹW:/9 TAX- 4pI˟R&&3#g?{(ƒ1[U98P1{ )d֩%J@0VSU'b_9PխK5s]TWmON܎ 0#JZVk,VWE*J[ )OY^nGNVIUP::KZԪHpnn)VNPseݫP^)8%yjusTN W*L^Vo߾*c5X/jUo'XӔpk_0b4cjUr 8`UzyGfz_P '1JԡEYhwɂRe}=[9 (V6b󠉪G4t3 |ݤ| (85p坉NcV7܌ϭfUr)̚SWTZ n`]̺U 2Sʿ-\Hz+%5Ba VUg'՜>j??VO^V*cgB*SFekGBk&]JXCj{j>[&k'Hª AP|VcAMVUTAoz}֪:Pƪª2U*JWvGF=VZ=3 ^z9US_.44ns&#Zpr2I BB30[' ^EXJ-qe ldAMKozQzхj-*+B7}<33IL13 3d))jRQqJX{JG(AXjWk[8V$ՏcNΞZ% LH].H\!X=Y%~X$jjyvm ]=1ke q}2{CDR#⩹oDs%ՄVU/'?4=T,WӞXMBQ/2`j5N*Vf:sGWs٪n#`<7bSUv^ v8do<PU2a15 9gXm:U\]o*XmjpҟX\5cEWxɵ~@O۷ V匟cr6(-VU5D囥qnvW`Z,Eu#OXq?KrXڝDPܼu9By A9*'b#-6Ώ03$} V~סeDuX'؇ p+!FjWIL ZmWK2C^ !M#U Y /ɰ1ެ˥S:; $Tn;X8\Rm zs!;=vX(bY` PaYx մ*coDUay4UY1~E_ZG| t"JM+%x:+ Jt: B U׎գw [%>ϋ~KYUO$ym sŰ*^~V쁁ݻXg9U9ݻgGaj;U=jÊȮ<#W/JَgxjKP#iٯp$ "ǪiM$Uk1 ?Ġ;ObJՍ JHZ}s&**WB>/ӗZc X{@czYV% Ve`[@ հؐWj^qZ\׌[%-+ ju7F_( v&a*RcKq"x Bj Go\U|8pj~.; Vs U#W,rGFkɪ,ܳcND^,QcXj~yۈf/{I)P١/_^\Z>|rKŪ ˷yVJ0+ż5_P_*#5FVzrKSWxV7okbՎ_tkie6.zwnsEC$iY ;ժx2H',TՁ=V{1KrKVZ*ce(W;ɯf=ZªU$UW`uT Y#-P#WA6V)j}\m*jjgb}J oFjbA}X 4N6V5N-ځLs +%ZUzz 5r9W:: 9jҙ~IǵXU8'1.#}Ӻx2q`UQy7+b{O8iV枦G j3Rv裻OyAŪUo>~^hɼ2NCzUk9H*+ikNX(ݵro;Q)qq4Y+*)?A͏+խg_|vd>4*Y'}TW7e#p~>sczBVP_m+:CXr`W{{E6zDr @Gߐsy56u%Zv􂓫ujΩ;i͉a56V)TcUXMWעWF`Zb՚Xof5$U*i%7 N>d{VW5]΍EEܗ%_IObdM}_HvP;s` Ha}q [Z+LZ FJˑgRf-ˍw9w;Wr. sOQM89geq> s\nI*cgORw2\yXĎ}EeV3*aYEX%N-Ǐv9HI8~H&p۔n[ R5~ yVU8K SC\P U;Q@=j]dN r{=|ut WC!:~2 n-<-eworO/VOyҫzz`cVMp^zzGXXjbVv-l|` V`Rj@j UUV=OmʃUV4VjDWt;c5Ī޶!EUkrugzZ͑JNV!U߶5&@X`U*ꮐ"*XTgpD{|`)ٌ੾=ߡGt^̈DE. l*VqNٔO' yN21oUNoPSg<[M(J\EVE#b8,qa@W Ggu VSΓ, Ż6LuQMD04ޮa֫**o UڵWYqbWVTaiJT,U_al*@uӋU%VOœȔ?V$zWժtTU˦:x'UvOZz 6WUߎ+R1xXc_ UQ~IPNcUJTż'^"n,U-V^'JJՒ-{brG&HT4qZFF VoÓT6gqP'Pt --TBl:fXrJ0Μ$UI7UZ? ZKI*rs*s{Lk |#AjPC+Ԫ*]SrR*`8S -0VUNxSx8Eho8`UfJv VQ{-29'דŷ*h2VTlWEb5c-!\+dEzˆ``jUNLcXRXM&70W5VbU lrNTqT:&o5%\UCUD7c5Sp&ՃWjOo[b"U+,*ߕ!؜c /ՠ57ƃNXUjO [V]̔nQ4T`6HVhHx{9.؝~:&NozEr;6Wj K}V>i zEށ(MM \ mEK$W'ALzUkEVVk!Ʃ_8jelFw V#Z~3ZVm~K`;(jM5+ÀUȢ. R XXŴvbwJ7OkUXX[[_[*X}"XUjV.mzۥ8z&l] )yF~A"W 1uLTYrB/g=azfsM"hKt값{ d.f5V34څ@YV d"@`j5X}.J*uj&~ē{*r=Z L=C0"ZM#VkjR[MW&mD7kK*^-U>3 oVLjUp*aUelm5F@(Z*ޱ6OjJ8Uc^V"W]k vw6pTFtj%)Y UڈFby*mմg Pu:z0Oe)R-ΐ ^%U%,U ,XK=mCs/Vk,VWVUj*YU9mK+7HH;_䏏(Xpl2AG#4Amy6|wXiڐ'^=)\oVX6ևSª]({xp.`WmDXUrlL=MjEZpXa.EHX,~VZL^Uzށ ,̦ @#2l}_U7pE0tpj:{RazMw)D*Cg70~{&(b5^c *t\Wݟ?QPYٺE)16kXN"VH]=+ W[VR^U)'%[\qIEk7.ݠV*&o?0V?W EBq V^6OUS\EǕ!]%>C:V5f[V+2eb*SjU* 28M"~A隅$qB ـ֨8hMTbk IDAT7ngwXBA"ea5U!;A)W%+,ꖶ[zWi{<9~3WFɟ1h7{sG7o:N;v V GO/ `9svf~Kw_3jol;MX-2'F4k۶gj `u7vxjUU>pRXUV[Yh&X H+@UXY:a L:aX.=j PbuaF{FlVѻauGoEYߪaFc5=M"XXo\5:B W;^ZᨀSaۼ VCAV틽Nm4U՝6 `~ATIƠܑHNB_tfq2+ u{Uz sw;K^Wwa"jsݵĭ#n:A&t CgUŬBXg]SUT2A/07FgTWf?FF ?7ʕL.wS<w%yԕ+Ot+TŢ%#L^Ҫ<0/tnb9V1$ݝ7eԛ)^?W}=JgX)ܴU6zz妬ԇc_jYr̛-.cĎqb%CTU2ꈧ*ET=Ԇ(5HUZ֠VU0au_U+b\Vgæ¨VSƀ^nW7B=jذ"V Mkfjz53hmjZ&}xKaUQQ]TU7jyuU/9NMX[VP*f"FjGmj +R-\u\p!OcPQ[ǡ,YeUYe\Uʤj.zu!]VOImFq{ T%V,9ƼJʵbc@jɪUoo6VU[]H;vԕA&Nx~Yf3xR+{*NH r4F3 f}ZJut>2\.lq) C\ئZ-+H)W-z<+*m\•X=U<0Z\*P^> ]]V=VǪb+Q L4kZ6WODZyXnOk X_ݯ4W[ PYd2biՂh2&XZ\P:j)GV#OLѭѝL,GQjUr'j"_ E`?Q}ts¾H܄ճ:d}hna=dsMuJ߸ߒ*e`VB$jPR8vy r$vbD(PJ}հ*ru/RѪ2ѿ7aR! `v9 %*@:nҝK4uk@:ahM1#X%ZJYV1vn9N^T ƃ*U~UWh*6Vҭ{xke E&\;8@ U}-Atl"V@,'xg> r6`1U+% Zs`!^͚uINCn])DJ'|yjªtJ}W ]}c rd+LxNAê{w<XG":3ffY-b&Vx~ V)|=fmHFXFS5Fq&&r@o[6 hٿ)X7[j}WSv7e*ruavW pE?cUʫnuNG.>[8 : PV!5-  ?V)^)9IWU"龕+:7Ѫ.+g#﷞e7@-3PWfhS3&W+M6?0Єa+oY(Dv,zY)U=TL[65 36-:Ttߪ$YÌ*Ӡ&S⮀>Q{j1MoV'Q?zA5`u_gXʒ $6VJ\\Y(ɟpa5 *ժV'HH{6P8 <jncy!pnmnܻ <"V+ZbaUzX-UUjAYmZ&&V&zVAuP 8-S ('@V~U>,:{ph WصFX:: JO^^eV+[JZU* q@ 2|lw)e96xh*@Vj"k\#FhR(Wc!Skk<|usUjJjǥ^Fi*Mxi).] +R, h`@Ҩ && ѐLgD/lbnLxcfҝ!^^`H?g<99=j|jfʇy~cUsǛa\5K?l3GkʡZuCT?*2IO#+&&޺É@8냕ѱq`u$ɽFCQXy4x_$HڦY?`}WOM*'{4AJ`ݚ!3|K?US8Iw ?j\!ikd-rBH@]jO?>?j\" V6@7r."xj+~X%3P9^U͘*k AFrT.?QJ B߱V-+V \vJvKޘ&Yqj1TإPzmV2}Rn=!jU$Tj]PIC@k:JrKUG6CpQf=1VMM^*K?~BjFjՀZMŪWL'Ux ڤVۂ,lYy`ո2о*lO}g-@\:ep(#ǪVG𖠪XŤkXE$-Dԓ!VVF`jvm5ӁA䢹\j,ֳǵ\j5 o^ZMZ e`H_{X*3`0^e$\}ÉVT߼1ru[PzrtZX=CX=dWр5KإPZ4TtzHCX5~X} tTINV\zXW+j=}ʹneYFK΁oZ3z_* x?Ҭ24{rTkk; Wیz]zS]꾟t7V+Kab%sZlA#`uΝƌu.zuNHJasJV.쉊)NoS @OjVy)ͅ>z);&\hz󼰡^O]oӞz:D.6a-{j"k>>% 9Tn-VkH$A/uVJ[,<=*qZ.uȖ՗Q[E_r,WO/NHU:!+qRc6Uݴd.(UW`MEyWvZťђ"u&aP"I$UM.a%_eB'zNNW󪫌kFNV[Rm޷+ws*8k%3#=p>0m1{W`^6UvxW'uvj@-ZumVLUZmH~[mֹw'EC.,40Xs%((C"l̟ƌL;6WL#W9HMB#b*;w^ǁ'*+Wj]%.v4)p/@4a^P2XT- &x¡TSp\6ѭt'Г$7ugSK`5nse2tpkU-ܮE\:̍e.v"Yd"W|:E: NqWVQ]bJ*t}%Vr6{uiC۔zaJBx^(,@opӪ.VY^]n֩[?:S!wɺgNd. h݋ek֫UTsAdDKGD#cʚ.ѿ?zʏUbXI8x$6UwhRaj$UݱBb`F^eI? %jY1[>}6j>R)7\PIBzr2}lezuUK& pkg9g1}JVhDlZ7gϖ{7^%4xMg`ՏNyTj;-y/Ptv"D4ʷЋziM`sU`M6\Vi$K) NX>CԹgZƸuyqyPUJT}PO$5a"bN~1jWV>~xhUnt;jI:;;rC[:K~'R|ٝLh I)fo$9LJdٴr ZM6~@=0&R*o^S8z=4(&El'z&Xih9;/B_>K_ߟ&7٧>{Vc/?:-+gV>p|_'7UƥyyԔ`5juLVMVL:\ek6.wv4ejePrp0?*cBPp[f`' NU.SXQVntbuZWMȕ pP{RަUZ VZoZfX8xu`V+$S"gtO1zU!Ic{{jo`>Gb5bg ?^dY-l댝zU,W(\oA28N׶3Vr:*CJU0mSĠdX|QJ\Z/ x5-(ӊR\M^5XU)e,_NZj1\*,-1]!Q57իj<6֋xe6>[MA7wN3ВV='Mw'6VþH2X[ﬧNX0J!nUߧ9 ZiPq׾蜓e iZ[XmpQ6Vn~Pt[k B^mX5]e3!s[+Z]^+DQ8Ԛ[XVtCjzVL!pcYdg$ܔӁ)aX=y^Zk6k~OK*ñZJgjo{ca`-[7?V:zXC:|\wyWc~;cK[kƱfKTԛ6~Sbf@"&dmc86=RZ2Trs(Fbn sP-JϠ*đuwkBa8>ŠqvZ^xX gf&U6nXEeҌ0jXMXW%ubu0}9VGZ2TU F4qq^E:j?If@Uau63%Tgdz'|o zakw vsM|1'LV޹q&vs=RsZm82GV-bZ8l u?0䪛BLTnyTuz5*qsYt̯ZuXvX%5X/ L%hpRcɽGOF%b+Vi? ;d3R=eͬX\U>;>Lu r^(s3lb~-guxjU| yIr.@-2UZbc5WüIz])V +|.;A")YfpZU Hf`]-%°uf|:^ ,)6(x`|5mʹLe\eduN]@Ջq pud$5b׊W/c`.{>Yg6DUyyWfC2Xj!JuV~5:eӫjlcOEkRUp"+B<{xiWV3vK7Vb_&` F eՙUtOӃjT|nTO=F:ޭtz8ګnP]^v4}:pz+vdlinNڮ~:} U*׭X%Ū6@ȍÐa:kTq)*GMK Tʾ*/~'AuHzOg8$Ymy-զjհ<cX}vlj9:VI2Vn%l֙QhVGK@z'Ա?ZQԫ}mv`Xak/U&kVS= g VUVԺTxx&@=[wjUZ-r~;+,VwEX$׺hS2꧀U{t?9wVXUHUi>ZhvSKrD S Q$%&,)V%o6;"$Y.ob0f~7M,My4z3k/g< ~#VsU[8҆# 갚q\uXu`M`jƪUsHvCML CVdVe΁ ej/(xV+Uytt5絩+'!* ]Wcv5,#Y@@8eMEn6 UU"W_<^,u/l?KԪ,+`hy-VpYq$ n/0Vo˧G:Vsi0A;ɊcVS+V<Or8>޲eXLu*sPUjFڒ$g!V4kɡ/[-,H@5sU6[D&D Et ]6lEѳܛbظ:U !V`x׫b r]T}5WtC+X6zX}|P]avR@g2Wi*W=nA-ZjìDNUz5GԪa5z茹Z=0qɶX;G3#Y QާMUɗmjUSs zRdM}VcV?'wAvf{K:Nח?4:N+UTu4l+v|vWX*B\VC`}=b!VİSX5oԤZkX3`uD7Iv:vNFfsΫN9ĭ}͠;NbWc5<= Bh@S`XT9N+[/T_rzUt$Se | wShU*$^/n**c!Xeڣ*Ȋ@5)e9LXqvDZЊA }ΰj%\O%TlW5jխ'Uo apU T'S8?d{`5Λ*&v B敓:42ŪmKMU+3{hЌG}U2It:VC˷2[ ςUFh(V{S VMjLjmK1V>+XGr0UT U*X0WV-/;Z.'d +ruؖU#CU,?F*β<|(3Z; jGX(Duh1*?{|8QV:WC-V~?F~E `|Cw^iX*،)(avWQbVbGjꭹ?`WRX W V ^JW]VXݍ BXRiCjQu_%q*tp -USPSM`Eꎑ1 U,bULX-bjYL5Hՙ)&rl=o5MjըJ#i+Px^GdIwkXnW ڃRKJ:;#?^^dV\E6@ͷX~j[. JVCFOU|-r*:Pa0VR Of 6 176 %+ [RQdVV C5fު (v8+չgpcZT{*X5XWgNZD!d/viGrZqʹZ `E+~3 j\U FaWmx.@Z!+쯊cuSϟiE]KXpSlXghwNҴkUl8f᪹r5jb` ZXå:tXaHy0PRS pxA1X}\V9V̛^RP#Uݳ| e^4(DZ lS(l|XZPAb@'Ѐs[S)Oժ V;ê(Rl7ۨ !f*B0e?h&lXROkJbR5ĪNi=-YjU?aU:`cXnr *dj\]';׀kӽk1{7XjUښЗ-x?^gkѹ` )hbfէGng8V h0>*_La7Vh*X5 z`Y!V :7aX<*DZC ]tbڵҊX,D͊g V)K + Q?"djZjz/Kxe}qV(źqaI]VQYCoN<{ o~gq&jFTư:1u̶ҩj\*\uXP\8e"VCPV  VFKDp*H VON./O.mU6keVod/5U@^9zU"#XoZdԹn\&]zL'\ r6&b͇au6m0b9c VsA`yrUNՑX}J۾WXEV6*TCR%^Rgg `T~X<jtOUܤBT-7zfȡAau/KPET9dbՃd*xqU _$(#L*~o VK~rp zPH~X)۫ggWj*u/-U6hZGzXKիı$X%X} cXE)Vc V]/'ޣX5-Tjcl6U}UlaC|W.[S QN~j-s;RƬV>JINx+%Gxb]?5΅VM=n$"4<~ޜ(XVZ EP WNreD#jIpXW[Ԫc߷V5HVL$֬uKVZJL)I5FhV|⿯;׷%j _`n=Txv@\M*p*Vx X1UTUN huAV1&94XնޙĪPDq+:aݛ깺4oϡ4..a@׮ժO|Cłb5fPŮ+??X}bUn -UL噅XC2Ūj~Ejսs{ZS딌'r5)+*Ԭ !=3 V1ߑX^NpsPUuS𜭖~jԪjé"*7br5.W`"^X-sv!ZVOueq|x:Ul=D $Si!"{Ag ZE0: W-g3ZΟ?ju=Np{8,9w9/Gʰ%xESkh7pʹe-k"ߏnz߶cSUv"vYX=T'~ [V ԪjIYu=QR)rXAOX-PcFouj:C6\U\UZEyR`֦ZUjZUBU_xVh3qfE]ETDWF]t8#Kz/ZK#KJt_yJzX]gŻ'NqP]Ĩ/ss}97+3,)V 4X{ Y) xO&X]Pa:G ⴬XM8 U0X3kZɤ*:oVX KVC̥L5\b V^c_V5a )j吷SqB`rpW0€Ү8V*%痎s|ü ASRXPV-WIЩ4V@;Uߡ<^J<DmWU `m}%X}q%VhCZ6KRRbu iPX(`z݋Ns Usycppmb[.jP-(1`nkY6x-V߽#KIdX 25\y#jb* A#1e;6cq/UjUkѶE$+WM@Ҹ:4Rjus@j35(TM]jco ѽ*|*}6=qtr!UTfbѫa<(;MmXM VYhbH)ZLQo6auBju<7hXcxGRi]/pOxPngjmJU;~ / TP_anj'810 Se`nί[AEWJ:UZd7<$Ɲ4.! aWjeX zN^-"JX=%UcI @cz{Fb*L@-T8j՜Fc8H"M3pyoٻ_֬`vj)'j2U!467J7ՂՐA@)5,pږ8h;"G =??Wp3@ajs\/YvJ usofK`@k{YeZW+Z"{#UCC 2VϰW0e[93bE/SͲNJZ͕X}{V*Xs9]LD1|䨺*rU ʦ_T+b5_*Q#*cRB>T-^5"uo^amںj+_84V *vNd@9[RN ʪNY)U*VƠR%HT[Ցy3v쿄ܒ+l:IEWd$uTohj0Jto*i?Vm B$*Rw#lrja>Fba'*dIf' Vi=GVVVicm*5jըeLJ&VgM+fbayx*7.]GI6ؿ}*\>kzh- ~pee}l%\/V2>Abu 몠Ӛ|EkW҅9D\h }V4WP֤g-U2vvpSIa.AߕJ`6[W%]-jVQzpFQSnX4g8l XXay;mLzͮl+uS9;EDk~=_[?Z*)$]P{ }o$h25jS-C}^6;/迤G{.Xo4 ,ѩzmp )o; ǖWPU-KUUx+VX^*Xv \TӷR|-!V+bzr.Vx*aLH|f VK`up:~BGUHyud9T3u•n,Qw.mgMS@UAUYNh먂!DL ZT÷rtKKy.κO3% wAbMSQ5ĪꖈKB 'Xs E.w--J.4EQX]+ei-]4>'i"VՒ~42{aX\9%VtsXɾ{\0c j:mMwC向PEoHS%YSLؚL`'Ea%Rg?C**a ;=bl}/gj&? W!%LI$]1%]UڷZE}+]9XUәv#X},h&ЎzDP)+l=XRi@UX *Ttߔ.//U,_j8. wVՅЋ/RuOtqbQ (QxW"4#JJQN`6a>jZTrU]ֵZm7UӦZiG TUJ:`iYp lL?VñZnz Ъ֑R8#kc;>gRoB\c ;xfʢpERS*W EfWc5aU.T-J H꼴>#2X@ WZeS*Ua(ĥ^V{Xŗ>\Rի'!TdK6>ZҠTH&`vTELۛ`wתѥY W#-*r߼s_|"V&=_]tggއX1(N$Fs }5LLOʮZ ~ntXUpHNF #ö؍`_Ϭyt+0Ut(&ED95gKyjjd)G3V"*|  TV3)b=:#e9:U rg#Z[ |j*W3 IJb~DPsnehcETֆN2mrY@;Ajj7u\wVLzw(;n[ U.mm ֻ]/UWzbVzHoVoxޯZX3ps:ʥW>"Xx~1bֽ`R eUTmLc1k3^q5=VfiSZej#ªGŤ)RMyw[ZVmWl^E?Ď&&G؜]ky] Xk{(X@a!Sk=bOT4\ʪpA VLLi)Jb 7ڃ V1rZ;VSF*F[`(ktzmPbu_s5WKU7\pHX}On|Qu1J=ZS0Jy piVWXV.V::ЯZ$kG曫U͜WQWmMcI~{Hfi<ǣU?[j]V݂*Uk> w1UL36:и2o(M VPLt9PUu%6Z` j_DF\ֹA5 Z"V?u? }eVљct_0Q/a< ªi$U>.&VbU۠"`"`q2f*$WNƴ59ɻ{m2VZVՙϼ]o5`${6j2>e9v}Q^USXHī_z  `7֪NX򊆫dhU Vjiհĕp\ddqrj/$W"VT2Ue~RsYI:];@|һ,X*RpcpQ+Hي}: ]ɩz&XV.Vw Wa9աU[ڪj\][\RʷKCp2zp j}RR6xUaؿWGzjU-|p:"kt8Ud[XM/1ы&R4UZ7VKԲ:hB+Gsc<&^B_`K Ґ2aŞz^e-y  gelU Yp'mb|('jHB39B19mU68??U%W*pӮWfEhTX(j^Q7᳕{S( -GJ^0hՃIWGh>+^UN`*ȰHjJ:XMz` eܮ`u(Z)pz-VK?ܻuPM蚺fX5m]1T]C3rb5?y*Mس,F(LfWsVPm9VaD:Tʤ0Oʦj/2v%YSu[KiAJPP [AB@⍤uB5>}Y`@_J/`rYBգ~DXuu?VqB RѣG֭XsY*gdAU \y=K7Z%JV+%7o@ŵoݻV_V_04sU_w)J{PcXR1J-sK eOr3. %\8!T-p^qjROn[ۜǽP^}:B=z UhzFf+2Hi'roX]1~(9k;j!N:G3 TbzUWb=gVٷIJiZuWoM9i?浭 ʢ8BFĴ-!JTB2"@Uo/c(( U!B@&4]te"]?tےm߯s{,\Gm%y/i)ܜaJ3R/>M1$E?e BI\ 𳫛Uڵm:`={xH=`e OZA`߲%.^ŕ+׳TBwE `pjjU&6QE*O`F6hȤ]-}*ww5W?*U[2 V4V%?J?c0Μ%5<ʏfGRXRXNVʛV,w`uGPĈU _au<6Ϗ2\06jTٟ?;j|W~"T\MU;/$|4N%v}*4X#jU"ƪ7wB\ۧ\f' ήǕӓӓSTݱUwZni}܋M-u@-YXsU{Vgq"b4RTXĆ BWx &֪ȐN}R͵6Ta8DuSt?̢._*b,Wifc_aIJl^@8*\mݚ L-cg 6X1 3/^яQ⚁e騚 SA5^b+gemT _*sT€*?: GOZ=`uJR]xH*cY0훪WbRVHM(`eO [B8قFW VZM3~ TeJ5-cc5Gz r ܗ_r`6=)lOzz%~cV#b[Q&tFPS<Ŀl6 "&X.xXU[]UgԆ,NPh;NJ/wNdfxlVYra- 6;S,1=q$XK*X! J$& \TC:jGZIajnʾ̢FjٝBfEXjV1:.J469cM̄jz[PwF$WNFobj3UB~`݅Z ^ wtQn VpX.XΨ%\UXMeTfAJZ3N_*՚@U`NM1Wr[[}vT=hсX2t Vo1_1;\Vq6&Pm TYց83VK;;,>CUa*brR8 /Z-׈xW}KXn"VnO/cW,!6Wޥʪ)B% q$JY-?im/FnyY>aޢ0u2#H]zƤj"|h/rw(P"Lu]G{KF79Gc9\4cUvVg%U$+A\ R_F ԚBSqUQ)Ui bT/@`=h S)+@Z+!\7c6RV%ߪ*W?R-TjP?S$bhe OSEUʱT3KW&I4+Uj7lmU>޵3YAH-tsEԻ؊zՠSʿ@RM LX2?J4D\8!b3^`ZJ* s*AIl޵~  pOiP VVRJ3j\jU_bʪX*;kKU\8`zmJrUZX*bu"U"Dgk~?Y6#jՂ sk&3ZfN_vE*R= zQX*n꥛ދ^/f (ۻj)?MCgOOMUVxz(VGӪNzXc'&~87߉b5yv4@Bc*AaE,<.<ƨ\~ꃈuFY˨WYZ5RuVAᐦUAnUpfLdҠhiʪUV>ՠ-^x,yrqzAj nJcjj X%;@9X7}?BU;FUJa5#[\!ׯ?TE'@qGR0uWGmJ=+j= Ykta% V3VEʅYcfjQ꼯V Y-V?x۰bj+cʹV@>U]V/6z<vϗs?*Ud۠##Pzv 밚2@<߈0#VRPS)L.m8fgejjV8@R{aZ=J\ QHjw8Dd޻N`t钔VQR?V] J2= HmV Xe[`jV/VԔ[nũzۯLMUGW7XE bRRR`W++(V.<<8 d;ж3K" li1<9/j6IHE1v48e|AAphrz1zJP2d44!,$f].JayIu,-Y-ٖ~sGŕDR 0UY+}&*xUJX"VI7ê*aU溆:C.A=\-V+=gfŃWVێQߨVd"[p8c5\ ?OUgjX z@ J6čGLAE՟VZZ=2C&O,#UpU¸QL1h &t$/%Q_aEr\@Ö5VWWm0@*ӠZCNJxR)]`eVڧJX%k=Mjs{sJRjA;㻾}2wg7z{sw?XoHbb'+bجXUp0V"H5U`H@@ $ڡϑ@fjyNx3\-(ҰQ<"OIr se VmZ[/e(ܴqh Mƪ*mKUs{jns\ M銧 f^@qr@1.=\WUz;,V{;KpՖjD\o/Xj|Zoic "2WqT]@N[;wNeU%z UKU])k%n)%tgu¢4=dP}4VXUkc3Le Q[+rUt!2\*firҏr$OWj^Wj^bտ+kk]lez]ac߿GU9OnRTꓕl2oXu:eKj tj~Pn+Kd'S XX`V*UpUf2TuQ9' %EUhV/yQZ t;n: ?L ӨRxKWiwsi[N1OJ8㐖!HUo2zRRongT-sSjW[4zpV#3)`8էPw~IV-X5{VJXܯ)3b \=&V+Y8T:3]E`>՞T*oV΢CQ}cjVahuhjj #"{l+VKo.-rz=z,6rӇz2jTS67,6W*F\NUȽBZC>~pZ ^vL;.@ZE\UI53 *iڹ#$H(=D9EZ)f%%+C`}R?,sy~y]*i/ ?tИQV(pgZqװ%7J]dJz@o6绚c5B{phIg@PD&Nd3DƪVȔz U$qҪ4jWC3 s}\Q/sIq_:?^+VYK\iqV`([uNV)$WQiQOxQ]auFҺt"++++Wjv]KjD\3E~fF_Vˡh{GRW)Nt5@Վg@3Gڴ{TmAV*ru J3UM+bDW\:6VUq*ld & IQiEUcߖKKEX ^l?Ua UhEձ}/@رVHrjdQSw)]aP:6Z݃**js˿YW7ZՐcvVխX]eZhdkU[CmQDY=6{̎wi#!цUZU*z 3k{[j*Se!7KI2y4?>{hw!@U/"[S,VY  xe"0'nSA*Bx/|'4A 3W X_ ijdU'J6 tt5j\A&JUaívTEE`E@$vӣAqa5򚫉S V:)1R+Zpj(9?O3ٳA7BF> fN xqZNRvUn< ajx* V^&HUpUkWݵ m+0XRE j9`N4KJ(E*H{aE6\8lHjm+!MO)Q`UjAֲ: Nx"xI&_QH3\g5k Ujev\mٍb*W!HsuKaI-&A+MkL*U;ɺ<)i k{ۻ{?՞0ZlU-WkYiU=wU[Ij.AZZˍdɭ4°WߊP}ԇo!Vj*\~AQª?%jo!C-.XS-J\(Z2 j4uQUNLX*hBSY؏й3hQۀIJSؕI0W)]Z0m 3`6X5*Y :QQ I+`ujyY*)8kꫠtuzHT="U>XQ2Y:t ()٫XF8>T)ۭ?9j`50t5UULpJRoQwfAՙ;T5RF%XMX Vj.k {i m*9 )AuErSU[߯q2.^mj Ro2V,*[TmUѫú^Wr!jژ2/.j7J?{?(VCCNhKcEUibqESLVcMh"Vm*g)=|p-aN!z=ЫGwXX-0MAtisTKQQδ:EM-!="T-+:o5GUyT@UȝٟQj/Q5+F ζX U&ª] ] ob'V7X5r'IGFLKBzWqeا:=xzZ`U2sV5aHC+NPՕ˗*UrXUQ:c3VTvj:SoVcwS=y\c5)«,JեE;¹ڶtՊTb'd*"(Pix:@c @{p?l81TEI⽵{ 1eE\buPR*R,@u~j T݁ϱ_V__Qǯ_n25o Kwo֎_ w_D[CCH6r5@#Q5Miz>*XmyBJ*&U+eȵu8\.nnb*qUJ"ZJ!Y%oI>iUU $wK{(Щu\6ՈH\lͮFD)V^gX-XEX V[LV2+ښFU#4jzg \|B*V 46\V}ъ>ziz|H,Mzx:u0E*W-CWt*Ec)Eb*vZm萂*U U`-gm7L@V+\U=\e45Q%+PEѓͪ!YE .r >P:*m>γ2>V^*Gs*Z^?g*`'S'.j+ ;v:Iʁ PaMldhҗVXv>RX-V&rL_x71 ƛc+ʫˊګd +qak_O{E%N*uOpVgf]|@L]n4Wf]JDzek#V*A4Xi;FA2^P*wnʩ[T'VW7`KXwƪraVZ=l_S`B!i=IB4 :s\?ԎV3Ty/g \'j5j^C *V| zKJqbuo֫ jU])gOb|bH?T˛ *U;W9[X\%#"zTeUdvhfzW,Q+WZ53b*+׸ *b)uj!EdufQ?`=aULG9US59Ԣ(҃+VY3G6XR*1oj[i ;auzNhP|VqlX7`U==a^ӷMZ_^@zJVx=i`h\=Ej:*+/E-]ӘE%7acEFfZ`R梳3ρ`}G[s<\cVTUQvժ-Z$W i*PCJ: 7Nbc:]V:؟Ȫbj cjOceY=TGU ~E^VΟRU!W3VAD6bP,mO'Oɤ;@Վ!B IDAT?VE lqd0֡C43!j')۶c5KXXe;K~j[窇U*B }PS\e Jԥ*c8"@(VNAP}S!U*:PTKЬh:5Zj5t/UWP|,L:بQI&tMHLzM8XU5]tQkjj: @K0n=f" @+ Zj҃ Uj(ZeZ.\])r>sOR24߫5TV":\xXyT ,ViĤA+5W'Уjޒ_qwjV9`m ^UV1)ŒȲÊU⪋UjM]=@Y[K/浭 ௙hz(J?`q\o0\o7j;"(1uB)լ0DKzyy^]}dH^Y 5\=yi.|zCuTB8_7ڪz ٍ`FG2X~Q?fX*VN WrFu؏z8fU!>UU*4Z]dR Y$ BV*_ d&)mu$(yr"*XEUvVC WXjyUZX6~^V)cՎj-,\K;zqpm_?QW[&TL:_^;5ĪX;:=V?B+%IUiɱ"9R36cV=T] 垳% A U}wjl)աILVݫc ggB8VF=X ֔>,W4ź3CV ǪwXgժZ`GToĮyxde-iʝZ9Xe~O5KqU+}=VSժa(Xy9Q?ӫ; HhI15V9fznmTP\m,mڙC|$VZWK%DaV߱Q3ױƭkКfHUmZۮ@Ȍi}XFYyjMj5)V2V]VU./B~SO[T^W B_q1^bu<[V(XS5ly=R2 EWكZE_/Z`m'B ُbXň5\XLUn3W*kj&QbXW0]-`{!_UF!rUZ&UVWZ juic :&d ,\U=i~BO9jU| ھnjWQIDF'3VQ"K \'wZ`c,vߺ +*O]bi)XlR<];;_WTUƚsuvTV`Hˀ wk` PZX2︊ ?A**I<Jՙ 6U*%du)4C/RT*)yXɢΞXx8 VK)~VX:s~+fC\Vp~H:ώBW2kVTE(. ٫8a\Ϥb4KMj, 0zUъ-&V%:zU_P*Zh܄TRTWIƪVg`t˲Cx:*VQPJN3GIMLŀ=VҏX3VKtS62*&"UQlFUdD7^ۙT_rمu԰ViPmNg]o |f]Ձ5ۗ[F1b Uz#[:UQ`*Vm6@hXsTw ;¢v@@ɔ6*3,4VBdoL`HLNK^I }oL{{ f|o)X;s{<uQR+|ţj$Qubiue:L?[eةpt֊՘gb{@&=l^UU6* Xq5j*f>pS7X}bU#^qd ,V/ԥkKPoZ'ZEԪa5Ū㬀ZM "TU$Ԣh&8EP.ЍqchuP ޘ7KMzVU @UqRU/``e|Vc: U0'*տT] RK׵zF!`ll԰ݪIHVVxk(ڈIC֤\]eIV(XD*t̨ cLPĠnHpU|ܲ UG j!Tmt<*x,_US&6P W7>"Q}'-E~y`2&&611'ky,3?:M74s7t-xk~ԍ;7Vi'5Zk\9״Dy+v],JS-΍OLؖ׉qJGEWV#Zr+5T2"G^@dzNzjZ`ۋVEo[=:4ܒ}JjBW*Շ-vjzZ,Q-T3ḡP8P@e5V%ԫY= f"ݏw=\6:yqjBU8V?;кjFr(jt(#XWUkXeJiebvb.YNߵRQl"jjaUtL76$STbhObHVyUin6J%\*mgêO VaA\CmX ~[:RuRRRQWG`_U-X ViV}jCXM V內VFJUC صK}+a*]q#BJUk#VPT.Yڍ`5w=:>^z01OB\Vު!k" Y{-#*D îˊTrXW28E*Uƪ|%H@ZUjʫ d%$NR|Ŝe u>BZ!qXm|`Z{y׉w 9jEBQTʟTR5ֲ5XW9SUG\Rhjc X*XoO"yVO`Y,cV8NVZXVƈW!X6X AuX}Wξ *:5)Uq9CXTMzT&"4/~.\f] X{}cbwTV";IXW+~mokacQ|mTmp߿˶U?dԪpX=)cxQ lnjW *ēZ?To|>?yDO=dZz'"61RF* j+Uv Q5Nv<>%v5'\궍SQISjV0U_#'{亊iRsb`Ja5Q؈Q?[ZX Yw}.#{:ž0-ѫ CmiZ=-v?lЭ@Ɛ ;+zyҩTN;6+/JM,"E80BjL!3.2BNBDAB"Er,(ReJ,Ewfe?|n{sιׯ 颥9Hr5iUb~^ Ohjz)0opMBU]ӘMbuZn%xtZ='n*b5eM8;aUoLNbxY`S5+~h* a ,BAGb8VX}৭h Vm\HMp9|Z V[VΊֲbݝ/$x5SUڪj3-eX=0݁R[5Zr"Y@<[_H i.'$r*exo7/SjU{TUzbuLL>dVWdl+_9V^S?0TsN!Mk_%$p荎ll6=nj٩B'Nľy_ժ,-^wa9+)}z=pĴj?v; UmFRA>BRTyUnª/$iUR$Teu0TFoj-Kd7*Db5V1ֶMf*Jbrٌ*~qhh*FN^> ė򟮱{Mmajx_ c4V7V`onl:Qꓱ:JP:D:^ĪU ,YZB\bOQ_VsVD9UUZq] rxȘr.UR DMwagGU <aT%+FWg7P.޿~K)GXyy\?ݗkQ7@4*Xt?c PB\@jT})Q'Xͺ穚yTК?VZm>j!_Ub iUGhzE:Ɋ嬵Xdu+%kmXTTe~%c.>/n, OkxX,kLB}ine_j&hO4kLUnU.G NT/UcUaUYr5Va:5u_PT]ƪ[*y3qjyxH\K<7zXXZsRunm&\-9lUZ5m+ҪXQ4KR;kq H$bJ- ׇBUUf0W9hT *<Տ-C oUSIUjvUg0P,:gD\6z~d'@&U#WV'4X7SVU2Jв*;YFD~?q,:NNU{]w[|VR$&-UPXuXݥ])&%TIUxU~:s3RwT:\ZJ4+zXGU6(G L*U@*>U"\OjVaBTQ˝Q+WhVeN55`˗ivk\kr]J V?dT W!.+4%"pIQ@ Ebi]=VW=z]o 6`Q%+=U 7*U΀2i:j>ro*X AZ3lz깻Ed.Tu*iV Vo^**>x뒻oXx9+RUBVDzڏU ac^'*.emVl~kDTCXmu6:JK%F իJ**cu#~Tc(֌ >}UףUp5,)ccNU*|Zj keGZcX]^4 Vm ru{NeeCZ,q-JU`Gq|P}٤ 굿q*0,d|j$5:5eŪ`u/+KeuSg׮bu|26YpWc2`J:n^:E vV]63jzjBD*LiBR]v;uo P~Zt}ZEZU֤BZ joo&zT˵Dj6T> UO5j>ժqFc5*mj'VeO>9Җ.ª*QBU2VDW`uNF+ JK,{V! R傹ZkbG0VXwjk0fuE 츄fЁh+?U{#W`u~>ªFz`BiUUZRD8J*D;O#VjώSOY T26@#9ykA9j?VwX*JQU^(W_pjʫUABUjglPh^q\V&T?ځ ꉉ.~V9GTjM4xZ baա續q,u X ӨZտ&u9 U V 3&7멚*Y*Q]M DE#ŧ}F㗛0jzdZvh.|'ܔ%Xբ=^IG+WeWܭX1V  *.F|IYiw.`Y{N+NV\Gg=?STB؈E^LUsD`U(3WcyX}|r&t' t+V126T2Y V Y˦7-!b5`u#3VN;#\U^&|C8!QVݰ5ֵ麟E'ef6Qz=Ja>([a|wsus[sS;=Zɾ?R/KrW޿yOd3G~s9ڭժPZVk7|-VgaVZKU@JUçpʸ #mU]L5W?_Z?|.N,ꊕ6xUe*hR hL*XlI&9VQGyeJ-m4Hh~JjpUUmfX*RMT4XoMWVj"\V3~3a+p=S.WmـtS{ס`.^GcqXVxX$R~Az跳>@U:꾋osXYcLWae::9I6IYg>J.Bo WxqnjU=,}\E ͖ V^.`^}]O2&c\iHDL^Wbb NcUjS>H*an`U Vpy|yj|@ W-&|f"Sf5`C`IvYӂ b[ۍ^5\RzIɪMV-\|UV L`ɢ*H.`kJbbzXxGOkAm^La2WZ*Y!bU*>XY> BACUU+U+jj]뻬} ==Ue6kUt\kVXV+AF*B%BXµb$xna:rjaEfxlꈶ=kύVӧNM1\=*[K,[V%^- IsO4bz+.&*+/e(ꅴj7\mK\͠ZU`U!!n㧓:5SGxuX5bu}z6qcXmf Z[j 6V:{dNѪt WyUR`vhncqhR GDTt *vXR`Q՛\4UXS4H*[ZB@Y{#L&4r vp"Zvml4bn'DS3A9uHC;кL(Y5=15hCzxorpjnX)lچ&_',&L,Vp"QP`սsU]*r^թ1vY b1jU>G[0˚FCޤ屺 #\U`Vuj ZE*Q("WV7^:b⃥يCW>FjVÖ ;,tnLVVnr R^֪`"UXHh+(.uJF<<Ԫ*,0:9taAx2'E*偪#5bkKWGo7VhEfc^*<ǼGyzvr.jyn[&*I}(X}Z gZX*wX *5ؠb5njٯbm%)Ҳg`YvWTsҊ^uMX%')ªUm. V++[J9Tf촉kV~d\+P՚&]*կM߾}X|4&C3Fj8T71VںqY{lh~#Ph-*{!5_}Z{EDy$^oJib"k$ju~ƫLtŬة$lgmRq4b\m^Ku%Puxu27n.aupٷڿښ\j"W,Ld( #VVׁZmL_lǑʋ+T(f *CZ UZ̕QN+2NwZ&+pubz?bɌjL&g;E.Vo>~b+[׾xfD+uL_z<3HdbqfΧC L NMԻX5WK!yNf[^j 䁪j*ƫUquX}wٳo Vej[(C5 \)ՆHF@#cUs5*YJzTyUA^/ZT]hj]E!J7r]<ԇvՃՃU`UtR Mu-o/Y^HG o߼y*@j;H~yyIĈUF֗z&`.#W++t <"X,[W֪;@ju} V|tyeq\r`u=U"jJFSԪ[UdH@ViZu;˵UNWTcVi"pmI(W>aP.-׮*<8f6sN_k\/0煛9*-L*s.KwvtǃB-,hͼ/Q1 j OmYKz/o,GFk;WWq,֗y.jW>ej45`BÎS[ZvezpR\DSgyb[Yh MҪ)ev`ЪgMa8 p*Un/;\4kUo2[U-sUz9~w^(/|Ͼo ?wo݋Dh֔Ū% {X V{v3UV VV[ >XX, Ksa[+j!uH$$DWYڠe/mw78TENWZjRzYUflLaC:5니 VcntE 1c.9# A]LK A2⸆@P͚eP&Vv V,f%&d@՚iwHkĺXJDZš5}II]%-{˽y,.9Xi#Y:cLթoÖo+n>PhC*eV]nVuզKη;[X5UU 0PGRf}5NѻU#*B^ֶ'P y,qȻ<JmzWTۮ{XUC'mb3+QU]Ъ?~vH8.[`:4906n!sQou_]ͯ?ճ7tmrlZBU|z`叔6O&UncO;\X% 0 CUWw0.՟gZ=hTjLԩ,U_U+VgyU:*cHp'EL&UDU4V߄rujۉma2-4OZ|jj֭X㎁֨KV@Lɡ9@bu*wG*4ꦱ*ZJjV%X[tPX-UGԕxW&*Ր*(uFVm؊/jCjy 藍D䦫qJfX)mo;PRX[oo1l>xCŬX5,"Uׯa@Y`_KYIpʛm0 EZaeR~!ݝҵsg*qUrՆ?(R7WZQL]ѫ6&BR޳*e ZҨ3 f*~ZTݾ\-Vj'0Z~R5TEVFX:fgZ$sx=`oyb; X=/ aj[6ME`>1!Wo;N 8c>Fjxt| IDAT"ԪU>F,_ի%խr2e᪇VH Q:xPtSC/w:Z:D3Yh͝vQ]mGcU}Nűjج>xjUR?5jjcpAj':VYҫZ@l6j(V<\X)gVπ 01wSJ Y6a՗&B7@S;{/U.&YƄbUp*;^7WuP-GŐV MV1j.PthqΝ\Г:JcM$!Ȫ|&&ni?+W;aAj% 0VXeUwS0$PUZ[E*8:*VVOTJz>mU~dJrՁjTsR/ƪ)}u-RA"n@p` v)XiʠyVn5 N( *zbĪu1XU*H,W]Hq:=_y^ȯ^X/ Qn?~X-ZU?΄Ϯ8Xm G=" U8n` eUgq9*"ґP5YM\+*j/(0BoI,XQjd'M1'QL5-o(WXY*dtg՗;Uj}wO8HӹO?GRSm߈F"VZu(ZVeŧ&' ZV.@ .zJ,Ve Vy>LSu%wp@idt83}CG@]HXV͐Ld34EE;ˌz@tC՟ݮ69A1H׭0X.*aճ;հ- RÐpufK;'Ubu/McmN\xtoz^uKsss/ʜq]݁&*/W*憽u^&aJ_ @͚e-vTl`TU^2w-PM]r +~ѩ[VfzVTXYNzmc\Q4hk23U)lO=iIQdGZmݰ(VՅk`z&iWAdsƐju2ӧ{h8on$V5Uj/A2Ϟ6EU/qpӭ3VIC;^`?N~vtȁ`էzTAj`8܌q _tJeg?=?]yn[-5iKdpu,V*$TvRMPM$)n0k7 /Jifz*2 Vq9}*:FnU!0#WnFܿgawQl.ժURuc._.$ xX̚,ٸ )I #vt&3E5f)%"M.C)˄^tEcEfY"d*:l6)݌+7x^[J $j-ײCO Xחrs7~jnkOE\`<=*>Ī«^hɢ/ܩ_J걮#ꑛ 4 N^e^X6X6TvVE:0tn In!X\d#դѫ)zĪ*+UDZ:̟/dkbyez*>@rH h|nJ~t7&_UED#? +IUomUwQ'Z֫>E+o0WOyJ/~BG !өvl噲} UX 5_QV\ SUBZ%Ajz\vȂbZSxwvv6>57?'7{kw淞|!jUL++ըZQQri؉r<HXnR[m /zJ .6Fz&kaNX"1VVR ktV@jwV0C ҩtԑa9$T3" {Cw\^?WcNx` FcTp5~~*j/l`a*p^5XEB`2 Vc˫ KabTް*+`O^(yc7T BVLu:VŞxJ5DkwK?֧MUL`[XݑC (Mۻ5ڕ۬`MuVHTg!U+DMUj_: }߸UV늩Uln47z޽ƪ"D6jVX"XՎ־<rRjK afRZTjUYNXKMVe UY`W_GjXexAzJnL[]Fz+VWU+Rօ ըRN)4UU٨~8~-\/ >̶Pe6 V#BUj~ʃu͘\ XbI/#WXXBO _õ~:\ƣ?ڴM63dqL bUBeְJ/a,V:=6Wޞ6_sޜ*Xi)quW#`OKX 튱V-/VuJcUV4Ux*MWXpUVii!3/x«;IڒRd'D,jgj .jYs("CԱJfIf.˖`eK*Ue}U.j_%C(W%oGkݹa*Vi͏ (@P5N՟?S5&(]:Ӛx|Td@J%"Tku-1 u)@:g}onϟ3s38]?Wmz6΁.b5l @@}VUSdE m3y VO B 5q.2)^H̳v @}]Jx:^„+V;?XҠt22a+ձV`Q\!WT}PfݩH+V}R,<5Ǫ*rrUA`z֩Xm+@Uxm~PK@ ±WW @hD_4gh`kS[f ^ExjTs~)!pv} dA&LEv V{4wp&z:z7,UorՂX- X++z~}=65?X rvHvXn(Uܰ9p=8uJj7ci@k>_SqELV&i`w^ zh[ +BE+UcU'{0 j_HX-@I_y :XEf\1Z`I=egNij+dM \A_ă:}M7VZ5UIͫղ(@ԇUU~H9Uն UCR/aLRɴC=Xp5ځC Ui7U:g+&0ߚʠ`Jlm/u|A-b5bĪP*\ec+Qe էOgoYS ꟾxPbW!='˳z0ܘ 4T] T*{Ven/n80z.{/Ws=*yQlUriUGV^1ᑒ*T,tMurɊ<5BKNR:m]HζՁc5ks]%,.rD' er0&FZ cg)p#"> LU3j wMժکuSUj6B}ŇU2v TYr0 \uUʟ:4[nt$z[gmR ͣx˶,U hA{Y5B)}e;Fj "VD\V)"`aRK6aas +={753j@E}-gDFYgUWc7U-V[|_Ż@5|l46X/on *cJ+oz Vz'Ek;Y*/R([6zܫz02JŲ?S“*r:)ි5)Wq,dCo sgVq٥FTeVȮ(Ai[킰2lP*+ĭe .f1{d(3 [|#s1DOMN>'}Cݭ_}Jul~1Da>}`'z U%uSJ=7rEC TO E*ҫ.UDJ% 4{rOPUDjmK:Xk<ĞJ5P ٯ*hMu$Q?*ͺEv"WU+ʣa*F.I5,87и*RiZ4J`'aaQ sk1 >Z]\]Z]]z#7򲦙A]Vlrlnnh<݈ond|2\>E EVP(xf!n{ VX*$I?yת+t̛|?Ty?/VH nKڊg?ȼ hj\np_+bvTWT+ܾe_Ҫ=dL|tWhs&Uƪx)/lv a0^TT< b@W[9-FQ~Ľ ̶~͞GZ#ZuKU=D 'OVyz\:W-lQ y4OQۍ /(855:0M(UW?p-?'UI+V녫V*R2U[`UpNSU] Wow^~z;iZ4j`tCt3}ogszªXZKa'v)),uf!c@, 菭TBD#dDK}tw"ݲjQQVL\; Lm+r/DVd\ aߕL|eCX6?x[.+SY*gOOPyUaUJ^V"J.(6+Zݻ*vѪ)J%~TwVsX;^Q:tI<2W7a֏X |R|/)7n Z=JjNP3DV5bPVLr+V9Xݓ9EnbuR\m! ۏ޸^psmBީZ^nQz L p_1jhZ`*S[S H&uEfwqtN'D>|$V$0pVTj Db?U̳Z\\]ZZzmDZ]7:tc?ojzwlգfvRFk8PH^$Rqt@DY}):AŢv@l& SwM,paGF qZwU;*f < jBؕI|Va57d(.ʃUi HP\ᮨP},X+QkVOxP T}^Ӕ:<{U/XQˢ]e5+JJZUrU!acmCʬwH(btykC4汭 |]vk== LIS8AF1ՒnqqUc5;UDk35yG#'I'F&Aб$Xkj/龞 YSz-yTtTXmb.g ؅J IDAT9XUS _Qƪm_K gϿ'~v[+VMU$aa+_Jn¾Ћ9XE[3kGzN:U*x$G`X=LCj`gCV) GbmV-X\U ꜘyVXƚ GD dze:ùHfYaLrQ>(˫jR*zݬVQ-gh槭N=ZC+T=]P0Ta<&b&Y˜V qUCUj^\e2cΡ;U݅f*ܼI:MHk*Se{@UVXJt\\P x2: !)߃#^ao#!jg]Q!E}ɪ*POZ͎ UR&Vڶ'Vf^Ώ} ruHdiݴͅRjΆzi>,j\uVD8OD_dՓqڟO:j5 wuVQ>~ZUحԪV%X*X FhZGlU-UX+U&U)]\Ū܃ ''C4Yߗle>cXW.BWibjJ2V۳2Wۍ*yqm SuH5YVk(B {7V[_ҵ80d^n>W;{i{<;B?<B_u;q>u^[EV_O>glɗaUpO-ƟmeP?<}Q'7O_*2XIe7_Eq5Îb,tjfZV-y+QGUVٲ2-vy܂Vwj?_:fg!yRVh饅%%A2utA4.^Q,3UW>mg-%kU;_嵵U;)r`Z<^4QJB9O?l3VkjigΊW#g" CXU2=2@s #VlJٵzֳg϶ۆ:jLU UfBC塦}c_wB18V , S5mIl:@ҦߚhIT׮;X0ZX~Ut^]U2c\nIV PTY*=4 pn=pl Vj&K<!NqGUwP H&F ,T%;X d ȇXN OZ c+^\v]>JaU:PaVy2EFq*x5=JKUUհ]>]V]6P0lspX ]m #w\ D I d%*4j7Wo3dUZ;7w $J2G[{>UevjKwWU\j% d?Ͼ Ħ4lO˄VkCjBw>Vq,P{6h Bhp?yTV8|a2[HC=fujF+AAcXUXWGHлQC ҥ6yR O«rU9+I+3gJ< Z[anJ|Tv2;WXW7l  +tX譲X-_K5k%RVZUy*Ч-ڽj j7_'Yd/͏:=z{U+O%P]E'\]T]S#ՀZ!]F[qՁ*`zdVwoyҚr"2Zk[qgn@Pk/x08+_ LUfF_ (+<[,Z]\ېVBD_3UZu>ECۼD l$ P`~"Xԑ@bU\HdQUW)V9nUP&qVX pUfUGsUZ?4v'xNQ)r/ rBUiZ/)Ua=Z5XUMfg4ŪuJ+s-YKG<\@5VA'PgU57*Kxʎ]9QD aVy+ˏawwLWt N">CdIR gb:IVF ԵpŗBU#VP&VpGč*^m^j^E6.eݠV+JQbŪ8\ص=5uA IIyֽ[κwOy%%W>YUQsV* DzKs tuv(ZHՠZ&~7D֚g-NX5\թ]ժ"krfh9S4V DfaU9RTXo27gf\Vzljg^Ӫԭ~5PVÄtQP d8$>O胗㈃LU306R+GYC~6&CF2VqgU2sM]QNUDjY5_|ZSU]5s`V[4VOUSa`*HTi*y+Q3OoY1ƫ'Wm eԩ+*p2o=OZ{֞7g*Twok0UY):mR#>:gΞj:Lz5Ui2WOqr6{*VQ @\Ma\eNæ.'DKX VrP :;T>izae%U3vUIZdJnI:Ru`cp5 1naR+P){ںl2i$}x8iVLGzrpUBXhW H[ZBXK8MDm[Fp! ՕO/db kenqS36Aswgո=R&` Uq82_ŪZt`UCK:< (xRLXe2KjSu+vo߉=LX5E˜𸊚hɷU776h^4 x VfuةajɵV%X=Eڎ檴 rXf/BCcY4F gzXM34+X7j ;c5޾!NX-f뮺 ZEZT/ȍ ZSiW?l7TWnQHLg`NP+J$lӸlUvPiāj"auC+b@Xk;D,UI%Lez :z"1<ӄRJ/D5{G-Ʀ aRA*bYrElqjgboR2W۰ U>U-Vus}RRLUkj=sr B\;g0V]!2XՍ 9 b`kxXQ}CVg3`'T=x`55A`.UҪUlR״U,2s0VO*}ҩ)::aSu`@f\;JkU\/W޴\=:9[t]6ڹhe3iҫ8ozUЎάS^;f>#y-`Zaͫ##$X9`ΎUb-Gjxub@O2T=_1}:XrZg7Q8k֓' J*z-ξP&<ƺmV~J$J[~D"3`"zQ\Qlb0Q#{V,rp*AU(={6@3QV5 Ќ}|TRO:k,QU8$\`ɸ%i X-UZ MZ.Ә;PŪ`uV4k9k}`߷T:JZuak U]d.-Sp Z-?˜TXWG?q2zΨ=BBV) V3jUڎU7ϊ}1V IDAT@URwn+ڶLrꕡjvzҫm)fmb+Ҥ*m1uVsY6pN䪙0jGN^YXCo٭?hS>JWHy-kРu!5A?q&E2Nh ^y4JꪴKV@WạK _7YQ֤J`]m<2fNjʚ,VlPAuboҞ~$ΪiRj  fl;2!j;r fvLhդꤨOڎ)۳%.w6MY/rUu'$Nx0u(.#E& F,`eQu,M( bqlgMv{*|^_' SCP99v81 UME+2S48ɹdt+}{6[8G{7HҖUz>ra?А6Mv*„kHVjHC*ꝇ>rU'zkke5W=SC `ZurSϦcu9J ZdZ V]D&N|rRfU}dZϒsVdYa*c*> dN*M35Fk.*15ZQbi oU+H&qWT玫7_wNcÃ|]d 3WW@nٱDUU~buUR=ƭmSՌM" Id5OSd-OߵwWOKՍt&zwJuDHRuԪ| cWݜUI=)P5XsrU*a(B)“n"3ΝM^aP?eڏ9U9$kaIb`*]DU$S8Ŗ)rիUcT`+WVtN<p4UGau"SՃUO g-Q/X *8T+@Lp`H+oڥ]AznASnJesEteFh J+)UUZ,H]4"*9qu].[\gv,UpK*ˠ o/@3RIV7L9'B\EVaq(r5 "SQ͒Ve6j*ƯMw?-V {j*9g`AXI\ݣP{;j8P}U`Vn"Df؀r&vɊ(j_b3o)\[ձ$cBV Y KNzbv,jUV@@]ijXM*6J]̦UdYb-WzzAza" T_Ɠ/ L+bk\ ]F+j <dgԌIK#MQ@] +QMNFcH $FVr^i:h8@EUy}QUJ7oE*"+U]d^-4\UcvMxL@ΫռX1ru^[c ybsB l`NJF*Woxl%sSu/AU/WB`/]o0Se%g8:`/duZ_dWU,R WKIKZ͇juH*[[n5rXY, 8@YUca@44es(SDߨNFUAbpۮj:Vi֭ eЃXrX4EY x( ZoI"JŊJ8v7TL`u`EEC+\+QEJt.*U%/"u=C~MY 1u Uozu<:۪'߼Fo@a~^KR}$V }慨4~*W1[L+l';{F|3A\.MxRI&SzՀl+.Y(7w^l+Pl@[BYZd%'ڔNCHR !U,SXD*HMZTZdz7y7pjy9x(q+.lR|5z|"huQļ?a՗}V{Oq [V_ƯCɓ*6X]N<8jRgͲd-VWgIU^;*Pժx?͋ks̫U* Wc* 1>b XwwxOi׫U|I4)k [);CjCM4ϵTnDra)XOqX-+64* U"yrּ`R8@Vi>?29_bjJkI|0M Sz+FbX -`㻟RJbuJU *Q5.c^ ]zoTmoCf|hfYT@ Vg V| )5WH ] RרW"EX]xgJ1jZ=&gX}yaUS WlCU '^3 n :l=V%+_&΋\ɫs~*ZP)>f@ 8Eƞ$+ZރhsiY͔[M*|>Յ3?#xS;3ծt_ALz>&[=ƴ~µ3TQF*שPE VWVx"m/elN(=YBՒJqCܖzpݪJ0#I4Z}mlCFLZULVRUUriPulEjmXG Juό Z@ m:Im juY0s.V2y1UXpwPd%+_[c>\EC7|GFW<n.sEV{ UTf{j6궲}jJ`=WMb~'ex,U6ЛVkwX@~^gs\yfWV6z݃DE ՘*Va4UG?7 nXՌ`j ʨW 0lb*f֪A*V?+U*qe>ޝsZkM(U4TJmJs(\5o)XݻsF]8nct!i=߾פqxFV* 0ڧ@ RbJ5xzUhhWbFjR!㯠T>X`T*BZ5TR'l5ַj̹flp(mT5h !!A*iV*p<Ǫb(B|ϒLkeVԷxʊ%ʩj ~΄U*1ATbt$Ӡ˿*X5e 5fPXUX]oꋠ? @ꊆ] Ub*Xuu6v_Pmz]~ #/^l6G<1A;C!#|pwǛ# AȎ,ehDgb)WDܭK 5vj~~r~93vauft 0q>À k*W`MbT=djv ` T+6T9S",nF*TP_Vc~(R"d+Ʊ SJ{7=UoՈ* Å1FVѺ;zƽGZ UnUm*uY`uŒUԫ.NIE"fӪUJ߹7S5 V1/"kZ(6]ӑЧ{]ؔ@VٗKjkeRwWt0P Sru! ٓըA UIeP(${BB?[%aPki"*l'VO.ZmG#[?RGaYX xdAUҦyQ`'v)\]֠V5%R*S:ŧ-1 V#SMD?U* NDU|ԪwfRL-yV ܥ5}'eR=U=][[N2(UXR)=S $U451VUqc@UVSH'a5ˬV)W50URȮ2FnD8l+>UU7XdOPMW.dcԖ+ļwZI7Jۥbomϯf sk@Z5`\*WR^{>a^Hz2#V?K8xV*L촒@ PP3־r*đ[X)bvUpJ5Y/||OU2aQV&`RDjS[K%t*8ZQkaZ[u_N_UoWGz\JGleŒ)pդU\Ό3[+y :V]\ʁ, }f3V &*U[UҪ&Bn? <,ZU FaChL5R.AUD[ *\U?6HmaW70AgXc5Uo\|&[+%ǭ䄫= PHעUEdĊWnnV;D^V+bH U^`rcC,MlkYv/⨭Ao6c (F5YT+UX\ ``W U VT`%WW*y&AV| tY+VOi24V>Uvb;֤VžUuJ[%*/@իj*]T@;V*Tƃ٩_wrcNVXcjX~:V}jǮjj*aՖR{;`ׂrrro˳KCڽ;\W}-~31>6=34ipItDͳߧχw>a76~zohnسo7>y>8OIt =zzp"h"zϽtT5bu3VG9XK jujn)ӫ4Zl*RTV! P'UQ] Q(j0'TO)uX'udx1ŪیSSJU:tVOG3zecFx#ͼWU.q3bUUiqbqi;}EӪb]Tfcjأ2aUT15*jL_2@18tT RTZ^N$+wXe*⊟N?@ot)n555sb媗*Z!*j0HVvVQ@U;!&*S1+gmi%VׂO/L]^^2Z VUmcESl?1i7[-VW[K_{whvQxEauSQVV/`XN֢XVN-N%VmC/`j5*#+>=$@ W6}q<)hU˙k]!=]Ax I])-=7?ҎLqpD)=V!bCx曶;y$&i}~>?h hb4JeQV໢^3p:[A*LTJv]K#"#\;T TNJFUXm(N;X #2VKUXe.q⼕ k2\P`Q8JFcʝt3űV.'V&sϮ=TMTVr]VvXUjuPVc,Ws 8. jfUb@zڰBÈj) ]X' k`b`E[X6fpvCz+63VRpE9.V˩3b[JWy+t"R5t>/fi*YG?= =} WO^>L=o'bwiBx|9bsĽ~mUxdjj*T|;XB>KˊUߒ=km݃W W$VWx * 5߀Z5̬Va*ڍUY d +bkОUh䏡 k R! k.R@*jv:VH/}aGF`.R]6*LYQ?Q췢-RUb)F`Wk }ET' VU9_e IDATTRjRu):ǪQΪPbu3*Muu?`VeJQ(WY  SB+q+}g}ZQ~1$z{X%KkbɊUUKbz- \yl*Yj5eZdSt6ؐfg.MgW=)^z4S&a 81Yr)ٳGM]W^֌?L/<{aZ2c=\;^O+*"X]{Q<6JUW`UkVk2<]:WVUjD `R/{b*d KX>"t)QU`1\wa!BC^@B5Fc6mH^UmlWw>*nV.]G!US!ȪѥO0VLXB*rtuwu!EXUʵKjּXujX]cH8Hrj>J+n"iWU[Fe `'WU)jMvWՆX.UҾ|UĪbt95T]5:X=a稖hXJU3#\wnUTΆVXU܎j`* BfrɊA WߒXm{9 N =`S Lva#fvf'|z0QXxN!d5C?y4h`iP z<s9Xxn"?Wy4!0gFV@%\Mbk%V%.!,HRFRՆ)Tvkj5g(%W Xcx+=Y! b5 X_S?]?ⅧV mW+BaD>Du}}Va R?*.I zDXRf\:?RIF6˶]wȘkw U*]auj [QvX|T]!V".Z9`V=hi]De>*#JSBu*3=iָ$hX=~8aU{!@)yUS`j]cuGJ fT@qu2PU`6I^Slf1:Q]Z57Yx$\ 9;|s3G殼B&3ӓ_J&/u}UĪG=W Lg % 1+ϗ,UՂjEu *&MV{{{w{{TPr핡YИՙ PvTC,WcRJiW@U(o .CjV݇5Vh[HmJuo^ +"6ЪZ?l%vhհZX-](Wq@ (X1gVeWFF/٫Z5h>e^֭*oT\7`.=_JǪ𯷭V (zh~h©_Q -8 KU:9Mz$Qqt@UͤPUhBajQVQiV%?BJcUXmj jm4abv|j18bEZ;z;į*/M7>_Ɵĕ¤wį)wxj|awF w%=ܼpo|%:WJl$Kخ2TyX`DTDVXkΉժg% iyvʜULT+KD7P /IUZX׻Wjꈔ4TGY10,GWYVZ-ͣVUjje퀲Uve uժHmݸ6+2\U-Y} >;-OyVeUf CXUpẊWϫAU}}Vt\*C; mؙ VQtbUc+cf+Dxz H UA+Pl&xHgcbñ(j&vXUUh)$_hxL #Y)dIUĪazցA jPnL$0ӥ;ԍMX\O`5zG U+UX Հ{UUT((s4U@I;Ш38Rhь3Hg7S6L] nhMBaA0df ihu!%nSl}N-k2-RHs9IF{n2Ɏf6Xr՟Z?9IC&JU\QE{Zeae:u/= 閁&LlFUUP)PߪՋjX{&*WMΜkrFTQ~T>!"PGa]2: V`+l,Xŋj­QUrUe\*jWPŗ)gC<~JqeEmSۂ3IQbc@]cT1Wnaf!է>X6 +_ TN_ TEz%OFQXrijPfݯJTki 8miM+cJ#IwSu*p5=YSgr5ryK \y\h^SONֽ:z޼ xg?U\d*;CI? wd0a)5e,ѵ%Ơ¯Ւ+``w7ǫJUeIjrUYQ3&z^V9 `sTA’sJb0 @Bթ)fw,R {ciFsйUZmj5(Xm._|R,npԹ++), u>6jIUJV-FFNi\7Abzժ[+ٯK Z1ʢkVEt@Jd &˅f1`ڢ*_k :W[ TZE HT]N#Vr]smk-`zB*UR}~\'݊zO``,')Ҕgdϊ<Un¾=qbR G {'7׆/бJ\eZ3:uinZ]ַBoE>Ɛ-VWKtTqu ^'\]n W5{RU؛էWU WgԪԫ5agT. Z\қSUԩSLOZ\Xnol_!j[h%jBYI0V ^<z":՘fXUv*U/.//gÎ^HΫan=a?UV7Z2Us=m&2VWji³A VSBV:&lH 1V[Eصr$^2ؒ]hi]^Nfqv$eV.+չIZUզR\8 A&pd_Q PR7Zk mYQ"UViQJ:Vǀ)UXU2؞ԘZ6ZJ r_1:6U&|XMDMp!lzTx ^(V@za V'g ?V=C}/ѩ K%댤*T'ʑǦV|=[mUZmVoi5jMbu)Hv*nw]έ ȭ:'|FΪ%Ʒ.MSfIU*UˆUϾv`U% *9l;mRUf%J$\)%dRKf@V? p) ~W徕V n!Nc_4* XDjoo |r ޖnO&r]ރ } t.YiݭJD9 @TEJS&1T|j5Z'Sxxwn2HbеU`zp#F%@ʞjs?Cc5TEvV!'&TUqm\x[uǡCaBa^Q&>*SuV\ VPAAz0l.n5.JiX.tg. ^o:g4X51sƠT `V55@W *Y7T᪯eU 1W++9 ~AUeUҩ ni5Gkr mY)2XSxBr*p6 ժ*UPg/|1&sV F;\{*˵@5VZ9qj UUyJ0T1@ jL=g7=g/KO>xrνOos<?xrZ|k_ ||[X=-QvL ̩6zMz꪿ br ࢬ :TeU#@zQ3UU񺾿) U "U! +]( (le&V4˟YW=EV AUU!S[xXbmŻOj[ǕZg.VcO`qaE>\ǁmqb*%z d*~.bDnQ ;Ȫ+XUװ~\Iނ8xCG,}}-+`WŴM+?Wqa Ve %DyqHwG,0 8@~BA#!!#JDVhjrK/a\X3fkt IRB4 [n(iZEX>CͺC89y^AyUT3A5 L &GQAVz@3@.Z@L T&UsBU?YmJPN?UVeN@+r7jJN-TjŪ!SZM3<V7X{;>.>/筢 @ǪUVpVWp1l .R {cnVFg'O˺f9j!Zx+V227% Jf6mOn7H\[T)=*l ;C$T66͉3TT*- Xm W@@U[ Y)UҫpPЪVP%E%EbKgT&6Cf6nU%Y9U5 >~մ;Tـ$УTQu/HY$X ,AW& P3މ"OZ@zUѪȪVX#WcT_5i:FD^`kCޒ:xKͫVT8Ju*թ^R=j R;^?5AMTuMMGjX/p0TMX^j}}Z5뱦Q/Cx`f+2@UBK vx%V8yhӌ7>6n`596*Qbuª8~FX2ܞ Xm? XM-drow XM.og$Ve[PP\XW viJd!W3wVXE6d& pkE7WYүԪj ;֙ 4X㜩 * d`WNUV`{s5d=עV8whСj^V:ڒ9*axVVۘMmVf~CXZT%:NV`&#V31p!BժҫIJ0?SY $u:Q専U?FrSR KT򪊳՟e5BVWZVTz$u/w/juG5Z:U%UÂ筹U"'c_ªV-6+eBHuFaNh+Bk ־C]\~ Vr̄H$;<N|8H?éW{ C x/*;ߓ299;e /|Wsς@HjڳQP-torrrz/bnr>XW-fK]a|};`M\O8wo{9˃Z]/QzU$ mij )+i/CWUlBҸc|mJ>*R,Zł8Yd\f`/`{xvU2EjF W^F[-dnVjwӦj^/6 N|E'sfxě~G/O/TZu2Vmn~s74TC:[ڄbU`ݏ 4**6Mg'?wDzVcU]0XSS5^5W*աX&'aBj!9ԯ R_ѭQwWyDr߿ZՌۨ8@|ƣ!VcZd)S"^[;UjPbb-V3^;7VǼc9ѶPjW˜ \ݙ?:yp`|7t IDATo獯{UeU6?vwx9#s ZDXżl`X ,V/+Ţ sKiy1~|ŵ/_H9>&rvUM ]tܲ +y9j(@*$Sqa#<6qMT3j#fiYB,@ekc?:FU-ڢ}&[PT=̂ZDZ]Mݲ`{VЭ]5$={Zi**S@μP}ÌUT?bL)hjħ ^ V)D`Z%n=5+QM7U\@MZhÌS#uҪЁ\TmuPvUi~j_?2, K-*>8v:դ"T%R,Xb,7 Ƌ1!ZhTfwΪUUVZX5xFë*֮ VWO6zxˌ̬P?fF<5a}՞EVJn}vj(F2Д;ww*SW&{B8^غaR&@=Z }P;?"9oH6?4~ƥyy?{/8 DoUZ5;UL 1toGUTUtN8 ի_Eրj !UO3WET.UղeXTm)VN>OF*jP#V^e @Lmʗ#ݦU2|=UUUZխfU-iSg :~]{ߙəժЬ3rjD;2w;'@}.]b5X349EUͲe/_c5V2(4 UU)^L Q *GOSTJj4o)P%eB OYM+V .Ԫoai2(\VqJLM*jXxVKWW-xrZjB(H[?OD4az&x:o0`'UJYg.|V\.Do58*j%`dm%Sx(0/< H5smײlAPՅ.UjiU{jknqmX%Sg:3b>tWg<<V/!V7;x|K͛ЁZZ%ѰeROdnUU`\ W/fX!VVsaYRᪿjAU\?\y(x1uE-L T\MB@ 6MT2%LW۟*jXW9k^%F$VRlaAJt. >0V[ZšV5V/5k@URuPfYXzK u`_Sg0[C+T>d_tf IǨUȕ[@e(ȷiDUUz5LVO<V&.C- Zxr^TGOTp+ RMŦ؁}ۀ6XylّS "YZd6ZmWXaSnUX5W:Z`Mbp'KoܷjrU!)?~ XVT~Xg MKssVqHV꓅WB.=7anY%5H/ΆWG[go-?+s=ˋCYxjfȉX(Uk]9uq~m,t# tk\ofoTrɪ,@e.1ߚ6]#{Fw#S[Z(ĹfQN0s[ZtթVZEzjAA CFnujXՃ3<-  a՞,u/,tʢ{(M*`c~^ê7UV^jU6̂{ \v~ҫW%YnU+.G}ץU(L_L#zSgIRF"*+Xz`5VS;jS=ej&W;VS#VƆ4kvꝊ>%|qaϗQ]16?9 ?\|Ti؃ZzXE$ 6XV<[Ī_`l>qj/&h-RiOzV/Z^^GUa*VwH+(3S(:NRLM*Jdmj&+?IE&gZ*.F(R4l UkۦVZT!ki>N9A1rHX@$~X8jU}AVj~<\'v%**VɴR![HY65iڤG8;4/U{9HTbT A_9* -7j0V8 ^I&U73spn @̙Ο?waw/bzE.]"yx^)#].-0|bz lRH V_gcLX"jy*:DUgZ[_[,NabdhҁU},j |5\~}>"=~քV#V%}$>):dJP$D"Wi &oE"uaת~,ᶶ͖p7ynUlVU-WTJjL[SSQa`CĞ{-M UGJqUgBnzU^LӆXQ7V%U=T3HYM_Er}qN %X+^ڿ*TOV[T25hX9&DV_C2FʦVyUeQeU*TV߆شlSו$sA %"!VlDU7^ ?ׄtpH;V񸓪ۂ@/hf*ATy*a;<|sitt47|z._>}Ӌã{t&ke!i癶y9jb]G. ̈́Ϣ(\aNOȢ JԽWҔprdE%hJ.]EVBN0VX6M69ȹv\4_l22TU,@e [DVPu*H*.ԈħsTj<`6bؿtKUUEij4i [c'U_Cq* k=@u7Rbf‡}t(7rU-U>:E5+TUkuV.Y誣a LnۭGbUU%X +B8V_VG*\UdckW/!֪LYyUˎ$q{zhT}z˫ccXT#S'#@(~ ꀫwiJm|a /P{yˢ" TJK8 J*a%@ߺߴ\ r1UztjkUItCUeb;ek$ :b+!XPF`?-rV.abZEjԜ+AWa7([շoU5S=cT[Fu􋖵*09JU*SoZX o#aue_l*!֋_$` `5D`9+ UO(՚?N7D>վ>QXm*y@SVuuio7X5UPX;_]Y]]ez)`bjbPUVU;;WUR=LV*: +܊JNGjfA(WE(nQ%]YjbV7ԿǰH//RHjԨVԗ`x[["3;:YN;sV*2ƪɦ*Ťl{*Nd L*471carpUFgs\EXe~%SuO7B0*Glo5XUzՁ0|(Ve^7< ԪruYN\}Xqq%V|z՚Om\ϗ"do䭲\f*jIid- O+UR7d0A:Ou>ҮKz<VġGR(XE"2 ,܌Z]OgӜ Zs=9Qf$.I!.l@(Wg * iXT1ӯ0ֈk!h κj*wUvDaàj Q[*wl.iIyEߪPMuH?wR%ObV#VU5)NſU)+ *?&F/j^"TV}SU aZQ \N sո0U.h/UQX6NVS,`5 \E juuFi=U3W?(dT=Y ;Ka+w}ԩ_\WJN"v0Yq/G.o V䴞&:gr U`*_xZ:=VVu'rCVZՎ5 `MO a+5a6Ъ `d i%/((ɫЪV U)ZIŘ*U \Eaj1WV+VhɎ'b3 R5!X%lӭj b`5Lwx$*ij{I5,zb@G Hjzpdp*HHtZX[ax? 4e (PMYiF17cXJSHPPO VJR'q] ώU|`X=,m+i;Jw Z8xF V'3ffW,Uiz0ZUr5U=j3-ggnVYO&\\Z*ե3@ͼ:EX Vh*a|`=gs?~jUCxb2Wh@f3Ul u`|cnz$ 6dݯjvtVVhU-՞Cφ6&W`Z$zuMD@X\Sxˠ#%b`S-b WV9Z:V>GR ]X8^+y"+<Upz盿(*iϖ?VO!VkZ2V[ðjK̯\j-kjMU*m zK@3P?:f|DhE>Q\=wܿ}p_8k`5U5@Z/&"N7*sK*HՕz#V6TD&H&p!|Pjդ$HBt[Ujŋ<@ SU\͗LԻ$<5j L4aqXeZgbV8CXpYb*Z\ 8wkl"H uΙQ#}MfnvyIR+?vh9U,w 6چU#vX4If]RCHH@7Cw#Za`Uq͋F= PΊR\k!]@Zd`ЫDVUۊ)W@(X-*򪟋J O+~//_\*2QZs5] g0bjrʓXJ>][9m L9UBgkBpbu+Ԫc2\U?BoV 2W'-sKU@ ac@bՃ5/QqUP YyרdygK՟fMr??S#wWa3QO^צ?I$h|ƍ)dT,9Jb5Ҭ*c5->̍KuggED`v\_ b 1X:U" <֎ZP-M⌸V6:of~cT |[U[qYȽS Aa HQLG%O9f?y`J *!T#$+'@k*؅_~%Z}̽^*w]E*unm#UsrvyWX-}sjXz/ߊQ((uU TU|jQ\m3b1yF߀AWVX=wГi71ŭLrMVoΟZ58J33v/UAUKOT`z~K q.V\z+@Qª,j67VRSe^b^˩LԪiG6Uհܗ2v=Vե*  V{ ªKTlPDF5pR2(W\Ez*rdžccezGI.K&.L]UAձc0Qj%U1U$PUnx(c;*ށ#ec+U9pCUV tHX%+g*Uh hlڌZ q^0ti|L\vk=][*.>{#ϗ_Br`ᧁ ܷni4V}wjգX5ݐrwl"P\9tRm[:lS"U#S.U>%ՓX/S5Xxl1;1M`]۳Yv\-(u"UiMa5V`og`5lXX\ 0\ڱ+;ꩮb5)Tƺ1MunB ZU@5՞jH5Dq4߭7ӭjDC`KKsiYֲ#IFoXZmewb#՛DK*UrVe@vW{2cX%OȉG>z,xrՅgʗB;/ ēp΃w~EŧKVŧpYC俫++H/lU(V=k̀UX2asj!$쪤LXM* bٰAThzeV+ZF ZjjU(VCŪg삱ZUVyZb׼VU@a%'m2BZ2U6%`:Kxt|WЫx[ Zmz?OcX5 btR,H%gwqDZ>\WxTSURHGjAԬҪLhu5 j5C6@'%." 5J8*U)X/ZFz'.^=.i:㸚trb>$Lp(-u^+"4Iӥk!XHʾX-h*2]i"VU!Uę-5 u T>Ts6!=&s}}J?ys%bZq=Y>eeq Nj&r5τ8^qnI <=pޙ{d,1@W3=9=U.'b ㉘F$OXקTaRnbu'c 5˨%a;+ 3U (J@#-#ΜQw !KwhjU b5PKޛ O3 VeFlLrinQ^q;%W/xhuQc)avY :Xu&uZ!A=bnq Y3pN[n1FJ 'AV)ԑ5P^1t X >ӷOz=7.2  TiRb^ZǓ-ZpU֫U)퐝xbޚ "c\esVߕƌUBP)kưpnӰT}HBG!WXuj&pվDaVٻ%Ufɟ<>$KO959AΑp{.g.0 *8VZj[kW&VZc,}3UUXA 0VuQ[e*<Z%OV!W *fWqժx "+ZTGقTU[)JYa#09}j\_v [A+lLuZ0t0oxdqJ|b+hj9H9HUU ZXb~1,/L<7Oનx|[ ڀVjx#$V}#UuJ'+V8p ~cbjuTX=E^Ufe QWB߶ߏ_IwX*쎺UUJt>ӭDtUFՂXݣ(۷qKHЬVDPn aSVA<23qznT@NД"X5c5RvOZlܬT bU+VU_Lhni^jƪyF-XfRxjjZWjdZZ[MD5_@WVª,*VW[46j xUÑ;3t&1ѱ$C[ Ze{G󰪗b9ֲeПj?KKWA^~f<\<l_(: j*Q/1v X!^(h@JυJbOUxj<~xxpSZQt;x|HW{wV? UJ\d%\}US{jPVvǾV8% ڴ^x~B ̇`߯c58R%&: VYנVmkŪub.6z,V̽t6b\)QeU@=|?U[} Z, `u 4QUbiǣ|jVuj)d 4ӫŸ҄UV6ߘw'LKuU!寀nણYCZc[`ӄ9afn'v;f ClvWXĪ]a}}1v'y#bNxl(z|ln(WV : lz P6cORlD-1L9r=4Xe"V'lvslSӨc)GxTtxǶC~$ t2/@+)of\5RVU?WS-ju긁$ @IY"5)*9bҹ%Ά 3Vi8° i;``ju-iBrlW*'a]aZ|ZTl!"Mu*qZe`JjXԪ`5?`UjG2) +A6 `XX [#waLdVU ++ij,*U7l{H-Vsg6 fB{xbZjX^#V MT.Y{X/ >K%`:}:\V 2%0eu!x<NZtv/x1.4E?UFtկUULoۆ\ۥ~f1elQaxueAU0&!!޷nkk2P{J:1+Tc5cUj1(ΆOGR TCe{wHM|ٿ̒Z,ԕӑ/ވ~z13+ G"/'\< ErԊAxnyXyPj4(**BW̱UKE"AUXcUVrUj2U-RޯAzLՏÉ-Gn#͙?f&#cṪ]xl(|:M'#) VA}]E M {ˑ0 u|Oݝ7(CJߙ[諿K F|jxi4}+>xf Ս>绺~ ~@j#S}g<%ۣWl )U:톷 j^صS$j YG~WgUUX]We*vU= ߼*{v.@g! ǙܨnXu2 %.kcD Tj:'+K9O/·h8TXuVai;^k1dSjuX]Ъb;7a\x+{U@967˱TSEOda􌙫I+U_*zH]VZ[]I6N+U/z\"G7=d6~J?"_ZN2fluEbʪݦHj7l/avl'[KV>uo2;4MAw% XMn XR??$4S}N唎Dqt;V<4V3B[:D0B.;o':#r8kUoMͭr>ߙw-qR^?w'gbU GxԪ1ruYYۅtZ=9ꅱ鮺q*geaI+-Ys. jc?&_Oj[ݍT-y`JŬ| 7/E3Wy3YVAZ8iiC9n\#u{97 ͿaQXZѾy:z|0|pUjjC>qkR S #.2u-km*nZjxv}[X^KU /+'O>`e#&bEic2Q5b94}cX`Vey݄͆Th , 4*V &qO}ov y! z f L~92Xɏ=g`u?bjW%Zdc-UlTd~u#)ɕ05s1jmAj"qՙn`QnZ5yOâ&z{:r;U_N[,KV+ϊMj+ Q#՘F= VYVq{#d9wZ&kdU*`XӘ#azTw4We 6lS~&D꿃U_X5Q`m@S-(Tp\G#Bg M\CN>":8f5ULMrrnNUWo3*JlXTKXMJru}4J6F[|݀UZ@&7\8t/vV}K{[|%"bU&)4ZUD'jU#3U9]b UWZU bU -jgX݂h &DԫGQBҪ `[!լ[otfXM\Bix!R ?iT ` XEd-X<5EuUbvK)-[V&Ŋ^FI`ϵ\3V L*T:O"n-f&U2ᎬTջ3H:=l~lX? ,ʀw'gEٯJuT6lY׭"V=(]=z8 7Gh5ޓaM4-IjKJr\4,@_E+$&rjMQ jіLLTŵ۴G4:uEV_BEұլw LSmn+jF?r{X%RsH+dR*w %o*NqAjKpNbEMUbHұJGlU~;O%@XsM\`Y[QǪ,A\UUZRhJ>#ժ_{G#/ X |86߄Θ+@h!ɑ1C^{ jJ'|NДb{5s*Xe)XE:,U6MIXuRl`H,O%g`g~U:l%bQ*ybB%`%Y;Jf%Z8'&[#3VigGV8H* (ΪNs+ -ЪUjgIFҪ4n*0Y^d.N49PbVjx_2-4.TOmՄ >+T *S6XSBUS%cU ] *WԬ 5~M7KJIAr|ΧVA1SO}as$044f Ll=cݞȷC'{`ZZ5QM=|'Z@ t'i&%oᥣ4h/|s$0`$V? ˦\I$M\N]JXAXR0KcRw/Aq@+`ߋŇK8=JN_ѝ bDGTB:USHU׳yF]ݑGNQ4rUoR*,`UPfu+Ozj}CT T-43VZcUѫ? Vzj[~XQM J4PʌB* adtM"㿚rZ ;8T/8/;/;f( V] Zɦ F0aaUXv&-͵hXtt.r Z[Z3ho͂C5jHdf JI!n׌ K5 -^<~!Q)&fXEYVA`L՜"zszrTceCjTj)ٔv{58…X-*2HXMfՊ]&jiB|~VV)3q#쩊&Xi٘ Hx6l>@w~!qewCo}:1fEkC@Z > QZ4 6T,>҄5 ذ6,هeMMPK9νwt&fs'׹:|W ֟ i>czcww땫UԪjCBj~k_V=ªiX՚wBO)j'=bUib5&2X8!TUtFXMD+:; lpZd*׭bj4 VX6!-VU2Un*}gS%TWXZӋPV;MjVzO𶡒!x3BfUR[uMɝQa*Z|[4TSH2vY:߯ʢp]B?ëH^?3hV #oWoVJa@U W 5ӰMC~{ԭNԪevVwO5, \};0 Vjd*b~jަQYq X<QXx5b ػN$ L`DP[VIF!UNV  X%i^ -BժVy VC"m^ՏՖd$0mY?㩒B+ZUnNXMAjzW;@jrx 5kH{^ O*`pSP]TbusMY?N*)g<6J)폱 #TER1UR#V6z{.VX VU]Ucqzq۽"m婢*Uզ&!&AW_(bGUvKm@Ā'mhʖآU0cz."rc`,5 ըl1X}kT0j6MZDUm @QNԣV?>c|yu *)*Mñ V#ATTMQR3P 4SHri+@p@KyZӜ-_]0&1greerer:PiejVLUz/@+3F ФSa%ѺR)%n?rbMUo1Xu*Uƪ? ,uzv~iKݖFRuɝi,yIvX = jw*UU햞03N#TbWU'd* TeG8 iᘂ* 5CțxAg"cFX pFѼ7jUBڱ2@4PknAUoժJTӺ =X=w}2X^Яe)Y[CxQT.mJ iv-Ti'UU\э*eU+TInˬ`zpYM m1P+`.) l-V=uJ<Ǘ02UR 02'p_R*ĪܳyP%Vd _P[XZT"WaV\jz\Y*3r55\~y:hIqJ Wq~a՚ͯT*Xug^ivho&kzk/^@<c1TaLpr+WVo`@FKơuU::lU]/^j bzWϛdM҃n+P.:pZrc)ZVZ6.֩[:JcVUUVe*hv&?VEy;0:#lZ3 V/R{ijjf(c{\9+hdK oUUum.EXXiʕWMM&-N/FUsjT=;T ^'RH@zLIs-Uu\ł*:"_pꓭ"X77D*'ͽP`.G&@Ԅ}Fet"4v$xWe@|՟WUZ͹MV,V]jVu)@oO+J3V-NM\ buJ#`v;|wS Xdc]Ѫe@&Z~@V^;IӚejj(V}.TJɑ]sԪ *B-!Ub7 @A@3`Xmjj*VYzׄX-Y*+RU5˾M'܁x@j Lѡ{V*P/>{$IYVVn8 /;\?}gTQ^MӾk9, H(,\ɝ rحVX4Rf֝)T:JQg;0790uQTզ =IBԌZ.߁i*LOMzXEljWmo[ΠG`jY]Mru5}R)UVgqѕ_tjD7a0Xy33UG&}MΨ9u Luxꢵ=:֦brV }@L*ՙ3ɄaUSƺ~;n-@ؤ.-@cV5'Y˪RۡHj7X/PxnjeZ_F$cbUK_:n*NpeR Q+N23R=,1w?rc'ۡJ?j*__LUj;eKbX/撛%Y%VQ^>n֯ ]UnJu%TKb 2+-v֨TqjܝmH[YYb.D7Ĥ]X Աm˖PNǡ nmڅa$"4 ̀HT F&c\h)m,s_K.잛ޛ$|~?%2X/q5SIaDO W XӐU](\*V1VZ܀R@'t=J?-U^xJ X1+j6>4t>xa>juT2ROS*䓹*{NS;TUy^ŪfSCVqLjR6[uTWU_ 0i ~hr*jWQ '"TvXCAKZF#ԊB2}ԏid<m*3m1 )63o<\S8bP &[Xz7Qn9`S,h`x.(7^^eX ; |q^(b'U}DXI1HX>5qtc&XN Pmm2t]!XuO9q^Y@d' #QgfffnńWTSbu `5Ac.sPU+@P,+9 F V=:b^%X \R]M*Ee *YaUV)jn=Vn`c=M{ʃ*B]]yסWXMuF&հ y@1$A1dmˏzǥ\?NX"V 47Bkb/6@C,Ż})aD4tQ/q;=Vl1 /؃abϿzh]N' VdŽv:]YXE <$|d n**S U7c.UEXb:wwݻ^%<Ճ[8Pzb \sPU*]a+T\bbu YJ7)g" pD/AZKK#S JJFj0T>Td}UC[4ImLk(fKѷTZs~2 UAPH!Sd'A&16 j6+e.FP~ZF=Pd5}(*{;Vtz[!8&Cp.Njibs› Xb.ʰՂk۾7ybӦY6rew};M ]+[5Z;*X~52\lv!VQ{+` HUiX=z{YeY;4622po@qݯ25AW*0 XR$[:R;4 ĢP4)S}lh|9``EҊVe -VID 67U\j`5PnEe:~FTgCP9L:Dns b@ԋDY V](WxūzKvtMJ"U}Ϲ&VF˾Áȧ?!%:0j UKA;mh l0Ar!TVO@fY<|jI*lБe1$9b0Pbk;$V-\fR'KUN2.a5X]'X7^'ᦉHY% `zw̝MHǙMH@b|a`PD3)i٦yQ v]ۥ"$D7LzCti/s̡,c)sjPK/-l顧'&:nbfyG+'m\-ċ%Ī;;B ZR |gwK$Vq^Ճ V2V,ѯp@delkU*#+T%\ũ0A*Xm@Vss9)1V!I .=RV.+V#괃Uc(刪*W(0N5ʕ[_7X}RV* Jχ`U 0 GoеO%*1{ / ]6Y+ ]KV=zSz_>V׃:~/JDՊ֕{ƝY[eU*T&GUTV)XXِU|?``'%ծ3Y#Hg iۥRljS~0zh0~O< ` !e%Yʛc~*րCdgpH ?ſ>Tr9򲦓L){%8Ć 1{Mԫkk  > 8P'm(nbIa;TwbB/1ʕ8* :>s\jpYl-2aX%#YJsYɓU6W[MTHsVPFmz=X}}j7 ƕƪJB0SU{^]۸YZPVѿE*HA3jj J˂fѫAJƾ!>ʗIak{YK˶"bu~v1Xf7+ܟg7Vßh^e9^ser<UEO ڊ*H@H.Ɏ0Wkk53v:1j5DX}.Z+XbT<)V_T9<Ճ-Vw$Pim5-^I[jE= js\Wu@UMW]+k*nf[MNUj U'X5ޯ ~ձ}X/"cj VV gnX7_XmOsJuUZ .U.J]ԪX5N)@2RtUUU{|!φ_e*x][KKK0]XV[_YZ҃ꎟU8TIEhOz1U:& K k4VrSb^?X8u+ ܵ/\=6UqWg @ Vnak&VWV4db5ڟZV2^ծOwXOA>T !Vǃ-(V*oaV*P5k\ W _R>EŪv)3nqR"arLApj:$#RSR"Y '.mj- ҧZ\ s՛.VY^V*9Rdcu7VX%dq Ő5~c6VgZ Z"F b1GVC(ڪj}r0'np/ΩpY5C;zmi~=ބ9xzdDws 3JZ}'4E/AվAGՋ]V0~Z^~.&V''Uk'Vs9xWUWGTOJuر@VVǮ* j t`5Dm]L~Q,q,iUAӬJ'AQSos9tU⪫V5Vq0qN[M1U&Wk8pbKc$;!VZBt2btc4ߗ ҄Y몚W?zp\4[e@8/quP@`<ʛrPX*S{{&[ Wѥ"Vʭ~FerV) 6յoU!W+I4RZPR_ (|XHْIsr:6v!V%U/ƪ=5,֥ĪNlտdV0Ԫj}VU jj"^j~ K(*i|>^+BuJs]5UmФwp\a,n eW߬X*}J`4@*'x(rڭ)+lj:U{^}1W ؽrUY\>Gt_'p'j'UCݹq|qW1F@$B!ٱdlG\"ep)q:`ӀA^0d7wb-l2$, L4C3-CfE7=sN< =\_H{]F`U*3W%ĝ 气 ƔXM؉&XW|I VXB\=h pG4T_a@]rϩ;xiS ՖJW)v US{zBV={R5UXL0Vzį2AS3A}\ :X}:XUjuU*yUATHRJ}AULpkSSh95Ъ ;!ri݂@ r6QPd+Mi-19YԂb&6%IZL],T5Uvlh¿><<|62S'Ͽje 1mVeSA c&/rcc3;jT.JdIUb՜X-WS R%׵ŪG.+_ʣD:I2ρgg*p+l=zjJViU!qy{ۘXW4VgXVbw V"bu`SX-HZTS0i%L&f5Lp (VUX HUպ?77jEUX|u`u3UquGWgql SVZCo3q8m586ru6"YrO<Ȇ-G vjl28V]aU7ZyViW -j%j*XkU6Xҿyd&NHS_/ׂhO.K5OL'z U*zWuհ}>hkup2*Z tOaL똽J*@/(d9*Pc}kj"Adr%X&-USU_+: Pm,,T*5p:v'7keE`n kZuU"ceXaļ dt13;-X\ڗ-}_$1VEpW)}-M2V\vXVjXBT^_TPu.V UNGxa5ݪɳTXE n9A E*ya *+QՋb`e1}=0=*jz:՞Nb5Uj}꜄%T5\gh05-IUd2iހNU6qibӦPll(| !gXVʪ_V8' + uҪ@*BQ>7T-3VJlUT2VUXqUiV]%RVF* m.--1VW+(D&3,A&ã/z'_}g9uK>xn||&.mR1)^}a#@DjO^*«U]:=*:yjW~#J3TggV)xFUrGM a~PبhoبE4~\Ղ\dNX K;2}DS4mӖo*L.D*Mh)q`5W!Yh,(֧~nd34Z]o0U}I`#UJXX([,T  ZZaekÊSZUwYSXjUBʰf4^]X}Oc?_m~{Xv?"z:TGFVf,*Z%aXFnYOV 0 œӡ:+`T҂r,b+=S9&V[:GBN=;: \c]Ua*US]`5:/e\@\ @ @$X Iy,cux&>R 5`]U7uN6V Z}?Cj`XX7}Y5Xe!UҐbJB\uju* k&6^כ⪽==&t\2:, p5^IGGr)Ȁhc# @0‘ [Nƙ+VwMAxT`fE*czl@^K ?*@Ri RFiNg uǖbUyijU :5 XDVYrnj+ʰl]Ltuq*!X*0A#V{*\Q[|Od5 $] J!ThCWVNjU/HYfbnӠ+FS $*L DXAkkDű5Dxqae,Voj㣯'GO0{kS  ~RjQXZT1.NV׫Y jUAX3A XEckظ_J@FGa*RVV+J*9U Mt:A#/?>IRl 'PjJzÆ*h$W 9sWT:^Pf4Vd=9Rf[bJ.mKcXDP2b3;AF Z*SX#V;<$zo + C:M5MXUwVnV'XU T_E^r5#jYqJ9KY֑R >C-Tl‘`%Vq)ք plQ+%BL8N{SBB̀](i2F'bi蠑uS/J, k ҁܔMb %tw׋ф$ͮ#IMd |=_O$>3:3*sKH:uKXr*U(5᜕Jr\de0l4&?8MVZjT X4LVu/ %K]Q遪VU2-pZLU,4DfPMbE&4[Y"Y cuy1J!/eq1_ȮdsJnEwԶX-_JitV4*ܪR`UU*VYUTUI⶘긨U֏-gRR ժkJVxVՓK41a}NTUg`05XՂthiV1E%Rh5±UGWݢ U&l܊QrkW`UdzJvjԪ P+8'mj$H.OG@kjTfqTUD#Ѣ X߶F RR lɊru"a-VX].0PWrRV!G7s+`gĪUʖU'dcUu@lrG}>Mo߃'._R*DuU*ѡ]RΜ@*ViV*%u,V2j5+ԫs)LY~Sф=.JtH[g))V$z~>#Ҳ%:3%/GpVRDզc˟b8ƷKbž_ V]P=W¯EXկ bKb*6ruPsC\j7`{Z߶eլFdUil5XёdM,$w"|)t+K ׄb/kߘ+27].?W_qj:YdjC}7_MB/7iz~&hՑt[gT$pu_-`5JFӃ//ӗYhաT@!c3Mch,V15 \k%v V6mBNaRe0Y*a^U.pAk8s "&LLmvca,VVX-Sժ-[AcitWapuzFzFĪr$1F$\ Y ^KķNK*2 5b6ccZc@NX VUB!A,`5kwsu}zs7onWo$0A6}/V׿dbV{6/l`uxF#|^Vyn&\ݿQjz}IUjUV"JNkN9nn TN 0Ц RijjV mFZ 3Vpo,UY%xPdUpVԪ-V@B^e֩-E8gUunXV(`W V 4.duVytj2MI8CBUj[lFD% 﫰*4A0VLTBUj ,)p5t|{:тj|`a}O^p" U5S~| Bwr'յlWUO?dF>#q;'Wp`u<8OS6XqSKUNT4nJJuRQණ\X@=Wn0cT^0>~Wof(ʚB^ =*5VmUa!Ve*@~tdL*Unrۭ0Uu$uu-_~)-kEK|}N3/ecuˡNZu,wI pDZ9sWxHҫY@uѕP\%U5J.xoKQjijO54"-mB(91 4a~4=CGGhIXP@s֫Yr.Ѹ= 7uifWK뫏-ç|z(|U,rѢUժn|iׇO6n?O_'Wp2*CWqlZrb{] x@J Jv N1a4ΏI_jpsZ]'(W[Κq }V2V8 R5s8 L=ld>%U䳤?V#TlZ)_ªw.EUou[T&ANnU+jʫȪ Vm]IQ܎YuUGD}HQϯVrdZҡ i V ȶa \,Rkp{-޾~djcʩ /r&sgzNf7/Wo.WW 'f}`a #wq4#w?>e˕1X5yBzjZvy)k67?sgfq:?L/eB5(SDDMS[ Bm7",v/2/fafʲ6YqbVjڎbm(7|ss޼Z[hynmNb(Rx ᛉم's{s@k#Ӄ{n}7 ouбdnp>=Ѹ~X}[U`m(DUMm>\\.;׏NM7|)V  㦶g u2,ZB*+oYpg'֭4\]V\\:$Z-wk z\"_$Z':"VVqk5;ajPh ME_uVbV8vQzbuBYX5z,oT`gXmvoJUJj 5@N2UHJ$NWĂx*IK8ֱX5,SjjX1ܶjkw {{5*Ɋ)3U%궹U*L*$V3lnK\~*s3="E {h \‰x|kzfg֋to |A³]nxʨog QVK1Np#LΦ&f_n>OHXCl>"X 'I^ ZUA* x#Z%*SU*LXj^|g=l7hnZnh/`ց@pRleC!E+5 8kE^ { ֧t5uBq*9ƃlf7r%PM֧ec~*RWzI^fJJ`juƭ何j$aSra<>X-摝 V6JvA-nVu,?^.V] bLYU{JQ*Z]ż*MjX%XkX~?3)*}UkJ墕_ `>`}ӊeR6V_m,YeV]"yZN3X0Í7VZ[)*VpAچ6@e(W X GX*c~PjP?wc6)j5=ՀgG2@l2ʂʪ s^[/ jh5gbPB4߂;KN TNW5hT=n-9VNPRTa|)w$rVq(qVo&i}8v 6Ll./x(hб "МWZ-H qBMV{ nfJ(ZUn 5r_]AZyjQOUqGr?ͻ[)Ώhb*6ګcS0^TV~MŏtBGcjM\X{WKɓUJU9h zކrV-*^}EUt8{d:Z<8*j`Ղji^6>POY78`5yT[ζHWцՐjP,hZ JIZU .OkH|Ep_nZHY*rx"!y*8(M鋒DٿOA''#p1$бzD`u4fg] fgf,=4y6檵o&kߕVKG/F < m~a=E*7 ,W,V%~ qUBĒSYڿ_/BM1C:mS 2U*wTYj Y=UK$*}BjT\rPHdSY  Jw֫R:UBj6X UwVsv(eکjRQSmI5vKPjPJUJ, A! [pǖIP1kC!"oƔI{NRU(JAjj+M, FIP1)f@md[YmVQWLJwիx2KX7Rcd#R%#CRĚ*03:X nR$61@ GOF~mL)ގyӏ g׉jr[-*\`9/WKZ+`4V[mE:J~6Zoz_Zb8P%,=YAZru+ ಂ="cEXE>_)&TjV)cjժk%bUUfjH%9qACĪH- +Vc`up*7$5LbQb@gEmVgKI$W@Cv!PՖ 6\a,6NۢUDO!z'VUOL|uudȫ{Lj:N@( /VhL˚?PE4<ӵ2WR]3KS?9VFK7oS=@FR|TIwnOYopƟx܇{Gx^Y&Pnuª*4 m<8Tn QX;?eVre;HK@ 0<4cJvzQiƪRd(lz7,%ǖue1ul0z$*D- 02*ĞŠ 9Xe!U3ajIZ%AFלVMZ]R*`?130.8Cx WnX#Q)B4j5Y )jYY.WW3LL;;U'ݸJrUyJ "VyxVh^zޱQWbjqzE֦+j-Kz JwOw`oT 5hz?}Z7zeZxLjh0T~zCBhx;=xګuliGˍvj IJUww䎨|*)HTRͿfQ ;(Ċ*1UɵUڶ~.aJ&~[kT0ӿ&ʨ/@g*.> PE V uV `/U:gz^-:aU'qPpkQcDMt$&w @kά Efh8,AXLj)~#EOe-qǘD\z݆UgRfܮfՖgϮ. Ln!Wǽr~mŸڰCZ)j"ս|8ɨ>Of *tX<*2V { SL!"X6ErudpUA)Ք6ߏ] BuOp*ZW*6*`J5Cd Tü8A@d/REV!5G5XU?ò2V [GZd[mUyU.W jPR f5uRT*H$UwN925Љ&8KtSVmU+(_)/3%7^I29d IDAT~?֜rXL 8S@6ruD kVFF4:&f2QF5d(*}(QPS 3 ]Bz)ք* 0WUk "PK"dAX}NU=v"qDJ4#YTg)jA(UعsLr=;uɺDUP1,]Y$drEOH9RQ b]&-  sbܱʠZ׋BaZgP)"%=#V#j&Uv*"TI%W UjgZUQOl6]rH VGEAOÄinwz{{QjW*UrT4_1*FWPBFNb\e`}`@S R l*RtuVak %V9WIZj 7Yڤ헿/oƾTaQvۙI|2X=AˈmGyI,Ÿ!fmfU⭼VpՉz+VA?ՔR> ]`W *ŕUV,jS Uy _9ݱa1決jZqiXT ս[{x+;ŖHT[]& Zjn;Vuvju _B#pfi} @UqG'ѫOXXRaIP55Mhm\Pejak%3^H)1J.ʗX::8FruXCSbW/BVU[*NjթDV\@u򶯁BtF[x&Ǫ^?֗Psx%`z,PMSfdi X7go޸əJO7pZ4e)0ԬZu.HҦ UVVUV e@J˵Ĩ[OnV?wvv뛑S=^eZ$8D Y,˖tkչtPTѡZ4`5PFyi֌'‚9.WbQ%*~-[0jk.?/ dEΓs43g‚VM1j h[Qr뀜V/ [ w$V}< ׎+"xz0?IVy@琖+V'"5zI5" ʼn'LaN?K}L3r!RC#Sٺ!6"ʑ,gY\ ](K"TjuEhUT22urν&f۽;s'EGh6/`?.:^LSJFH!VU Xgi{JCRŪH"TcUbf x8nJ+U`$gPTqIf_P0tlѲ=-:~OH^MDgZT+u\.W_%V~3Xel"j٠H8ŵWr dDc^lퟎEWP`Zs%k]fLY]D\`pk>Z'Z+* Mp"+ Vp`m2 X\ TYzVc.qѯͻrgF fd]QnX: [FY!`u5l.q/1n8YE5Vy~ժs|CA/9xoKUC IJPDw%W9|KbUn:x;3To / RIႦ+XɅU- fgb|*Vi ,y{ hT(j4o&X)n5au7c8xV3waWӌjՂ%ŭHa("p] T)X v4TK@꬯n&lz82⪺%S@ ]] x:Kf : *fpfpyRbOUF&>ZUZE&WQ[1Vpk;կ:<\`TxϠbF}[v9TӔz^Z}XaXE+OiaX} ![SF {* Eu,J\ XF}zm\G}K+'sެvHU`~*W]aܹ&j 5kMӷ V-m$&a^_9Hկ56,7Mc\ԴHτٹUM{yӃNvVjIOI故嫑'/c Fނxb65-v8u[z[A+Vᠶ'a̟_IkI.,jUf/>YPXmF"U& ֊Imn,)LK kk@8'nTj[@C(ulz(_G,Ƌ:Kڃ~|uFc" DnªE.ȡVQګX}Z^[p ) 4$K𑅯M&%nin?Qa*ДzAn?O*r[bL4U;q . SL@Z xmhJ :eZ+.R9V퐁+R΁Ue]5h^C&k{PQ*PEW_L`^jsV ОGkfzji2ju3A;Zjui}G5V7nߵ:eս[߬ux!ՅٗĹX3cnؿc̈ﻨȹdwKFwИ'.Vd#m᫷ͅ"`F8 &c. J[&`@VϘw1W|zKXPUM*CVUՏ2WۄZ%YLY : 8)-~bh/46 ~UYZa*=UVT`O٪dV55SV)+UdT+uGnªP*XxW*zսX[yՐ9IUΩ%‰*ZU2QyJ'Z 0PC^R(*pS39[Ű,#U3\N(r%:sn{ tlXbvۋc"V %ӷu]"ݯR.=tHSj>ef6bR>뙨Jw֘~_D}k5o%V_WUr'bh_YYRZb، F|fb87zn``wv*V#[⛱8`r||%c5).W#d'cCnA=\^{5&ĪPR{KjRsEbhh=*V^-hY-Մ@c>"@8lhgWmV')Q @Ê=A!k3:B\m$vT#~c V?z Nd6"ugrUanm8,*XL~sH`pT'csKF[ޙM( UXF? }sXU+X d"6Z U'X>Mрup+ǀtKE @X jNKX:m'<Ƀ׆ĘðX`g&QP/ʃU7`Xc$^-Vlj{mƄn* ĩ,O UwzK`up|ߖ ZF#VbT/ jU*g]i*@U} bYBXNmB6p@R>!QELX/˭o8 pX;qJUHepㆂU _xQ"J,} Ac"Z3FkVycF$%#{ xlU[v!Gλ./t}odc՚r ݆b'r̊GV U2NrI rŪHGXj?^a3Eo Je)+DUE|`p$VXErn}*iSB|65:RY)ê ]ԇV Uc|7l 4Z}ttT`"$qm#VAdhG}caa?K[y!ѵ:kBZdQBHgZg|lݰH M(]pXbovd`wúK@hf.zgً`IbS_4/'x}ނ j`*<4y[/H6@Z~j@O-Z (ʔඳ>@A\"WޑZE+ƵZcjJ D][VZŴؑ mUi{o)!:Au*JbqT `KmQe`ȊJjPY+]j_kV%VZͱ)͠V[ p$XZmx E|kRE̿@>^ԓ g^C־6 _ojשF ֍sA'Z|^o]>s5ju4\+7A ruJkr*S1KHUZò(6맋2EU*Wgޗc_ W1Eqp xom[= ڹt_΄j'wdXi V}G+?%Vj}+y!a3R14UBM*|!S([oƞn6>Y'6OZ5*^:^_TU?Ujo/b7rtd\@U n IZUj@@9d$19MT?ZoK< XֱJoj `7ĪgEVFZd C!zR_no%:Tlm-R'@WwD!g^{7a׹(۟x^<\p |aLEoϪj@|;:4W-ԺT#.ၝM]q7o\1*>u~>K2-θjTͦ=MR*ı(SDKUUj*Ԫ⧖3VeE^Qߪw!0JG͊7p V~)yI. +;~mb1ʃFO>C'7_m[Mǘ*h~{{I_|Oo }#-+%p{mkssӟPʼDj5TMֈ V-Li*^jkPU&m*i*iV?Օ[cc]]qk2F3'[D*18MqoVjY'm'kXZ UվU\V*Fx_Dg% a`vWŵmJJ'RV쭺* *-]y$:4m?2U::*QM_e#Zj T&\!UIU{`)ZݾV״i [Avp~Nۃ=}}=W  ,L;>3T1΀MSC':Nu,.`ѩ/MMossj;wxߵZl*b*xu] mbwww±c?]-v,UB[V]a@F~1zdĉx<Ì` Z㪱 RպGW XIVR VBFPr{SՕXWJZj.UNggˈ4撀Uĭ]==YmHGZY} k JYV(O:0~/P Z& F%|ʋխ*UҫyT>rB@jOh5$ZEyR`3$*"VCULy_zLtVXչ/7Xulhh+4pjur 䪪ՉL񧎵n~TQ\3"3$d6 IQ ;XruAQXœ-2ni\Xm*Pu"*U_5r:Z:>\t'ԪYd˳jT58N+FEՔa^V91A`W߰kwX͠k P.fjbreX2ʹbU7Nփj~7U_#B y[bEڵ䀫c%z@u%T3eZY[YQ:gySFҺU=:,J[>V1H7b Qbxzz. u_V\x%nSqrpb% tX<(UQE%/$EX:u:aoOJ[Up]0U#4s6N\ZPbMUCWkT)ߌ`5g329`&3Jq*t(gjCV!0Q,YvjKX\Ϝ-j5O*kqbuNư4+u ]EIj6@pT IDATt9 Z%cuΥ*UKEwIY@ZwjtjrnUڪT\jSWA~WҫY6e0ej>MQO]|>gn>q»sZ bwŪhFj4V^(j9 ªUY8 2X4V^ՈVYUXe`Uԫh9~45TV%;I|hA5qI(*rƢ*7[ֻUpX=-`HV5)Y -W \#V_5 j֊Ѧ%hhXjU+$5u d%FI*/\3K5zeiXUm F֞2&.RSs* JZ ՞=GZUWx|D#Tp1Vm"V~6r O;:*fH*1u(K*~o}̦WV#uDgĵZ.W rqS% Xhp)gdV OHV ժ&7̩Il#8 qUKn{Zvm+NIYRbkD|ѩH-r4T "cU1JW*U1k`E_>~y Ue_ptVG$=yQiZjXwR䦆6hYV/ix?LUVDUߧ*N_bu4K +k+kZ.Wcu^(CaAS5bŠJN<HeU`6̜%7s2-jcm8gAXEhp͵I32V%`pƪNV^zS#X1}V;HGj`jiZ, Vfv/V=W X_>6<^Q,@^jY'3΀V 4\SoUi|i[Rck;\m];(:FjUcMjjBV ~( C'Ӕռk3>ԧ`L&8VT3LoDUrF\+0g)URg}#A;U^Ht/,*>;hdTkwjgiWn*&TpwrUx-VX-JV*\1QSsnURfuVտ:;0V@!Z7C.e+ N-/ col7fjBgBIJ1J(`vaO*FaPU VgfKK:/*38?VGVE -I־Xت@9gy`*ө4z mS.VGSuSY:3VM=ηy P#Wׅ `KjpKu LՌ'V]jڵVf&,n6V {WXW|ճ_&պ6q?Ycb)XmB JQ'cujUWUV cE/e%V*9_pMTbi^,jP kL7(OYWJ1Q lhXX  vˡlyj!YZM)m! Z9mCVV\k#n`FkKY:j kǫvZ"gBl V; \[hpJ5#ª-hy&@X VeVI沾Up`LV?/{)˔%njrQ".*q{ ;=I3Q<ʈ jj1X{m VS)ՠ k P lC Su"+&g)ۖ_YtE:4K.LP?Պ[ZP-jލCJˊU_UVo*$)41ĻW|@uITcw_V/.Mw# Z@{p XRD*i^kNQ=@'D6*ch X-M02UkTƒn[!T)WVVOuNIUo֭j^U ]T}ֳR*VO1W=t#ULWl@P*Ru0jjŪV'RhKFj>m7u:`vRW2ljACuS܁2ekN4UJfA:Md´8szzW+L`iA \77q ==2Vmm%A~J`^1"E3R,<@1Kت𯴪B*@*BꙂ->+]1uccf[a+V`zz̡ꋗS R]rGW*A**c9z-H>'X`}<+" @A`]rUVZa@+V_fj;=*U\uX5A2Um&bU5Agd` 5j'XuSрx @3\_V4ڦ_=ֵF)M+m!S M. 04Wa*Tsy~Gpj}w@bRe:v9]$e}{{pVlzƶTTFQU7cXmNb t!K7:AzU>7!&i|>GiC3 P^VFk;djzӶO1WVW NP3ZSUNP1UV)_ j6@@Ar1jTV'êkN6Tp=J<}3VVU7Zgzo_rzm;6-S\J[DVWt *Z1LչU*yCUM Sg;CX O4~%.S,Qft,ZCBT%ت4 DX-X]_ruOVjN.}}}4,Oͫa{a74xs\ZSUqGp(`՜ka5X "h՟ zU*ĻiRN.‚%OzFЊUȫH  FЬ"(?,cQ*4^n*:RJZ\M+[pae-\jn2RmIͤUzz}*~UjnZ)ZBMVW(RXy?**eO +lg.H*ʩڟʺ6IE5zHUo:d@TVUvRn x*Aj^T=aT^U*hUF{jjV>0nlɁLH4jՒ] ' Y!V-VB,P2 j5 ~{@HyJjntR-BzUi^V #Wf1+*< USBI)#w;aZ ZS?]k?8T;.6'T\cZE*\+Z,*5K-+bޠHeJ\ʽW *; <_©{ 1W3VIjsljDnQ%?bNVekj_BWD բ!\*>s?rlbw?iVusZvo]ժ#UǤR:7ђNc1HɌ'V%R|zl4B\&XAUZUUET[7YJbU/Dw#(Vg*UՕI@{j'gfԥ~':H+PUaR vxU&}Txn[ |XAf!VѿJU֩W*7R-k T`A.+ ҍVU-(XŊ{x˚ 41\%S>޽kWiuw@ @UXXGLXmm*i)~ֵxjЖ"ucUV{pV@ e+2 UU.!A45C2Lbz2r?A(ͪcTMJ{RqԪ6KJa͓(guU+omTP:F}z 7|*V&=CR-PzFA:V5$0DͥDU9jf(x'Q*^%+Y"f/Yd9*RhL)iMyS6NH\:2z*^qRtz Sxz?q|=|#D"j!հӇPE!G SU0/i d?rױ*`ªڗ-kܪLY:T tيxm||wN+rgK.T2c{qeUb + -UpBTXc$0` S*x JuxZO9e}L(7x#4V4^-Eŭĩ(P8,MUfwAyyynij|J^Zyb I@]*4'G=$bS*)t\ [ 7* [$̨U9uO"*Lu\TRPẞwy*WZי#yJ{jꜶJMq` T=GT=I~Un]bpkW~}LmXooA\aU\rcmV={/v{^u XGIaT v6ZU WmZ`U UU4'q08^եUPI$+#.ÚhV;VV,~Vh2Ȋ~CJZ*z~_RiyKm | by;ܪOz|DŽd(`>ѷX5&뀯7V9iBɚ]^fAZ![S2a-BV5FR!^uNEuhWv壘;TUX/V'{ƪZKeh>ZCvU;_O;܂,Ec7C/JVWk?;ww^muΧ ~6?yV5MB#|.jJUBYˏ?XllsX=хF*~c3ukY&,2,H:XD\հHI$2%V3}Xz[Pu]`:SU_2 bnW 2 :X:*i%QB dy2U3e*sa+$XBqA]R&&n jH5>ZUae (JkAO,,k U1Z@)ii$Qɿ THѼ`f*ebUBԨjA  8gϢyIL iVĶZ}vh԰_>.~uJ`}w7-u ZGmꍏܪܵ \׼Շ~vNX%Y8jRM=/dX˽H%?Mjazӊ=DUMVQX% RU.8HLM-@ (cuuFYjj4`2 +ЫrqÎ֤9T5N;-^~R5u, b2_\ y'IMHؖzcթZ}.t&NtP}Vo:?xXvxaYLU^].2 Պ'TʿpӝG%J;v(!{Z8ˬԪ?N*!!q: IDAT8Tqۏ>!Lj|Xgc5F8%^z΃'7N<{J%?^Չ9ՍĀ}GcG6}q4I1&Ωl`5EJ#;gbmPZbBYLNnt+1N1o uLhe\7+er4䦉][Z?-ϊRa+WYFQ)`5DuWVʉ̽c{?"oMv!V1OVn7;(UnUUXeFXMX%Ԫ6={{PbAkkK jUWU)qתYQU H@D#Jv5ԖUwJ ϴ,;z+`OdY`mTq 2A5Dh h uͲ/TA+vW)j*&>9دtO᝞R&iiH&zԎUw/h*U5/,}fVY$??@yZUZ=297t<`u`59Z9v!9:̥71Tm_Gr>LV7dӛw\͍>]x> ZCsc;JE-qV}5X/X]Z'2_\}LUtuj`uJX%y^VyD ۫] <QUjW P  w6f{i lk/ .;^7ͬ)~x{`~PmڏM]l\EXLmDv:v1`z8N&}jD]<[dNnS]~>X>l)/ Ijհga+zf/L̵֪V+i*5zZuO6v1nTGy q*7^KŮ/U >G djC Ɋ@FMUz.Ev/a\V'eɹtHjthrO&r~=XBYG^3^ar?f[CRᮙoՒpa5jndwbfŋf#,YaE<2 BUL=VW:I*B`"b_7tf(wW1vd*:V]SeQ-,XycJB`pfJRzTn_mk4 px_RɘV,Iyw9cj!f >jU?+@O Oj9X6VcS=}n@V4ܗy39X](.L-хMq;Չ܃w'OhX-e^W/W jJ#VuF<>l@ |G#2+KԪKzQL\KYیh`S4J54HzBCjS Ub*t5@67 +cqgV&Tߨ#դCYRp+M$ա#_0w]'-Qy P ؿn/NWU;V [['Gi0$VwUWzj? PU`:VOWNS,7uJ8 >'j9 ?XUЧSIځ#ڈU&*?_LViM @zʩUU*RXZjTͳէc]:UТUH*PX]%UV?+rTl[q+VKJ{J[@Vm饙;˵Vx[J|҄@07l$ᅟdn.A1g5 ZM ݨ@V ]ԵlrUNOu&~~|~֞J֬LP魥t.U7Tms`*JUK*GOa5)5)$WK@+UWfIXmnѰH(o%ZűJX@%%WqjTSQEX??b ~0_pN@Nl|qG6ÃmUrgՖgW6mh6ⲫͿrٹ\ &/z##O~:D,29mŖ л4dcp&87`lWp'Sءwe< 0M<.A\ tDdd"2(^ ꡃx~e1RMR|QP*>Da=Ɋ*aj}?vJTJê6LS x,crqUTuP5Qj[)UEVc0Y[pu{rAy|?:(rq2X" Uja5{F׮z8z)uB18Z(( +7?]{zqKj^N5;w7;a(mujhfBQTZz\-coR@V~=uu/\-AXm**\d"ue]mjU[IvƓ'(FVyxUVc]Vֱʭ -Y^YPŢ|f" 4߇~/4I4=N%\ WY8:4)B_SMM؜g8,<:;{j7Bճ^eZfDFa7>`Դ|OųQr1]6O&ŕ00i1ij2Vom3cpÙGh)R{:h(A'$3PaF@RF4W91u0UW퍩%kn6]'pSoWeUVkVAKUQ: ;lXL+iQ\>oiS[~様"`>}hRl3it;R{)=Z u9 <-\q4`u~bnƙy# -o2V6ޒY g1p5=b3C@];ޡ 5 V[%t lQVkJV</Yj*lUO=?ZldUYiV$t 㑩Z'&(&`?  #$η XS[/ֽ{X%`1;<<1KTYjܬ?Hk魦ZYUDKoZETRQR֪P^w )5p((ke\IH6U,SxH2UEUXy4J!Rri8_!7M*'Ig$N1JD備Tbcjj6&l/Xŀ)Z=ԑ Nφe 6MqXu0GD=I܊F"/X5+շUSahvIb9߯yTtV1rM~sjikP`)z=sjW-kYB5g$v:2 ER +* *i5:p?[lYq0}@x*ƲXչ'3&Le$!WM)U&aU''GYr@5#Yӳ4l&մ4jqNRVjEW WabW*UP_}@pWW%Xm 1M9U CdhXzQ]T9Qg2)@)l(qҡJ)T)Fֳ6zP*# y31j!7 38/ܺŭ[C#p^h"Ujh]J#x JVY^ZX$)8av$a*1őu+R޻fU^\.GaAV1zEVYPUGN~)XźQZ\(Y_ةNJ@g "*V+XZu( g 8XD>萊膒]je7*6*=РO(B4]y*X6I^aŃUc*તjT`M̭+YjU*VV *xUZ q[)T !U 6EGQ ;DV94P4WRV=Of_ԢM9%!Pk9%iԮ{`U[ksKlX=X}P0*;'qXC(*)*뻁ZZkU`aXE0/?3$`,xAz 0h|qc~o)VZX%aW#Q멈 K"Re. TE~٭pשL Q= -G8宾Ñ- *Ĥ( mq++׭DOl>8aV8ͩmoIRRObL qˏF\4: J)Y`l WhNl٬VXi\HIC5KWX3pʞ__w9]^S ynjyJ]V`wwlU+0̯7+Ac߿EhR|v;^E?.y؁diLFD [ͧS2Ueժ7W+F6?y;wGyf_qϿ^x> O=hF"+ކ_?߽w/ xun8*5VDum LѶ[7ZUUԝgJ%$b$>--m1:0IdiW}N QVWVQ 3 AO k0kЦ+|l+z7+U5V @w.V}@Uq/wL`(t\ĪQ )Rl s*.I\riVy鳶VU}!=SqjN8''njj 6n2é2-cUwm3#uҜ9 X5~DSTO*t  pHZE@τRwv : ; ]zf;<{˕g71CHlG}h dIՌL1 |Ru5 AjvT%jegec%& @Vc&e8ŎUٱN IDATK3MX!Y;4{ \\ jюh9*U eZIL'܂A/q_uf$?djup%j*X]&{n֌UWuefjJpVA*?H 5\NZhzu*y0z t=F[a5Xʑ).ΫZVH`mڤD*BJ,$N&֌lQ*Ưaac<GթR5{bU^}["OMbZ)jΐD̋+&q+8VU6_v瑌xzBtqdw4/l8U~KWd0JYu!] 1UծLIeUZlG.Uzc̨UU4Efj6bIKiJsUZ>9VehLO77uo qצzر>4yfd$m>9HM;~i1bhԨ%D*#tF#[TMq&*Zʇ[~Rg3#Y KrK+*TZի=ZWRxxfhVP-Vgjs]H4Й3<0Fݪt*ͪoX1T{n;TXp|FV=2JsF XaObUd'IV!U6}&leY%kY' *S7IȄRW>XjrTX}HO O*X%* 9 Ħ5j,JURljPS\FqoPIҲwݣH\~ؼcvQR()lf`k ARlU>o"~ZH|Чj~LUWcWCԭ܀L`5p[V @v*AjuU$Xp5J;9`Zš`n*J)2Tݸ ؖHsUtE?}A)ٗ헥&Y P#1Ms6f 5Fv'+vDl`UVVfu2{=JSt^+Ū* ixU =_\U:CӫSZS:h5zF2ak?l!+գVuUVhʙ||VO FdTET #--U\ZĒu#YQNPx*NJ_5VSӰ:L&F7L. ycͷUM/{w9H0jĵۿHįyy'}H|ߊӧʂNXcdjdȈP=2*. f4jv=Q4QvЩpSI]AZ![|:AX]vV=gǼRjnZ x޸ƫέ^qûj(MkXt@ʌMr^?J%jT٭~SS*pJeUϭ6+nQQJi"*W )jqЪҜLQX-f"l|PS [Y)X]]&73Cwۢqno?-?|s~|:'x䔾d}}blbxNedNO #~,Ɋ:0be/dUE֪NjYW!K+XjTUss}BUq-ú8 R>Z=ycW G)Ei4j 7zDFE L)J@" ̪U͐l}ThDvAVq[\rO ZV*ZXՄ3`-`id~ZJU՚EWjZBQ*?jlZ-yTd}`>}djjc蟐ck=k:lαlJ8N$FUzý=@QSt]VkC;ѺVou:ph īe-ρd65sCsHUz6_.ªp}t]kMT^1g?Q*wKTidVX]T.TZhUA[ bĤ蚀.2rUdy2kC|?%Ԋ%Օ*^) f ru6tZ1UǼW nXP>c8bvϰZ'-bu A0Ej`ƱJGS>aJp8 YN*Ŭ !,:@N6V&-]j*`jsysU=^Thz"Rozyv7(X©*?گ~" j*aՋj5FVv| +=$~FM6YjP; PN"rF\2+Ϛ ]A%r`Ī5.2ajF@2%q&F*d|Y+'Vx6PҊ.͎d%_VUzWxgt#,FzxmX6 ^[V!ev?lU)UXrMM]%WņjUURmT,QYXj#U$k#6Õ#OpȅLR%s>9VGJz=j O˥Z UZlί^Yj Vk+PpMHu8@:n S_X/X*ūHMy@FU,Dekks0*Xyb:XOS*P8WgXEXsaL:r$E}ݧO3Fb1"1??'L!Y+S"ZFW̾AicP5V"L-NBS EX $ 3W 2˝ZjJy܀U`cXL瀂UF:iNa S'y݆^LR;prI#+VcƀjKǷ~,~!rcu)V~C^@';㪯C:QjXo؎x`ub5`_;S9mTVw U;bULHgF]ծQDˮXeSBj!ݫeh.w.ތ.MWZ-Qe}hΙSZZ8Utv95pU`SGGj Z7T=׏`p.rYvSLGҖev*U_A֥3Xb Y7WԒs?Z =EL-ªg*1&zԼ׍qo lr6kejL2u_$ <{Ы@MޏX( >>JޯV0O&Kg2ŰҊsOK` aW/8j"6×LwL/s)f<7ʰLI=Fxu2ӑ̠T՛}m$0h^*y3VL+)RS87aqX] J@ؒJTHwr:fUm8ƛ >4ƂVfwVUvS*6_Ț6OmMJ*Q`TY&U .AUT.W߉ՍSuc~ D&@ψսQ_qvN7]ǃ[8?ՅXݡL\{ _6MQJh~j/*n )nIXJ]6&U+ͩY|+ͿbhmFoz@Z͹K{n#UA.Z#Ŭʶ=esbT;:PJTZS1>BASہUjlRe 24sP1-U-DS:S֟CՕR`e=(G?(# VD8Ve\b:kd|zzx{fd s̡!*q5w$: } $5EWԥUyyNWŋ嵭Sva٭u1LRk4&*f|jkSh **צSܤAV$ kXWWx,JUkV{X+d&hT]8Ux *S 7I\-5q#Oe1,U{ϟU{7l'lwA\;T XeoiX5tvB+ݞp[q3ج ?Y!V_\-ի_I.^#"vNi"q6Ah=sJs(Z)9 }sۍBB]w=794X=Jn:<>w>cU> #\8ZgG1Τ#}L<$0c%au)oԩrE2l@Z\S:  kZ)V'MtE~AT/\ը$V$=.\*rե[U* TV?Oz4Zpl ʽbV]bs,efHcuJOSU-URiTCjCCUX 5*o,iYUMZݒR] \0hbJrZ+b:6ԋ`k*254&o*WԵbwjM.&G +Uy/4]/U=娮H4[X .LcDg/TuPVbzbjӽ#1,vxgh{V X_gҔ|b}J,+]z1;-Ҷcu`XW*tqoKV3xwUj-$`5B'}bhôXU+U'$;`PCuU+X}= kVRڒjEVbUK*Ukմlhjkɚ~CQ5,3qռ|ÏGP$\(!Oӿ۞{'U?_MD'dH7=XLK5Zwu ]V&*_Lw1i+VrXVCTM[5*Խ7z TE>ZuUVY{0 s4 ԙ̺u'^]Β%T{]2@b5k9'[U*|]8'?uNjlh}Eϧ~ UN[j _흐<VAZQcFt#g4X.9V; *NijXVbj麊[6 `#]dGtʅXu[jX8&.3Y%,P]T._øN\*`5l[*^%oL?bNe)Ye?NSEǔjUS Ŷ(uE8v`M\*ʪIYlM֙XЌS8pe D&7ڪU mmmF ss(`7*ĪRUUZUhЊPsU$P{YzhjRjs7*ۮtu+щU\j!V=`o  U<zMxZ5*I0GXT9|t}5%ZպD"sV- ejfUzHZ==6fRgwN&qQ$.1Qj-aHz_8v(ڧX%} gA(ҿD C[E$u}gCgD6šVc}'NmVG`:x@ ҫΆU|#oFI՚tsg{F*nz( {Ϗx\1XZu9Oai<`%c]%PSa ;/Xߎl*U3er\ϮSaU Kj"qלRFY&9VzX*ZӞ;J*X9*Z!Z + ժ.V%ЕOrPe 9a V!U=j7s}:5+V7lu$FA]T:8dBʣZ}}z@N'>K@q]V$Q+cmȕ#ylr[>5߇'MQ^2=|Dc΍zҏP{̖3UN^OqXȅX 6_N'j1_sJbH.$4bޑb5cXVCDj8vd1d4'ۣ 9 !ݵ;E2G%F&*<jե%XI`r-K:X\JR1R. Kմj s.T ҟ\ˋU+W\lU*Fen6u:TcjFJkz\[ԢP9XbjRӪc9YL|&䪨gZjɷby@SXRʳJRU=*L} IDAT$cYK Zz5ZEmۧ["{XIM=/ QAIuuS,8G\Iy+]ZomSWeqґY8 *m4 1SR ˔Kg8)?:北 9)!u@/`T~8AeAH#W8!Z1(슨"RtXe}o0q!ek80B0 >FAd's+r" #4Ғ&IwM:vQk%:;wFTuժ YWjՅE\lUUu֯2+˴?P* ˲Av(U41SVI 3*|źj)MrB]& Qj_k_ڲZXZu\*xAWNTaj*ߐ*XU+bXjCZ86^h&%MlWfjuwNU#Z!?=3됗j=̮GMU/T3U-TWUw6nj5=~RU-0X= O2Xtb)g2oObg U9وcL[z^xiROHQ9}Q?s!a!(]WxtJYm) LIWDYb9o6!F\Tl 2q"UK1h#jz؞UT}_鿤k[8q 6Ec*$PGT?@pm;ɢf"kb[Qq8`uw]vM{ߓcSd[~x2sg_j2`ephCxucV *CMqXu\ŲZZU6Y}'=9&` jgT)૾Qխ;cur*eUV\u2fs)*T#Vz`u̯V9?S/ I"3u[u<:"~d9RG{MtɐUqw{p? 7kyuw=~F:^{ZIu3>?nS$/GQSVq,-Sc2NiQI@$Cc\ oMͶ([Z@ZU# գTP {J*qDUv(zǃ5 2CΫ@UiJD*aJqֵELUX+4^G>dxqu2jU֬ր^k)3Pi_Lp6 5[6T]͙w(Ve<թ8 @+U pz- WAʨU_}*h@=N[ P2V?yp s'U4L}6j W[T܇abrRafOv;rP{wRƪ9%-1ЄQE,6JyI?z׏ffQHLT*8 Y3 zNC'nRw{HՏ<^z;HQRIKU7fPT,/8,! '7 V_J_Xu\\%;h$ Au4V')V_-" X/;8jnniɽWf0@|@笸6q*rƍV *PWg0t?ێ !Yvg y+rT `r@ԆWpKo&wO!N}T,qH_P*AW2q~!_Q%h^v)?'9,Sm?NU1~V?ZV|[qUj WUuTu8"㱊E+nW$e:j%pV؜A*5LM   gLUƪVP`Z?\uzuwUd*o|oYQ8j<eaϜ"mj-P$NQ}2]A5KyLLFC1*eJNJDkYg=-Xa*ᔊTdw!+*u+jCNy h)X=l|}Z/86_D`xGkRY&< ǎjں_OJDhPjZ.ihlG ꓚn] E/UzVu:,,*VIIqn *'(V/qžkNNErt fV֚bB bVVrnlU* bVh"WAW?8 pzmuw_@>V!:a,@} eUZJT)pyN}.~Q ꢕb _qA~*RRU7j1%J!.WEޞ眊+4 FbU=Ե: IMl*V * ,[wl5|L>`3reHR_}D::0a*}=PF{2ԫe O($IYVh2H{5UHOXa1{ |P AU7?V k4 nSglZJ0ǁیliH@}DhAp0 јx,61B4 d6ALլ{3?d~E3u>ԽWt[Џz9oZ!!ujVb,PK88@U& rTEl`tr^5`_𑪯+8y1ݝ\XR0я}UB5<(ZmwTm&]{jT* zpwVU!GթX՟nM|6S'c5ժ[@j VUX(Wk,ju}!V=Uij# vm,r jc]FZ]> Uj>o[no/عڟ6sT&GL^Ezچ+VNDXICdMVKb/dB$CաS&*OpR?jUw@ђW֧)=S٥yץX  T5XG UЪw䔀>%^;=\Xꏧ*jkGPӠT?H;T 2E!17iruVM/Vuw K"V8٪̈%K`5b5`*Rj.bu`u;z2/R2].{U{W TgXE4@+̲zz\ur6t$}K U*ZA`*;# R[.uLIʂw䧲?= ]z7$UxA(kz3͠M鋒~ -uuj)ь:6|#IXvr ZSb7IMNJKU J J9;rPaO T^M'QqΩ*GRӧSe 6?=5 xEZgbPxǨXgUq'aX*5KZE`ѱxxjS|d"$lgS%;'I cm0SX,DWP4ƹT5: @TV(X b*zݍGG>QbQ K߄[Sz{ VnJ,*~ue+=pUQ:-q\i|>c=Р?3{ V*\}W[a.jXVQB. w'QvDT zh.VtȜO3aIeNS`b; ͺjf~iJ>s*Z7%U!N8*(Z%0"SKZ"wh,NiT!0rj)KīUGӋ]hTSOTŨݧ,*r3lFL jJbk|ؽj!MC V}\rXw诂VBs>_[X>FzU;VZ;-hQx Ch5AVT\}׾ms)9BB x<6\5r zza4Upfn0b%jݨUUʶn^sHMRUlSf1ǖA} UPoTTn>aup*˹)):Q(H΅F'mMj/NnSQ_~i`.kEjT1ʇV7*LUm1*,pVoU5X&QP>|AY.ZնU l`o ՞8!VZs}*=W)8@VE0<J'q,.ȁPhuLZ\s,Vu1UɯYlXͮVQFNYU< ZUq䪯W1V{ew6"V ^BHX1sbY2VJYT=j Z_H(3$W VdC}UpP+!+t(*O:l~%PeهP *N:q)]4 (O5Qr}Wܧ>u?5ڕQĚvq5٬ 8}~CO:ܕwR)iB`QHN&'-UTԱxmo)0^t:cJUbjMOcƺ?R8ә:4%[/U~PMŨ#XT&XW5P4[VKK~EQ&VK3:&u VD`*6ɩ_:eU^WȮ*c'2д2{*[لuTs'U0X5_ 6^b=G>Wo!:ǃS-LQ!qdW:A{HHXHcnƇ)"Ox}Vi~0ճ$2i?̨`-M`+45+ {jݟWrK48% IDAT[Uf`*R5?+B`'t,d'2y;ж,؋|@{lMJ6Tc- ov]MHN 2Yx7^yԍMfS&̢L(L! Jf0s{33eHJ=FŗjѶsYR<"o4զu[ҿ~UaiQ7'Z:&hXT|,TpxjӘB |%T$z? ? QXͫAlNJ81UcGCy*a3Q*5@VE `VjBF0WX&r2rVoi\%jAmV#Wq [m4h W-/5Z- Ub|L ;z/QM[ =wj5OV<3Eϰ`ŇC)%}jYƀ\OUUgJ75WkηVV;N:Ed7A1ludIU_ׯAz%bGQhM9߿&1UN3cܸSCHN!ΛxB>AKITlߔ-JP*/-:u *HT2VQ5|`j?aW/Y|V rTOj%@;lV11U:cb5IRCXJ+XeabsbSM-R]}ՙ͙6վX0sed*z+m{ 6[nR @*tt\5ԫ)JkǯrM|wn9\=q@IU}ڴV{MjҴ| N cmFO*m+Aa1PUoWJm)%0婽-Uy(z)qJeZ1ղ֎r*jͺjwRFZ j]SugVᏔ J~FIŢDCi I`j!Uǃԕ7b@Լ{jY)/z2VWVZ)A5bMtb5!B+a Te2T?*ĚZ[;IX.XZ|n 2u]RR `y\UWQQj(9YTxKݰUT3|ѡUc sWjbL: R\ZezjkŊU>TL΃Z-U >lu|bxpZjTXT~WO"q4He-!:*UxVNF) }u_Ơ<n8u2Rx%A˚6L iVMih P[ TE5w* C p>-{Q[Փ20JЕ!W E\V0>u+V&Xjpc=].o\ HtUU8lYK $Q_\|h*JU|k3F\jj*[! ` K0@ Y `^ F;f`aFgc{VtTmVFp?|xVb^N6By V:fS暪$뤏6*YY6v69QT-Jrgy)H * SETiuܹZM}>*_NUQk" ]Z-(_ ÓjJ-Y(Uv۪PZf_ٳSݦ;*Nyj5rX-,- U m| $G+[ |sy0nijٸVñzg:zX3TMF>Hr##9PUDUdqL40zS7Vŗh^j\":DF+E]8^\*E ʱՉ U*q}UX*~鴾KA-ZUTenClp7-iz[:ُ.gTy&ʿ~R,TU.(AY)c&R{ovȗ*JfbR.uN^ 2|p{)|^J!r{jUр4CC4F|:Ezs)tZ}JRGU yR#a+2BTNe z14SXa5 ɱX=VՓVE^aٺ`M &0 &V15]%}S z7Sx$>FلiZXXZ]]*e*PXIڀB0HJqeseڠ#Vld *CUUVhZ۹ӱwHi PϠUSI{TBo*bz6+M>t` .ruLgxn T˯^2pᄅV~>귳~kޚx.y\gxz9a~' ۸f.;x](L=̙33?ljbkV{ 7mCZ{] Vd*9bj9x 74l[UrVG Ī%ꤸ) 2n6]i-hכ.@v([Mkcz"!{UfXp}ɟ/v|ܼJq~LKKϧ 83_X+/E6V+<`}š\% TErꞤ*jUi/q5u. uhuV@:89J>iRԭڼq]G wY o(uj$W_B@FynyWѓ׋{c]1'_Z [\Sq{tc4M6}8=@+ 2]\X).|2 u5FxF PS:}NӲ D_TZ㥩Cyg6ֻp)nZ| Ϻ`j&U%UA`0d36 ѓ z8 Dտ6%щc+}ɯsNԟc\ 7da Ikm\%mU.YUf0={j^v|F{9Vw WW%O_Aj WUQSJ߯qa)+j,:TC&P"źժZEj~|*c՝qVա j+zq(S0ڛ0:P"t$V9#p{#VU8A TX:1wUϱUfRUF5GZ5&zY:J^ Uzz1UҜk[ xXݼ g8|N:>  U`nTMS y3,Eަ'xǞ*뚿5==xՋ(濒6柘RU4@*.]JBL fYR/oX=ET."M2JJS!3dm}귭%UHVne^d81(QЄOw$Y;PT7v3rD#y`ut*Rr-Y'l%˽Jrd P"C+cuw>Sj*5 Vbdk&JVC5[Ҫbf `Vu.F oy x(oiamkeJrUb;g)R0<^o}Øv8Uu"Ⱥ\{fhj+h%Zᱠ*U8AbUoM/ T{:AZO ^} LuK>Qhz*ݴ+U<,v"ȸY)pխumڪ XLvZU@id*iV#]h%FשvFi^U1J F^U7ETJ巉`5ªV"V#Xx05w*AVX+Uvie5VV눫V>&b5p-aY * ՟ؕ{uC Rz%SqU>E OPEq6W,VJjamj\'Nju}ܪ6*:Y' a\B'SCD9%'IA0i6oۈ?lGϟ `/:ZEZud|DZzfα_߄w XRgM5>痙 hc2whNgJ{ªSHQ.*#otM#FCʟL%n@5J&c1paCl1Ԏ.F7zVᇴYyOWTBdU]*I*>\8/u-ݻT] V\ԃڀ&Vizɾ}‘Q]/cu8Q} VVEBRA(Z XjU1_ޠU.[4*.kYշƪ[78O4~ZmkVfʫ!QVV(Vn!V$X=wvX,WJTzAr]Tru2'CT/Ĥf\;l"SILq$Zޕ#qVܙ3;GX=R\o t fFׁV?ql?˙҆w7`՞f̔lWϟ.;R\Phޯ/@iGs8.NȈ{ܯX=Omt5Uqn[XMX@V@/o=~aT[{\XmaZb^\EvpTWND2V91PbuhgrC(ቪ{S7&TExXR8^Q.XՋ{*lQZ,[5wRV,Vk$N |VLڵ-V$`BSUbM \ŏǤZ|*+ׅjEjD+U w~m^gG-f #خѠ8n١ TM@d1E;8վ.4.bl`c@|32ed#vͶwyGΧHOm=}P|V8s)q]>P{X Ug֖sj3Rb! ܈!  >D?G7Sc~~o3V􏀞ùBy j_*0#OEoja93-4:_2p"<q:ɃϞ[ w^橜5Bf Q:J V܌T颟*>˶Uhڄª2Tjbժ'V @zL6ɎXU@4V)k%V}R D8+ϴݺOh9q?LLTI7s%o8$z\vZTj7zVr+n5xߗFߊ5bX=OjT O p_ (=n>Fbʛ8o5E J\! RZWZM-nV7.^^cNrU*W7o+R4vЊjvqgwU~sl1䟿,[ݭ: @܂ wq3%:G{΋ ddDJ~NxQ<*ԔU!NҪj'@08mᴌחʊ3& hԻS6VJ jUx|&@3BI5H;D'6RW+R.5#FVN kuxJͻU Ԫ5ެ1ZqJpPm*vĪV͒bj=RRdnLVqEO-VWRsT^[*Xu:]ݽ XݒԿ,V-TU8uܴxG,VOyZlCVDV8ݖMA*;7I)E'M45j5?%dc7\ƛ~qVݞ*Uy:'vLJ@U8:CZ5,% Ti UԪ̓`7)ם\u-!=\d*~< * d:Q塊]@(V*֊ڭV7~9 T!W$c6UAأ2X1l9FXNXC[McZX֋z7 U2sAR54fOV,X`IVI2You1J[We՘R;U]I.ꃻb7 Sw5GʵE:` T~x2N>=qo'-U2B%~G1sVѥ(/A~ǜൃAʅy"ƤW+@-pt:$rSz'U*+/@hUTƪ:#!bL$UHc0XUf/? *8bj?&Rzj =KXLUXMײ^VMt<dMm`Vg"W/J/q5YXzRI֨Op*buAVqQ6/`Ս*UVץ@TcNUˬY]٪UoW鶍][Фl]]Eէ*U>MPuƞt[&0Q{-o?$aUFR:5eZA] 2!+!ٌR@5(&S[-VE;jof'F {eqX-8̜awԗ Xe[5VIF ҡsb*)U3zH1*81E[f?C[h*:H-Y vA56w XRX-R1T3V*zQgUV VcԪ>U)jj2E7STQ,VCd=8vX-5PDnTS)>+*8מfz)W X-Um^Ϟc z.fxYJbT^)8Cev"fg/-,`fXEnmmomek M 2jUzsUcQ2-x3.zMU"(qnz `  @ЖK}7a&RǑ S{ _ӎCNTj9չ/>7^}DaՆN]ʩi*’rr_{(=2Ů6ԿX|OC.@>TiUCV[Ϟ4i^e\UcԪTR4 (SȵKp%U('}q)[| sc#V0I4죟Q:bbZ[+lGx&WGvRa_:"c5MeW^3b5U̸FŠZ=rٮZv|sNijK8:c)b*F2L&3 8c2IMT=^H'F H@B &L3@h"i&7؋¼kvm?wk_uae#T<Lթ `uX.zVAխj2I&@h}Vr+|+/IUj*(_@U0Q 3"l apXދ{ Scϯ4U+ϻ1G3))kOkˋQr1[y=} L=պR?f~,twE+{P.V`*堺c[ ~װQ,`pA›v+iVu#juR3zltrqr-Md,2__O*>Mȱbj{S%V1tQ{::X2W;aLe2 5^X:9T0Culh HGz cyXmvd590Vx,GQX3@LӸZ^mmVYR7Yo+; @UZqTͿ-//_^tiH]QTVXJuG1VX[(:&*6{AUzV7@NUO#Nqri!#NSYUG:'y}A'NS;rh'~-+#0Q= 0XkMːU@* LLT2arxTDLy@GRaջd2~@VFqxT*15N`!?ՈXc1Xݵ\ΐ^U\:c0RUD3ϡVV)@SU~X-WFٹ>*`839sp,SA⃂PI2VL<'!W?V+JcUjA\!*jZE;"-,]JхAjUb]kVAUV#V_B \ݔsVk糖A^B`Phd*$U4X!xG~%%*Pt$,JY^冉gߨJASv'f*TQbZ)83"A7zIi*VLĔG^) =-zd uOeqпd͏T&X>[r{")/в),RT'm8l-8 K DsEυ2JfOFspXn7adw)uw{ L^(a1~0t> gw.oOk)V:>ebHu7l yQ Xm@j5PYwUo9rUՓb6WkW֪XV]eF[ KX?=uK@`OV1NZ`UF]wHF9 H`%Nxp}j=@UTW\F}Jz+PiXbtnӦ‘4 7֐6W]CzN+'S\=qŭM/H|-Χ2۫`hEgV͢o,X 8Q2tY'fa n5{p?Mf+f#V6Yd4 T~/X/fp9Tf)[VSl+ @S5G{9pvf܇H68[DZ2Y+} ,|_O,g k !7n\B'@Lb\G\V35*VS[Mu*cs7jf*rtHbc@>lmQjurCxj:bu٩V'Ua~.VeXG\mՓ  \* V]Ǐ~P\gZ)]'c_B*d+cTˣU;lr۴C,Ҫ;kk#թ(l!Ӑl+:A[1ULVI&)?Z}}.t$V夸T4)O9ѧ pAHLix* G;uN._ r[r &Dbt yxXzduځq8TalRfOV 7.#,}$}ԪYX2B6VWgA3V͙m0Rrc^a5i{]<"1j"w?@`,ჷ@۹.D)Pۇ`N&xXZصn2Ģf, xI9J)h!+aUYx,@&Y$V, *BQ-p J\1[w kO7Uh9jROJ:J,Z6èEUumlLCXP"[WZCǪUNWa!k*pEzͅzX1AV|@r_YESVTU?F.V%FQ!,$*"5Wpy\ c!(P5T ~G=>xX_SpL2dOk[|jT%-^ %nU*۾lĪ#M$Vag <%Xf$OYUbk|g=@}% zCe':j-ZCfZa5j/%<^I7c& {U(Sj'&FYwF%WLy?q*)kIU1S5# UANT3b^dR•%lR4\KTBފ\vtw_d *_::)+#`vj*:@`X:w2g5[b P`UT"jmFv+DZMn j11[X-OXAKHCYkkkBѪW `RbMURsHtU*JUƩ(DҩZ+'JU TS= J0*VRX /h&@;ᵡn:R{mrc-Q!I ن.<2wXF-QLW9dbEh՜gN{+-Ro, EMo֋a_ϓ'`웴O|$ixߟ_YV[*$}ͷu.qVHEfj6NP֜7[z=g;pXUg-]'j̩֕>c WT9؄ri{">P"QZ7C0=jTuzZ{ \q t4.*R٠xUUj5=Z ,ʚ]F^%f<V-"bXA:46B`͠H;RjOEX]T(VCdTQUQ V} | uL48 C*MVQʤ**'D\]# 0'ƪLܪs,qVDQiTGR|>`]f Inak/&蹮(|k}Vp3|a%HZO}sv6;;owZsPg~}~l{O{Wv6/n|j@p Ͽ\ ;jȈXzv~y[+[U'\m=FsEhN3WIgVP%ד[^kW-jg S+j~5ށJjjLT_ 0` P7~ ;?}0/_v4gK?w^vQ=^gw[l<>Nt ֪2XQFKX' LY TZLTf VVG&`jU f84W#CUmLqz JK)Xլ"f+kPVT/XU*Og\iݝ΁Z=FZ%Z}ZElo3%ViռL ``OX腖2 eubHL5 T~kϤzI2RZӥ$YN?akl*KUW*eb.AΕ*UG<5u = _xQDTǩ˝M|uu᫺ܗp . $u]ËՁ8Cƪ"$ q'A&t"vɚKGqڧ`[i?uJjMUO) 34X_Ƴ{GYϿ̀H{꥗R Z; UXš / U!US+Us꟧RܖaS^7XT9iІUn)-`L&%0h5ɈF*UՑx `T޺zXƇc]?րbRWXƴh=FV1G$V 2fjjj/13W8wgkO,FÇ$UBVe"*|(le@E`il;R奮I~zmjiUUWjJ2a~6d;ըJym>yz)7<3.@JUu~RwUR (V /AjCJdeSb*h?)[wչrkiNoD)OB:'LSK C`[DllZ+@M>!ROXwR4խ֔nLl4[w뎚.h%Uz\UqspUMX 6`BZ+U_B|*UIZohL$/Ϗ[td1)1U?4>U&vr/f풝![)Y6Y;xOUOUEj]l4T 9$UVƜ%)JTleꊭ3sQm2_qAJCʰ%r &,϶* *Tg`]wj!/\?9/E̬n1?[ճҷX0fN\rW s'FҏX"Js_5`,%y[V7>IMS埌XmJeURe*XE #X T[LUAW7Xݢ&(V\j47|esʧ<(gHzT3r }J%VjNm +sYIT\UBJ]J*Ҟd1Lt&@?JbJѩV4UeUW]e?܈BέW^ԽqA|BUBu iC`t!FHV5*UlY>&hu'\xTojTͯgsP]"񟯬nY U\"`*]lQ3WmkpsޖYGRBbC`"Bsʗ4_Y%"-VaZJXG@u})yiKg2hYdҼXn ;N(:Ha j7UHyQT;SPr%bXlm?1uUĐWԒOFVͫKVJlD;۵Rs_u%#^ТTYtKಪW.u S^X$UHyhS>cb%m TsF3xjJ+W4Tvv`ѱjQmDFq E2H4GXsDU^5(ߩ2m5i1.]֟3ll*ܔ/kƐ#*Y(GbuµFpU+O\ :9v!Vkе|@#{Vв WߜWjUU+5ʹ߻Ћ)Ti6%RO=jZ Mc`jCANk[Wm%duH9SU8OJ]b*Eigo~}-RJ^g1kd<luU,O KU!VpOZyRբ >GE#TpթbQRfJVIJ][̳, Uؖ?Y;xQZ `FQ&8?'<%nRmR]hЏq 7|[GƮ򤩿)Xj~ikT<R4X&RXŒ)A:Ez"l uۙ l m~Kf"w3vT/SRP4ϩ֝4â*2JTL.XUqخY' '@cTA{z9*XMcڬ;69ڶI.]뉨[]ue;&bjWjF|R?WJBv#;jr>`2x`j%(XJV UZMou;l=*qQmmIyDHVtRTeMq o%j4Zjƨz" 0@|sr/T<:zr'λg^;?Ӯ7b\lK9fѓ})UPi0f9a՞{&S!Vkl[X8((ʟzUW{cgjvVyJPP|s3骴F*IOI_a5Ymcv (][^+%OkV(Ӫy_%"NnPhr4*^ 3kVU|Ndzsiuq\U%MI|@@@WՒI6X@l@[j029$$bKQ?&M"x`DnH:y萭@sߣd;}d6?s{8^|NCCաjjW=껪QKv-WoHf$"`uWe luIZu]j#V(<  V^bpErѣSU1lUz[Ǖ?DmnBITyZP\`*;u~qtmh:?윕~8ʧ.iO 7nj*ߞ?VoA9W}oܯjzf?7xʼnCO[LŘJk/RbZeX 4`-^,\TYVHJW \*w~;AiCZOx޶ꗛx OcO?Un~n|η"W)<XZEZ*sWLծnW]Vu`c(N9-l`]d\,X3+xD>X-yX]Uh)BejyNUj}Kc&WW0ApVX- jGW(Ph;Wԓ9P[{eRw15]>sdҟLb [U\\W:s'S#jgG |7oJB)J*eő '1U߄oTBW꟧VmºֿP5 @h ՂUxkZwĪUvWU-*X!X(WyGĥdpUZT:jiUr[YR8{lf:{i>&4%FnqSjUʥ!F5{žSYͺPEU:˘~B[X݄*@5)隫ZH:HM]밪ju$o`VUaZ׺mXT^@)c5ܷXjE]?X]YUT]Jp)"V2HlAǪ&UX"QuVk*UUU[㛭Jl0c5CY8a 2| T!ZApBaO>EFnṟSES& X Nٟѫ3{QxFE*ЈT-I ڼo?iS=xRZu>#j0ʼnÔ^z ek}Wk2ՖГZ5XGkPV&IV.ϊVUby^5M5_} M-V=m[,th9$T9ڌU\~^6 U`pk8ᱝʩUnZUDZշ=Zb%vC;Nn,'{"kbUdVW*Vڪ*ԪV*V+e/[RN_cub5U[Yt\m@V=hzj_Ǡ_a?5j_O~9 Tcb+(Ӯߏ5xk(UsD)2JC˯KFȭtQL4+9 9 TsO\dE$j~_54e'0m^4W-XZ:dZWe2 Wi@ZH1 bxZTV[R-'Wg%jr5MgUz0뉹thjج"W PxhH6NaT[mj.ťV0ăՠ۬V Us*mQ ֌\Y0Ђ]k%`_[`~w*dzjEULU vK+ R@9(bqYj ~!bQt)&&իWE#;&s pm 9W=SԪjim?ό旄jDkZIqy2qQǮ8ЩU? x5G]:Vcɓ_|\T9VQ WS@PU}aX(`j?業5\Ym Q֑ b{tY,1Ea./&(!7gUe`E7]B7B"^syNlb4޲&8 KRJP dyDkRlRUX[u8)NMd߯/!P,6|Y6\5Y?tU'Ū 0erܢ:1 <UL*?Dniտ*X۷jcVlJ:#8NhMkn%*؍lOZ}^\R [ݐ$XRIO]ge] *D4T+ˬ 2Cv> VjD!'{|TTȬ5۵-MH Rq_tjP inSOqg'V4JS2Fjp^X>W[<V{zt혧Vm+Ou1j-8 bHLX%✌QAғ՜l՝b@e_ŽЌ)M.Tj@\50%<k8h\]HyAj|caӥnC՞&SP~aտ`5U՟A~%*f*UVdZ)IV Zu?)V7ޜV,WiTFщfcMݿqPU=y'&@hZh{k@?uNͬy7F ܲ]Z,BfZ^=ږZgNg.% ]5F;vgp}\ {V |f$WEP%zTm W:HF VlևXL@'K`Xʲfr6Uͥ4Q5׶<moZGpBrUT]ҕjV Ujɿ߄4b۶-R V'/ M%UUh-SuaX*58dEj]$1lNk$Vcø(! |bdKUV30Sn@/űI3U 5xhS0UT`VnJ~<.6bk*8!Xms"Ty9sn*Vy6jDOK21-yVRaJujUcP)@eXZu*6hӀ&kEgDw&HD)!U&\ݻ:eYvE҄IUōh&߹)Q].z?:L|(ѿ/xKIj5=~\I벵P힟,o1LjlZmf *vqql>d6Ptꨖd:y/Ug;Hg׸jhԪV 譢 T=?+VVU)*Tf bxxtBV[ %njv:]BMTmZθԟo1ʐ}Oն/:hS&aJ2*ZĽ82$Rc#3Q'7vsP9{3^7O-BuqeX-~v`rO!,ppQL`?C+TbCK-VDgNr\VjPu%j/I"T%ʱ+TG]^)CIaX $Ɵ%脥TF'JX?#U:Ҋg Cj9k&08_^)=E*rΝr5UKQ٫6Jd͊580=$֨ʐ&&@m"!b#W)0VQ#WQ63]4=}\Jm8TE@j6Ы'$VwrO,+.%P70z2uŨvoժ<PbIzRYtD7\T*UcR1&>?HʀY.[rKzI].3/#~yy|0j\j@ XEI´X% YCp;ϯhRp\Z6 ijpjй6zՉiZsrtV6m-*9"V V1َd?ART^J%01Bm JcZu*GpVjwUSC4T=oZ(Vi&ĥ<2@ͷT1zmj7wH:DZ$ZN|Ak$`ж~r+X@c3,V3ɇa6595X3<*N#dxVw[EWU LWնQT p9{+z%IU7[fF O? HY.X_T}զT+I6MqHv);fLrʪjUwՓ MvuڬvP͆eWyLeZ-^(Qf#>O,XQzvpp؋_gx]>Eqipğ୿,0V˝ zC|u׍~| Z< x~yꕵ|Φ`:TWeĪ4 K+ (i±97˄.VZ&Z/\ͭV9R3ldTQix.-=Ef9Z0չIگ,TjC,Eh VmtلgVe7(eb2alm6Tթ:,{V@ձXEdN\cd넛B0H4Qv (0buf@:m}Pp]:I9bM\UB..7{zDr R_~Ui./$+miL"$o; VOU2ԇeGYjٕr=gc 8a*Va|ƣmUX )Jxꔎ04ƨ*޾~6qfNs(д Y]be%BRJW-xF5P '.&΀BAƍ(4n xsNneNjbV(~<'p_ρ{amp7L'`h:G)%bu q<02o{t>MO// U*gݧ9+ـ3 +ڠV1b5VOug1TZsSx1+PԳIjկx@ZbQ1Y+l7t#UQ7S*5kQwU.*ʥ}Vy `U-(BB鯮:2^ 胃-V8y\s,WEOAG[DT)2[@Mv?*X &q\Q'jUG*m2\}1&.%=G7xH~tzA2ingZl赏MԿ *N/Wo+ Oj^k3`[4TrմU%J=V+:PE}5ٯ.;v"@jaM*^pVsbY!=ܥ1&gP *=ۧIIaM<뿄)jb4RDN =8P:JbJ*_!MuUUxzzNƴvXm>WCm76w3L+]#VCdP͗-8ghWOu&K%W(e!aUo!yZ9Fey`-N05ZSu۔7XM۫.VE_ ͚'zWǚJNcXUHvˠ*H]XI|. Woչk)~VX ksV V[o(U"rkas Js}<NjO{48J*upUA0#iUlvRU[U9V6':3橎U_ >ߔX=TJb5q)JTPA~(bTL.R(L/BMըT]NJ5VܧIZpGEY Ь}֊@$ji+֪14Qw-UrUؤ/:V %R8 hT} R>~i73/[={Σ pGO1G4{mQ'nj$lzhWWTK'~ߡ_{+,b]=-O#nJMu:쑩zLVWo}OU!l:/NgӋQK:)|[ kuYe`kj8SDT^U=U{_Dr@@ lkĝV46^pp8,1[Pg5Q~Ug&ZEVM+VZT-J# ií-mZЗkvKM,n|V6B:)*faUJ!6Xj#̕˥Xo[Yju%U2_nhӣULUFT} G-NkV[S'xǿ],V'1pju X`|&TEzp\%RK@ 󩉦*E\mssdm"@AF1a(Eӫت R "[=1&[%5BuJުi\9Hn3K`j`zR4_{YŕU/quf#5 ʘ@uG*$F`.(*khggň{bGaT.{9_N+ jL,-1z!E8ꞈ6T[X\!`D<5ӆjh4go߾{`u9Ej&s8᫮ ,WڷQEԯ*zV9ʶ&dzRh,%d# =fOZJV=olnJ7\?uEU|*)ggJ0K5VS%(-KaX4VVX573Leգ\䮂˓)e U,Uc+h^es3bsbK8*nTwPoڷ3@ꆞl gpJF@S5l3TikB͆j U}O{TxT+wae}bիbB24\ʵ[P9RphLD~ \:м|V{[?qRɪH9+p FNWP2LWμkCd4d5mUV@wiE⊁*X{e)FY.m(d"5g 5"+`&²x!lMxJ5ͬW U*UVeDꟖ Y*y)3jTpVX}fxu^l%zJVZ(Twn~aZXZὗOhƊ,b!ǹFf4r"{H'Ng?R}V[ewB"ҹcESn+0l6Ug0㭺d[{C_ V&wR! pW9h:PNAX @墋#= z.UQ鬫4 V$UCdyծj0KUz!kVܿf6}EQߨW_=֥JbW#uR,*Jui[UD3AfUwЩYEk&g눜'~/nKmq;Z4i|V8X uB#@A$=Idf;xk9oǫh]Lj J*vր 2U,+갊_:<.'3mYt`u6j:{0-VANyҵ9յb@"PЬ &] ^^^:G2~_pdsߠ k6>#tX$kv *}KU \J7ܹ$ۻ15@o`Ꚋ,=󰸓>Q"#oyTMc..#=SjZ nhXEv̚UZ)U;:b* W v"Jx ,UUTV TU&^o4d#S6XůMclV=*S׊Vc^]"6告,Pu~QQkH궷e ˍƪPU 4ZX%~)˃Hٙ2P5VgWZv IDATj׿֖\/ HfVzxrr*,}[jZV X`z=-XZ}X@@J4o@"qzWWU-d3H K+K1Ԫ00?? Twoo-P5gkk*p2hLV/w]cZ5X=XErUT:, $WюՇGEh:ETpEo̻~t lH["Yi,x$j]w c LQ٠VAC47rX)tGo Pס|CZuJFig5*2krx|^ĸU{&՝,TMsU"Y p4UiijR!WT_U5Q'{4$,:E.gd׿kuŇjq`λZm՜ԭ]Ԫrl^O{nT*TXOYvեg* ~{B,Ua( V< 8 np{r@ >V_VA# 6U7hοOVE<,:a!?PQ-mQ"8*UwL?Rv߽e,:Si(\Iq̈]j' u]#oe=4k!6X_ƪ<* A' ̾(ШܛG^ĺS\ ;:c/`#XbHSs^;1hBr Yǀŧ*'UuL>V)O ~Zw,LVoZ%5& 91+;{A&\/-Xd ƪ?|\ Jլ"Von@:u0C!g J'hЈUW55jm+0 Sm X-VU>'@ejO6- %;l_dd2h&noq YqJXZJ4f^.؍eLN.,,XԴVNAUXiXBJ?Ъ~@թgSݛʟ03`c` [`~8=j&V :BL=Hc0 BlbB Y/3-R\>e?n*.pe*v kʯkd1]Zu -'!ڦD_g\j)DPy%=#e9\كZUZǁZe;+[ZW^4cQe5ws+W XM~ѫ暞:$ *QTU y*,@Rg(XKXbuָd64]ޜ\oNS5փLsuN R(hUNY< ]V?qTJ_kkJ@vXuv?o,1 6bU鏫{y{;9N5 TV Da)jdW~ؽ mtm3VJآ*DizvrvpFX%۪_IWGJUߔXEHVwJX嗾KPXIO*f<WGQ5S6V U4Ww)Z?  `(:3\ᔅ4q;\ZMyݝA*S%`M熪V?x^͡Ck*0Ūlͪ/%*FmsQ1VqcUˮC:ףێ֐JeY3TbS; Ԯ.(jHxr5#`8C[7RYWG Ӑ=u% |ϧXaEm SP<) Vl?x &MCUjUpM\@N͇jJr`;q?楱4 1M;\P`ຣkP""4MFh?j@Mz]AHfƥۜ}FTLu_sb77f7 ;*LsSWb.-kbW?%l] VWe*4Etⶭ }$*WyYouqLe WO^ TMa5JKbORzQ<VIci+|S˶V^V*Z+q,r5;tQZ! X)'}6\EOU^5QtMO Ն9*V{$죚ҕpW? K_fAJmej5SP*[R/%ĭ7TLSUSyyX WRX.bU5wL{2_f-m;՟@*lub'HzB]x`hqXazkժ*ٞvnxGLY ,mի/` VoAI5JQm";;gyWu͛ 8j5{Z4Zglm uEVL5bzEj,A]E"C]U]r\nR*w`T:-n咳DŽHS8IbԪסjeV[ #2,D*ג#\.$VUNj(~*ֱ@Omp-y7նGMDUUS6po彗r\:dn- : aKRJ_[{ʭN1q V85.5J*^ ҁtzJ7j:oUT*sTuj~\L=ZfFX;pX VAq#LOvlHUQpzT<[)")W'v7$'WԹjDV5{Ry2PZ"{ 'tDE o6#4AxO34UeD .騪Ԩ^4TqBJ3=U,\U`]])xeg%- n̊Uߔ=8;xn8mNDLmL<\cZoi&MNQԡNv鮲MNy|@6_j;ZFL6 asJ署չ6Hc5m%@kBv2r5+Uz<ν#{5XYb :&W*V;_ O@wjXwvȞ%Ju(lIBr)KZҜ_\BZ*Y sտA2WɪBP:+N%l*XoX0x󥽿lv%#j ]:Ltc.y!SN:͊g{wJ$U@UOHzb hVSk9RK ̂rCDH'ߘ}[Wc?%z"Wq( U_"ҾI'UXRРX}z,@r%D#-kc\ ^n`ۺ\Rj3j3LUgcu.Յ5|kTu)5Y|SrX]>jUӹc.BWjC"kJV)vjŎ> TU6"`Tn!,}zwEynfTR{$t adhܕaOX4^J *WuJ*" #ю& W\>9fʃU0ȸVo >՜7,+W_~/aj8mj'g(K Ћ 8kV/o j*pT~UUJ"ju:U*_GZ(NE@UDGIuFPA5Jd^.V+JSՆËz"m9j {eӱ:j`Lp5?*^l{)ZMxU<+x{5 ~72I8+l%SC!dVA+P Zj|9C_ :uWՑ&y\MR VԱJ}6 `eg 8u[^t)^Ӎ³CT7wTp.mk.V5BM*]Yx\VuWKZw˽jg޳\JSQF+aіR=]].,qUO{2@S8޿;r0rffm#naAmU V>!;,\qhT+m:A0^MβPjRE֫Ru Vum-7P=-UAUSeb 1$\ZGUP=?\9ZҚI5L>V]*K9\+4VaLq@շ+Vwحʪd>_ųpX;qX?d1^-V"X?S>grt PY:VG#ˋ&#vYU*ߺyMUn":MrjS0v_~ܓ\X/`WZrZzyz%{T..,*VWw5GbZ[~W3XW!#sZ v*3+eUTn>|HngggLU*a1@P>PǠ^ @ 9 0+Pʐj.CvaBUЗ~0՗]Nj4lU|uZŪjr5RZ'T"PIiDkig ^jL_~&ki*W3d}IV bxObyb^cz,&6êԭ#/֬X+__En CCNbD2sn'@uI}`PTYOxn3^AQL*|MT @}DG'Tߡ KX]=cTՔu٫\ UQ.@`^ W )[If_?i/,yl4$)rUgdP=]ի1MU)TŸ\A=ՃDU Yympu B@ZWAT%ieku 4,>p[\C5/ޥTZ~1VfU1JYʾ:es-(ޯ ,oPjUsJV4U$ftB:33jx T @+ \yx~Z{A官jn+ WF1 -g@U/h!*V㏣+,/o?^*ލ׊✒Z幃 ҵ:JI\WVubty$Uב:X=H TVmVV\%ch P5}&+pnkUTij Fjj`Z^a\q Wx 欞B4[oB3r MH-ݨFJ աXTTʍj~;l'([2fZ?R7&Y5rl|Pr'Qkq"5h(eY2cbU%GyJ^"xPZjA WͶjJ8 T'7qj9RX%ztn5d鹋א(K`׏+)u_[nW 垓"@%*|HU@Qᓃ4KK?Tp*dxlnpUbJh0d[GURշiThB@cUq5fʂ5Q@ ]6 X!UqT k(V2Ns'jcz8 { .#X5\5t.ZwT}BAnZt5^*+tPjsfol-V}kQrj]@%T MUv&7zUz 4P,iVmQRZ-"VY˕[=hӠBNz[[}/kU cujjJ*缩nEj@3lj}#Vw;Mش^#\z/+Fe& BU*Vw1s|Z+CxbuqNjV5P_%!UO qY0+ IDATʿyZUVV<w:L1uGjª*d*ձc#m^s.Wcl"d#iU߹z]#V V U=\3*Sr tD,v xbzjG|{|tE u S l`PuOJs2T%*KÐwO{"6U| 䤽uJS0#c=VVKE.͘50O- e8v'ӎ%c5-X-%V~BotX3t_9؀6<nU]ݨjf"BJ [XUZa*V V/_\~`pXZ}xdEs3paAT*JUTUg(ӅIrrhbټbp,buњH:P)K23, ӱ:xRkY;WuV \f.cU]9Ojh$@1 &&=ꈡ?EP~(T]XY3TVWG VVjLs>Wy],JSk?ݪ+S%0 0{żJteJB7uԹ5T $K{ߐv&_q|* GJzDECPXkͳe ѪM+H \nFr>tQ4N%e@{>ͧVӜ?/z VXgUӡ$QwϿq6o_U #[4YkZ6BfSLi.+9;ЬWsU]fJjJXy`uy߿rXX}V?3Cm}c}^Psv,IUmGuJgb PigQ5 *[_sY|ZwWYjnX2Xm@W3`C>V݈$ zVWSQuS兑pPd*Łji*qP6aXe{} , pplި)[5q5jiW1tfh%֬~ ػLl ybX"U W-Ty{C8oF<,qjv xÙ+H[{*c"Ձk_mץ_|7ǯ_}ԪU+6h^ >^ jg`j7/߿J!Ue0TEJmUWPE32WYQՠQ? fp+S,o+Kaύ l~1H+V>'2L%l^N+|.jIi ] q68-#ztMB {X}eŋ`uj&? )T!QJza˘g +R@5@`A +V‑(Y7DonZXb%zjx89p6k׷uŞb@wbvWa7{(2ӊT^]+vrN*QvVWo;ErQ0.˪Z\I!_ ~m`z1[}֞\ ~ݿg1XWX˫-8 P-F328bΠ+զcĊ;ջ+`ypp#gG=cVzP(u6]V2 {Xp(; ^K\A$@&%rb1cZ):˩ի~ߩjujX/u|:wƗ>OjdTVzLu58fT<8roMbDV6W !gbLi|:a(XMNJo_c*v@M7ċ{ᒁ5]!8Wץ R8wiGǯjJ5rF:B;ۭζn4B+\/gd~qAQ,3Jb%L%ug0a-fX=<vγQxz'^Ng{: ;y'~O%[/?|f?qLax)\{Ϟ{=go--?͎k/kA;,phu)[dׅpi~ms8hS@TVQԽUU5rzGUd@0X1Iձټj O+V%V]cΚXh?K̒6ߢ&\J~7abV)@MZ-VʝWQ"`JJuЪ[Eo[V!^B3{4/#֭U1 Kt}T|YI~j*&lY' ~q'oϩW 7iZVT(=:XT7c7ZO^/ V T plVX\Xzsz79v'Ѫ34ӷ$]*PfrÎFGA,vdmr6a1@&TE|"R{PUચjjFq[N2JrU[cS\Zj:>Ҫվ6nx$PPUZj*j0hOTߒ+Ұg1X%u(2|3ݎp8.KNz=7#)"26x5빜OM. V]C9 t³{L'É2є7Qnq :#LЂ\<-'yKA'@:vC]p'ˀ&r&1iUUJ .S ͇T <\fۛKsKO#)X\gߖ#W+b ;w'6'%mnJDGA~[l,p1iHq۸qߨNLlCѺ CZX V;1;X-չjh:VVTMWeY*Wݪ>_XŒeU\!TkJhSS 0Vܲ ]\--hNv%eO[Vs`'4ҳGAL~Z*Ӯ.R^ef]D}mEZ. jSmVyޥVqj~sXfOz(pKz~&:^8{6<|5xeP'|Zei _7Q&zIAV]GRp!m~ ,y0vPMc@s {kn^> īWqMjvWCP5*bZjVTg|vju \ 1r5IųWZغv!X);Q5PSs UumL;1/JoLm h6v8-%, ı\Pa͡RJUBX- tn^BR3xLs.`Uw ZPM>tp)\"CKJ***,$8.]gªiQEZ"^^+aTkVWn(jנ@i覡rE9Vb Iʜav}3,bյ{Ki*W!NN'!Z$}"MjY:~swV=^*@&یU)E>"pڔ*͍Xݽ:o VVh[j3?C 1VsXu[T`ydb`'**ޗSXhhBJO .MBUj3륪I* 20%HEj*Lӧ,j>qkĶjmМ ?z\X떫U57So%U^=@\WUUV/ŪXޣb%ȂU-_y9#~PܱVu{z}1R<~$27-]}$Vei\Nm b]R9+quQJEPBXSnU^+؆Ovl*޺zK*Q+n H?Lr(Wh:^4>pp𡝰A5-2Y*M&@gJzQʩUb5VW`3j"bubJ6Ud:4 RO_-zDj_HpݴuZ]q{FY*\,RzJje-U#dEX=ߏQ"VjW{VxPVj͋X׸8ڈٟfXQ%CN_x[ Zu;z2@&oMLmQ- wttS77 cԪynTKSts m5,dl <[e#ltS-56KHJ%7 ò^]NKM}NXOyz*uW1Ug3XWkEc䪀ul#Tj'$ )*=Lٽ׮{b:ChwʋjI=BֱrNZ*HZurRSxkbzjme`5W]bX-]\GA2M5I0օyH* *F3XMJyWצC:d d}.JGtu[Vyї|_1cd_K_EF"}l(?'w*VRm師V*>Ǐlo?:ʇ](|ؾqᛝn>~l1,TDUjª`'9qX*Ҫ9ȩ WCs1b5K\-_-?NBZUT>%ªbq4%|V)ỤXSRq*N-#rQfi.4^~S& d ՑeWuWDa5U\h*OdJx޽_k+IfѶA._Q*C4WHvj?}MEqusiCenNBщaJꠀI}3\uE*ռ%VcU[sBZ!UQΡkOS0;P*JpARI8pHo֚j}OV/=~cũ޵d[7n~ur}sw3qKbǻ#mF/<ƪڥǻ;jeUҟn>;Z ?yu5O7Vd<~PzpI.71Z9_gUEPte0bզj`znCEE"0`u!KUWWOfkPT@d~U0^ڊ/;,EOj27i**E[RP9<6bӍˮTAƀ!O;bLzBE VirE ׉6!zs 6G@]f;& @&h,jSaB%CyJ~_%UAYq V.̆Z%IK%ZZY*kIYHYWHnYR J0#Eչ9:\*gTFU=v Pii @]vS+@Lc5*V*p'jS6q Wm!REK^W@x 487Q|ly26 9`W|2rV¨v˖Wq9jEVUSŒUt$D[9@`rrS.kb Iu&V Cc,,^XZ]]^ec*QXMzȌ U<Ԋ1wK=TVE V;<ϫS6sF,)RT\ջ}5 ur=ٯ+ -Uzؙ78hnJAb5 ؠ\;(-+jRPP&jU c*ҫoLܨ+<0f*VU*% Sw0[*uXV9Zvr$Fp@AK`p`NEXdLTπYb?Vp#Xae0H?35N)f?LQx7ۭ̭ݹʸU?r~j,B8W7P7IM(kξ URBjF t;b IDATU(T ne**ӋD*SѴ7”00/GOu h`&izbTVNySЪ-N U\iD"lp.;`աQ7ϢjїL,BTdV[Zk= y[g}ֱmk@.qk \-/B|BښmauĨ*PU1uBW6FǁL$V*cux|`]ɖ=łuU~~}c*(UFFXyiZy}Dm*.֘H"Sq?}*QuCa[VekR[U4{a+0Vimа~a#\մUT];Vc**iZH.UE."Wke(Rjf*J [#޽+ݪf=55U,bfJPU`=-a(}fUrUXI >\L-RmgҎ}vڵ Íݢ_Uk<隴KYV#T@zpuB4iՀ'a_qN_&MO QنSzT5>ͰBzw75z~WkM#`U3"I"XMS`>1,1-O(V\$qP8- }GṚ_3O`<'Uuӆ{1XYYܔA**X}Z^փ @C{iC \Ckݺ2jUUVmjXNRPhj@*XX oq=d9!X;x0PU WÏ<Ԏձ)XJGJcPRkUWޑG옍բStw{ru#m\/T$WYªåՖ .EVF^mY*d56vSMGk,ӀՑ<=kjcƪVQ TTBpM%WkZLm/Vg/V2N/%՚ha`u:Us"Xmy·/dkxl6XW6U㻰ZU*Ԫ*}7[4-G;WYTxPTjU/;d PNĪδ?\UYWϹ `$1VUq<#V5S3%:*_U*UV n0܊FSٵJj]cakoo cǠX[Vu$}E\V?NJ۟*٫KU9W  zЬ\MNs6Rw-*juw7ٓe4|Agn!gV5TS8nzKkli5\.dW)jū,{ |9EN%X*0&u7=yؕ{КO#+MOeȂ;b`5.UAV wuiUT*$*ԩծ~mjRaV1*A;*eCɸ;Xտ1X5'|E>W ,k uGmZgSghJU8H-I)JPTP\r@Ǫ4ZpI+ܗfjR= IjckV5Vgg_|Bbk`YE$]ZzXNOUwU3n EdY&+kouuè̩$jPovntg # VT0PeZ!0+|e,J,Xo e `ՑU}dTc`a5Y9(ZU>4Vgu= ^'(V'ȴ:I:Wg1$͆ [?O4VlH˶}-;j]'KmĠ)Ze*"VzJfK.^Lm%augԪ$]~sFN_3WKWqYH4ٽ+_81')}*M_=J_8۪ HASktV,4yiA \R-[RjuLFH=VXM-Vptt P=rv||}+^?==nUrA ;;-Wˈqu_HIX:լ@[RYu}X񪵀bxԪ6M؊TP{U۷Uմ1mY'QUdzX*՜p~B wXNtZѓ Mj ArS\6FW@xg<"ʼ#ԤUfca  6Ƈ-v_]Q[PmʞV;aUZM*=< AF J۪,}zz+V7۪RVX[}g`/~2+ jo]\lӭ;UPgcox{8߾*WykXU㪊:Vչ\u%<zP@SdlAU+dUӜ QS X^޼P@*bUc U~rjX2X[]KͿpi!CN'Lڙ`s;|֊^A@GO sJy$V9{jTRq5YƙauunSNU\ڨ1+9#X^mWEҴ'XM9&Ta5ʉ3:0#Z Ydc '%ZMN"Vsy0f c518}D{[WTY">JA2suCYE ްտx*Uر"qP*U՘JkPZ8!CP-N g5HӖ7#E8Z t??\e `?y{puz_Lugo_ Zu H.=Í EiЇ[3zZ {}Ŀ)Z̠GԪ`:Q|v+G~xWTC&RDjvwy!-̠\nPV3nfOl_JLLyi;Oxh v1YM` sBU-mn!9aHI/zT(ˢ`C}gG~'jm^yQLX-` U VP"V*X쀫Ul!xs.%?f(ޝXMW>LPE}=7}fH/pG 0EԝAeH~3/Yϻh_]_{\Pj>>OkO U%SYêeMUAjUD.?|+Yϛg1Vj7cM`pЪoel~,`nA R:v*%uT\TdUm hUj+;S}JYW[bP]`)RVTZFkj @@ձxqtV]jWkJ.VC]TeGk[ 5>֛z\&L) 9ĪՌ@2U{gv RIjlx)O>aJjtYྤ_-+gUaa">\+Apʆ+;6| .0/3un\=|}5BۉUJU'5 o<"ޱrᖱ5opw[Cܩ*Wv~H|b%iVt?RŷUjuj ƿX-?VXoJ *wZqq55T״l=ѪJo SeIBj`Uz@P+~lKl 'ܷ}Wp8}I,IުO^u" W*:Z VAt}5jZmoCZ5ϠJ#Z r.mުЊp9w#XյN JZ۪> `FӋk:s6YK2UŜ^}ʥR˲ swL-XU|~.?n7@iA._ө:J~ UE Trj0X12^pJ;R3 - S&Z?L/+G2-/.kV>j+#j3\iO|f5:i`JoV a%RfV ^Abnt_kwԶҪV\fLSQEiV \Ɂ@g0lհhhjա*)UְZUKa1hK6`^h+/=[ÃQu)<bx/僴{"WeekڳgSZu~\'O_J_m5j3?LJd`˵v)˛[b5 >!VaU ` r5ށX 7ªqX RꉾT`UEf*Z`* 3f*ccO<[G?:mXvBpL 7C\MZٖ`&,:]{FXqdU֞q$XPj*s}XUTRwՊXpSn[[{& Ot2hԛ~Su5!k"Z]< 0Vg+{y=.W\[WQ[UX=>h5@zپS1GV-N?JTm7pcT:yXiTHT&sEA]SXm *j-pB \;&{@j0z/s(Y}Y>nKNyʧkj9+jq5:j[vauST\rqU#{_C-tMsTMT}+kª6u{%F; ªbIaZOX#Ezq䲪c KX52[UUP!G '0oXe3-f/1,;U*| :AfQ U PGBU$XmXWPX\@ \8\ՃNVŲr.rC=`5X݋cbiߋj'9838ɀ"VXfޙª.f"P }Jw@?9: ZIU.ApTRրvP@jbV\uر[5eOZW<9`k$&XUē`_&RCq >Xtڨ@*=?."@VQ TN @QW*ry*raDԃ=>=zWVbtf%˼x~',ӕs~:%QJX-Z+O}U6MQ&Z491IA㉤4bK%QVuz>,Ow6DZ:*vWm<$6Vfw뛝;X T';e[c%K#53¦q*vlW &PJn6Gtv,[εͫi2g`:]kr9|]݄ةI;Fn@- O1Թ1kӛb+/[L{J.XՇUڊT}ѨTV{>[ z\Q՝UUg{]gUꊲО!+DN^1SMqMUT]YeJy?$7ޢ0}r|Y.z9&baJP]5Zσa}PrA~+u8 kRZ._XvUNq4*EZ.Uヰo^\U` U}<;Qjrٳgrf^$@ҨjtA(J|pkSYYWMq@MÝCp=mAb̋IX2W T ICiFh Xn(.,(l)5VCC}q o\U9%W&k~r{ndM˨(!=2j"٠]} `GUjiԚ5QuڈUzȥy1Ўkj28FMKMDՕ ^5ۮլbb%vupYbNM|-QqXM52X}W>lB5@v47@ 8VATݭTz:XqCd-I)i Xh U]\mqu8:jnK]TrW3BUIW\>,=:TWK&i:i!U%VmzTGwS3¶zVlU3[N,5RT@NHPXuJ;b(=zQjܜ7 IDAT?Φ3 Õ15%pR0ȡJ-"3BB)ț"U,D/Ybtþ#͏_0>9D3I1y>4l. ۰~[--afu*U)a6XeK<U`;m"YoUr$VY' V4X}mrZUK@ΛUʗچRMquKrV[4/7U=>Hŀ#X3)#5^ם`Վ檙?ˊlvlXjgpk*XɚJA⪤ V9O3K;vװju*p?\Uv Xա0nIW91VW80=Zgmz9YK~#VGߵ"wA PT>^ZwU5UF#i s[Z @MV#6Xv/.zEUz0 ru^sq(̪RB+Sf*FzxWU*ԥvcUԉ" \Bwq7,R 6htɉUk*,[_ȽLJ*8gN?vywջ5"n= U\U-ۅ\m!֓rf\|*DUjJT݇c!͢d`I>mV W)ъe br%%sj^j}5GYU_[AZULQ׹K6U,_pbb9j \W,¥W!Е>Ku*RmwnXP~_Vrd,>ú)LUѫ!V%+Z+ V;C\Ay:O\-gϣ\k5j;2T'TTS,z,^Mfu7<SX ߱e*`G;ROf#Ij.?^5IՂ&oi^[DYj)հª`5D<0tJrc$^^HMhg6ErǕje5 Rd}(]XXYrW VVRZ.XJ|cVխ/jVLU$z"TrlzѩJ75FsA2sJ:8(\e`uƪ XQ@4a@9pJS g^ܟ|8c,Wg|3kEZTTLm&L0u8Bs/; I)5_<v| SΨe2*=@)W3}<@+,\j 3Ue*ՒdY{9*ʠ? TUK: +Hi@Ǜgz>'1Vq}ktjl8z(rwts n'Uk)^b⽨Tm0Qw# ^" 5jS|.j)PU/E[dMa&2#W j">f!|E*ma uU~`VU1VynUj_1@UT2kvljW0+"sn>UULn.b.e1?;7EhjS3^GgFٮM a?ܿXM- VGb5nV6p4VCXzc˜ZM|ڵQ%S07] <bNN}KUzT*@h$E3h0A\F#8(VۤWǏ2 j@ F7vmZ=r\uM`k׵ksk$u U6U:d*XHjX͂%<R\?`p/qK^ *$i}_R%Fƪ"W/tlU¬)b`u V2avXoPvh-,V\.QeQj[(jZHFv:j>'?ZJ I J!8DSY&k{LXb^^>b$0'(i`Zq(kеBis%Mm,Ua`_GU ڂ=F^Tel5+Gi:Ue*sUo5`FERƥUjF HLJcub$V4HXYoeq߇$0X ˆ[I,@LY* S LLQ7R\^Sg*bG5nRr5zuuwu,]{5`W2"Q_3IJ&&1 ʀe]elP/!1i*6&H GbKd. ˛g{U5/˿>K+vyY&n寥Ւ`=\T Z:slϏL%^=BܖG6Q?Wbh  VS*VRjoպoVK@U'V/t @2 @Xj5+(m'VNZԡB,z]˫ 3g  09Nuc̣yonbuq 'rdiz4:>%וY(YyuyX[Ȳ 5ǫ&c54ZJ\w*a+ŦH`5l84kЖX]~Uy\uiAZ%Y\x()Aה lwĪ-_Sժeĵ}YmJnVfyRUɚ`3P%j k*j%!+]j[)VӰ:Yf{k.-JՋiT u}*"Uu&UR*hl0d VޅUUS$X9?Xt-UbWWcxbyfRo詺eATܥx{Ruu[9ѝp逅] i>OMw`4`5✸g*u,ZޕRNrՊ-wV^d[_l$;!ږxkuN*YH'U/!Rg*{mUؑwJ ,Aޥ`+~C1`oPU0qNBU VVU;2^4 |XYViDTvP*X/H-;Uh轧֩5UtrǔU'A_7Pqq^%;Fl1E'UTx׏UN!ëDni) ~w=3>MjԪJؓ`OM Y,^G>F#2YDխԫ3bu>ΠVVf[ZX]cՅ-Ū{:\L R9:P գu4 FV'հSN*=h΋bv եյT$y!ϓauɰ]@ܩqJ¥s91[]V-xP^cZPf2=V~Ԓ8Eb_+RE V?|@Aߞ$HuK_ X-Ӳ>WcZ[~@j&VTJ-,b)X"ǔ+՟WfX&U9r<9@e%P5Q&yVuSkzha ^̱_4J ́=4*U=:&Rv^CKNUX`}Z,WX3@q ӭfePU:VWvD^auɰzDvZՂjaգ@U'VS >%WmiX=99 쏏OO"F:7VWZcY(5_RSĂGVBU# VOO{\\Ԁ鞘$Қ=!ޞj= \7KX#SX{PaLz՛]1HU`7hqA\**&[KEXDjqa!*OՇy6eu*)A髱tL!j zA TTxUU UCVgLUJ@SWbu٪YI*쪠o_Qr-pUHUb]C]qJ Vr>SIiRzboD\#a0Aٽj(*ΌjUbUlcM񕛭YdJ׬)d.uz>3͢4YFɅ,!vU4iL:N0L.W˺9O(Xt@+XU'AQzC)EP?23YjY|DK$g^zRU Z7DWVKnr)ހ%-\ bY @#n./ZH6-in\5Nc 諬W#2o늠&*3WtR\_iߧE'Z$~5-Z:CU&\`-R2ت.XIU U&]ԫ`%o_9PX$lEV 8:;^&cՍ bB k:WU$0 v}yWA{#IazorGkA{XMFN}XUqhE^UXUhVπ )bյVϐ'`5>su ?յR[r$W[YqU@Xu5rJP8պ HUT)hŪ 3T ef<3ݨı:Q|lJ:CnWu^WVTBo[<&qkǽNGnW3EO:YO?i'TGyHfbף1VlknUvaut`UZ}`Ӳ1H:O[Bc9;3+*Y7vոTD"W* I}<S+0r 3O /K`VOUge="􄬄\UU CWYf^7]d֠eU}_86[UWad; #|zڮ(.LvKU ;x@ [ B"$R&1Qe Q?&={?@z <@/>AJmX*h =qCufXRւohU7eK'/l+bs ʩOS/RTZZ*U0jW^;R^U޳:^P ;WTX& GU'H KR-*gV'nOˍ`ub?֡(:rmˣү* UhWU'j2{1Iq_uHWVmn itٴT{|7SUT9ebj;vr9_5LmՀv;4k hJ j 6֪a\UVAjE/.ru$+~U23rߓ;O~ UR:yʁyB,7P߾}^Nm"ғp:kUT|*+ =$Eci`c%q^,VICoݹCo3/}ՙ4RHU@.֪7og%< z?^[)|5D0t5#WS$vg\CVGblWi)H87w fVVZ) $ 9'BUV.juQRͮ!]S~kUvM>  VVnX:"@T~Wf Blh`u Q#M㾼ZfھUjMeo5rWUbX,s ]/Ju#*ۮ集~( _}*1=%qU޴2vŪv'PG!R0(T=WzCVٱzsdpzv ν }LW ՑUMjLydym{ U5U %s: XEΓ,ZPt.$ZQ+aA6v`-`WɄ=2r{3Y-o/KAw(8bfqtDxH~2QJ[_ ɫg!~EGI.? Z^vji8W[S*Hު~Z ڊp[z ZQTevOi괋0ݻk+}h2#n׺'JɅ$~~dHk jc:fPU{;%k"?cyaSj7Q3^!K\ Nge*WML~fPS)f=?kY=֬BWBy\噊 [T:":عqOxX-yCyj_ѯr +Khmu,ވ4gǪ_W ] /܍~z9e $*:-eiVwTNv:7$U\˫yWbW3m|U.p+!Y9Ac`RXt/0UcLjqP2Zp ÒAԿӿáᳩZ4s :ru XxWQODn4U'[V:*gWhU}qHNCb0'羹SVI :gG_~ܮJX95f^@$/G#:<20.Ô : Z8Uj_d) r78 [3ۥ=W$'jm#Jk$IUd6X@ . 1xn7[mq5YZUﴚ\cb1yX@U՚\ C`d]1^2X՟?9Rʪs:"E@)8<VZi^,_W–F6 GpSˢ*+XkTTigЮ+Togy}Xr6q,g't8S5<Q#(A64ѐx໢dfE"x&l˪urNJCI3NUsћǖ#ǒ|X?>jm&g?i*MꎆX0˥jCMq3*bݫ/MOcvUIӪ'1ZpXH6bXJ@0&ANkgXivf \Piv>uX݀T- ,  Wv".@.86X3Ь[YV}STXmUnFTq6HrSaUZxw>PEhּ EJ>5|BK`G*H*"OfExGkh *:權.saU\vqe{M4&5WID*#XkV2Er Vc'V)"+b5^ߣb- fмaJxW QF{ZBxڟPմ22e"lͥ{bbNZCr4NJXZJVfo' Vֱ QHfW`dVeѶpwM<ZOH4lIŪxG$*݋vyT~W3XQV&_"C "^zyYJU U&hog!ߒTeT]C>Y<":t@-;y{zֶa*mDȡlUj9А{å`S'>jPAT%W&YQ`̀Q=p`Z2>C `ՒB~0Xx~ujV;ۥd.ٞCg)8zzi>q8U}ЧwCN|orA auXqayuR` A5㊕bi>V)@VL<@b`z[Ѵ(H\lCkG }V8MPpjʪ)jHbUyUXU+X5ª, jv5VwUUmU>UXdJiˌV:aUWZ嗰Z~H ZXh]Wn=0z'^q|߀ <a.UUlWe<TZߪekS/ho`53 vVXj+ jg羘[l;+ڣWsV+Jvj+oU*rZTjmѩg_֒aGTk. JFݍJese"@#ja* XXշ^RX )P:=LR qwjXb18pvn4l9_%3AAU>BEotjV8VXaVS(5hXuѻfX=f\%:5fVQ.?`Uij_ $&`We\Ut,\ F,GȕWO,`N՗Eg&l"NyJ .XERjǑc+1U:Xm?C}unznkNnSw~^?.0z3h߲|PUY`[)R3R^Snm֊*_rc]hLǾ^[Z._[Zƾ׈աOP_iS4 !XP2"+44$ioAyV 1jaVI`5f?V0U_`5gw-^0^M7cê*Uu`5aLt= ;2P:V,J45I?;|~* Z͔@FZ/ {moocG YǞ?"q\6ұ*:I{*nZ;'UeV߀ kưB g_@>Ͱz`uf:MdLv݇@_}x٬amĽZ$u{}-(T-{ UM-~?` l+on<(٩7WǶ~ozZU 9UU 1\*=C񷘲8\XçO!Y0J%j!fJEflŃGXZVlPV굲JXU4%eU︖÷x\Y\{Wd*[;Vy/T+U2XJElCf@KBVb1-':d*’Vg__(> <ƵPh/X\&Ok^_S ?4OaQRUQp;6յ"bvdԽ{=X O޺tZ48V2Z5rm*3 ]Sg0:Ջ=b\tRk Y] m{}QKbɖt76L{0'1& hXb37Vf%U5 󚍴#(o٩4`UiuXV(5 KE V Xmj @VŹT~֘0mۥ\NSW#`uY" XEkXLUX]:~k X_?=\%r57BnUT=+VeZ-u{zf^?ǫ .<zK_Ɂs g=!Ow3z?Jj )VhJ{Vj 4!hYdN*:ND{#Un,*s|A2.F ^l8 OZ(۲ê.UN gbp> &4>O;`l. aRuصVWa@bڣ2U%V$Sr<]Īe=g"WU*yք^IՄ*x3V p+WW@UOҊjpQ4&Vͼ'VZm97VceӍjvCh`j4*j08a&YMj\d=6XӴ?Uܼ*$_Qы'BXhU0a!{Vq 5_ҰV}A5Z@Sjrn[WXU*.\%K˦vlm!X+귺 D6w}*l' 2p)Ąߑ m U<'Ov>صo|y栮?ZY\\U_o*7iDuäyZNCpC^Edk)Y2]Ud5ƥ:mI{hŪ2iUh%*Y˷vX뜱o@ KV#~,Ua.V2堪z`mU\ ဳTwQ1qM:e燎rPn|~ 7Af+ܲRX#KiWY,Ky|)*ϳ. ^ .-eAb6~` j Za2Du 2nB8`O J꼡V^rri#b/@5=/jF>z8{28Wa #/Wu!PP5!<J`bHl(u냤W͖J-G.[;;N?XڎXդ*WYLz֪''%XBX\nThH25;Hʋ`L,Qdʠ&WA C Cl{Uui(Cַչ9cD׶+CT-mY\UUU*VU cĪXCiP@Cʯug &X˜N*q3ЮUOhzVO>` +d?3s&r5 VQh+ =t 8)Z;Gs y"LYG,5ד*7f+j0u uNh%*uߧuX Հ7Xv2ۢjւGҨbLj̣j\j5WNV2:3퇈WauRjT)'AG#/% /8ғҿBy/W$VU@WDTOVYJHEk`^4qO;+)⭫Fi`L[s1`y;$?9 fl]KW+ i BP"!עrҹ܈lHPlk;3sچd6?xgw뼺cΕhtD 6Tպ g՞L~Z}ߣZe@jP_*]W)Pn랍SaJUⰪv.lz"䍠Qj?u_Ći? Ѭ~B75;дVVXMھZ[j_X%_bU&F [M&$M+_dBjQ7ĐUمaubzЋ]+زjk^J2qO?V|n䱚\-+Pu6ʧ:AUu2LZ&0o;Џ<תOL 0 D,QQY.p݆I5̍nRV=::p. Xͬ'TC5hjF_mE2dګSOq^ckU1ta+\b7E>qWs9hU#<-}([տӫ>2fp:q-tRIU'bjz}V~!U֬wFR :)y.O!>+5Gʘ kE'(uŔ7?:Ғty4aucT} օͺ`-m T=3kW&Z8Z@.j[i| fá}mTDņg<'g"VY w!V\VVkbA. ݝ'T]e8TMkO@26¼">3%T(dY}q " VlnsaXMj U-]`U*'j*VZ-He&FVGKig̬(V8=\SЦW>؜5CLâgZu 21OљXUT^V6`*V WyH[N0VU)3N {j82s&VE=*<iPUӆTHU?2?@mru,1q%B~ zrY,UUL8%ϋT][CsnVU4nt?&p%~>+GV+njr5Md1{`5ڒjY:کՈJX*VUUm(𤫬V &Vk>|::|o{SWQh.VqlR媈4TSC+e=M-6wdظA2sGQy?OXݖ֨[WR`hT}D.T=L5ʺ"*3u2uWftLcv:X4)@@`@@ϫdz~NjL VkQ{ rA&cUNKo4+UŪG+ⱺ\5W'`1O3@ΖJV#T %Z\ k̫->shgkge[J,snT H*ş U[a3[lY_SMf3+TOdJ~2<,jLN:>ޣ/G:0ՠZE娲21'3cN=^ʫ쓴~M#Ѝ/`[5 pZ:ZE3+4N冹,omvUzWp hLxH_O 4Blꌷ 2Tuұ_QX V5\Ap\/)wKKHx usqmR YsDG￷}ww?߻V wzgOw޽8kFƧd#quE8"רVS8hhO'u3=#ش ӗL*i.Z'-\U~ۂc?иdgB*?qԔiV\ú*ӬNwU]b2uڜf#5 Y^9qӀբRAɄƮ|Q:p,fL ZQA1W}."Xjg` =C\u ;Ciy\s5^:XcIy:+ f[UIDjAǧ'ȫGt& `bu궘rJLLư~+ղ`U{|iWGGUa0Y,V眝LwY/IXarhXZ$\|q}vhTPsϗ7MqzHjzwP7Vʌ P=*3ե*}O|K+H';>}|$J͎5u@B*I Q"R=PX-=(Y[&R^E= ӓd뾟}_h]<4i/pd@#ӷd%XM0(DêQuh{jEˢ~mR֑@=uZ+V݄'111;3buvU2 XikB.Z%Tnm)WGp *Pu/* Q8U)KAW0luJfݪ(Y8@hƴ ;F UER pr\VR]vSq' g@ !UJ\^C*`&U\7j@51*j5+h]8Q87Z*@6TW {H9N $̶]nwsfUA>'cbkw#+SRp\E`+ilxw7 ]Vaj?%]Ui2Łיuk9a,j b1Ҵ6+WުmM-?xհUuzƃaUm cPUVQ䪍^p7p`%Jw UZS%wz!,Wj]7uH*FR `|FG04ZlMg9r_2 ɪ3Mm˼lG+jW?陜'JVerQ!Ֆ@f";Z4RTd"wt~&l ժRw,칚N˦JVKCNr|VjZ.j$u` V*Jlc >tgU~*WQqժ-ݔi՛(B~kg5^5?kJ~X3]\-uܖz0IJVeϏC:MR*V- R S1;ozRuk՗/B*]{>z Rժyj^踦 R uoV햪UjRݤM@H 6YQU"UK%r: L3I;'qY=&kjaŸQGG`XukL4fRek)_-9x#TE^$W BaJyjN՟h,5;tbkPIdFxrI&]QW@b{JS>ܸꚷOqF΅!2iC!ŰjU \"@u>P] IDATi;.j<#OS-"NXmXmP9܆?:"×ӬCԲ|833cEzz05ԏdžնhW[{{km2Eэ!UQ +[Knd VU9L\yM kըJ nx67;PUZT$Oj&/R*HCC=HMUUsȡLj6&'\$Y{`/8^ݫT>~=Ւ**zz䓐s^[jFouyeX-TcWʚjհ Ԕrt GnO9q35]8]TǟƖaWJLhE7=- Jj%T)Su.]!U*OXmjb.f[ꕵӨuk#5pj@(VU!WgfghڴU`5՜z:Y1U$_E*XH e!d{NO VJp3:'yjUصBUXa&zG75Q5Cy N#{[7"jTdY}UZ(V zvO^ \ WAYc֪hCbUĪ\u:T}P=qէVz~busUN-EJ[aőb:_PN.g`L][^ڵkD~۪s;-4M(Uvlk2J?͵Ǣo@k ەKFJ 6H{Af&nbDSJ3 Q Φc&\,b8i_V W/jSzX3Rb\߿V) ^%*zoza%LQ=Qzǖ@>yҧ 053d cߌ3LTC*SWUUŹJUmN >Te<˰ sTf>j6M#tCFU^lXw+5UqUYwU _r=kJ,bZ @RJgUj*W}hI5*du+.\T;"Z5W[@hu8Eګ|ӪZꐩ(Av/îb"VqB(ZZ :Vݥ.oUUVt SEmZzzXN/ (t$,zJbC8_#A.sns2E)DVeuҪd0r5'֨U&Ax%X=W;n)8/FUPw҉ ++WQs9/vq5WfeI**/=mcfGY8 ?x rgJ5NO^sVUkXFIzk.0忒6"'бR(2S1*d=v7y V[[E'[>)S'fFfXE.- *_>lPBTb@ [~oCUXf]rU"uS1? veN&Y'O|7AR E~InD'22Jl jWd EnUOj"9mVԕê7tf\KVEzn:bSXW@}QS/~ʬfH=M** VG:ay5Z=O'TV\+:Ese(X;2@? W7ıWA]Z0uauO\m0cF+t"` ?-2TTʥAp~2j˹:O!p8e yVeZStX׌%[ł!p>b*`yvnAQ]Ƌ^_ĥX s2lcw9VUXmWX32ͦ&8gS,kd7%E槈ծ,XU\}Z!&SCZ_9Vxd_[ <~)Ys/R5 L\E:u׈K:y^/m.gkfϟs@{4٭n rC Q*Sp JUHFU U"gvp5{Ƭԏ`q=j}ǃURwU>Z@Jk2&1L?znl^ /+QI]!X-Z\ý*`UX9 6(W/D8ϖȐkEۢ@cGJ^/ON+imW={ަ!V7sXeW3ZY]bX-6`u/hUB0܅Xu+V@X-jS^&4R XC^bjU:`Z<Ѿ7GTДN<맆U|*ۊ^OI:ғXb,UxvM+ T*UۀUPGs-ku/_zKs6LU8c%2@Xtvvq+ӓ{B]PKރVZ]2njҦS,lU ;?LCNA8[uQzگբ#lWkOuo\%D_Ai2φjR[RV]jTٰXeKjn Pu*|g6waiù إzY{F8HP^bbkZV* iUہ5jVU VX@FRHޗ=c5aUPiDEXmPVJfX1~sq100*J̓Ĕ(QwuѠ=ehbC4)W~rRBBBZ⋨*C+ & ӕrTqJXe]/U˫ۏ&)H5_ÄQj)A=pG]SuE`ͽ,iJN"v|VϨVP,1aESzsCV[e7! jNXS VDFV=_osU+kUn[kkWWLհ:wSs21 \MdĪWF<b-RoX9kV+-V3(6bU '5ޅ ڤz4Ua'FoW.+M -zMruhbSm Tw{雗GFúTP2EWM8zqUX(m0BC[tAswz+ ˤD*c%Uu2zHnUԪT:z\&Z%T3]SUm@Wv]NXP3]kvP8V휻֚YooZ5ʯk.|NʩlʮŸh젡Uʘ uBtchP- X$Rğ*MӴx`iD/D[TE)"H~U!IW6Vhx;84KiRјK;pt  *4-iq/ Y\΢wM(k]t5 jΑj6.qZuSP1.鶃{x:[Yj]c[!#Ͱ"+j:69 @HQ] x<0P\;;业CZP]z{}E^+\jZZzUP^iM*̥"*wjoPR1eP` VcU2W'[^JQ27:OީZ5Vjɔգw'6y* oX 9b,W,tti_sDUjnV V='PE>9"I;O-.jL jZXmPDY~s, @ "V/qp+)U U'XzԘG\L~V|雭 ;V-Jb5*nծ/j:k`I\BoCOzRNJ7knJ IJUd/1`XE%U*-Y0Ғ.>u04ҩ_MRPueTZW9!@^53Q&c:rE5Yc &z`No$ɴ^SsA%R|30ЫpU.ǟ+W sUb2zFj+a+u1G%X%]e>Dv$Zh/`=qaEPU2C]@qef_dW­[`~Īmp3iдByEX6UWj\_VRdlChS,dtF**uX} U9`v~Z׻w긊-\YS*5[b~( `VOJwH6cPul 8Vz #uS[jgg/*X/|CE.Ht삻RݗXcBUoDpaPN>aU(Sãw5`WX+R\3ҳrYq9%k֒\J=?<@gUTe>A* k&V\5*D@*\ݧ|pUEժ\,6TM4wo L;W🚺$\Uw} X–H2IX=(Wcx(&Pg*3aZ і)MU!ԫUiZYh:$$*BZ37J[Zͦ\[dWȍʯ͜Dj9@#!Uucj@]/8?HazfiSUjZE.,o.Iv`P]AVE=9'L^4Z "K*عb-\|U-JBqT*ֽ#űqߕ5ΡUٙ0kd,MڮTKJU⣾Z3 Si8$cqU,VDҾ g**̔$kXhEXB3 Qhz/43" O{,j.@{ڣVK"KY2a5D[W'hvX-4mtl''<+Y^f;2$DG,60op\}ZOU,>åؔD͸4|Js|RЈr߿_KZ}PkpH<|e(Mvok X%'W%==7د3%܂ b3*|=,J+U@ ߦgwNPղnIzyHa2#5WЭRZ=ZW![U1?~ImHʣT&Vo^уŸ]c͟71uZz'ZTG>\%W+Ѽl;f~{`vW 2丞pHCa49E)_4f2U: T].eJ˒J_3׽IkXŪܕ.媖C5Z[4lj}`=c51+*\?f+b =تS59l'G(GW8P;>کPd ߛlsğ#s-`~c[ם.vme+tbCo2ωa#R\)_D#K܇l*PQi+Q=*:;#.fF ?xzSW`&GFiS΅ lWEsto/$Jps+wUN([҈jYzI?*IXv48Уq aLEEhymgZZŜcFmSIUê4dVqSTyt/W]N￈ IDATer{Or9'y..ʕT~wTEߪ}Ru_*7V R=peW4} `5)TwӜ+❰ZzuyooZVwwB?*c}*YjհdX%a$pU`U~.ǃU4Rۙ&jf7:Vm.Y ac5Fb[ZKwMe'M[zz)?{֢bQh|~Ү`W~"I6>!蠌/ǒ;YbcuM5-RTUX%{ MrjVRaUYzׯ_rD}f0|:#hݙ@LgO,J sP۱>Ѥ&OjkUCZ VMR] : Te,6=zljul47wAW(zՂ)tP (_g7êV|Zjְ K}iQYêlOy$5TWE:ٖzUyisej5֫"ڹR zY;Vψuф]p:jɾ6+ hj`h,RdӖ5zOfa\b5ZVzo#- s YX EU6b@D݋0YpqEW].?3s&7$J9jQ5*U*!hR`=@p/gDWIzkRԿ?V)W_Z:E iy9݌ YlѻZLձ(VpQ`e5'RxsQr x2_rZU"U}F Wٝ?ÇF*z}p\ TB_*Z=Wm[֫~({ VmjUF۰|rP. [-b U'OGU㦬IV"j'&X[0JJjO$%j1UqUDkyӫJU~ZXUeމtYX'mkz_ HΕ]ԂKů WKʸ:`2@at$VR^d6@"j3n QVVVj$?9w܊A*pw[* ?obg9M;IAz_/e|)TAU |v~/St/{FU}mQI uJ\t>~kLRcaFf#qU._ -۾ĪѶ"+V)V>`u{Sqs2jj\-B;d敎TmꝘFUeIS)I@zVG0>˚UKj)fNʄղ߀T=[yiBոed ெ7K5*ղ(2y*T @5YUba +=3;V?M0TUXJ fúV>'5HW[Uj:T 훭A5 &*eYP׀*݂ե5=E`S*:S&cUnk!Nd}E^(i?X]+>ZM]jWW/zVo'bU-jui#-kcsf|VB޼US;;LU$PZgW#83Ct5`ZP BcgR5ٯjQzSGVXsrb3VKMeW+!\tͅe֭Ek;U[ư1V7`kV|^0êRaǪ{V%߰:\u9FxFLSĝUyī/Lh:kjUs-@qK^T5KVJ_߽ѪzKK.E[u\])/K%r{/Fx|33fD3dWUbuca#ڷCE)`:TUYSDH]z)*z,wDjjX\5V4zS\x`KmJZr5秜:Wi*U~T-Cs/L20sveR=H:di']*?=V3 ´bHSBѣ0#/X=%5վ a+TG}\[*nxK҂@ `JJV[1sIFTCĪUl꺧*9Rժ(HU[KRyb`hYyZO.b UXmVgK*?|m3 2Z,QىZP+뾻~!ǡ*BV:˪Vd+q؋UKcX9z\e*JW~@P %Xa Y] ;Ov՜L]UWi!52j_ڥW;ݪ1L.2z^FE24jSE Vk@~ WuDWHyϺ]gX9ۈ? gbPߗ'3;U6ZjSUrU%SYQ#?=X\Ծ\:ٴ >njo6'&nLFˣ`_%%cSD)ib%}KxxqIf dlLPl4~j4(_ lESQ *ɩ54`jL-_?}.ZGaXy$6RKqIO`XmjUg`m#V髲eY\ +ê$]B~|ƍUI\ PM#G`jjsGGȪV^cTa5@q1ZZ)iO ~p'fX r`}ǿ5>/zv O1G[\Mbuv&+W:M?0cCæ_ X)UX*zJ,3>b$qudnMZ TUkZ<@wZ=],'MkVm'Y|VB5+cB klDUL 6=)Ho$Y98j ^m`;s?:i[*Hb3b ٓ; ?RJ!d`)}(vlؐI#'X OB9@4b!Y2J(8#dR-(R؊~g~~9+e^` O8V奈Y՚窺 hJ<X͏Sә~gauL?y1oXt>m\di*ZYj钬Xէg$: $` bڒ,(oRJD՚ӸeījJ2Sl 㪿+U j5Rۯ&p#N}׿}?J%̢opUstٻSPBUZfթZK*Yi(]r?Ժ.v!EEIS,*UE\X=ſE[hMݳEm*FZkJoHJ=9) R^ U~F?E UbV;DrAZVcw_լ2ê&pUjy|[ݦoٱrX]e깨VJ\5@*o\5b7[b5醱*|x^#%Cn`ՠf3V g/fD0n}!c* q2zw%P.FUnM{\n޼XWQ6l'\To;-'*.OCկK.ލܧcuq5Q<0M2O>޽YQμ=%ZKd5KN{pKJsQ?~Hz*fIUUAX==bSbozī< ŪpU+UUj\RW1XʎExmgP7aQ˫ĪוVb2հ,TV`z M5(P,SW[T2bruU!*sVQӚrD.[]jk/_S|l25$UGaիXf\`h%]nMa‹˗d+Z]75}$tK6CA=9g*aQKu+iZǏDZ_1VSU`篯Oh6g7QZ#V-(8iu# ֦ b@̩ TV R6aI%JBfJrBOԪDRT bM,,BUJTSpԫ_ڋ?'fX% n>ȠjòuXl}6wj5*ꁎX1!vX/X4cJVԾBӞZiKTEzB JÉR)2\-Ë"1)k\*dLu?Zf T2,JJ'`&?,RMln+@V4$eddnGZcdkئ/R{/JWgg}C+43'.ㄏj4ւ޻Eի7Vk.T呸YyX~+ihMG1Vm1J%.X ;~0kلJQX+`u+؇?[S&~bǬnjZ*Ԟ8ߨQ@B\MUX=I\bE},S +ӣ;! ЌQZ;q)$`d>UW-[AЬ\K`)ie+WBWyU@?9 mo r5i.")*DbUg 8wJ@8~ E5s1E|d^ŵ˒|.Fiþus ߪUHgkay5BlgS P5̞NxqrYJ}띠6s3Su*ѴO%䭀+VѪO޾}7re@ޞTMs)FtZJ~&V{2m4FXr**j|9ZZEUU!&/JnסT{YWY 8<jv2&)Kb`.1wVIWRN^i\jDD _Ҭj[ +d맯_Cp7v O*j}Q&9#(TKi:bjV '#U2Z r/ZuM6܅HIYJ[*ײP|=OH5{hj : caԝ$rUGTN\9IK ac=dժ=aFQXTkN8mT˻f6+ uT`[5&V8OtP2T6%ªRW !ȿ?ՔwT>^,uJA2gd'a6"Z ~ZZEWޘmTAi\Γ5ҫ9UurUWU`R P>f U?_yLX5XUvQNIkASHY=  "UmhUOL9 G=r^.RG,KE} H)὜ *_V'jYaէDEHNۿg.6TVV"IEJqHR1 tX,:(BSGS*uݦ92)L@[6Dk*w /Ws?с/N Tu&+(jb*ȗ[[C" ZC[QN\͒1 /[Nʦ7?V+9ē(}!OCQ-z.ZkPտ VzTsW3!jazU4P 9v Ooq*'mKxܤϴ(rT'5:TѧjKM2]uЍv=x5$ \UʶӪ:rR-"UrV} XݧJaڬ{@=')Veiu@7dbu$ \ 7LjܬY#B-V)FڿK{0\C(ܵ(4Y*n)Oբc`Wa8_j!9X>@!cXCRljH-?X^ŁmْH[ v&m5 w+p_pXˈ957eF۪1dM+T>* .{7(}"DZKP{\Ajê]i/zZOXMKՆuciLT3?)7_ Q~*$H֫$V/`U|\ L窴v9 B>}& XUw2c^m!Vs*X5GR> 0σ{Ի_V57u IDATJ p&UG? 9*|!W+ kmdYdG؎rpCZ0BB4aUѪ7v7ecd/ *lfӫ|29J%=DJϧ;ƣF`fV?kUZ45{~lA Rk\-w-:. P5b^\V?w\*"U&3)J_w:X#<&:VdkOONXXC4X W#qгݛO#zBbݽfL`&FX 9k2"^.xCFb弔j S&WiUH`v~2PKZ8V[|5pը<~cߡURìՌxx!VWUT󷂝(Oy c:*eDy29e殺0tpvw'}{֎h~ZUik#XuT P3u_+MKx*V US TSjUN<РJʻ}BoGqu_3SϴV=sxl06 6+әmkOm|.ZwBK=R:ݺa5xsUSST/K;vl^V`FoƽFf; (K+]5V usji:ZV߄UJCâK7j\e~ojKJ?bSfks f=-K/ZGTYC vv݉Zb.Kpl9DXYC ?@Kmt':L p9@}>\S|؎OWf?Fd*:*V}j״ ,ZXSBO'Lw&&ǭvZ=9ٿWFJXݭPQ\@anDqpu*$[ݻ>2zZ*z^,R[`y_2!EO+\bm D=U Pmώ,3xA7,YZ7ʪ[:Uaw\ǪVmom:YgCƢ.ZUW82ڃZbu>ƪ/tU|oj%lZrZfRO#}UWyVmNz*WmGFGlKoU}c0M^'DzT-n΁N/n"\u~q@U=jXmqց5kJl28{ە̼Yj^Ͳv,`cZ}jz'JUUnt\RQ5!OB坿@ShUo晴O=J+ۿU)JX@Tmpm[#Wj7T r5:[WV.U_5zV OBʻ:fQ&'tMMM3͂[?<>cWjEVA*V1U#+i6nTZ#38\\Q:`iq8m y\/s#5FҬ4X<)6Pusii hPX8}~!/DH2Z"7D{Vv~d4iij˲^Vs!T=?LڎwQM \8)<&jG}>{.הQ~PU>%7qTSEr -[ƦnڍT3}P%%X%| VwX]Ŏ;y,O5Ip\j|~3hnA]!5sZPJc~ZXNgfkAڟ$dWXg ZUh3e߂+ezTiUʕMjW.3[}GP t՟0 (:)^ Y w;' ȹJsylG`uaXT* QQܖQvOb5?P=Lw=2j h=O{I>r^LO|' }SJ}SOyj}-۪s4@N S:y>1Fީ|j| +"|;*QIV1?֔*Fl{?~ãSSsTE'mjKjUV`)*Ͱ& ج`UmO0 :ŠAOhVsZUlc(|]i W 3)Gxl#GV:`uO Xk^vw X Wd֧4s"Kݓ0'@oE3]~lp_?-wpNAHT3<Ï(VMM8zǣS&vI PER}Jzu/,VSK=&[g{D[-?iGFҒ4jjP9D݆YHª59. ͖x٨3YUNua{O mB0$ cc.vNDռ-ηc@FpW(5ҮVrWaDm,[f'ՈZe ukaջlV0Z֑`U/^(V?)V/0EW''WG  WOĄS^cz TǺp?:V ;%Kh e.:W`![O)WWqR %I|-)EI?Xɓ?eG>¤>4oFJUЫRUzT#62{9_7BuiSX ^OsR+l"j,pmFXmw%MvN\Oh] fivh)^]'{CĪ.* @TZD02^+hQJXqFJ9 Un`f__l",Iu# /G +*Vk$.ba`>|'U eOUզc5,RJXo\C-/Ūji)VdEgyF6WMW jG-|>i.P''лIg؋J kK tnr`Q~t$%s*O(\wrGFϠm5fż*<q6 < fU;^ X)$OR{")t0e9U5i(HٵH 7Ce4+YS4թ8ﻅ |kfN@f1cVkzp5IJ1UX0zggC42˪$;EZvVm˪ Q j 3 U2ZYJ9vr>W|{:hmvuYX.*OYYVZe*4W|9R:ZA(&uO(*/[1a1,TqYD";x+4]Nw[8գ˩|}Ȁ; ^T4?CSba\| 3s: )w+o"ڤz˭QO:u`j*$XMq#KGV0󔸌jA6pf}cU lXNݵ,|XsDpvj' rݭ X`>ѣ_,W=V6ѕWGu T]?NN_9ܭʾ,4yZ+ZFXqO_*ژ@#ZQЇzTTJ>VGGhKIj*5y[ ~[+YOX̩Zkc=j[Md5k-Vi먚Q1Vͬq-uU|yJXm\#VzbKH%;; $/ڨVwM\]՚UA\/кSV;9Vֹ\۾p*Xf`5jEx*USJکiW)ܤRvbXMЊBI*e RKj8=*Vw=mXMqR-NXݫ`UwªW8:1X||՗5X7]z Tf-En2IpZVfMp js IDATw``5SV6z/ƨZRJdb5ǡz@\;v ٪M.Ϥ[uTU.lT0O:U:rײ⍬`JuW`jU)EqAV%oSWcUfAǼy{R|=K@X׸2 v,5{]e5 4(RHRt;VGVUceI-36Uus,^K*VIhqr-9rhkң`U/^?ܴn9tI>s9-}WUB5E+'Wၫ쫊פ W<êoqUʙ:~~ T\>u#BUlOY 2V/cXRuƜ.eU½ԶQWz%\EBCjF~漣=swXsjêUjP ѷb&pfVٚ?EܰVy*qA?S2[\ꀅqX w*|h-j|xܥo7&c=QJRUS Udձ%F\>:92* RSoૣU9OؠUj 姧Mg(B^}iI֕SVU*S^%eW 6U ƣ.T@ܩ7wo~sLq+@+H#'Si2~<lE{,pT2alV =/.B3AhtRS=s۫u|K4t腹:s:@|Xr/ TPՕv!Z!V솕xzUBgV+XE:³ݱ$u яj4iRcʫfJ.<&*aactVŨWhXɪ W)yjbLTH&L=&FXP,b;URI> ǩs*(?8t.yZrTEFn67BVF&*4b4U7/rUI RUE X͞r$RڧlWUrXglZM5Nw.gED!0*vja5bV= Z =7zf._]'PUe 23y `Sϗr1W+V إGb_UiH&'cFVU2ءXFw)uEZb/`8{edc99WaLP hݳ P^~TԻR3n6_8\i*%*朏J&ՕR)LT][|I_Є3wTUZʧ,.\S1HyNh,0ubR[:ʏ>AV3,@Pe\J-Vlk b5A>_Ո`C^X}aU*%Td̆ZgTC6_|~U&6jDv`ݶ8P߾6N?A{"Hq W}`>̪p=%gg &7@Uqv䱚lh=w!{Y %Dz*P/H^A4%c5Ô"$ o Tppc}* Yaŝ+"JBݫXT8fC >]\̘*y2SwƪW+VaձV ZVVjat]L=!VXfw (YG6oߦfP w=%U/?]#މ5y WI'a'n+ .*W_J *+ξ.\뻀uUom3_MW\3+H"/.%`ywN4czBfO(ԀʞSvXEEh^XY+ZYZ}Ya?fd Hǣ߽q虤(چLt^>%惾wliU O{X%} KAH88zޡŨWXY4 Vɫ^gXm_.ŭW?]X]*_܈ݤ,V]?J>m'3ן" U0BO{յ*q3Go0:5{Yu`£Xi74<-LsêSO٧OaNH915YއUCVÅdd5Zfjq!WmPyނXTd%JVRB.O5k.aSUgV4{LU^v \!cbaaq*UW7mkȰjS`gVr$X-p _!i(R*FQ%|bzP)PWJ."9TUV?:<UMPPEhX(R7TkY*CN: ?OIXJ5Bj3ZUjӭtP\:qVs jl .XU> 8CVϚ>.P堠 7VR<H-!UjNXUu{x3}~ XMx*4W#xΏk4srwmGh+Fw Y*~]m bb *؜SqrID$>oª4QG΃5Gj%֔տ^jZ 7hqc-θ%1 ʝU.VW+W6L݀:rMt`U ,M*ĥ*jK dW\U+IꁪZKo oXUM]rU JYԒb u/Cwh,|"~Q*j 3,ÉWZ&}j+㋞69Ç;?߆Lew]U& KU68>v [ư궀_%DLӰEpVЙaa+!_UaՃGUV 0^jEUߋpe# U֏X 9LX7jGgJ|ĩeݚb?~Q)$W* )Vyw@7JSZH(_Lm'TnSI?LY@ѪV]LJ Ud1U`ˆ I(OuS+V7ʆֻc"CwXXa THnLT\-eVxWVUF뤒Ut.!R 8?G* z ԕ4zH̞iMmejc:/_HGQ: 7V|AX],.oYXXŌOWWUEyGky %MԪsh-1H%/";0`Gw /MЂU{Aÿ> kU%1S`W+HxNRsUٞU^}˟)KGUS(Q>U8^PsoTr 4T $e ޶\Tq:M*5Hw4߉I5 T_h/P U/dLT; WM@ݥԕ )J3KJSoUͩR57A_LGv*øFD@(,ժ9u|Y*RLރX5'oۅwdjUUji4 *q%M>;v֦`]cuJ˥>r%Ǫۑbr0g*TY89CV2B>V$|&68JeBPU+Z>f*dTKjwpV^3S*WuT[RO!S_O*|Үj4rTĬ-2LWr7 VayUeagb58i+\*>)}*gQ*WZ&@zʬKVŔI\9UxvZ=Ge`L,SWt\=hc`d Vg*Qk;D:Q龬FE^d']S>*f*?3`W W?}\Wv;G Tg2K]bx† >.P7xwjUb;b&#8X UKRUj:TX s,X}VjK kXU5I(UNTÐ߬,j*oRiVŪ: ͵ңXMnLʤdmYA/JUBՒ;ZLR~f\v6w4ցFxpsV\-=V:c7'?a8V'=Xmsu X}5WVY?,g8eN!gӰ+<xTSPfxHZ^YrA3WES[U&[qI*;ɫ2@j,K+lJѩ&U\-M`^eI팟d]-tŖUXVfϪF%a'HX=XjU/Ⳗih_:nEkp7FN*9 m[J5oƭʲժrڴWu| V4?S796V}JY-5\X%6@Uc}}oEbkFUQ%R`*Ra&Wx _c( RN[5u,Ņ+TS/e9R*&l U* {ERNUjaUMJ[V+sPuLirZ6USru)D #X;ՄՇ7.V/ⳖĎ?<(,é_: VR}8 m:~X~EMLAn&X5][;; cD2KW0U VS Ϧ_u2۪;.LTaOk6RS-g>5+;{h=PЧД_[ޙ:Uj$%%+>5inҢȬ KBs՚쟷KuNq 7w:R]c {!T@%J"UeX>\ӂ s4hqZmqi-Z&Q{qrmE-%~(RX9w\z'7-Lqª,[M؍d ̗na**#vP`Zú%يz[<+JA XUqfSbWOjXRՄUj5T*)I bUVL@Sc1շ0g}Xʹ:U*ba5GU J&P^F:_#VpZ?BviƛS7PMO*_nO9qS]۷Tkh',Fu^X} >K.L'>&F卦:\E҆$NXsKCr3 I$hhͱyu&V2))nĩ-0n_Ͳ]~> ⓋUqoulkՄdmMiqz#Fǰ:utϪ%UEI/V)t`uX>7Pzj[hJU=7^1<#eI 3BL%Q~rTj0SM6s p{b+0_[Jw-GLkBPyE Xif/+efVռ}ONURjʋQ+3`%+?]NqY&)?p@wO u+/4LcS Uu+cZU :<;X JfyX6e2޷`{_-L V)fR+{^#1u ^DoV+U\iG6.~3iݳv7U%3hS/-9(cw̒}/RI8rTX{ j]NA|bVr`|a.mB\Kx OJҟ4&cV=踄o,i1]|B~ IDAT:SYϐ:W՚5WK%0pluR6Ox+.(G*U N17bn+EͫBD DU0 `w~d"8ح.棦vDMH="J+pXU{?Vc"]X{'Fue kŪS7+:$Bd(g5>W}l(%QV,:uNeZ n$pzirNiqQJzd_:EfSj)1bA`m4Tj_W&JGeS.2w0LV'vOroոCq{*]}> @)-jyn^n]xfD)4P cGTJoKU۱գa5Q5Ԥ`uGw=#uc5޹ t:=aٕjO@Jo)o wX5jTPA^ !Wݐz~*|q91Tr#ug4B:;aI_:y;7)Edq v&"*MmTVV53Pj Yɘnz5'|BdP敥d.*BxMQr>;56TxЀUt o(gC'6_&]}luX5'^"&&6V*V8kêjᰚ|$XMoVتxaK*ʥ>cVG5*B \>T9U5WE&e6emѨ \u}"%,a)ciaԒ/+i":W(T<J'[ݺ*Wj\LI]۠U(ڵ.)6ȄLMн͉[ *K totD,ov"v;4 :hV}lbGdύhjjҲhtNr)UUQD!P)++K ?jv/b_(a\9 L]{fK 7qsCݰ q`|`ѷ׸zz {t p܍ٲֿ́i3_`T fhcCADUq2ZV-eD:)TdI{/'&81\+ ߬%H9w]V.<)p$V!m6(;VII |^O u;љqT|QK?&V= +Ls\j؈ohJIKiX+VyG'k)af)9tj+ig>HMYӆ1ʷG%:?HXb:AxH>ΐjj&:p>hP35n=cqUJؚtV ś޽|] }gPqXwFVW! \uHYcJj$M,#q,A`PN֬&vhAA5v5eUUsL6#;uzV(#Amh`'ͅ)Ds$aՔLTREǟ3ՑaK?$/ 卋|jjI|+DTjj4wC1}S\a`tFY WGV \{nѳWW&P5ٟ})6Mn`Ҷa@6XM- S`Bp2*%olsEV_SL0J`v#UVkE|Uey#}sÆUnVd&nJx SW*?"2Jd:l'bT9Ǒ5!&͐1˦K;ܗfqo NU) >LOl86:K@7WoQ}wJǂVz~QRx2n5[=GX}DG RMm'ri@VBωw$U>Wdal刄Vq Nc*yTQ*رlu7hp9^M7YWQYԦ'b2=Ԯ%SlEUUP. 5όcFjjSE@fQ'jM{2:o:v-ivyK`9I3Er2VMj,8V*i71Lu.`Tվ[\ѥ~oOU_șX@g H/ Pf3h&Bu U<xdzP8 pTUnHހ)[ U^jXQl[w'5_ZTEuWxsA&V4(d/JR8duAԬo<P;t8Y R:xFw S@1l,ʁO|NRo!8$߷RȄ aU_& S]ęWW/w׏7ֻQ@ _M&,؏VCII$btP дLj9EUХd5Yzi(*S S?r24zK+`%D[l/U֙WU2EOro: \Q 3\Y4SBQ=)UBڵ%t/mX8Ƒ:$ZW\mS`2ća5mX(\jMV_*fn?*OG06A$N9=lṵJ#(F\|'__<` \TUxOڪI*SO'jşpYLAfВ"tujY.rQ>ʪ[PU5@Z?+Re>Vg5nr5ooefytr7eY|JuPfr au7\ Uc~RVʸFy*Iw]xv˿MwFf `ErEZ(Q@`Mtk580BjV O?yv8ӈW堨*OoE*TqOS4,TROD.IbJ랗BZp3?wWvqH)~W[Ug&SڐG^gPDH)@ D.\h//UVcܫy߅TlA2uhҜSZU0r"Tm@1(C뼺_SffL l^ZZTRڞL)0UgcEn,oE?Y+ ;S,x/)r$?V0UόѕA`KQhs=җV?vpn\}4moō^f{/o<|ߣ,jxf8z?{֛Sx_e;y j7KJ{ OP˳Wpg-~Ǣ*bu,9TKj RV"LUL.wn4ILuUTZs "5ITMR3+jeȩ`emHHwH"չ2L뤬̉OBKw㾬Lک*$ZA)JO`鳪 75t\Zu/W~>ܦB.e2+X W=|jX "Vʯ}݋|뜰XQX{JY 'ٹ>r5e%AM1@ ʆWI&US*5LQ~9V&EN+E8,U)TVeԢMME&WrmZ 2[]7JyBXMh Ԥ)R ִ&T8@5Vh/Wcѹ`5 &V0d}XM<>˓ bzoxsU$ >] Vٗ⋷6Gv|\ebJUrUL4MWޜj z^r[T 9ZTAUQY:/)Uk|ܲ)MtTn]="[Ԏ;KGMV{S2%۴fv T7"Z|6Uc/G0XE9תJ-@ w ._$Q]D/VsB]cUJTWn҃?d[)k-Z(nYQrD£DVwG^fNj0yY-ViXJ}VV-1zINCr9(VJZ,`tՋkkb =' HItP$v4pioXj:JT @]\%hJqytڬ^|#x{^\2/cNZz5FH",0&AaVtU0R*p$W$Q~o'*Pc \AmW˖c6_Ī0Vos^=ڋmnjՏ}l-VR XTkʰj k_s VWVV@.K.U!W!JJu>:Ta%**lmk(}9"1m ' (1>0EJPP%CZy֎cŪ&˰ҤN:iԒ  US5*FTVERs;k"U4R3)4VM x`6;bx)mGOo䫫ySsN/ T-OaՀĥIoTU ?"VU{J-#U%Y*[VGv?d4s}bO}$lM[[:rxUVgBӫB\Ն-VMJ\e cZ5UL?p`h^2H7c%(j6jZU,@eXĄNS UdjښbUZŪwT͐~ˏĐ)(M@ÈCհ~gԁ*2Ej)H[ܪq%c t(AX8M5`+0:bU,DsT,"= \^bPX =Q٧/^Sr[QE f'zSLq4 @^'wP9U5W'lJmuluSqwWU}']d̒ QK2Tnjry+}'])r+TCE .&tv1r5c;F[Xbb>jU#sJ/YnU-ZX3 %>Y Z?}ɀE5VS}꘳w1V϶~U}**GhLz0VUAMv{F0`HО-a|gUvOV8 iO]YPc!jUH.#)z} I|ҏd@ 9RgJ6g\M*}mkAgs f J*dm,\{lx" puj$_PTXB0`7"6imC){%ۉ`7kkI~7;A̶-)%Lr'SKmfXV=z\#>XUbFh Z)ϺY6*d2gs!1!t&A8Ⱥl9 W(+@VITpx 7H͌1z`ni-f>ʑ%{*^>=-m~VK77LHP5X:"րsҒsTF4hd݊W8YmMlyn߇G`njNXnbh^ ]/VL?ٟyVTBZ9`wu&$տooLUiIwR5bRpZRj.SDX-|yrxx+0R;襾i*{Γ`s`U*p5^e )yoF&TYIJT$fI NGcUi z-K/u~/?(ZbWTAy_VA2'# ΅ZY5sc6RzYg'O@DJTZϣLR\ꬫ{UǪSTUXU_?G֡>ĩy[u|X&rשso6*{j9 Uw͇UiJe=aVA߻g։.7Dv͛ª4~httF&'l˱ N"~VerUS.YղZ%HU[%R8.ۃ"dWLjɂ[u-X݂L^k\}ejPuz/?iB 1T.p>x9i xcOUbSl5U82VT34WB= p߃_XàNt ^j.᫈)#H5bռe"Tu2oZ9&d{d_0tW} 1@dsjRmS\Ց˷;C]DB5b*_(ªP*`@d>|X%`YUU,V+eS\o wwwRj:uqZV{ o[5\}B'kd#ճLo,$nK _34c4Sc}ljڻA,S Vl>ռbn1ЋwbXmrX-ֲ}H-=U=M]Lԩ#>*j`LH=ۑ Gc5)&*jEu䴚lGeȚP}jՙj%8ݴRxjP}F'ZRSW2 VZ˱b[%5K`⦷bViؼʍYQ ]'/mS\!tWp5Kjajϡ`[ ݐn̈.Z f֤j]l;mX\( &`IJ׌@$)U\@7յ³ RrTA^U ]'xzзm,)$ ]ڞ\Wz*eXL|-- VhLzy IENDB`mirage-0.7.2/docs/screenshots/05-main-pane-small.png000066400000000000000000001522171407747233600222040ustar00rootroot00000000000000PNG  IHDR+a#PLTE ., -*)),+'0 ('3+!/'1+7$1)4&. + )6*+"- #(3  ! %!5 $ $@?"7$.,9!2(e%> "  "'(? #9&=%; %' & 5  )#  1%,-*A$" . +!#-D 1I'+ EQ]LU`)4JQV@MX)2NYe,Al)J ;V5047A)/6e+ )KKp $S3r>AQ|1Sл&,4#! pI5SyyO1.Lv.Ǣ?C%Ɵj2FAwS,aOCnS~TQ?8ͰȻEaiq6>13sfPi.r*On^^`mUpJ~FpᢟG503g˛~t&noKofXI쮰>FM&ə9}<τ_ E7F]3f,ð,yzBa3(7\a~\2|'t$9Ēz O apjf. sudhoEQ8M.RhfLD4SS 6 GpQq=Nٌ<XZ!WA.0s2bl pMiD.G lRӵ>t,Al\8NeT艒exL>B9lH6^rVЀwbB,ӍƾiDt ;$# +UDP8{ĦڥdX1T8, OT#^LW9GuF$|W+wlM}@qM >7CyXeϢґA@g!l߭|0rpzA+UQU m!nL41-L G˖lt`&&<3@F] rH6#ldl ma6L05:gN0TwBx峉W-y$v+N "9A\as~'q'](-%[ k|Iu';Rq:Z$#Ƴ=쯎g=ݱpP^ųA[ 7|R8(۬WƳ8HN .Y}x䖆Il.54] рÅmP8h"m]TIyH(*LJCi[p֌s8j6'sxT1O#ũk8wa¡ѰYG3ct(9FjZH6>x"?pFlV.tFD]<֠ d׺PXfm}-*+ ԝ! norn<^ݴ OEA7`wxR".4S6|_P';-ȐUlnOCO Vf T+GÅÁXt2klcϽ6r%).U8(?P-xگh*©r|8|u ڡD{p!jnh(.ƫGM8in1dgѓZnyPK.peml`+f[of6Cu<F6j8<Hqii|G*AH֢d<67>Ǒ,N5x*5BNDIjۨ6 'ԧoT>-~ ٷV/ɜQ"g}|~wnd#z+KNnVᐥǭT>'p85{'ghLCgKPq GjLFal,އ*pil[{6?6zGv쀷|31Lwti8CV6˂-`3:~T9L'ME%FK|3%VQ/*<2YqPF Ⱦ%^ҩ)uBAeߌ.y fuD+KydcVf@l66 lȕY'lhbl<&[ tgӅNMv,G Adʪ#lt '!bh%8tɻ&#xeENw: 'ǽ%W4@ 1_ J_xwlo" T-Dӧi"voKԗ d3fuͭΗrev<1ݢ6~=676y.αMZg2-âKx.}P\8OIV͊dNlx 3_..@Ҏ[N Aѣ -u$Ao+}ao暃Wܟ},|xUO xҾG[,s;0 nݤpMi?^ vpu#tK.>:v)Cs8̝8T&ДC gc jyKؘ1g@@ ĎAwo򥖸]d&ß{KO?9nOO|Wz$:(R^z #tƧt 3 ;O+li,R@fNKFlx~:@؈Pkfڋ)rT=M"X{ =*)O͵ږ/}4,S)ȓ{> 6w 8cHv![G->&ِZS8:@s y0#GOE`c!ޞy@mDHy}[,~sB._{ڥA88̤cpslk. [6Km@8}8OugM'ً|#1DH%yr;${v-^+b7èٵqq\cH3]&$!*XQ(t-IB0,.W-{Q/89g$uزt3f3>,uz?BB[p:ΦO=I.tטNY13֬;12u]،hGcD{-Fa;'u'j^M7Bag#D;=7*j9g0[GD5TrM@Ld52zѹ+ ]8 'kmHma[V3^~ZH)`C}gݛ'2`:f=ӣVQߓS 繄8~ M3@N-V4sTMMxj>Lkޙg{ؘuro=?cFUۣ?x{,V, ti^{nd[1pͩ fD6'.DkMes"O)t͂7P}ÍP}HKgwO`6OX6HrvNwO=Tzj{: :fa#ɦfb lklI(8<}`QQE+:c nް6zanx `1tq8NaSymzp/v[lʒ3G;ν{)T'XecVQR$- "I93)[" pp!#a{Sfa'c6QY# (/jGnR6`,G+kvwkDD3H$P RkHwLl9N^D1,mZR`Hd!W5bdDŽkӈZdV7on6Zl"dpKl$m}"ـ'i{F2@҂cAGR nGlUd987M Ezp ţlpa"q"h7٤^?g?yAЄ֦6a8tgQt (G25Y@p*2Rl~w= {]M!b6@tj>ޛ܉U={O\u$ɷ6c== lhb0knP$lRſpE P*sڼ͆Myp *Ϛpj>sTc-H-njv40g?gOHݠb=bδ8FvJd.GD4E!OX_ebL*m4&&%Q-yT4݄G\)/X.WWW׽UD{p6FV.FW4o寂hFh`F)8bU}ܺfrD9eJ%X42.Toc+zC-noİ@z޽F2j9٘W]aΈtIybase2!Re#,-?,~U6X7뫻@Y~J'?8q!p0d4$iC[p6,p#V|4 F+f .6cQedMs!DZԏ4 %^{y>4+,,ru7bSl+;J3)6JSL)l@9w4e/B,j鰹|DYJ|G3S3$hb-?P6l^(!Vzî?)l7#g8H#k:+=}J QW&\*԰Z ;b hAG^[?(DmhlaoDPYʇgZtvGz>uQbqfn~rAȹDoX7cI97@PB=`hEj?_99mJifl`DQjRlæ]Qb #ā߂IuDTGKX+CACF(]UZ7|h(5Ҹ۽*<t>ؕ8kCOoe_X mgHƫv.>,O[Z^"5nx1~vVtziIvk>l 7ƿtdlnh1ï//_>7?kc Ub[#nx@NpH,o8u%'"k*M^'&rK Kj+jge^Sw:l} *ѠqlUClaY%-e+?6IlNT^b_pܺA oXj*`t ^P;orWm)LsJŋ>3! ~#eH>7%}QTp[>$ygP"LÕ1[3kSv1"1bdsR2f54/^rJP7ܒBswݬ0 ^l-p=9Iӟ/ )Z:FcLu6{6m0nQI5e:łFfE#l8 Qyysh˸_hS-_تf}Fl>_*k7E6olZȲ02š RQ0S0Ddzz5fB夃IL, 2WWe1RW^_Q헛xho'6NTLPhˬCx"W4WLPD>o{S]cqW6orwqWfMt)hZ֏aW?l0,%~F ha,qvMpnlƹF`b1t/͍s`C͠|'Hb3[#6<'m S]܁[VRґC%Ʌ%7YJ( zeV88|:AtwSW. K+ooig.|lIl8 |"A Wsf $$+ҁiaf:|cG5lj5a=vǸ(=hЁQĐ=_<ʷNW￰]?|Bi~l.޿Xؽt:r4Q [جAS*䚎$G+1ε6P#6(v]2SD&>c6ᘣ7EI(S k\oT"p*f42WZoClrM5t:b+I&9&1BU!:`SHmeN  K29~`eS"d1g=DOs|k䳧~Ż8 ƒfe`6؊hE%%BRb`8lNMMTϔm>TOuR} Zad~hMvi\ƪTe(p6lIVSvWe!KC JhQ#G3+Nw 2DGy)It5%4hlkQ$NaPm;=8X *jD!0s^e3#oCT)ࡉE IDAT@qݱ=9TSG^>hN^͛Eu#+dXm'NQP`eHi&R2u3k1,ғ0'DMQBdGE"'sڣTg?E]se Gg$ڎ 9kg#HptWMU^H8 +݌x^нK^FA814s[zTBT\JA *.6&fpA]h)K[t鹓+=$ L>:6N^v%á 3VN9T!*}kC.[iOFG(ce>n'GnMNh`T_ O4R Ɯ 2!ӃF5͠s#$xVj0ʼl0no,O*+٘K7=򈍶l[r:nD|Xΐf>VZeꏲc,RQ=e>C4Ckg!*)!sȦ=i[v6 G&z'vV1a,eD<:f~(C6 f+wWcMxWSu7xm.Ֆp̥l4XB5i3zibVh':O8Nbug/蝬BiUn|of!zxMTaS8v(SlUihY,oTs ¿0,NxhKhTI ǍX:ׂ#8[6r-a\j? qax1/3ܙdqaE&!Ų6Y  *x7/p(yy[}U眖gq{ Ȳ:_թyܽw|#htCG1T]$h 8 7ѐ#2> (̕)|*Gi[T/(S dWڙɼ?KX*L8&F潹<V#,u-N&Gǜ$kNX9$Dд[G{K] Eȸy#Ӌ`c=o vc24 6A)?dXP{p \>U6Yʨ-G}:NYZic+ļ! (EH̠8]^5ϡGHt 06/M_l0P[x~>cI$^_;ҎKz/rq,~(q($ﰡGs)-$ة3({ ;~իp1Siy, ,bq08 l\LW^OCP:F\al$L~d2@kƜqʆ{`_m3ٌ.u1/Hz&?}p,ptG7]]U<;) ,aFaR0p5}Y٩h"UYnS8 xmQ4j*ӴԹ2k=-mg4"X|lQ7# 6ld# 3 656}ѯEʆ‰'S'p)z}̸`ZÀ^"ʇ l>gSjv= l۷Άh({I㒲![`3g!y+nja\^}Z>P"}*aHt[\cS0{3eʰRBΊ ' mb NfQBcۮCÜv&|qZr l)hu|ۍA:yZ <["mB, sy >|*ˬAnF`Y\Si\5xz|Lv#K~0j1U*56MN8$p&^.\6JbKYT0axOkbgx+Dq%AVp AAFˎ`#p f>gmM S5yI6@d*l} SbbYuAlJz3i^"w8ׇpL %!kZp{,d;NՋg7=i*w2AnZ5i,s`[2Aw5`/s#pp(-&c==JsˡRXff8,JWˠ9ԋJY.S=?oF mo .Qf6o_.Jr,Ţ~Z`])p#MaSг*4I!P0jgfuY6v;`+48X&z ;yQ:;b57EǗoHq$fվ1 ;ex͂"7 56rU *̈́l)Q8dYG6ķQ4KY;eAS,Q6Ddh"Wy׈0(uW(h6>d~h#/Rfb@jσ%Y@}Aw6t)8)Bk%FAzC~f,ڍh.;p2[靲)( o?dzY9>AfL̮ʆ_"?#h\ѫ4mo@+* +6 ;PnUaS|}Y6=6UKP.r~cus)@q0Y,(nB%hr]Ab ޮ/se#!Y_oA>ss BXL!- O1Np9e:{l{8୊lO$f`sAGLKhj_"m6YD8H>V]_QeXkC+o2q(yfTS'SU/vϔ͙ *eӪe7Y_ I:0 lpy_ó֊M7U/l%fF}qOٌi5l2הTy 6?uQ,lScIlGHSTv3r]6]y!"G=8J ^"˿fdCYDeٲ(wo>]袤wMgƛv7iEc{DŽ XYlB3(1+A?K!H;9}l#ojc z`''ڗؼ[/ˋ{2۵>Y5˝OF=:*ߥꉕYZfuutœE6n"#DۇߟOlo$onuJKL IcړblFu@Sʆ0z?fdnq& (-l"Yh饶Ћ,(A8.gg]"Ϙ[uNխKʙ?KrǺu #6!_!hUWyL.{j=~eFl ]<})-ԑiBM5lF̥vzU&I],u͙ ]fgA/.\` GG=6?cËB–adq iG; ZdTZ3挕1css3*}{M qz]{+jn1%>c1\&u2171+uRl$6h s2t9!].0=R69aIlCIcsI#lya9͵VFMQϖlV|Q5֬̈́AHլHLVOVWM$&,~j85(g -0(6`8| vQ}º{.=lƢ kMF{Y}{lZ;IrMQ.IF8.\^H>zc`y|&pMm.<ء6nģuj:.jD=m^[lXh[B31C_yMK@PXPSo$5BES͵Ux`QRC!fF^\GeOR鴅f6*Vph?{xSyww{ޡ8&ֲ2Kڏ9pp R#>B34޳[XxЕ<Φ"UfcTM5WL4dev;@o鷻/cC>aeBU_&<f|Q? F\>7Q_3\ F)tlĐp* g+J􄲦0r}#o`߿zk ۦ$w6𢴜/B/支VhsER5 XJ!+ S6&9zJܷ~^(2a1o_~>t(,^4}3qوssc[ d#]ÍAu.~r\wh)7l1bg8vMɧkl#6h70hOչ T[ڙ{nl`wŒQ"f,Eb55n:qr [~/~&Ӳ܁PCexKqbNl8b(rX^4o䓉;336:HPE4[Y˗luy8;1N`cGThP07iqC&<֍tXʝBd3U 仳Ud2 Y 9Ɗa0]KǗڭVpPT\a'"_Sveokefƾaʑ##ΥҭM.nzw֎EB ⷖ:v(61Cԕt,}I3@?p D61Â@\z\Mp:`d6|DT:J; „Ps)b sB#U5vg|NIyl; pWP|MoEM &t7d(lbnFM~"H Ktɭ'˱#z!$p`٦ǫeAtf!/qXR_g 6=]6T4U^B[ٜN2`s;lR\- 63uƫ8u:&E}_ՆKST$umyC~BoxM 6E6Fᨫ* zgīryO'A9Lv@|bƏQ.4GB[LOyHaHDe0`!h7<4Vquj6Fd4U+tl./aʀo9ZWQw'*3zNgjl>$șc{9z3]{9x}^_ 'P7uWYK(YLڷxAOd7#.g7ʖ6 yL(۽\f**w u / ZN mlOJ *q[m: ‚9 YN8gCYf`(Iq8WfF^Nn}Y^jL<.{PtCCy5im8cd%XEYI3)Lo߹|ZF6Ɏ/{̻KXf2ԝIw+l4kbWxɳ,GOijwS|iBmQ`l”Mɹok$K?{a$ךVr|Qvm;`ra?*e9ƈCnrS)pl5V?P]jMp!w~Y604M&a'}&~־wN+mDŽen'z?O ,w2 %L7w :NNc2 cEnn}N5onT3Hڕ^^~~aa!wx}Y}=軘LԻR1aTJǩ LjAe%eSL5FYnW5V 7-[.ԕsY_z V-n ?lpRQb[xy~vKaz{wr&tX iye!]`TՠFzNk'*z,4$aGc"lbU >Jg) 3\pj4[Tle -aP"[f)wXˢ:,(gs%wnADR"2Klm ӆUx]>.>ǻ ʇ#~,BF4QYP࠲lsYR7%Ta;)bhڇv(~w:T#_khjqiWfK泲Z{؛A6nM ;%6U- vFv|Mlc8]vL񈚘{ȰpP=ڳW+r΂ł~s{;c>ƺ;뭱ΎX %6g~eܤVF1ɹIhKR*CAc)8Vj~K4ش* *(5 |Dl@Au&EB|wKDi[Θ Y`}2y:"\xU_zctr 2b ⚚ɚٞ*b# 糭F5 S~9%6ۇ?|!JtXrf>F ⨌+-)x\C ټ28)\\2$H2 >&V )kT?p.b8W+sBYW-`7>\ rCl_pڗ> '0Pv=܌YBV@߱QZJ9{`iKn +@;f8tN)Ƴ3K?\{Kl{Y"mH3nQ%u~Dr| ̦zxx/>pvnieWM%} k{vp7"1MN9$Q "6|kDl5 ~[É_9R85'6rȆ~N/WZS Bm|@oߎW I0b,fM*NkltЦu(N<{vrِnc\:z͕ͤ! mv՚7Xכ $9&y}b69DѶ}/0? Y<ƜXn(c}6eFThk_}6g}evÁh\$8m]ml!l͛7`S\m'yk7|qN)n+?մ&ܫ:K40Q”^Q|`/ksB_N˲UlP%nyuلr?\\%>Tfb06vVJ$3N"6V U>4 q4}VL,#l nS7|[|Kd ltf\)-G+ iᇜ[w tȜj"߼S:Apy2MŅxe-}זG2 ;/(&vꏞpwtNbRE';~;/#>2J#cͽ׈ ʕ^%4xnweKOm*BF8sq9JU`CM~b[Z(f 5/)ޕT܇1 1:b36lkf 8?aG5Eg14^9C|NYj!6tM ځV ǽL L]j#zkZ5k0]ʞxڥ11 Rsp/hnMP'֤8Xbb}.%C+CF\ P-K*tkINlC "ޙ < AQ=HFۘ ^32bh8!8Lf{8KZ&y -IYġp+$ǩ75ul2Rg8Iɱc|~e/^he eNNG 3Mg$%?d蘹7NEC5X_$N㪥3\T޸\_٦7y朜tA^Lhv+pF{ l׏OHS *0lPl4D3މ벬ݕIMW`P;c>9=33Aßm4" )uMnr+r˥YY'n>~Ҍظͱ>B*;?ly&2<ZOBzŎ `3)D4@WlJc6Wa*$U Mߍ14!!K 99Ayhu者e'OXn_a}#*֚g3<7 #Ny~aDl0<ܵ6Y@cW0 tQxlmnh2c3P6rϲahؐf _%qhge$ϝJ)Mq96:i+7w ;FeeXpTc;d#h/27 3c@$L&ceD ]=6'Ƭ5#lA .mv" QtlmOlxYerמb\ٹg'u&7PpRFL%X2_ugf+1×, ]'iN ;p{=P!# ;RE,̈:̙K06NԃKlRVtkX:ɟ'M;OpJe[4#6/lV*틵_#׿m8SM1 S֡p*y^xC!]kAI8TJlG%sؤ%Qqq BnZ>): ew wLmņllJw߯V*uapIWuF*!EJ2"2YENb :!1MUO.ųh@b.UMF=X#)&ˏ*T<?fek뱢Sqf.@be4%t~e: a9JW#nN,ZԔ0KyZ`9Y0^@Hņ36gcƏ@Yj Իuhh6]J" ccгT10zRŸ"|?8ݒ ubmMQ%Js6Qi9N]uYlB liSf|D#YZKE y8aZBЇ1hv6q9'D)1bUl~@wը3JOݰkTC NMH G5+6n[()aC1af.h`^\٠#ջdDJWh6ՓhhAC'Jc>umia{-v88Q5CEgb?%2XK T+ #ԭiG>.U=<UL)6[] M]z b/u }FՂ.c1!D[$2r.>L=Jz`hl06}*j2!!|?ݼlnkc4+5xaVlԘZK@敜a-M]X$Y촻Gf3 jc?xō݆nԦU/pMl[7:_q 7^ldZۍd2=bMʯr&=/T;f\&8}7Wgޑ7~3x+ͻ3f LF k;xl><ph"&μHr`GH4f(E<+-0 [O0ljm)'O7??oW𽣷Wd<+I6 wﯾy{i>뷯-ԒtYO6Evav6 jNK&Y 1h8rff=h#e9ά̟46ɦPc6n}l_I^q喰$dS*9IZHL"RtEaЏSpHœ#dzyG|_}Z{@~]<~$IGt؄!Y`3fsqq lK}flQ6>ӧlՂЄ_-g,gSMO?/1vFI9q"cK36* cq (4\YMlund>\U]]l\&n>_i*A{xb FgsQ_07Eo\M-eMŎKk^kV[hql0/$`ngYm `l$ 7y !mF4:l,nnp^Rtt؉>}MϰB3Z::s@B66U<^]6r,NڍxnK^oN[o9-JRNOTZ\86y7^j١٨n?Ɯ7Rݮm4{Ч[M1 z睸^][:KaJ +d:j;ūP9fCR>Tl< /icl j׶AБh:n૳4m?DtuVC,ѱTu :XL/5GN5ɐzzI*Ko6.^\x<:3e-6hFC1߷\gT}.Ǹİg)1TOJЁ'`y I0id pG6u QEI)PyG:p g#jefǶlFO0!C3aK֪cʎ,K1p0LڝuIzjyutb1Gl$G+gPYE6&LaZ ndCp:\[,ha?H, 4l6l N<3i6wblv8lb:%//cq+{~~#ݕxH(!E}-IL1[`00p҈@[S!'i EȈ@lK)dῠH9k"33gf~#du) ]Aw1!t,\.\v`6m*noȳ%PʕU QV>=ȳq"Z3rI^tPjc (xSm$ÔZηS/{1Ǻg_|R'M <9d+cGaD٘O^qmHtSZ뮟-P3;?]>{zn֓C kd#Nl3b#GgK +aw3Vߤ }o5{F2԰JMDt&ͦ26vqZ5(\cbsOZ(ٸBVW  -gEP7 acnH<$E-PbD/_ʆ$ګ^יnRR +S+hG<*~iTnƎ _EɱDs9W/ ]tnҥ\}3֠^>$}V\\0)ZDiʫΙ.~j ˜U, i"6-hTdka6uHh*l|Pӎ(S0OɂfYXM ~0|6-a#gc4_ ha2n~k 1JEH1f4b[L##MY66]DIW- ]8֖/}Rpe> c1Ylt=hk7> @4.Gk 8ٴK긮DX;1:CV6mrF^ =6.,M%LpR#"\vX MQ2JrRМ#O8GNq{"m]FvԯꅼܻO4RZYҌZ3 H`y_`[;R&qc6Vvs.s )󌝼mAM 躄Q_¦V ӂڴYIX6+7͝O * {T׊B.#g\x$}Zj IDATpT0a_[˦mc&N^$edݠA-o3N v4fY䤢Fb5 ϺLdɐ[PtOUFdpy*rL-q UhݻgێL, 2B4SGӻ0eϑnfdža3$>oERtͬu<e+ :e U&Ĺ { Ictd@2TN7S@rIY*LOmuT`݁nԠ7 MT6( l٨q(=g#څېa#3(хk.F lR};E a&ߙ8g!x[7RW]uV?N켌llA+x&f_B[,lAm'*Iâ>7R6JX33K r9 ZԻ&Pk;9Yj շoďSτ &rl ~)e6݊ 5,iلDF&J- o|i #A$F>a(o&my#c |̯3,d$n.TSu֥N[CM'DE #JX?kj6tki0}Ez(sݺpMa69*vp6pna;֫h C#EHJ/ٲzYZrtj6t8R% a"6cT8>=PьX↯lGZYzKBv$c< $W䨀DCL#Q+}!mw:Rȝ+57&Z3' 8]}ɸJAnsBj NP\ހz0i&dyģo] WfD+YȾG =,frduLI4oQ+69x^;62Xd#``8~pġK8;AcO=_?N#4/|vpu,+?J  _l cUZ0nJi,32ͮ#;j2;Ae+2IelsyeaZ suQ o.\i3L/L}?y*K_Sl,V6a؄o"`&ߝ]ܺlXd32u# {Aϖ c q9}6yϲͣTe;+6bI[.Ȼ -ҭQܟ:f3J=M(Oﭝo?]:\kxM 槴Z-0NG>7f^ Yd;$ɔ z؄-l쒻;̘}oq"4q]{&Wnz47pیãc[z 2ϰsbcy"St:2" qboqD tނ91vuz ԅ;"3dĕ+ͱl:]@Ԧ']^k/ 6̂x-o]j~:~ag={Qe6-y`IaOFO7$ϴb֩ujϨfL[s m^mέNl8C ظ1l~ bMԪ&eGc+*M t'qs1q FPKt\f~~|zl8u*p53\^^^]/G_7l^cĆnM(nדL4 v ҒscCM~;yE|&rU&~<ؚO h.H3< ]86yO=Yv`n\y듶]\RA%O_I[U,SH6&1s'5|@&@iؙ'IeCgѝq T5aIүh;fonH.%Ѱ'VR-N/l:ٞA.}ޤ.V/iasГ'OVM =^hOOIpz{{q.c|~H8G0i,xC,R#UMqL$8UE.  9FՈ뀱8kK9R pDJ|0>yg 5$||l*Na-ꖘco`mL|^R;x6-C Q6ٜ={aDnqx`@ͬ&_Ló?oxvobХƥiM lYEp2/![:] -[dƞ"hyЀKa*.gl15ǔ.e!`!(VInLR60l 3VhfӛO_om6{74۹'4egE0+Sn͈lX͝F^V? eA>r6jN܍MXqɭh4=OFƆ۸=a}ww5Uһ%iP%~u) *-"r"-d:GG3juccssI&Y>%l#viIg eƏ([H,)I^X+s_V]2Y(5m05tDؠyjyGEl*Ӻ>-F?s sY-pP3bv-5sy;sk5ŋ/-6.tG@,dTAxD6Й?^-0;X `2~O&SɫƾAfs5 $eN.1]U>u)5o܀ޯXW'-ȆT/H!X#XQJ.HaF(ka䢄WfhF5FӤs)3eR,xXX 6^n͝ +ⁱRb62T|YiH16ܥEF&D64}p8Ӵ"[`36xh-=z3n~v_uUUʄ ~iQiJ. #,إ  #6bXnռAY5F` ?7`7x :43F/bB'9whJ'M%_pϕa$gZEP &I[J v3؀p-: l|\4l'e4(PuW!YQ6  zJ$CX3Za87kvcJ; w݄v{6`D/SBW+h )(WtCX@z'any/P6+Y#wGTar4*.xD>2t8es&5`Qz5(R!qAuV\:a8c7=|OjlA`PJA!-\xz.m׳r:s]G/n\B׋>G{{^X _l`}:Y]J=u5Oꗼ7hK&^Q^Wݰ~ |3Al> UOW5d``cCnxa`C'3%G,%fqr%`=];حՅs*b܎41ǥZ|1_Dc /.[Pmqtثָ}/KA}|QކSJu1ђ 24R-li݇YD6(J5`#ڍ?SaL/|%u @| _( qPMig䳅>cOhgJՖUm-ulbOt) ӧa鴔'#1,fcZD2m5]&vB䆳Ndc {)'+pI$l.Y%ip"'rqM4 ^@ dCR Vڮ ڐ3s̴~ҙiΙs}dA|pfd]T-PL^ :5cjQ]zl Q f"$hd*7!C8{L&1kq8O*-:- 6#)(=SHkRZsdlaCTb Yf"48(f#%; 1@[q:siy /c b Hc:u,zGOv~l:[sҨ ΘSHroT*^^ihf:_BZ$K!ըv 'Gd# Mڊ< OPlw-I '4"1d%T402ȩ_7,%ja;s#dB9]}xH$]D52RA00-)!rt`ȧKHL9NB{t&;icѐHϪ R)4AS*㇟292ON,^;fdJ$L69d6lIGS*VeP-A yO?0ɬ74zraVk`463&饉0es(Īd_M6}G|BGb#G^:Fҧ?]+Wߴ+Yu2ROaV!TxHy: ;ߍ#\ F "2Ā }2MvJM lj-"6rAOwq=l 0UXE'7σf ~t|/1 fY0Ϣ/`h 2l߭_/?-د|~|/#0]pnͱxE}nVp2ޢRNe'ew#kwc[yn;_ߺɧYГ}ByyݡZ 2j6WpJ Rjќ\*Ba3H̦RhGtו $6?~Rwlaz{a$NO/q{Go<> l35mo/7lz^-Yo~O+K^\2*(/&\! Yj6 $*W1lĮM FS&:Rx []۴j_|ɍZc؋ɵV3?L;86S0k-3zi+'@<Ɠ ޵jY` VtɊؔ-tu9Ċ?qEؔFvnwܜ!AB#n%>gJQx o z@m=%g/ qp{'F5h @kՍ L bs9Ukt8ZuN!6&x`S=e}5d8p"o:w~[g%6yhy@HŽ^oUtpP5NHl蛓3 ׃xxfZЀ-x 6>ы 1; wn@V {6~e:&dGoxZ|Fk R|CGZ2fJl iXKl!7~ Kfp2z'Q2$T-B,d{_SRW(p\FV7~Rxoqr 'gވ,{Dq2wҨndxRPP` R$WQ2 P!ǓqotUt|WLBf\lp%| sVġoJ%%7 Tdw*c+Dl0l!CwGdtdPD!l4PT%I%  ]_rb ߕa^hD$Q54=L$[) dMrk"?OusgI%15U \KJTGfғ`hK$3l ldR3M"E̹ IDATd?L\X `}!tC4 ,G )v2l(3Xlp*w)WSY;6;RBɶӼg; b@TvS%9s&XNmgײEG7\CqJz*\Qh ?~73;;2+3J!%$3 Dc yٹX<$?Ϥ~XC6j/72(^+9dҧlt!,h?S]A>}e -l$-,"Კ]z58; ~aDz6[G㼱i6k\+ T3I'<}Nr3Vg5ހ7-hA}ly>gz|)rOnhn7`3M >,~,-YZ,[0 ,8AYM`7)l)`r#,¦bIyi׀Wz٦f߷h\6##.:;rй?6VVUk}d ldZ}ef MJՆ"!ʉaGJ9hoMqAIFKՕe\5 mfb*/={T]_g/\ɠI MEY6Pб\FP! .>G6Fb bl2!@A$QhS n| ϪMa~l6݋Sk.y}r\4@74 fy^g1ZyO\6ӓFM : lVf^bS9lgh;4BtAJ&l~eI+B9Sͭ‡.9e6Is èܻ `lL/=%kF?Xuq~sV<ˮ,CF (4So ;#ljm,- x]q @aħ@VpV! 4>tȆT yllw_ɣHŸL4($^@mcA3mBkmQ6VжV+tm\Scm4-m3V( L^p|"b>w;vYVz%ƊMI`eXG,aэ(4V V\0ċR(% m`k)\f{@Q}r%%(\F IstF)'R>ˌRKl 溉.lR)d3D-9PŲ1#3\y!$ G)bEeBʦ^]kgŁ7]q/OIE^ d],.rKA#ύ)bܾ5T*)RL8wK/l6f *ߓu%= N@/!4=CT(G\OP %Q18J$2j/5,>.R`q B P!E *H.KTxq[pƟ,؅MצfM\>\5+Q꣛) o? MGPt8  mcv*t<ӊW$FlV}.#zR!dBYj]"PeF3UsB4ϜbmJؔ,CꎦF.'>^ywqrl`>N H]-v8kP,[ E%lNvkT.47Jxy7T8ӿ_]M:9e/9cxIȳ7OHԜ=,?yM^:yjoN;0a$OƝ/r'-k'2\cʜ}I A6Ai_n:{񹲯oȾ]hHK'VѰ#uH~uF勿On9 ߮s9DBF_0/^_݀]*KM`3p{c36zF}sJ0jՙ&VgƖ>Craz_z%7M(yr+Z7HiijLSnm놔ؖ !nJHC+ȍd_,~g8l'5{~3ہ"q&@lh†r?~'_ ʾa,6aC/>~uU|-P{KRNfSyύfoj~ͦ=*2D7d?$_Gzs7vb} kkOA K ˸"evcMoƉSa,_u6IMՏk&Vl2ya,!.Ў;-/"&} 㚪lj2(iHM_cﵑz:b$li7v/x?S z4%*;ay9up @|OCsYQ/PȆ\IM30{l*d6{@B4ܙVC?٠7[ ӾᒯBlZ\^p0q"b!fho ʆG\^ XhO_\ij+VllZMf@*ol̙lb{"5͆{^#bdd6Nq6G> %DSSPsje5NVlMC!hd/JaEb!!9rTfsݻFhWwlݤZ}9l|J> j7 5l lJb#1Kl4H%uHZ^tl_%iDt 6ږjos1lI X-qy5xM~'BY&Q&Y15$-1̆ jIF4cizgU^hU*i3P5ht}^ͨXe-B2U 6{ZɈIڃV¢i ycN)وhV>ߦ( M9>%:) y4b";:oϪD;U#̙(ѶK}b>_*fs53B%W5q:Su`w Hxج %1 FK,QaaTzD:Mb&\,UId+",EOhw{iΎo:{=L.iMld0Q1"lIŊ7<cAa|b11XjX|o-A\*Z"傃*-X,- EZ`Nl-- gv韃w LZ<(.sg€b<@@0Fl}G =#&=V11" CSKOFW+;eG)6KR%.w&_-LVPgS. IW#%xV u2D0 G!dTl 8T/K#j^@єb;pX쀺u]뻎kl' & l%f'L5 sn*|TEkWDzKyi+^9o^da4~@y,pf%SM{ e0<uS3(Q7 |žd5T.%l2Xn`ՀzzEMl _KY; ,&bҕNfc6{Ϙ_{XŦa7cRž6vWocӏ`ʏ`|&(Ƅ('<X`ĂaHbf1D~ #恍Gan?^(\h[ȏ:fxo%nx.yFns|wm/ggDrq%aٲ"KS6XrH, wĜjz?Q"RK!9bG"oQP@C=>\DZמݙy&*a!z*WoX7oNbXzޘF$=Ku%|qo ,*鄲%[ o`v Ʀ 0:ofybqjעa^!|Ui`k8cA.n>8x+$M/hѵZX.m#4fwGeB]G-{jjZ[6zkЅ{4Svm6'pw ߈i5 Z%vwGc"\c&M1>cw# c6w6C@.-zsN C+b&lA^lpW/~T4aXb_Q,mTkpLc Je%OȬ;Xw_ӵhF YШm>ŪM5!pZ,G?>y~k[n`m XMrbج y5|,6+q4M sJ_eƴ%j )V! b~FݺfQw <FG:G?!݆+I0;7@'S6Rho_:ŢUZs!d} ," G_X^ɦ@OvƉ:#-L]wJ ]sN4 dGkL8ljllwsX#,adt.hA( ^®'s Qs#e=MƷϽFjDB68vY!\8>:|!f`aϞ. Fr[wo8O dP dSC;yǃ 2b@ _+ll=$6R4Ԕ8 0޴ߺo.C$4%5SDTBNiSEQhmAgW ,Ta DY ]7Bߢl kmIa*I^ƖGUÔ}#Zob& ]U|t YhT'~H(S]qj$O6c?G,e1 "e6^^9 a[/xcK(!E/JД`1KYR1mpU Jxa!\ ץHsNRrTR|mrgә߄pUGGH\6 tIX)% ٘{UpN6p$:L(|#ȊTC'(nkFo|5B4^nT⧂ xfQNMlA˘fu9647aT3ln"6X|J;c.)[XHbO&H.ύ#F&ҩB2qfTrbV q:*qmHuY)KmS82*wWjSL&k|,c7yۖ " +zXp;cW,g׹pB.&L<ʤa8;6Qۿc3#DB`c!%! EC2BzPV`~M<@5)4 F:_ 4wmջ6WyבBNg  =I+^Nib> m x`?]f>#a!܊ 8A2 |dӈl O,?71!Cz&@u+?CCQnD~EU7Z`.RO47&=ٴBV7`PC혻u_B6ēܳ3_vљcl0t7ÀO>Oey@4tr(&]zs|Pu}&Ӧ%Fl0"M2 4%$@$6nCszzg M;R6ꮞS847RKFd'8*,'yq uV٤1ꆲe%X>z)BHtOyξ;ۅs9- l UM|9Mܕ+E]5cGwRd2f-Vbs^ɆF6w.PFK<`00< 0<*1xk$-9GD子q.Z|U@W F>QP$%|9a$ 3\K 1$ɸMQlH`sab|.h9A}=|a~]Wud އ`} =\cp&^r虜DPoGQCOCzޣ JR d$l}Q}:3 -`s?Z"a {;oFz濽2!\5} Za6o|i6jXO &yx M7\|b6g 2"[P; $3 ,}I]m%k&3)pj(@+/^eVnJIa+?^G3XT oa]eO_Q7v:|Uo*Y)TȪFφ2q,Ƹ4g!Bq֝e", I 6ɼ1%{дXɳN\;6'ĈIJ.sIT=9A زp\qJS)yhco(b>mFݡLfQaoPto(h8z˱!D6dXj,oBEN pE6bX j8%״x6dAhB{۽D#Q&ˠPhHvmӏgͪǵ[fm/J67n{q^@" l # 35grL0c? 3eBI̔ mȳ \$&ւx;Nz6Aê(P,H0m o.vPrp8r:`4y`D|5p٬ew4&JXݛ<1lj6v<  lEgjhhh2i`>{x¿T|LX.fu O͎$L1'#6Dh^q͗C߯N2ؼvRg 5hNAʻѪMʽ৊QKw$ {LOvL16p@Z{XRԵ%+D֏c}Gd3hHo 9cC+|c\=sZWb^UyTև>O ͱ^MW4۱E)z!1޴s:E j{HFeX#E 4L :yr$̲SΝi6:M: ȁw7UNr%S.6Hƿ%A7i6Bg Q~NfGԻfu F,(0φB] 럃4JS BCK!VE\ G_#;uJS_ l yu*}ek!!abv&6,reUq"ܐؐB "6P=bcǽ <Jt4xקR"`8p<뒱Dl6^&뼩j`t$7׬};^"/SkgZ8+MS@MidoJ 0T \WH=ͨ 3ˡS{t U6 6pԋV[>_ 9@bdhpB{&̆J_AOܮI(-"6-evaBqQr lzy6$2poNнЯ1ϱ8A{50*LU3ƍs-j Q(q !x Eh$W|)ZSz3zGjt?.A(@EդeD5:5uu>? W:5ALxG*ZCzQM0v5 H\مq]q|lp[D^jwTxbmgiqCbdI u+U:N-YRZBB,V,'~Ӌi\T 5!9~̮̽{D3$hWDŽgk$=Ÿ0gSbx1|u 8/0*=^NDo1֠+^ 2xOLp+1fs!ΧrS P% BMvXLx6|żUBBOp8lxMb/PBڮt08)xyFsEJ \$I^-ICGYB[kؐ iT:8k٠!8Ω hbUr]Y#a܌`gK!*X\ %4+NRmG\{vpL>0/WF2Q%@tS-afׅ6/OŦdFb FCa#ܔWl|W]ێl,ZkbL[LnVQ0}41ՙ1K44T!I.Ħ7h5B4dԳMI6AJVOk_Wށb"SڀBu(g?b?K|6R kX&ٕ"eP9JBַ䷔MvZh,ز {d,2zt[wD"Jv-X.Oxl:]DK3snV5^RUe 6 Ia͔Y܀ 7L;NIJ԰ci #GÏ'ħN!6nr9;Jdῴ B>.aK%\)t".3L_X)֍`6RNgfc#FNF 99*$!< :^\StZLN} 2&\R3k1MA3S87D"fiF`%:?vѻD xԵ-UMcO.;7sk7'dcy6b::ՃupCP˼{&PŔ O>WXjߘkF6zF85F'G]paI !%c3МnNM[vou5A[_5:[796n/ g{fK- ~vgѱ˷- JPpKgm{_wݾ型uuݚ/.8Z1ߞϮ:BsSp⯦{'fZѝ/4k.b$lځ ةaRL~͌yR[Se>WW"'CΡ!fbl~~L+9Wel:IISs n^$ؤ8?u]sp%<*7k-vRxǻ\}xܭ߯Y#kb6Wo}6/t6HVM(_oȵihXF3>T$.7h$xbN-0ݸ?K,96p|:IF'lfB}f 풱^b; 0؀OچF]&Cl2G ̩<>|'rb9M7|F9 bxmOS_8Y}Teˑ͡CKJngliZW¨ P!@da)ːz^oa`FЄd#GWhҤ# 0Ĩ`r6=l^AoOE<~X6F1gWƆGqq\؆T/-}hTOᲛ[*v8@`@TU@C0N*6J12i -%V(S@8>у)ЇwgIwZo;;F6}ܽ7^}Xbsyw'rr?tZ*ؤ~<'Vs~? IXYӷ>xɣ3_azo,>~PHDpunuhЇp04 EU/6/pب 6CF@`c1~Fb%> ɱ콳 !6K>+6Ov3>~ϋYڡy=u^,țdťz(8"eUbs?K-6M%t$|e4Fs x qtDc94z)$1> CPnB6,h ޵KB교ǵK 7hd1"h}BiTMTMbu|J9|Rp]zM\Rԗ,HR t'DD j U ;Q,O(5+g}ЈG}os}Y%6Džyb$;s1^9u_*^IM651 2X,cɀŧ\^-6)C$xMEt*Q[LE'%&ׇP*@Je#lЎTDMtA& *M0Orfc[DG7vїЀP /(]ؔЦ܀ödGMY)6BͧZ.UK+T>&040Oz?V8h8afq9g/6SQ-lc3e"b#5m0r<: > 'SpfM 4;fSRlZm6: P8-Ԓ4WhW i"=ʩ"RY3d7$H4Bv-{xgO>xRb#:w} 2GN3 b&̹kkd8LfoRPڍffGI.[G{Sv1y?`Ml0pX`(/rJigӑ}kAo/漎4/KQO^Y4l.FzTdd 60 cvk:|E>l AFPzr]*ey?|Iln̯aG  Y(6 f6]aXF%.8^@n9N/n*͹jmzFYB._s\Mt} n\],@ݚxڜ.MFkdq;::kIDAT*Gf0B(JEqJpw{ٔlF#z])v6vͬ],|nm-|sO]@C67obv ƛ 7WN-epGg7x8qzτWWf4'/9<)rGA1ڂ>c2x󴛙dǾ~>۝|ѭ-eOᴩloƵ_m|؜oO§+!׼;ApcM}~fnv~=ymV?g$ĺ wͬl8|v>vcC{bfnb`iwRl^-Ӑ XT*:-dvs /w8gG/̹o5-4Em3)iۍbCwUCbCYYQSW۝xg6a≰N٨a7T&"Tbn >^/Zݘ^y>;3GlfPz%dStlPnY`=./#md TfvStӍ A[|*mީOsdCb٠ޙ^ \q^i/>n;9 7&tYusTNhKK s^qv`9*IpV'?l4BXZN.~;Vg0ժ(h6)Ӝ󾳼B\Vٷh_&\qOzdc jҴѨo5CYn*ìe*}VepX8@jIhd-(Э 5ik  6ï; [hNI!5A @hq#GkacvD# )p (UEg9c1$6M+{1d9/ U*P63X=&/Nf L /c K>C!fe`g.3\"ʃ֪hJ:f ?w=lZfD\pt$,jl沈2ؘDZ.g)Oi;RS6GP#@O{q&ȰBg(wFXb儞F[YHsYEe3RfCwQ  x[T=U, %nZ֥ɇFڦJR^W+)6QWlF(6\񲒫+vGOtGn 1- ]ѠYL'_8k=b:24[@1?m72TUمԵY֣*ʎ*t*Y  }Y 6:Z,cG |s5fce+,1KI;\8{{6tGL (\k̍X|` 0tNH:ηuӯV [Tl2/M9(H-7 6r[Y"lȿXaMv2CRD FӺ|uz-S5˵A9ʷDd6ti6ۿZٍm٥d#B*?*t+lt-p>QiLhPX{W e^}"nyN!rAnKj,cG#f+2ƐEb[¾Tk!!{&P^ck} 4]sw#w~qbIM=|adDTb#`OD>2e<emqDX#-rLd!q-^WX4XV$F!n`#+Yh Uܜ-7תƪ׳,a3FQHElPv]P$2<4,> p'_mZ۸.+ttcȁMcv/Ӱe^:qQL,^L$Y69`27M钋,yhd{9v8#y9R i5R74Z 9M}_KTbArvDMVXJ4a`<_`s#1m;XlJ(;Ez9 @Oz}/QVߔ? \F(,:pE} T $It݈Bh٘kJhD|gZymi#7yVKbYXpdq:±n3ӐD*A4cy:wPhfpnNO4%ˎg毣cѰaSi25nsnb6H68rF0J.R͒k-󆖜v&b#ɅV vbNVR.mWx׳.6W3:~×JVV}Cr*9%nu @fvsk ch8ݥ"Mwf#g][l̇Y`4rq~T= 7!ܔ]*σtQز"3ՆFT[=g 16ˌD,h1,єsk}td%(+H G-omʧ ͉ٚrIFT3 5[N82jgӀ7{ mxmwEa84WAMãcZ(Ջ [] ldoZ n)4hS3xix0Qjѝf7t!&6$B*bSBH}Ujj:SȈQ(bE_쿩bZlB&NPGlPѩMh%&b]ĺYډd_h'ncThņ c1ap!C fԸu\TDXR5'[ƢwX`?oO njl6Pi=|A` qdFd+# &'!6 #'_AfFy66`T4Ȇ"Ivcuku-+(nINHh9- ~ ͆2Hn.Z. MHB*!v ͍]\t]vfAVI%Dէ166TZべ B^:̉r)dliw@ %=P7;oA'*/z2rflŀm`,*35@W p:b,cNF_& O k̹_Gf e8KM~Ach\ri۩^ɣ*fu7įr 6}lgfty**]%L-9=Xy~; d-Od_boIr? B SNN"7 j(뱾'lUr.߽9n_!m?~`X>y{ۃ6;lXbd[^2I{s|9DnmEo䘭ru,ג;.z~Jg[ų fg/m66`p6Dx9I1To1U{43_ ʦؚBB{kol}+o7Q=)F! H6BFOl6 5]H` ~ 욦LUD=*eϠw}襤՚dW[V+r#mF(ypJB RɆ<뛣TQ b 4[nrŇMט!dq LUaotľ 1ln!mͧL˘\z˄cĆ9‚hZ ULZ^}4 a҃Aw!Mq.̶-݆'JXqEnk V&ϹL}s3y7 q-G yY٘F왬K@kk/Wcɵ1KO䇗UnjGO~|>>bl{}{\ ϓisQAV|ԩo{ .]f&feC+j*3PUElJ4q6P1`e3'wؘ cʤ\Z಩as)3M&}όLf`Bb8@Q|&zu٘aȿ/pఙz+Y(m1fL j,j6(N*ټ:VM= %uf6C}LG}D>V0ٕbI,. if`<0dtGJ5 $G|oLy~N&r+J ʒH7̉\ IENDB`mirage-0.7.2/docs/screenshots/06-chat-small.png000066400000000000000000001711541407747233600212600ustar00rootroot00000000000000PNG  IHDR+a#PLTE  (* "   #   &3+ %-)5 &"A+7 *I%E$C3K !5$81(G$1 $  0H -K)A"=,E " &> )!/+M ,-9BN7E=I6PH[ 0:0OSf9K$M] "(; 4 =+R 4@?&JBT XnOc2=0[3^2UM^Vj%,W&RaGT21C,;T+Zh9#) #$/^l /3bp'-)Vd'$-> !GX5d#@"[q 6ft75Z 8j!=n:kx306&O,, Ql:_*0"@tAr}>o{bS&KZIz%]x9=N~$Dz$HP!AH-2(S=f)B!28Eu=@%87,AD*QX/'KǤ $X+3$|o~*:;C# 3t$Qxnswȶ/g̯}V.%Fv}m',bmnνPćw6GKC48FJ.6v,kA[MA`+Wyyg I.et-+3fV0ơoh9'yH,Ѹg=QY\[bd~GLP,\зWcʮxJBB*Oj`Q`Ⱦ[ALLAJ)2COЃW>XYuW/vNhl=z(>oVa2rJLd[G-lb'V(XDP 6o=~WT0hf49wΝ"֦0࿸Naf:Ĵ3p CPU52  HeQGi4OLߡ:owC@s3XL ]7Ҧi `@6kkk!61FÃllÀw ctE:슁p0Q^8`pNZB 5&_؄26O&pD6BD/ƚ{њhG_ ᏨZ?lL"D"ҵLFK$L9 Jr"7r w Y񰡲!-2 \~:X8Tb @R4BlI=r0XltI%v '1Yhmebc'iDHPIiP&48.ip}KLp\`F Dbɠנ@4 鹔\.mT]$!*c{`!9  :@cHG j5hؠR5#3O4TԌ'JFRxxq`wg]Yk"+cd7;]OS: kxߴΜSj  ]%otGg"88xXl6Zip C % LAhMxpesƃNjFpT*Qt.!uPrN'Cٔ}׆3 I)Dyb$3 `W$I-V$ٜ\T9WalΙw\Oᭋ~ޅ7YU?N,tVf\'5}*1siti8 ѭuͫBc7]T`{N`:lv5ݲlF HϤtBnx+|6$54눑Kl`z>cB٪w`F'^:q4$ڟ-͗jۛ"UQ bڂLhcMllⓁatd^nJAIA" 9TME{Jsck+U89)T ^9PSbM`c +E~ehyiKf7&arH8 9k!_PȄ&cV?9O4SYn4'n^nZu:B/_L'Z+Px H7`C6Ӕ@9;UF=vc`G닳!uu9ooF165 Gy'pU B(X7ɩ+pj᨜yWp46d9?Qm,^'`N6[7'gGNBE`bCa|}>d({M#q*j1VhGxURչ"c3}(<. b@uRd_GZew%XErͰJ`~102[Z\T͝|U-Y˃V$#Xʕ!k6+ݿ^tZN~ZOzGT*Ir)U9CPufm W̩?'\nn+Y̆+~𬊩'K\MS `t%>H=- TԿm8:¹u/:e,E/& 4)ZJ xڬכ__/LH& 1M H (Wnf9PսYlFE6h1ċy3["5GY}#-a~v؎lFeZO\lhxҬ DFFbk`6N" AJLM`b]XEYHWZ~h!rI}ˍ㖏&>_RdC=t,ᢛN `Qƈe9t)!Ö nˁޓ>hM=3MHVc]/ "YXO,_Ļz8 /-x]0(zxY ƐJ[؆cux&V8Ł#@:D qF~ff&_0Y/`Y[y9@ve Ep16aT{+g bhQ6[oe3]P|i08w&&rɷ1˷B=,dU1@aXXOR9{v;ǯOsݓ|% 6<3rYV:RE*! isX }.[ǵ8ib@9^jvLΘ6r#4/HhU" $G^D!d{O^Qw;?F>9y6nO'n[ͽRK2GO>ciPU Ŝၽ 7!myJn8*P;h_ZO`~vv6v=>'{vòoVIJwbi}!1^o8"WC-jhd,֚W8MF6uR] ޿':r^?g4ɸ`ڴW,gnCjj .߿r8q3FKxM7"c:pD 'f [AA6J"8֖{=8<>↸Aus; Fw~g(!Fn"f^klVV 67ƌDR|qqu./\CJEGX/RMjIϑUs4c3u4:5ؤn{G(ؐlW*y9L<gch46]ߓާޔ `3clMlZE=o #s mvg'>}V-N52h"p 0bA_͈q˙ bdOEaZWgu,Pƚbz5cޠL  z#tTaLyMS  ׾G%N֥gpD_9хh́׽m:N@:^Z`2Lo`žnH㱁^ ɇl3z\7`C3/grnR7|`դczCBfq6Ɛ[M) _L>atI![6"V!2AT^*'׶u#c/x@ips?A!KƕXP]=d΀؄I6Eq_ßy٫R7=I"T\}ϱb3%f 2CrDZIbV7VwykZ]zZ1^-\Pf^]tjMrS#rDQ,mn[;;g"8;`|׮W+lVrZS984 a> bEFY l ;-5],8fewKÇfg;ɬ< 6Sf#Vbi|8 fVmAlɿI]p*oid\:G/.vZ͚$kDW[_X*/l95>φ'yQ V_Κd3 6Na%Ol8"Mwoi2*; h   <85 6o*qI)C bg;Nbb\ ڂpL$b5ح-ٰlu00n<\%4Fm)dze>Uʴ,|]`0B>G'̓ C':ĚI2D``NON$!Ep1OWKIMՍLTfsC7~Pyfhڏۏl־\Y\YW w(`+V6]=>rݱ,U尀$_৸R,ш$nUMt}"ea/-g5F#A*! f  wgg'-=Z|4000*~1ə  JL)Z*2qn *=Q8'DoX>PlinnIhn/-}Ŷt`hjPdbO,K8u@0hG`Xa f$kA%lADkZ#QDa#Qq% vx6Dg^ o'% :3]ixṱ ]B6Ql=E$3u.Uqd B6ӬAFPa ФЌ)Ę DUle$c(576 51=4߿yO|Kob3/GF)]*2#oTJH ~bZbׄ>o^^(3)$DFFgTZ6t/NlgRio8ÎtݠhA8Na[#ڻb( 6 Cj~g%ccc*":hll.IGƠ%3J41W~ XƝAcoz2i*'p5RwZJxlfgǽ.6蜗"ud Tl04upPD- g~7ca\@T  l<{{}VpɆ3CX\<6+yؑDȲ;فf9@t d6JS|ھG9fG AfI7ABjh@624Kp4M1M<ދSpnܞI ΄/LdT]`t]z/Z='vs*M5ɆwqO+F9ccR8 ۮL/.p yUUL؀MPU@ӦyA`O Ϳqv% #oyr%QMv\3`䰌Mv^YZth{2Ԭrcb1WCM g\I]'X[g̎9ŷFW)}fnCEZ]̋p=Nl P'c᣺ZOUYb#Ȇ.yaYT7i1*e%.USsunvbm)V7 Ffnښ,l0& rv|1Iǫg(g#5"(SFl(g3Hު"S';ζ [?'4**7Pg?5 D o,m6 *`FF:VvQߘԟXڣT׳ayEoq66D56dY?^rՒw4N3c3!sQM@B3˂}O/̴>jB5} l0}f-ק_P628tw(N9ӏS?y6ҿTr0!Z7AafNK }4*'^z#Ǩwʒh;+-?pZD~)\+j,o<Ҩ tOK4Vcz.C pT^59D1L=4AbXØaMe,i5,>_5P#-qv<46OShY__߇Hof ֫2朂昬7xg]~Mߩk?t_H[yCC.ZPޖai,>؇}ЗqPae¤S ae;0iHq1ꈥI[[k V}ig\9?IeUpNq` ܍S 곇UVA ͨdwv;\בKmmTӈ)HI)r$HQ]aElX6fZR Ia ߤe޳Ubjv#&s\?ɤ:"O^AТIsBc `+|4"H@ਫ+ )%,I!E7)dlvܦTv/e$#/Ur t$ctzA} Oq>S0` n]'}l(-6D-yA΀%MD 7Q 7ۙV)c BH7%U: m~=޵.,ߊsko-`+ _)C ZK*KlsЌB`g%9XA 8&696Wg ?!6A7Ґ_Hz˘ h)WRS9 'H B}#I:u Pm]]'ʝg2D~ȹYz45~z}O׶^֘l upmZ0̿FSjUSafd0mPvۀ9Ib6=Zͯx(󡍠EycV\N{p%\Sa ͝ȆPo'O5(5s L%uqdRw:sv'7P؀1Cpv7-"aPMAPPBl6*c}*6 Rcq1j` #SML=lN"rb6^`>?IM(pFobJlιi\!&!e&pNMhh\3xzёDuFӞ[6 ;$26ӂ#٘p['VI`o3(1+s,Jz8ߢm+Κ b=3vmvC 66:lllYW̵e_?H:M@4C%. BKl#zM82vlYm[#h8a_w ȑhޓR!W/^NM,ǣ]>.LIjw],g_B6өX$<>l(Xe|τSvԂG7u4ML6Zιwg6Xl ץPX&rev#;DEWnI 'Y:R0x']];+q$xr7]]]6jtv=oqB5PЧflʞ~㠫ԬQK]7^c6^%V;ohM8s]kg9 mRlB\YvPF'bR$o)Q` RXpT ܺD>g?t:UqpݪqQx|h ~׫7:*G^:n/Y'x s'gg7fX8Z+:v+ܘhs1*7j1G{ŠR*H̬/VG Pcf!-=!-g6ds8v kK"vYjgK`S"4v$C3`6p8md r\@:+fZEPJR,@s6*sJ[{ Ɇ&}>9Jxer{qEB"8IS&4xnlp{64tb!tY ٸ,G L@Rk\*YC4h(0r6UDjӷdtW4&1:[̫$n*r(zvհ 6/^},Pa>n є++jL0_*#lBHT!"U m:hisXB۰S[h !V0:xlf;ED>LӐ@Vbn|U~ILiz7cM-3‰0 ltJ$҉YY2G Jw˺NaV#%*4f[ "qL%EoeX&@~tC|Ύ͟*7!6Bj l 3&SȈBtn$3wHGȄ Pt{2V0u6kuI2bd #\b e!=朚^j2c(ZRۨqyUO%3l_Y]+8VLVw׿t7Rr S7"STk 2hD)cntҲ9_+.f؉2T Գ͢p8Q W:zWL/PL͇ŨlJFiQÄ.^_{DseUנMGqI55(4545|'̠f8-vR;bŵܴ4[:Kc1%#5@ iU'pl5tVe.戨_᠒e 'Ufpk[[dqlw?Tkwpv??\}7f]skDzCܸ&!وySnTӈ;'2ޞa.Xx9BG\z^UKSq%G{k-{+NCw/_>{~h/-FKĮa!.W[o%>i*ҡ"/C=Hp΄ᨌVi(/b" ! .>XF\6lV$t/-wO}[unl4n^vh|hlz?7vfGf%kpwoMi9Ͼwtݔ*6[9rQX@ , dh.ѥ"t8NqZ1YB; jZ?;|<9ylj9??XxaNms Yx;voĆp/La/MlX~ 6Nhhf #s꠶rTvw 0_^Rz}ilr- f_Vϒ)~v?UFPA[G;8okZRGB0 ?~*;=׮u\;IA4|B5 tUCe~YS\2=r`s$p7-gٸ}+#;J06R^?N؎OEŘr1CbЇ6`cSmS9 }9aX> &]124XA#;UDԾ(WH];Rmp(`na;EзQ0н+T%'guGR)ǔ&> k+7I Г7DrYΪ*l,EF$[k d0¬9ȦGF]d@`˽zFڸ'a50ʁH6W_b3#lmGXf~=8fRde[97t2mi^&uN#HuS{)FNjFJHnȈ8e32KY ̧GM6A\OǹR'-wM=2Os6j8K~L4D|^S#O. cCqQkq#. pj@ㄌc;EfPr áv3!WZbLx|hWL@ ɵP)Wŗܲ2A*(sZOTd#Otjʯ$dCe0 贍e۹sq F/$'lHp촱%a"_KzNY4)1/fQoذ^`E7>@$AECym,ٮn̓kg#tpN6|U@"yX,u7@& fT{ HM2&WȮa#` #QE7dө0}!mcnEH9NŦ>3/ .۝ڮذcT;xJ{jT.clY]ǠC# MJ nY 2.b@5S3RkI;l6m!8N;dOk7)o)l\ftM%9IF&ɻr2 ؐi3pQ`SaYf9]_vؑggKlF7F8crH2Df^#8<)56]Wj6y{&hv#nw:?Ae|+T61#M&YT#(;$sMmJ72+j׭*R#%+g6 t8EčŎ*ERY^U΃YlR o[ r8:؀EKR :C=G)FƩwܷ6@؏|}\!gZP`׃+ڧN&avKz▐^ s"GpfҡV+ʾeh9~䔍6$@b 8Mͧ. l$>KP`9Ǩ$A9gřәL!Kqz Htڙ!lJ&dĤK<)73LL=|ey$͹ꆑ>-،ژt8<@UO~WZ‚# 4*6s5hU٠t+YtB y~iQOaC 1@uk05l2{I[)9~ۮ3׮;%,~=݌u @+4) d,G-5iWEUʗVnU0(LPfLT p8r)P46chD75X gz.]}osdVش܄!yWDlv p4BҒt| rCU7:sU n%#b4nMp0y8w6͖]u1UOplj`:j=aY";"}FŸ*8BCN(F&41}OMA6*$A7Kytf:?4#`4u3|Q j: a.g8\" pfZ7\mFw(:A).W2Jw,nl>ֱNۘbθt5\jˍpL$mo<: Si1x6Hsgj6jpg1r9E31*KtYG@(6,Wjd%lޭ3}&EKIJ-UX=}hW̶5<8G@3F(YaàO3ˌ3 =aQ{\/`N"$:i2_v4IYQѼKcb7 CcnBEa0ޮN+YQ1xGgn[&Ȇl>%!߮n7_Y[E#R׋˛9_}iqanV&|yW` l{͝,j`5p5Y8w*+Qacrk־ңEz:E}V - =~TJ g9a3|ݝ^rﯦ{՘æygyvrM.fV%c|Xnb>Lq ?[PaUa{7 gqi!Ŀ۩~B=úNO?^*y?6Fcp~fV/Q6_.MHXj0"T~ֺZ\++^nySP%k`pkm"H=el6oYoqS8/謲i܇&; hT{*'c$FP(_XQkUcZ(&zm+&VIW#6FɣWK4>{ q/7 Ϧ=N|[[BQFX}u3scl bc>݈naTٷ?u8+kyQKM[lԋV`;h y8#O[@K ovjᨊͪƆsks8 6B?wl>3{2F`C8M gmZ࢕tOEX6ˍ (sUȕZ q7QrSlfN $[n#fcX#3*{Fؐ]g\{c˦ޕtur6~Oˮ\= _b? m\8qKn2v +o/fCR!l/hcTCml&R?Ds%N%ä^$QڔI=Es d~ι+euXw~Ο(:"-nDޢ b/~OG|{-M41yr]n zȸN(ג \t*C-Ȏ`̣S'} E3dx|4IUf@^5p,6t{ɽtkpD+}FމXF8sr/qjHsqHosn]*;yQ<1`M9vYň'Hpm6V{9cq܃YMe$.8?wW b$5[4W$# AJS|eLC`2p(: ?}Γ61:hp ˋ! j]n48b]}Q7X .5fP2H+ϝ/pfV#v1yl:: /L}ݷj<ȯH 0`{=8\p6^ؤW.>(l\PGG NȪX7aTbrUzpDq4q,[]QP,yF:y'fo*nh} P2*-4M-ҳƹ _( 3~vp樦6>ߧJDKwdYuvIGĔx5M˻g]6$h^jad^| <44?iuaS3os(/LNl’!L& gWZtpuLfTh*na0@12E@1es`#|&g! 뿲@mͨ b`3lFu>issA!ֱW[y}23)adzpZ9hc\,.PX_ by^K ݭ/xv9[+hQX( |,vÙ3l?rKxs蓕Z!, g\8'^ŰElV 7.G ,ۻ-fb~ߠP : Z*L"4bV)3e;k|Ҩ)<1JܭS/ȳeB.ݬ[t*/7K2W! ʫaUh蝟߈FnOɥرhXi=v Ƀbʙz@h6 k$d8װ(l82K6Vl4FTnFt;8V7^*` -w H,hlIpu#/\_\ԛU6  Uo+FCs Sv{rMPthzzz5p \C{VvNM>fTQLfYXTv,8Yl46ߨFGM5ao`67ߕʇfp@?I4 G$2ԯO<ݿc̙j6Vd%5}O&6uJW bo+``7X6g@-ƛo%^ae*]2"6ڼd3 P7+Nj1xh!8rf 6XiT*a&V)wp(O>6B3Xj/qFhՋ[nXfJqEl lޓ L>;Zz}ݩU*s!(c:00X-V'|<_7,T0`I4jӠ7nl] eX͠0A mO{qxrP19 *L oީf&FNig \D`k!ctF&&myxBU)k Ag70d˭Pb,&f6\"jZ-¨V.lؤ3L*pJf2rijbb1:hNu7PRytF+E"+ xbyjDh$˗߻7/FmC_LFSNөN<6_:sȬj #VtBQ.Gq (sO8"S{JRCcD' 6g6apnzg> 6g6)dr!* x|m~].iS %P erbuizsp`9"? Y^J%q a4 MP骍lH8~4cԥ@WW\٫QZ,O6颿ōo==!<7x8FzaH\6gJΣqټj̳,W++G'N^yA!ҙkMA݉}B#Vd*%/ͻJp{h<6^r=8c C8oFĪZ5lʡ*,8_ٱXo&p 0v~1MYo5n&K&3FTr%n%&>,}p}p'nnkDaM-M3B%eʴ ١\d,RM&.A4I4}=~@;sN0 UHd'e~_wCgIp(o1X ꝩ;,\1700Maʈ}B65*(U@ó?d qECds*q4 zis=;0€KNX7&`~5w֏1{ievddaPvî8JC) lDv]k2l~,yO%3o镴釆pK!89\/6?ǯlw>.;WK]zq rvsɘRxsorɭE'o(2j떏]i w>H6,y#>A6-A {pç F]-"+pl&ps|W':PAG`|و  ؁蹚rlw]㉕Y%k1Ԡ~qۚ8x;(nc mW{w,|5 ~7(ÏCWr䀟ܛDڟM; e\k)W0%6 ^K % `Sd;6W1C{voN7jp8嫮 tdoz8v* GOvqcla5Ic'kHZ֯ tT t+XNd,C}جLc8¬"F9<}#ez_"՛ _5ڟ,L# >,m9ji\&ذ{w|M(Ѵ/͐;_8>o# p ߙ:% 5,~.h懋Xqʑ.*T mbs: }C6X׆|bd:j^-ۏ9 67T6G?،T~"ݘhXoGpgFEa jQ(W?cNU!sأFJ ,'%6:#ZueF;=c96 ,+qaq7GbpQ 6'KuibcTսím*=`2؀ ~˙>&"9l l[ p?rཽg t+d:3~/+-QWx4xt:ȟ\lK=wp.bӓ6377u:HWSog.<>:e6`sH=k?(MVSCP,d2`Cpko9dsH Lw0@6 faM>|#ik b0J"62[g7ofxM"c8Xtn_#7#ĆLFLt\Ä#3v?a*2IJBs*n [;PLfZرR$_U)N'^Nou*VŲOUUvsu7e횆Y괊2Mm,jZ>MU*ݘ!cD<dwuR}Âva7ҽQ#b l ^!6Xo}M-}bYJ}V[.f 2&踰xtVjDx * 7uBG*uң|(P87g[lMo$],tŲ*X<wxVlWO# =Om8IK(.`(/6l )"Fok#WmXlij~ii&3$Cp6Q$ҁ!]ti7tb,M?u~o-fS뱉<,!00 &LZ©(txI,C%)IFl$ik"!$uM6e {dC@I 6qBXdB# aA-ܝP "Όm†y ) 38ת'͖IF HekUtdd7z@Xb7YlM6r0ow.ÈG2,}jӠښ mnyQ|W/GNƹ XdHpdl3QxW/EdOH Kp5M)EaA9F4p> *<3!탡ݴm6y48ث Zٍ`U4a}hPV6(T" 9xLjO4NlRΞؑi[aM)fl$g‹X)FA NP8Hl$n r~?[+#Mփl엖zcGm?4lY?!)i9 `~8*%fVNvCU(#h!6 ܋3 i>PzbHIIlP@.WW}K&6˽1'S+iK+J1faʹS'#F1BQC)b]FX0feƇ790o_|f 2O{C6uWq|H:ɰ&Lh?(LDИY )ET'InRAHTcTRlHTbX,TZ<{3!wnv7w~߹=?qŝ{jMpu,<;ǹ8ZuW֏oNNAlu) IDAT}Rbjl!Qh?% ͅA g6nvE=OUݗ˝ rac̿u0k!$>Z߀ 7H ÞpJNcx3}K/=v _K~MeJY4PDcIbդt,~ទ8 xV_N`ػ̎>ksG3~Μcdj{ Y-B'wy\.\WڡgZ#m5/ѬؤttN?+FĆcDl4M=GiTE*:ÈԱ 6X3A腞gվkO},?=g'k3`;bKȤvSڼdla>% p!D6:Gkr C{0 kBĞa8zøG jGelMK7)+464lJ͉~*K6l*Iľ9#BӉb6,LE:I. _poxt3Y3p0}SÖnGxrSb ˰9П-=+w!dѠuFiOqE}:"w1Cr2(U\ziGΠ)bMUؠ>=6lFDt1e gL*"h}u0ȆT݊[̦Z̦X7 ]pSȦ*lc dkrb.n("d(X7W-()( Bò9~b-q͐# }jM`o6*GpBfulkbڝ &`'SS!,=g=j3|?fݨۑ.Nq`#hڐJPe 1r1ݻ>Ǧ:uql+ %v4a 3^  Q=BxT ?uxdqq֍Ty\723{nMm6:bi6vE7V-'6D1^Id&X fS>ma>jȺfjN>۵uNzBFҭ캥"Zbb'ł[̶/y!;cN+h\rf0ԋ!ꖏF_'-NA_sCVo~v|?_d&4 V6H,8Y6 {7Z a$h^apEM3B/ߋٴ62)?i,d鐚x0"Ϝ%fzK Tu-^so%6cu-5(u X7ɟQ8Iզl>i"AE9~׏e@Ac3eKoI≭Iر8n_af2EK7mXdX GbVSL a|6ıGrMO$dH5D`3س+1@iy9[= k=ky=ޘy-쩴!sו!RuAQ+2h60x%Xm| ̸"GGuFmndvyAk=CZh2y|3}e%G7?2&x"BD|`g11(5@8 aL@P9J݉VMw9}ڶEPnWTޥ{ƅFgZxب;t3qG3+7;<x1nKծiΰ9% i,+ِn񨷹 jzo$7mg('IF&7c.Ui6CBk%;D7ܸO#hl71NBstvV3}PXn7) ryo^u[}us7yD߱~&D^(W씝s_.x޺l㻿{t4/Xŀvޞ T3YTe\.K!f?hRl8K͂+tރ޺Sؔ͹_9p&y.wI^nûkq@|98Eöϟ~+r?/w|k۾\A-u3ϵWx=Ldz+V.i 3n]H Gn i_ͯGl>|2s9xo<;LS{px#p|Oi*^K&\W !*|D3Q⒥I@LfdQ"_֗ /k ch`ef@! 4ٸt$hZc481˸wS ֹ;|u|l,MN)8,1ff~>Z|Tԉ-:}bqml>&60oR.)Wp=u)S;.=*njk=lxՁt*wl ,ZK'9ń[o9lI|e@!,YjeGo_;\]CoZxlj^Aa.mܠaTUѪ*>dX !6DW*)NX|<^F8_(Lc/>}Bb }dp/;+$~6V^O u-͇pTY Up%p,+}NR>6y\uzEql D@ՅosdQ?H!n$odGmfX l˙~+]x}wrU<)4]"uxm 2$.Kvպe`Ϝ 6} tk,hLl)% s~ /mXFfbi0!YzwcEX@BtܸǼ}V. l1e5}XcVu9⦌Xh@ڰ'e}i"0׸:6o68 ݫSɁ_%rξIMp*WCK}ɩ݊:T^2`,zX6"lXbb_:5t>SH/0!6ٗICQ}Sj{Լm[N$jEл$ 7b696PezP oXxب1/@B2cb'|ذsؔ{b9Q l.j~9Fܐ2H X!qGfr6Pf;>>- J?gDlo=uz*B1mZ7 E"{am)h,?Ullۆvֽ{wЁVV$2Vz>#)2Ƨ >:Z'N,.Gl"U;s'p""٩]9-!x/{&~iڰZYGLΟJE gڍAjneC6P:x:ϳw//;"-p=V ÍrR%us=>n /,A61tNɵ|KPōVhv4]]zMj# |m%7jobla,F]-ҷ?]mu%F.6a6.'R9MX+84i?Z9=="@8ܮХL+qRq Ȳ6 ǂxeoqrN:}?*۔sTlK2` ۪3ǷJEV=UQ\a:G %2䌔hREl0P yidSejTێ掔.°>FBr}O8cڰA ;[ޥNw!4ټWؠ:A,U^61S/z Y &M blߧ,sh+[rؼ ~8nDzy?{7Gl,-2qV'hەc`KlHـi[+Ėdc6/h|27u(&Y[1z-%[d\?YCty>BjPtt:V]UT+̫́SM{~f([S-yK^<ϼҊ͕{S!hug-ür/ ~bi } ANGKDO3|b6rlv2y[hݵkMWLx؜-3#|pl %ЍqQa4d5{f%tzjЎ4MM8f#wZxRMDo׾Io,ewˏN.e+ ~v5=}ozB4ڎSKQ3mkaxC(oh+El8%'#hV.g (Q|p rZ5PM mx6j'A/ؠ0a*0 -ol,OXюo#>,g&G'3ql>$qWK0hM^62X7-XAsK 6(%(UU1,lC`BP'J Pulh߼Dp(p0تc~}Ѧ?nCm%T/lRN\T8At*~ ج.]/\X`r935wz_ylMU|Q= k$6z<?%#XL;h8:?LOOιq0eyUaJȿs1upC} uZ/R:QamP5cm,v͛ N- ,j^ z$/#\8mNQqH !2QFzl"l:Drf5ÈjZ[`+Z]J%"ݳo ]a.zKu]qahD"=lț' <!̢!1QqCgIw!M!&nCەSSMhK oK6{7c}Jdɞ}s=yAlF<)f\ݽY] tgQP yt FKoWrOɏeaOٳ8@2 %ʦiH~и.+Fmf^` `c 5OPP0b ybs\XeИ鳡s!N^ڑIt'tf󜮿2i Mm|ѭF9.KA04F^M܋]9Ο~q4ab& ̆P^Gll3{Y4#eC4:6>~e6ANɿ>e4(hES??qpbB' qrxss8"Wǰ13Y 2gbENhLv6'dv3Ӡzg؈\/MՍ)A_5"`Ӝwybn[wnHClar9NwI܎&#hP'ɠz&Cb;-MytDAwh47)\8FW1d*bAC@82sH=6nFƆ.d8-l@52rϵ*۰ˁW In(̬  sb"} ] FvB㙇Cu?t'f "k5i{\d3/)c)V}ӱŝڦ56E]|dx$KCm:lFNL\ToҠmEDiwOH/sMfAlp,1jV܁f4aac9kҲ@iP0Kc*5IGkܸ<KRW+[8M6l~3!`Zg2EvFI3Mɕ|'F/HD%BW˳kfNP +b69T6j8zA<:]Ԃ/}Ho!봙_agjBJHFj v8f.lfPt1sٙ熞lMd7OffL=}}W<}[n?|" "7UiVOHx9 %/"t0!c>.x4W׿{ŏ~CQ9KB6[Iڀ gԕfxfs7cg6cAW2Ժ|a7S^&g(M1"iZIEUXĘZЮn2"B`}(4b0ЌݘCӐb@霆xݘkn#Z2;tseyF<ͦ2ޅo7+ h:{9T8c=blFVKw^{\u5{rL/ jRs!jH\b×\%ӸmGk{#wSx|cc" IDAT)?O0+Y,.`=d\6펦ۭes||l8{ )qd`7dh6NBvKClƣQ΢wy~737wQyB8:)pU$Y~ddn67-bp@elЌ٦Qo^VcƥEuacH< 3G'բcM&*ml6$4?s:LR3 z ^6^봻gU)+EJ혗6.XJQ^ iy61MO،BI<c`sHv,'H6Ykr$xl:xejޞUSrPΠ QnqcC1@X YCcztr|r #Re4=VK<H_66ahH>p) xfxv=6TBhole_ʛY[wJaI] n mQҐM+ݸ==|Yæ&ڷabB961֞Ȕc c1شn`mrv!(S$Ac8y Ez,ae-}6xF'n¡]BD _yȎ uI9;\E6, P|NOEE{ MڍoCQ:tEHoUيiL=0}3p%x8ߜg%DV11'-f$zqvPU 03OlN-;"2F ٍg.rgC Í'اl2/LC3|K'6 GSymfJj{g]ljN2Z( {w&b;Ҡx_t"2A Ƞ.|b&I<1l`v;\ Eh isz 6-N;٤ 2{ FwTUV[lAa$q%Usu+,n%c6Tes n$zxbHWyDj|XWʑ(&a79 0.D[L7/ccBx#9ٲwX VS 4DvsrӔPU̮vn+"8_@($. 'SMDX٤ՒܠQ pdDNY% r+džRvp즒qlDdYXФh̀}Vcɓa-`yEe=MeulLݠ^<2n=i96qlP<*ڍU1Mg7E6PXշfYb+#1u t.S`rl`Df8Fܸp6|z[2. A}jzWzb&}2 XklMn+okV|/,K}Z'>⊠]hOfnx9>`̻`,ʆ5aPyq88~,׀ _R:[nSb GJ&2څ͖w_fGTj|jXRdÓUf Ӯ3!_/, }mJhJh%!`DlA3Y$F, ;'4/}Mjak_p`uQ:en>"U˽|{{87@#4 6 Pңd O!on u QH6;%"jd1>f)7-lH`rj?otˆz`|H SQUVIpvWLX]'INI?D8dRim:M\dk6lxuRckmYXS>E*̡C,&:DPm3=]x 馑• *i3No5ZK'-yJFJ0lH gS#4Wq4`-8YʆTItr6̭Uec撼Go,Xjרh*Bl"S"K+DJGF X!Ξ,(.u{ẩ;V9O˝PM7`2keƆ>ⷒs,$ ᳚^hKDT)N㠳A1hMzM84b24 c.?Pc٥ !61+ GC]/Zsd+*Zx@$Ca8 s0cstH>jv%F:c"R=zC4q'R *&;ӭXӞEsJ]FGUV*bM `tH7gsix-`r[/ԩbT6pHŊyԃmg3tK׻cxEr |ej30Ѩd?KݲjcSdۇһh6Zp;,d .yyES28~߿M#UQsjR82U%^pt d%^B.aއ8fG27uzjjNGMK75GpNU@O,AZrXzU Zn> ڎ2oCMCM@6R!?gn]va6tNSP7ytBa00+K?5555ͮr=̗4IBD7^{.HUORG'S$CSnjٱts 0oz*xq*=i #M&+n>̭:N ɀ߻:V$rr#ɂYCG_`2췑7~Š4V_b}KJ:k3 ?xv)mz (TāGvWs".O~^V߽>`Փ F&aəz\Olr 6t3<{t?MA;; }謼=֊hqC&3Vt&ࠓn|2 , 2rd~ Xc@*q ,Gfec`Do IJJ9khHwUKJE=| rɁq ,nϵY~1Z>XoíogaZ1M <[75FZpG?؀ÝDlngت X~6: ؄?hnnַYʯtp<6myh.t?\39pu]zv2kdNMwF_lFFO3k'G^o'S7us_߻}e97a%wqF._a3 zw_(FQF(M(gUN ,}xꦖ;>h06KԲ[ _q4p|)Z6|٩NMD2zlFoZF`%p M*)rB/!x6B!lPxfSLw[RQs36nb3]_)+^=6ɯ dSpu(\g|XzwK0MF9&0e /ז&D֭.ѿnSZ:vKIn'l4uxb 1Nl~q/g31@g [Z[׷R˳O /@oFp+ [ 6 MĤlN3DјO.C-/,cn4:z'|&5UƗr|%Tw_=Y7byl65 z,yB?/ Ӎ0+?qs~t T1| 4ߔUdώwF^!2l Al `33Vq6K[ C8pal}ԈO~?Q6#l Mdk'Vl8b#l|lobth7Z4j2|&f? ]f Lt7DgGˆ'_ ^OMŸ֐ŷ}P?ۙؠlfh *+\DaUàdzuocCew7c&C"iA9b&`%zo1xP;W U Ud8N~&؄O2w}}U;!|sp dUݐ8 ,t#M ,.b Ga ݂9a +x>v`MCBY>~ťzShLߢ@lԪAxzV$t(mQ[]O!7 ݻ2#lg$pz܈^cMa̪bF]2Ld#wy&9ӝʅic|9OL0LA bAD\;55u Kg lTgȝMQΫ僚hZGEpRcirg/~!;TŖHkǕupǚO6+A݄K+q&MR*Ï"&iV/m ݱH``Qgsb0h9< 3+pc09%:rbd2vPHch\~wD`b>5b\+QBL{S/Eɳe)Ʊ>h1%5% FUtQݧc4|?SF2||f@Keẍ́ ׇC~Ng$^}=Nv$uђlvLhiuEfbYAIg㏧5!'~/ٞsPd&6F F;`[ >.G^4wbDt쒐>j}rLz6[A0)v[/hϟGOwltҍ&hy&$|՘2D8gB;^R3* _K]2hJf9X61lmo(idpok1y[- G?coH4mT̬㧠)zY]oJ_:9R{]2N6DBN'_TYfLF8-$ ('" e}Ŗ7TdOag-*rUPhDlp>'Nu¹u3uu*vt`< BO݄Vg6иM%?N7&B2^08ULVljϕ![9#qP Bk1JERN(#n\ #XO+C¹M=M6,cpt B5xV֩j*lL^TQ=1{ ɸ|p1I,t·GHR8@Ђ}gnQDdCcl/ 8(Atl 3ݽCbƨ]Ô azac:˧0Z [GOѶm1cdO'pחr3wӾӿY_Y1ztm 6ݨ)lV hn$ e Ɋ J=G K=X-~ 29t5( JulJ[#62#{>Y MC>n٘s)c:}oPf""f w7StsE $9\ M}\,%38F>k?`w]<_#SgsjS$J;1!S*-Uf.b- P|GCh hZ iILJCCh as3J=nՍ?5PUS:O(c)V|kt]0;)eH3Hfj)#M+2Lx"f&LܱC(E(XOV0s_ ?W{SLzgiA,Ě0hRĔ1&r3`K}A1CBf= ZE?3%_-#l]4&0-f%}z\]Ug a\$T?>HY*sc(lz#(_|5KܬwG ]2-̦ljR PԴB6!LRl 2̜a@ڧϕE3_p@:3Yf4@Xx2gɩ6fNS^Lqx걡2)D)z_\ޘYǘҹN0V9Q7|sVV^1 5ijD~dh"Nc&Ð݇$ϹvY>V|[Š ϯו}4f d.9SFfT2 }ndjR,Z6pe8'$\f_m&}x/!Mldr#H&tzl.#$1V(eJѴlEtT5 RiRЌ!41.ǨkTBXmKdtoH(J fFLdd84KႾsE4+J kӼtlbD*9v!y ZU7LRbyw9hQ2a&l * j8]S`m$[w#pJ tj^/asgĠJi}.tVsV\ h#4PQ'u[w/jE; c>Ix<4sm!8=V6!]\4lӅSκ@x\LT{ IؚFC`J,%ĸ~h_.WtؗHlnDDœk[,BQ]\88e]fĘ*}Ywd^}}Vg$Ch^G(RZCpI .7`'SSM\$vf<6&Geөc+7{򶔐x"d 3;ˉ3*iuQ3W!tgܒ|Z.ۨ.Y1!1[UQԞPQ6LbdDtF4@̃͟NdU\=C4Fct₽1:xmNwc9{:9S=9/'"0 uPŪTse+Igg]{Ӡdz‘9Q}TZš fFAi"T}_ಋEus~=kܝxV]dLWyUS6-{t "<Fk]`&0yq䯌?r&͕m l 7UNny\304T;/l>'' .{>lsW8e>;Nꦿ&mtɴbf}_O,[ .DE;ʆM ،ٞdTev۔I:|;|g~F>Iǿ/xw~e lhVB6n̰r- fvBr)-RʤjπEآ IFxЅ w*O1@'=ؤe @+62 q9O 3ueLV>nP-([nSޜ)&6u܍QFulH&3z:+{uY7`S_Y] 7/gv(ԖnuӖꆱ9 Օ"_#qAHfn]-<8O۪ #eߓ?ajItds:!tuYKLZLF@SI v8Ӕ&QDl:8K,?.?-g<U4e Qg'Oަ5c jMit'|}-t@6k+M~@ bo$t0GzgFg $}Ȧ)-vk٫UXl2l+4MүaNȦ}?Mc&5Gsv+QGdczK-O<Wkd:bZhb)Ȣhq[yJ&lkK3?=\(\:ꃓ4pFJ4^==>7Őp`^,•j&\Jd_mNNG/{ _m!dNPad\Lr G/PGx$4wacp-:х_ s^y5#6k{5/wIf4,`ຑ%5PAƊ|~ gF#I׶鰓u 0$Mz,kpVm[:`H0|v'h< Ƿ' TS_˶TV) 6u2+ync7.'j+yav5F &ÑpVt7ؠp2Z8* ڧqFb|qht✏)HՅFbtFV$$'M9}bhwHw (FMYśQrfCоJM mS4at6ܓ`ՠE]5Jf`6[K& 64O|^<|,6WRg`=H;Ƒ4n@Kl u_!D /sC]1e9EC 'ˇLEWKnyҋ-َ㗐J%_^h2_iܜHQYƋOr1F!&i.͢1J5p ]_YDH {Vʬ¹4CSQsrL[f-.nM$T3`7@ JPi˦AZ#\t[NzP!5 H6ߍ+YS ?[N%&l@4#3uoÝhT1uE,$n`.:m-+52:KE(8ykxDEq$w Έ(tP2&~_o.6jOlTLh$j)%mǔ^SUh-\N3_8t"b[d_veg/;td&6Г%ٸ( e6j_ jAY17lH7튯5-մMRoG377嶸]=I0HMk١b2И9| 4:amA}?==٘ ﬉.:!k2I*`F3[|4᪤>r*|r{_f!2jLx$aC/Mg=3vS1y IL"ʹќE[7'l,Z3Xnnn]p[ݯ7MߤCPf/ SPQcҰİ1c.sT𼒴A;eэ6*9l` ~6pMf؏]n!2=_ G6~$:hml7dN78cZW5 q=*6S}yLhLFwflL+9չf97k{ ,,;[AI t@7%QxZ\8odt&S27SIlH:d7 l@jɘ i<ؖr(JWk?-^аkI5 tzZe6N#F3iͩ=S,m ) >Xb}U8S1V2wP*TUZŪ,T:IӞM`xφM hC#P`5A2)9uC RnQ ˸Ze_`lܨfB=/%DvZ>^' k(R㏽#Q]F6# òKƎse8 h 3yT1DIJl%@%*[Pӧg!ϼ cFJ7EqJkvW~t *T3ţ y[!Py !3`[W[AU-̦NP&I:P"zWek2D}~آ;?x N Fd"`CSݣv6_h--yr Et:g8>jBSa,l Bn6z0 J*u}1r+.5hBѢ"~tDzn#acd3i}t5GU3?9XqRˆJCLBG6~c]j2E|X7!R$UQj-QM, :-,v%u,&Wym׏/>hHO=J7u*n^5LCQA,HjR^92'37n\48 ( BRNWȪʠv{Q69` z^i,-6t*Mǰ|xNG$7ܡPauLXPMfH.{,%dsbIL߳b'Gp |?J=KsЏP $Eȩ/kH zKp )ds%N=f4npDG&4pBU͂?uluӯuSqWlwGjf/ (|Y7bNa@P^NEtɤzەex_Q/6\#AlιR\x,utR l+Z 9^UnudB!0~Bf2n@<3 ([BRE;YR#)RXQktsfCbCBL8#=o5@Cl18E3Ha.#X7Wp'ahIA ?1{lLwk<8,'MnҌ܏IT2'RX8ɼ&/ lIexZAl^l$W~ Q& Q=AU>ÕH؄&[, !ЦX10e*R !6%%*&U3PAӝcNH3&ՠlNǯ A6SG2GAУ;73K]7Հv(eG b{.\A[°r.i.%'%(ZC!C, 5C}0zʭ$}H}{޼(%U7@ЬrN6eC:Z"_țH "H\x,)h'%rYJH|8pBFlfvԋ:c7Ts F 6FGdw!0!]K2iqSP:^i" : \qfF{hZ 9ѡDL ON9]aK"-Ǣ+!/.#6Bl^g/tIqi{Q)#a&u?~g`Vlp59 Jѳ`2N ލ[ ~K @Chj_~~ 63»9QBtlʱxSMݙkfm@24f4;8},Ҷ52-ggL]\TPWL$I,M 2Vrp<|)}+OM1Ms679uQg8sj&D)AC|RtGݣhf} N«NbƞLʬqp80'.](2E0-)r^i!wdC0%4 z)[:d(¦| #"o,[>dZ=<iҪΠ%6wZ8*BᘍIbouX{tLВ]#\HѿF̙ssWWHM'd#MK"a3ذ e l x[PYcކ&ۓN8QEDDCC6"Njo1eYN$LfY֢nhdCM;lF*17^/56$?ƾ[N>Qj>ʉ )KW0\.:o1MaߖȲa+m\BA88D%nGQaRtE 'ffSUsaUs74gXgDgtڣi4KYVuX^6Z5ⶫױolpcA0&ZeDZ G$0]?LslIqall,J.eOTq/]:z%$W ٴz;yƬTMѮ6ͲG4`X}MѕPh Z/aۄkM/wzG˹V_2*RvW?a %󶡧NcLXT~9`J4h/%6ӬM4P7zmVNGlճnVMk#=%ϙfHĘ1Cj$l̘l</gK!7B-^ɝ#WPr86iLFR']S 񼽟j>%9q}T#\sVA#x< M~+qxԓZ`8]j?E&WR,[j0BÕܗ*nk>7= 1^7A=-aͦ{ٔH`ݽASo}qnaD8i|m1lmPA7CBE19_dmطo&{sj;+eI:<^0{Bfhn'tϞi3y/7&ny c IDAT#"bmrZoӪ6Q2Cљ ~ }N5 tR9RU-l׶Mt_<̣GFw֭}mj$iP;Q&cԦ]J)+_Eɦ/1SZ/EGi0؏{d4 u#wE8)/Q7ww=c=Ts-~iEc#=`O*N}3lf6nxlV,slW 56騿yN,aSd@8H G\GrhƇup nQ:C56=AOm}k)UtҠ544A^?5 V\Iu6D'q|:o pUAI@uS F0|0eZ>%s=@6Ǡ>EA6naaU}NᬘJio3zK e*6g_]H)}RSl|'41d<#DޅtC\th Er{RS/z)}Okӗo+.NN~8ԩCu6AL+ka\ic/3.*x 4Z%Nr7 B(1S_m SusD @s!s?=M_/_gz ']}g8|r~%\4$mb'4q`0+Mf-xNY2@z,E"s0ʫ\ߢpȲLcQ?r\/R(W/pW>߾LO{z՛;B)%`K!/.{3I[&1T[6g e2kEP67d8) ~ d>PS"Nm07Y)WmJ0M>ՃOBw?ؔ"~qQ\4# EM#̬`r46k!T͑[+2o6>([knAd)rjBo9gqE>Wt٩k'gx/0zb_;.;im$=¸dȒ<(7HBqgvXt"t1K!=1mU,d!C>@%,OzڒZ-Gz^ruh_h孩 .;\e=96PkN2Τ|7,գ9$.П/3e|a& ƿɤ0I&_6uH0G>7OgKܮVŰD6Mlꊡ9͆DŽ[|)B-Ѡva S'')-~Jl~oEILO?G~.|H@6lmslVKVtݴYшeNI|eؒ0x_rˇHX!ǵTD2crt3vN'6~]F@tFoq#+SMWRagax4HH'VYp׾ְ"g} 6wŔrHh {텅̇99td!V Ia}+GV@9a-h>7YK fuXJlevL@6GOyl><Jrw<|ijj_@7"UWVWJQG+"F>ZJ3.j(#k\jp{@ds耍N*C{:9^H 2+/p4t>Ls?UWJ1roEޑ `0HK&sL*W9bcrh>=ߤf)!*Wi&1 ?aS9<0'>=l XM(6e3cajeO=G9-- 솏WvnU4p}¦9U8P&I'Z2_!n l;[ 2 jI$3qj3g3"O+/;pJNQb|.y޾yO@yԽ 03 ,,{if1憸mQ\ɂ-|uˢ)(x9"46ېmm Sd l[vs>9?;'ӳM$T joEI3}RՒ ӑ &>x >Iec!]ΐ5퟽`j9/p:7RRD,##T@"6?؃cH:iASL(hgjaـXlxvh][Uun.\4Vde&ܡq@Mqwq626kdr@oMK8n`Vc"4ńN`*Zo\bE}x(y5uI% L9 1UyU)mVQdjIȈx7lz#P0e z7MOt l8YvժЌvit0v^h ܰNv/F}SI͎P}kIϜx>KOVݔlzFF[[mԼLuƽ`3*ygKl݅/prg),,d }βVf)+ hG:L꦳4Э,:xftzqZꩴ}ȥlrI' ē픍 ){|sV4ly|CTQTPꋥe tDxMKq{DS n&OolNmw e[^--u"RTJRJG2q{,ւؠ#;b+ 9l{>I2>x>Up&ː>y}3Zȇx#쑎t 13Rtl(##= CR;S~{w`O-(c.lʑAiDҾG#@/t+ CCnp(3Z3&0MBlj178(3Ú;pDhkLOkz&C`!w8 'l4dEZЫjb$d,r2Q%ɇF[!ntפN{^}5T܂Zq|QhMzvd>6A'yކH:=\ hiq}=$z ނHx* !$Dl c0)MMnyzM_ .g[y &\lLMz dOx[!4v )5n<ڌkuど~7.,fAF [3.u NUڑfsP8H$.!gAPtn@!,S6(KI KkSmh.݌Fypgⷀ?'?T؈_8 dXeh0hA{MDr(mB ͈Z6!mS(#)[;>T|hl?BTGL}t7 Xib6 .(NyZ讱m2Že98V|D/I‰2 s-FrdKA6._-qTOll {wᎭ(sXWTnL 7BwVA6]kct Υ"E'Ѩ`zj(`5 U oJȅP7))Pl|ْ_BB&Zmioyоub6? 2'+TpGNRd3Á? ^Ѕkxqpo:B켖qhƥd\vh`"u'Wrwh'A-?̍X~|m Rjq(T'3(R.UwAnY%:WĄ]݁-O^=ܟb3 rd_:Y7۩_ƞU Nй˱pDd2sdq<[.`j/=T"6_-u{K5>S*q+;-IY\*75럜>}@2(9xo|UPVЖgw{P5_=v>+ªK]HzmiVzol26^e<-n'nfpT^E"ϖ"@Xp,%l=l:d#hFWpN.DhF\/GSdSl ֒_/ԈV'w ,6e!IlbA5r@6ÏM)^u7#n1Bl8>#:K/T j AAOY2f"AaEGek> .|!6y%I7 +1,"Y?Jxn{d  V 6vUv] {Xv*uSulwW?Z)U }ln0|&ziv;fJ+ W{*%;l"7*6D6W9Xذ_NS]:@؅l{i:El+pfkQ!6xͭ&`w}M3ʲ8l34^ +%^&NN1&F0p2)nHdx<@_+ylJ.ơp]f/L QRx̎ex 6p[٤o*$pL3 ln_M6lx Ŧp]3&1-lܸ&rB"Z<6 fUB w།"ٰRX0Q?XЊߴ.m˰y}*P lB 9ǀHQ E tWtdS7l:\$ Yz5p֮o\6Q@TtEL8l, |J uBCu5Dqզ{oTL6H2l7̾7vB ĖORlyz2TTK/BkeX?,%V*hVQ3,c5?[fV)P>=1-D/>|uMq:s^{58h$&,¹ 8lS a),^rtg)Seo׫ƿHUo2.U\ s7562KpSSx "+ yWHlH*oP+Y#m{ Ce4'kï s'ԃTqL+'@6GVX8B^" dSRU7ޘKKT;1!fSƌ8Po]*at=sh8ɏpa4/Z$Źn|FHK8Upƍq|ދtU040@a9aJz@"AvjH %i f2!e8hN8Vr% K}GMV3gw4z?>V\hdEqzDꛊhO)}x5Oe\b6D+ˁI\OK78Ӷh{r xxi c}ol㔺H OȠ0eB [Fc"rĿLM-K΀(&uԺ)<]{6F3nfI7CF@S|Y8d%b[-7Vl~%㍄:ZMR8{WY´fcx=>uf%$M6 9wdwLOl_" zDx[[a:$M2Bt=Ro8P7^+/8jQV.Bd|(1gJ٘6JJ?$l.iCrSt^QΜR?i=eQ"(e}* G܆KFY.'T@KupFLdRH !$< ~L)cyxDnTqf!,&>lrgr hoԶu@yZvybDSNOś Wg9UY3(jPJ7RכR8͈x8B5^pA՘qRBlҜ(CLn)l @I@'AVJ4=f?Wc=Ň(-Pgc 2ЖIDATI`t6/HGXM=HeRgXϡ 3Hb{Ψ; ae8Ӭj2Գ)/Nƪ*Qd!pJ %ύ[&e"% dR*NoFմ(Ȇǂ m?RkF<3% Xٰ 8+-N˦fK^*eEdqKРyC86"+b 5&8Gۥ'9`ۧOX ^/N"E37la>ԅ#g1 GD2W8tU"1pں>Qt }ƂP1Y#$hUlX -C^ Lݒ6Ħx,hN\"[R:9Up6E-=BSȖܡYIA`FT͜bu(L"X>ST[${o,+7-MPm6٠)23"SlJŮXa71&EaÃԽ,|^LM$zۖ 7+pr۳ mT+LY4yoYWXyBE#ނtQaI<ʰ ^ ȝ}9 ˩㣸kQqg>pb-q1n)G l24j­c#gssJ~ƞf"/P9ٟs8hDdhdPrܳ`LJպ,i}tpq<'(c2A<nӝ\Y~fH#:^z][ q2<@],+x#c+s1K\ގD\&@КPC6kx釳Z6W ZĞMT<}U=Ŝ2(iJ sKԄ(!ݥΆij !XUlMJ7N8n 9ݔkL뉵Հ]@3V17x }@0ff9 [lј֌8fTlH7gl~nZqh칓 u0@$ ח03IMqpv\Zf6,M[s(IF[;֍gREK0CIiN dFYەpP~Uc2>vF()=D#!9eּ?ekm(ڹ Q.¡,!qcCh6qcDZ:}azP!mX: 4t5m;Xzʶw4V emPz&kY)D uؼQC5a8r,gȁ/TDR!l^GQ&qSZA=o6G;مetΚjmmtIXZFG3f336_ٸ}rg}4% ˛O=O ߶6ߝOŋڗOۿ}sSeI$.>mj>lxg{sg,&'l*Mi~cXx8h hTF;4_ed Qg@Lu3j5o.P^T~7Oq( x5ô~'Ғ!"bK4ѽK:Άf'w.Ч{RfJg!VJ2jq"3em)VtiF@‚؂.O1*$9#?Dܱ)¦n&;4RtTLeDY9># v &Nb"Q);-u qݓ-[>@EemwX.S4ݠFA.Xk>CѧFHHxzZM9]T/d|6|c6Ecuh29:jsgV .EBD$I~:75t/+Wc-;_S Ǐ)EcA|ھa   S NalWz+I!nzv>~xe>Ȥ, )ʳyVHT-]1'`^Qʡ"$[ܑH_`]:'*XoRW^ǜ\_&"-67A^fV6DŽ 2`מ#ERƽEð |'ĩ3O fIENDB`mirage-0.7.2/docs/screenshots/07-room-pane-small.png000066400000000000000000001511461407747233600222360ustar00rootroot00000000000000PNG  IHDR+a#PLTE) .+  +*)*,, ('3&2. -&0 *+-",0& ,(22Nf % ()4 $ '$- ' & )*6"./($0 "!00?<"+!. &*4  !6 '!3%1 $3 '> #8 '+6&<1/ "   (5 1 5,7 $$.$: "-,:#+8#0 &- & " )*A /.)7 /* ., !" 6 +# 0%-D1I*  3$2 - %!(!%!  #$#)!+%,%8KR]DNZ,&,&&JJ<+/  '/AINP%((Uz>?];,!/=EQ.u&NOWd08D)i9f2`߉wX}3>M""I*& CRwiN)7`Ao;Vo,[E=9mtwJHBaeit{RGq~vV?pI-]:.JZ+gN<^+43_nuvseocSBaxcOD58 69hg8?9TƲQ#K`J]zLqEm8,gH(dM4Q`l6:z|/{f~Y){c2 IDATxڼhǧHqL"pw&{fa/ sC )| E2vaA; 2)12jg i&ӇT[?qE<$M ~<}ۣ[`hl<G뺾XDI:FCylŐ Q4-c3 {8aOm}n6Q lu6MlOlM ǵ.p | Oړ~V6WM8Hf Ѕ<lODʩ oM3l-QGpθj}fjtp! y {z<=#Y>I3e_V2:fpFl ٨*3Bp"^UilX8,wGFU8Ħ&p, e¼.* FzYV<hv۰~qv6ŷۖhp.2la`c9ui:O' Nl*a0:^-xU|ɀ/GZ~ Ջ8>;Wul8$kK%vk%aZO驰¶YY#lr"~e.Mj˶I^)`.Oe$)&n NW$<3pةdRt(6&J\|l [7Ow Y"i,n74[cR[=C8YuCdcRg6 ?Dv_Գ'yf db[$ WB CVxLgQ76#MI65g:*0.hJ߯M?įkค )s 6R5(}2[ߧEޘM&1PߕZ.mXh8((MeN+2(O/b ֔fc BCoVm}yg&A6{hE  y"tKg46z- F\rJt )6H9tݖ ]/7:5 Sa#$}S]EEkhF\o61uua/Mا4r1D{5.0S׈zܵX>F QQSPbӜu8Y*>klYH`(iVBJH3'c *ynqL1F4 2F4^h2'xpMY_)puET4t!-Ce] ;Gq4"j$ T%yN F#ln?˿8zWQ =yMV5CTFp4l=)͡Eui |2 'Ƣ5ZpЇ"lZg]oB]'aDA #lp\-(hV8uռ?u|% 9jSr[Y% p*QVln@7$Wp:e\N%}q]|r d&!5H9͒UؼyklS\KSt4 جLj4: sXAX8eWicؼ9~*=ol~ETNa7NI2>/tgF'?6Rnd?X7闕UcaP'o$Yco l(F7e,lЀrӼ9dG Cm3 h ^;2W ̈30 "%l(NA;PTۙ/#-F{IbFy ؔ+RzY\SA*=6 P%} CHި yPleTfFL Yqj+y\SYymyAbdBWڰ4vޕ9r`ј8MS YyD&TpU6rU)Ql[tc=4*=3;JyYzkl`1ى\h2txc1ɦB\ʛ_csߌM?ĨBƳ_ J~#խoN7gjgN?j?RюQǍl 4<[hy !;[f;jw:=~nwC{V>8_u:{җONN[wo?ۯ{wetF>~JgoQ;;iF8s{pc˳ׇC99q :1j~mg?q0ħC ty`Q: q"ЦB9,6녭Gn\ec-7(6vJRi>~%c;>izٲڎHaoNxh-FCW+{My@Hh%m MЀcRPv8<T&'#:4Z)x9>B\l|7F6V .c CgϞ>{_Y9Al@.FeS-| /ڶ]O lfO@4H3nK`x:Кh oz. @.6g Acx1i"M/'flc8vөeӼ:B9`Cr^à;^>Pjx9 ,Nl>`ְV~kk&Vc%Д(_l6ݔL8|Z ,E7Gj 9.&% tayퟭ$ gfo~-Ɉ߶A8ƥp,O pBN,ؘU AU^Բܚ^ѻQOQP% -K7n?Oo|}/l~]@[ceb!$EHHL ?JdyQ'UNFU£і͝s85z`4=âwؼ;ڳUa6 .ΧzO>|ׯm\嵭ٴssn\\XVxIU^MҢSZ;沁י麕'(|cYH' ɽՕwLsQ4F ~.BQR5C0Lfac{69WE8 y郬%loޜXXۺ0Mt܋>|aDg4^lJdC &a B^tgS:VWdRA+Tl >cpptlX.l|v$km{}yY|^6ap&ylӍǍB,S:J8Sji*YST s4L>85ۖ| Vg$Z͜#MVs6Z3@#v&B-1P #$W>/ I^le ]`ӱ,-=Y8P yˁHA-}&+= ~&TUy' }N#OȾJm+sb\A鶤l,bӑڪj HIJb>m  ԧvN'uv;tZЛ'Ǩw8f'ep PsdltbBWrZ|7%ӝ)rZvn~'dsJDC=|]?GPGl{kdB>%WrLxB̜lT)F`G$$ljg،E?ߤd#S]1~lm4vDZEeXz,?S_e  Ŧ #Vz"X:ϹYGXL ᆋѠXJ13ʏcnKEhvS *?fwF=PĽXVGx02%P6'Swgҗ{if@ !8lDcihІ4KJ\n;%x\z-9Ͽk}IV_}~.U…?)/߭?  }Mjj/UȲOpar gq{IBS{/nF!N'4AۍU @O}4I/P?H٩?ߧsBs,c 27F罁wƫ6#bŬ0 f"Pg>gqɯcCQe ,Up2d9ټQ0ЯrmltW =|8sv4{&>Lٸ\즔d!&LY<K4)|sЋLQb&308Hqr2A:HQr]aʇ zVi8ND(Wº>q")"͊P$h$ظiPiHAbg-'v8b,lF胺3$ 9=}܅cRWrI̴-B~.)`7M02>gVrhC1Y 0pspΔkKQ!:&vjF :S N"Z[+{ uE:^kb;CؠOeŤ']L8%|.Hd'N/M?}1=BﴻF Q&0lffhGw_زXl }˫P}Jbiɬf3hvM%(c`D9ٍ`Zbk-^J^YZof5hdY^o[뭣b[Zo4MNyXM,鯛UDpč(\1\hgũҨ.R$ELS)l(pًOMȬrdhmwW*w.e\ VWzzm1 k4hw['Kfm>uy}x, ,a.=Bjl?ؐSb#x &KTK2apx~zU6pfzF`jYot7?*k%xUݍfls8oM»; nuilfj|Q>$mO44!nFgέT_]GxFxfr\B{w/恍ـÔF]۝.DEi;l6lf#уD4>A'Q D#Br,ҁ_^Ӌ ŹpiqkLR"fDvSu 6&͓Vkln /\0<=zHv63l7h71`tnv'Sđ`LɄ*JkXn8JOͧpk1ȋx4*̄H%HuPYo/l><+\e!! vS<[fk{udSl`6~bxʹfM1li\[L-i)48ď/61GuU(IӋr5Qm*ŧRl0xvNX@kj'zG7]e6S{~V) U;zk4 oJa `IMc3l=Z) ÉtcSBԪƀ^-ur` M&|,M YLKՒ}?(hx ƹjXYK6ƪa :=I6 1&y'gj#U N2 +T@ 3:q\3`{]nC28b۪g"P'l'bCPl\>t4!8&1in$>ȓ!n͔7,-dPW1Ll $j pqQRd T ENQ|ojhP`& '1((;L˩hA7*#llTHr|SUҠnA6pa T:yl=#F=`R%44N[B^Kz%vMKatCI[Yʉii*3gr1)\Cr"̢yK7rᘞ&Qoe6OEeM6ihсac&=vop{A\t3hgvMs.B0:hE޽=崺 =Cni qح_Z)8m-#[ ovVrdK+}y{.lLi4kgic.xd@򩈶j(}ڙ˪΢_D88ͱiEVC4f*TpxqnH6.pd6>*׀s98Nkj8=tn⩋Ujxn ApKG:TؕfP>$;TprYYV԰K>]2 nw}g"$Fq54 u?IֺNg{Y]I0'Sm"z2Rp#˪qYh'4E1ΉL"?&IV혞nzQC<);fټ-'|vADg,Ӯ |BCc%LW@3[FF_HY\ \rl$h7Z+%ڌMjGVnH8\R8-,[Y,o6+CSj2v960C[Eȡdiչ8NjщE@!|%9Jtdu'mm 136Ӊȕa=T񒸭VKPڹ&JMv[UlYp؈ !3*q N'M'ѕy\V=)qߒaRaG4kv.p2F*! O |mCpX׊2=$pI=5Q(a$WeL@n34qI^cTwg֒JL-k%>"F#nrJa8<[Zxٳ]p۸V>Wa ٨Z'hd!3Ӌ̱:o5/dvb*^y6tƲv e[ZDT6fHod*-F<Ύj5t")atfBlȑ,U GM Bl~ +o.9L1edhYK\Vgf<܁zm'pW)M4uxh{:6r|Z o98+7#يLWMxp*ΦG%#1 YnyB~ccJ1 [3n}HX1JZBr'#;i0?+Du΍لlгTp6eӟ#2Y +dhh|UzϫlKm7%)B |Uw*7S$n#\c]a}\U#I[E\=KP.}rw bte TH[!NO4Kh&>Psgy8݋3#k,Gf>DK6M<?1{["+"~D ?[(J]؈3tpvd&= rfl[Fx7p4n}.~3?-&ԔM.fdr.!X[Z̾eaG Kxމ 27jB ~Z:SO|ss}uG*l4_Wv`9}M=?Njwni`쳟;W0(yZ}aƣ,\ {:wTo7!bM‘p\*A Щ5.{[&mF^Tu H g";?ݽH6E6᷻Wv66bOܮ0S%f|]c(lmlh l6}W tNf8Fys\)$7J"U! @ԩS&<˟n s4׹쫫ކDf'|rynrqὨrC0e3rl}OQd"GdrF%7fz[B$3L ^tb&*8ylt^azzڬ  _2)`2>|{!oؽ즐JQ0٨՘hAbM43eYdlDb3rD]iίWָonʼl线SdͷB ;oE Ûa oL+l7G6mK[W͐~Y]b޳}9σ?N0G 0R mVʎ+ ̜O(ҒsCnd[qTC?$_lXgϮ;>yͽG%U݅_,\~CK~[\mtm:59if\zxmETM?:̆,$؈:k0v` [6)ҭJ8CbM~lj(ʑ..5g\~ʣl:ݍ}&`S*6Lw;fÛC4[OhÛd84 ڟ͎K{Y.A6^wVF߄͟X\DhܿMT",Yv6B;%=JRaɡO*/O$Ԟ O<_1vgLD|E3&#oiMڦb mN1 #KnNr14b;V W_&/ lMzLBB{9-] ܲ^6c6 KN5X7`A= LIp֖e1ijԲ^i }G(I$4l\mŽMNr5`IgsQ jB֐|N 鯱͞u[.b_H\\_Z8cҌK\r\69_"l$NW䁚yH~t<'S\ˎs{@2eG2un搠:rULbxWxf sh d6'p7JLGOpP.! ?qӸFi`xIcb63xOhcneQ?%$SScSS?Gi> SP585gpl< ;6+K5V ZdSp`)A -l! lO[B spX($Ui2%E*qQYC@qvB~3rtYvb|~4،ゆ HTߘˬuX ڠaǝry '"-ɈqaR>uy&$[Iy,4IqQFf64 6 C|$z1bɱh&tlbbI>?eN%`z{jx*(ԘF'4v >%#M+_.ӝgž-d4ٱy]cDx Ss4MVƎ{*%*Ȏp&3JR-?D;#$%uCHH*I1i) DŽ<2c!:i ٰ,ljK`eQgjRffNh#l 9ք~/R$eMX)oI1I,ؔ˧O.3~!7d#?F,#lʬYFL+9@ 9%lN5!dTdT$㌪5<&[&FoπTȆ5--bZOx W6s̆ IDATI3;Cl`p,H$M{|i,6w(QP1lH 'GBl`6al5l5SkƸ x2R,6>ɋp)f6j%*>-L }%";lळѧ*.^& '|;Ț.f:*|PH3]C!]D'=y*>AdO[6D4ĕԻ[05;0mQ{K+.VQsfa>fi TIS2xVt\d8eeSKsmd4,HHxO-MrZ M e#8"*ɓn6zwzF֨A  vR; e8"Lc7G }zSiY8џY86n4xltEH)>yLʁ0",ڨj~fubCZj[M$B2KH^n*#hۉ`Lq8U",,)(a#[,hafCOQ> 8> ézNQ˓ܡ]YyQ߻q2^GD!8aiX_",K/J/Πyld|NlXuj˓nCMRɽͅa?_^)oo[ͫݖ/r߾+7g@7ZE_ XYFN ʰ}53-N6؏7bc N)sƑeW@F 38;b}CӇܶ_-;+o:$I;wfDy`ceݜ<į!62GFI;3l( 'J h!Euʜ$l,?MתWWŢWD$mrڭ?ym{|X& i{9}Lr+7pW-o$QRbf/ABbc7գnJ2N ZrؤÜzQcWDna1)Ra!l:*lc¿M݆䌇\aʋ7MrEz6 9lew幞c7l94F; + UL\@JfLLQ3&cV'/[r4lJfjY?5ךV:Lۭ]n/cfcN,fC` !|Fh+#$Jn}y#aE ENX܆ h&Hx z*8/.^ō&sdb} 3bJRk. S.|ѾkժVpqi:  3"|4 ᖻ ΋W hxo@ Jo-:Ô2FbuMeda'̨25eXdTTtxLNj|ʰ v{}wߢ߸|mA@&{X?vTngoMpie!6Hh6!Tr12~,UaÕGٌJDhJy uɻ(m5 ߼Fo[$4~mqiE[=&\6_^wk[{ 6L.}7†-[MtZ93[ϳOE)vF٠|XTFOƲîd`Xan`8ל9l&拗.zo6{Odvv9UpXjtc7;yqFgۿ9#W#l4DY7ȱKri4&Z ]D[nsw1!sֱl4FjŅ  H 㔀1֮M'}g6Y|>A'm[yH QEaO֨Q6d2IXF3ԂA;txus)6tD7adx~1Q6f1 N"3gVWW~~m߯zfuo~/߮nưIКZv=px.?3G2 iD 70Z\uRiI58p_ = "7̔lsi>X|tl5[j KQ\9M>Hx^1Dڤf'8C8u96 eqԤ3zSلM>2kv}/mi8't4z:srHՆ,]CnڋY0galPMRըi 5^4^!{1^x?;'+h.V+Js޽Jj햖V?~C׋%[*Qr]6M] 24 )2>#N6bxd傩y ]ۡhQC /T1 ?{ym$XR NL`jVU{6כT Qy}~ח/Wa[.$8y lĔS^dI h)8Mg.ڊNqix(X).Mi!M,d^Fl($(MĦ7TRkn-7r竫.j3Zv14WtF Sn 0򍇷u#GiYsbM dB2>P1D~>f"cClfvЬ>VC.V+pvZS s[k©]2pvM9oKP.bZX9^jo~mGL藰F . TJ&qXďxgm88rQpMXp*'??9M*í֗ZGh۩ )OqMzҊ::snucðty+. ӌfLBϠN/O(FQaahT%Έ-Rv)kؘQ7(WEj [ɭ`98VǹVvl_g(~M/kSԈɯ5{7˹m13mQ?<57\v\2VHD͡a5DKҎ5%*ĺ1pReX_(9JP R-\ggӿ\..,4KsssVˬ A#ŕr_33R$o?(-~{yP3]^@aӱ{+Ç'oVpM7>_52F i}r~톛@J(q,TߦZlL8F='Dy[yvwfuv :tTUK t"wWԯ~ʹf1 D~-b06qMl9uX+Daƀ l3BNAl𝱑T),&LIa X.N(c'Xa s%DUx?8 sg14_5gҾ;r:`S4^um lB/y$6p<_,6y@}$n9O {CŰ]aP㢼q6T]Z*J! @xsrzkѓd6!aE )XRh6h OŕJ/,i7߼_{k< l7_\Al`}%' N)u/diP9xh5338NSP~ņ9v[T"incM10&&5_U?k_3bkJؠ+̯:i6}|gpN5Bh7u6r iSؐ&_BZseff-܀ND69NJ#uy먎h-ُ[XӾh^-iWLwY~Hd%!4z)kִ͔Vx Of9S&~fyGZUiVBÚbpxuhaL\F\uiֈ7QGJ e)ki4Ϙx2D*l m ? JO_Kq:9{]!A}Eg#Fj* |Gj15VӟM NBP6u8[ hbxz;}\:UC(<.G>&>O{VAB!I",rTܨTencNĄ" ai1tMW٨w/IPSKheZh`s^o*sVxoXݣa&06$:Ae+Ć; lRőd3IbġM8 o'64s0]bX@'GSUlCLWV!op`s g -!݉ /C@B hDo_#l džsXZXPR~sIÜRٵh q:rF`/Vs!&ƱdHb؇슧A31/Mc&rBD.LHfo.Y^Fq; Զ44RxWTFO]AQ-O5ie3j@4 ͕r ;-G̝f#sQiċA4lY3:! ʡoN. DkJu\I6!8Kv/j2&6@9х(1)Fᮋ!8=42(i"J_0lĔY1iK36OFfh=q,jMu`#6'N~R1&;Gs%ޠ#l΍)"q JlؖHC f㰰Xt('PV+ 18A0^䪘kDͰJb׃O WCi[ IH;׶,[)ޔgTQ$i6aZxѕ ;&d(S bf^vf8!@=dM;{}+$.k`GpjI /vh)Pm4m"8 6IZ,[2DaCT#Ҟ8v3iC#k,&rEmޡSKv%TJ$Y%[YpRE! ݪV#+w.X~H #Yu Ԉ蹦hbRY92ȦfIXl8&K%t dN0eTW ZſL6V*wAf8eĸ3]g@e*lU{&vwDdn~(kl(Y+y6M ׳A3Epڛ76r`Cp!tm6xĻE]fx~kBlv:En6JB6|j!sŽ g}lڶ*@3f#5O<0f0;5L(`J`vBtF֝%#I#nYTױ+ܝRb0鞿T+4&„͚!41縈M/V6 φBc (,Z:-5:6o 8LV+&άMg/*96$۔|ɿ4M1٭"fΕ+j{ 4wwQ<)7,2Vl~<ʆ0C6v-CL~ Nfl$SO߼/luI4rYժ'ˍT6qץMwilt6gĦP66BZ6x2NPWr)lL56$F4g(oĪFs(koPU7Jp\Hdq7߅˱GNX-Ŧqpr!slfɳa44 ‡Mu1ˍ?t )lT DZiJ[D;´_B7>6fW{'7*}XS+EK X8(X2I38b*Jl68J69AHP=1flA8%|w6//mqN̆2ըy p؏E/P͚GGAbQ+>F4քt{#6,sO&\՝5&`1}CgF3c96!;M˜1?C6Db߱)VCqJ6;;[[SFЩ\rpKc~\ /l4T0j_BĦӖs*'T6bT|飦٧P#=&5uQnQ6?OiΧsjo bSQ˜`CUViR|ޤD@V6K1c#׭vRˡQ)2g3lDZyM뭐 :qTroЊy c6Nno8ʎ /^*][1)xB61,xcρ)welh.NYR 6[[&$e/Y IDATq~|`Sb }~PocErllMNtsDa_,5lH#DaE -Ȃ^ h6gqO'7uoߜ ߞi cI&z(DS9PWl2H*ov0gtYOS46ZN{mS9a63fkܿ 2;xI //ݝb6÷/O/}e_.CN7ؿ qcsE+WQ#lrejb1`r3M8n}Υk#'+/(V OXj;Z0-ȉ<ĭJSn,@A1;6eeXB ħ[F-_e6xӐTsw]:s5'JD*[C6=Y$?TRޚ+2$y6/ʍ7 s>y, wջVZbƦ^]Zo :ǀM^NSp+9ӱP÷cCg.}8[n%l>6 tBR+,:5y8T2*`S:lW 8 2(T*= %k$զr[{M [np^=1;tEijR'v8ӭbx5oZER]؄3?k(C63ȰP,,* $)|]O0VT<7q[fAؔL!C+36)Ui)1"3`dAosz6EZr%WMa'Ao6 8`bS⭻Sj+ȸey rx:Gqf-&pj#`g_e.C%ɍI6͙l\_eٖRx#uXCs__;>) ![ף2O2< >_ʇ4xD=iJ6x^LV. 'huTm(rFdsKO(n?{wl?>N^ 1k2 |Փ1ħB66rsulZ{I6n,JNN lO+C_"7 4dd z5Bl(zj5DV$C#%reՠmTD$l<2+}}NpJgߖ~sm$ Zzw¦i#~6~FMkYI%1CHV)Jd0&y<Ԣ7^,$a &56fh@ZO}ogtlѹ_*?o|z9/FZۻf /_ꮝD|w ؐ3ݪU67+l-TG0>4xN_oM9mz>mJ`ܯ=rc5Z.Wr_",MS^N0px̿/7/i!ێԮ V†GySx -%68 !l4cs7'tv]-go>Mo_&^M6¢lA)$~Β]#":ȺNKԽ uؔEzlR+ڤ!fFSv|I'Uk7:]٥;6iqR%xǖ-y#\9[(07SW9CCl]M` d|;_Np]F9NB Cm6ua#16\Sp dl-D%usL8:c%x/k_cqs2L~$C`6߀Ʊ1C&Qp"?FT QqspI&YNpt:.4 Г ȏ䲶鰣l:<ϰXoʛ]o1>|Sf4l=`i)W oE_L spM$??†VS-63j$tj/γd9f>=;uZ`l0n `ƕ6*uj:A/,W4߸| %KXY8ˎ}eM*~Hbc$7Qp7u|=iN l0w:{hԝq/<$ˇـ²,OqxUؿ䢈'"_"4Y kfc#.6MdSKcͤ^ EԢJq$ԝ}i;9Ok:,- m0G6sZVl+Nʆs(I4n_S AGT$;xBMY=('7t^.L^ђ(88cXXl cld\X"U6fW2_*͎ͶAPI=0iN~{r:tvnI9axQ9PMwAF k|#jejL^/fTϪU_y 5*jLv#c#'no#viY u̶]/0$ ̨P"R ( eY"Ub$hPV*w)l"CTU}6"xW|#.X8xg]Gb=p$(,hxT֤ o<,7lBr5(4[l굔 j9UJUx\h)U ȠJ@5deXV}R <_zTPWxLIX áMh|6(lF̧pG( QL.en:PG$6tkie36Tӝ 6WddCLvQgMBhbWl'۞.| VSc9)*zVK=Q֎,tRzew;1B6\l7F5ܖ4FtlesN*l,j qcZ99L}gUk$7Gf&Ç^Q'm׮ 'x ?Ib#k$CJ 2lPohoQ6LŃp*$T^vR[a "m>vp;g.4 cc/ma|6c=ېNơ\%n 6nx[Z+bëԠ/ ~H1p`#=$Ot+F#16:gxGs]ܱIkr9幀RFi.n$QcqP]:` 4 U7QOGoMʆ~w@x'@lg.]Mq{`l΍S#̱uLHd :F;aD_giDik;A/E̹3.qUT4Z*,p٠jan0M6a8;3'a#0r㘍 qJ7:zo@YFR7SkqgJ`Ą r1[&xAg6=}qsr\ Suf̴qrc%gb<Huq)?p\VŃS!ȈUvMV %~b) "&6fT:[] QM|bb"lx(!b49yrRitvn=aeǤOofO- ?a6q̀q6mfwpi*BIr`ƍ5,cl u]T S8^fv^`_ )u}a46tN͘9J&(<ۚ n]ҲDfC]NRlC&&c&YcC%\yVLͱ$cd;L16p<> 7cqN6bNI2HMWNTW=9znIeYh !z .M:MM]G^!cd 6L88Φq- Ê ɂƤWCU/ݥo~~QUq4}s~|>*fiFg6wDH I9Y-E\`ak46FN!NLㆮ"(rOK?fg'c OOIb1#-lk^#&8n8tIf4 cOIٿ/mH%!1TܕGF͉OcDl8Wi6o0G,o^I#{Hflh$5 b{=Eorg񐄱lh nzlRds1+9MINƆ61 l46G*=z7 xA5aK/ J%Ґ YבM!6YBuD"ʦthDN/>QlrTxRMo lb7asu6u;myL269J]T u6{d^qW؈A z%O-BDoC0U(5dpK>Y24όлUnw267,2g23$c]L XFο~fl2mc~ dkI΢[p ^g3-ؐCfa}FP(6Mle$`9'IxA?>a/BD6M I D*afݏO lAtF %8baW,Ah, wn'[XmP55ɊJG2؄FźF&#<ɕepGF2V]8ݧJ@?Ⱦͺdv1dg0FJQ4Wl:{F26_ymK6'n$Y>BJͳv!)24I*ɉvIBp֍'vF3h‰3z#]A^ Iy'qh^m.BЌx9"-Vh, 2,gT|F ]+YI - Ymcn>CƷ 78TF(@&W<*ڑyJ!h -J%wM5z5_3Ǐy  ?, ځ89=<_:l¬@6yTl.YedSّ@P.lFlnS?cspsږ\$K7*/iD׿Ia%Y5w|S5 &(ijF E 7K=G G_#vɦClη(QT۔uR17n[tIϙT6hre)x슫:ʝj~8lޟv?C g*F6"B=a|ojEmPFfp*J$؈WIL+l*Fd‚:{<tv`S5iX~"(l zM/NG>pәF:95RhJuf0Ja&LP7:Pcbm`25l6zFAAE~Q6| nћ71K%"\ȑRCl5L 1< ч|p Bx  ]HDB*3҅bË*f23ze;~hV;/T`ѠpQ F,bhYTgWklR`3U$\tƤh&,H^ lSnIΛ&nxf Pl߱126-و-<2QDIcb ԙ/؈*Y+FctI*Ԫbk,ht,wn `Vq6/6}ӷ%>j_F >-a6J{a6"wʦ:_fdڄ _TvF&&i2spn) 休36eeQǭ;M P8a\{]O{C`\@z %rMǞz#8$E$lz"6aNSri:NGm"|6J3`Qk5bmh;}YTAf78gB 2 ?T\4.YWܬtX&wT41cuۛ[J/ ݍF ӂMldj}D|`uȡz\ 'k+2IU!&جF l{ۺW}eܣZ̀@.b0&ʅViWP 4n'x"##ql]hE9Wi߹,;{(|zruA4}'w,MXeXJr[8 gfΦ67az8IQrljmhc^/Rp_;bU>|liX$*0hGs.ׂ9n8[N3iܚ]%x~!C~8[~oZit.LZs/0y6.E ou6ĎXK ll+<,w0_ήƤfy MA뚶yBp`e`#lH DgD +2ThQ˺aEl6D%^:v:9ZeͣjWو!OZ8SjVkj[WhYtu cDTFovǣm:UQ){ʇKa7L͹c78dCWl4ofՃU4'AEM= H|M!Rdss]<(c @b61p<;u,cГ>l8G9/لY|+$K6c\t~+lgPljek2Mh_,NY'τ3X=V!k![{GKi-2,WCpH䣞61\Q>4W;cuν -Ь04C;m>u|]YGh8mG6":ޒ{by<*m1Àb͸dS]JCHk;SЕl->\/ IDAT-c\yA.Q4en s][K6z٧2*QfMLt-]ndTz"N.13s vdIv]NN ˈ~&JR! w1`B*uSM\~㛹{9-j{g;xhe`+29,*UXg$?Fٿv9 uBW.z>>RqM-݉|99&c\kUT|z˅3nj&zleq>=Џw!9bԉL VMy1w]. 9%, 7b'Hl!`Ѓżv:f*V3VL//l%l@6oA2!t@Jm6J Y<%U#[]T6>:ﻊSQ52S "5# OhB} [j#wiylŢde/?§\L`c YDy&)r3pw*4Ơ" J_Z.ڌ$vc}y696;ۺ}f3`8 @,Y7EHBeemM%yn-rsh66*z.?>u en*+v)l_߀U1Wt-±8_ ޣO|,so]jѧcؽw:4_ߌ^Ȅ4FJHPȞ8inW,+?xʴm_iټED&:^n3+f#8Ӕ0gq3%T{{  J_ H}M]S-cGc6؍y)&cq*kV?94FBQwL52p.NXJt>5lc3w-ϬQRQS:)o+,hX< /R:ѫ;Ƥ9ll56=i"6ބCTImGS?Y屆Å5\ex pPGK*8?nf'P_8G68vֹD!p]oXt5gx/j|d3l|8pzA l!dT1Vi]IN5/o1S6pZK AKٸ7I͔@5~Q_Vu#<mh*,񼰙=ZpIl&X?HTUcS!"k޻A偠Qێsc=]2db\#Y/\͎fua_1&mDc8JU@z+a9`n90 CF4loV ,Hw٘[M(1 6.+@tq` G2APX$f]fbJͪjmFn[@`ʔ-qQƏxKD͇tȩm)Xk^fQ|@ɾ % NvG08UB)uWo$lv)~ߋJW߄c1EbRPm<aGٹ6$Xv@ "! c-kp`5„lLFG63L(?zW5[s\%[~w]m[vū'dZv@yTڗ_=[ ¦œp6l=6/:Ն Jxre[)muSO]3#F:IoК 3i5u堺;{ 7h;LsUҌtrsyAۯCsWt[{􇃨#= `\|l %"?Эt.e%^!r Ɍ2,əWwJn%iz,%dÁN֘)׫M#K$4uͱޢgLnl:偖Fχ\͝؆rh,sEIwu߁ +w,qx *|œzgKQ2 :#h]S]d-b#&h2?#0+B9Ȟ/j1LpEԼ>!CBSj)a\|>$4߾*ͨN MZ{gI鰄S/;ͥB2<8 l}q[,re!ȚJy#u9WИqʍx/R gГ4)p 6pL(x.xT_}י@#u$gdD‘d֓A t zi8pץQF{/м]ff=fSY! .%ݯkBr,(d$R&k^NY9s 17IaHDu6k׏vɯ(%*:l bS%:B_Gm[ޮM֟qM(wȻ̬BCOH?1[*) yzz6k9)6u^*#6?ضrMX<Ed6…\59/ GuM:[bs#\Hl￶PƱYSIʸ]IJ hX;%EVj{N.H n 6bǙ|`Pꑾ/H^C16GTn:Dy!B`ߙ˨cӽN tC#l&p5G_9E ful&RR`γ| Ϋc Oȧzv?0Rnkq[`g3=6GlѶ#0R 1}a{g4X~O=6Ǎɟ0?= G[&Ac]v{N]|ټLGJMe403)r)iTMm}8-I l6 u0^hr wbGTży擵BN@,lDdzJ C+KfZw{yZMF4p(8=˶"Ʋ\cpNۣ%F3/ g(KV`lT !K MBsޮA6"d¤VՀ4 %0& >>8Ovzri+lzR\3NHpNjzݲx7"&TF4\žWΎ'B*JF8ÃU鱲 /#\KVm7Ay|fF((6g`~)Az(Y&0*H!$ui-%|Zf di2]ޖ &?ϑXR6(m_23W`6`*+t:4NZoNpZQM PTY&LLI[ w^lۯI*c`3lb#-/]+'a GlȦF6b5 Liazorr6]6?23T]wo1 h@4wn6MɕH)*ADeájb.G76;dӹ$6XB6݅DC)e3jǴV%02(!dxN sdV߃A*X{phNr_G`6`Cܤʄ,٘J{g+0cͿwj´(7p0}Ua%RciB ?:AK٠ZsȊ~Ld` zKI jN^Q2(RrVXNa3xQglv[n.Sᄲ,0YF6̦܏bqgLi VMR%ԲJIt B34'z0QB\&*||H;[QPɱ#_*iͻܱl ίF2K`5&B[NFGzR7cBjH>#ۡB㮛'd]y,i ϗշ);PW h6ɾςi*-ߥFku@*n69 O.zw[|dC!i22c HLBq {σ؀)%+j6]5F7qg9o:=&:Ud dF uu'9* N?Ku7QRj@n.viRcWa؀ͣWd[˰uKrʞ8 )Iy,25/G]#VAтl͏E`6l[ns]ѤU3R0pn5Kl HϾYE'ʦ2,QbD ~̞ͨQpVOFC40@~~ h?Cy1/620ruzfA c6u* iIqDؐP_hn loVSO7<=qxooq{:BM96ÈMI!D9cg|jRGg\1[ӻ)nD ';bs@#_b/pp ؜iW6^G!;:e * ؈mP3EE4i7#ׯբPtSq *lL _V ȥ9 mX [A'ˢhvr/vUb$6 dK2EGl`L^oفkDXD-Yt,][EFy4!7~)!'f5VݭY|Mp6ĦC6$8_u%92^ J vK &K˶d0||86l'n@ha} ف}W|=oh}&@+t\j&H#85`%f~כgJ>l,F/>C1s6+pnMXMghH1WnW (xV>^mEKel 6DldMS':Zg U*m sKt|O?>GStJo:u7n gLyF;&3uFɀ~yErj!+.1[ޡS-*0AGe,,ȯ} r6ALSR &36x!)nD>6^Q e,M^"r6ja3AƦU$MÚ[8ĮVx ^o/I)Zu&IL$sΦoWj=glTFPԚR}#DV?QO3yxC3=W+0z"T|d'0ؚ36nT2ОJ%r'wl&9ܷyf#\^`?U7:X.Mz('dF $گus=:{Lz,sIwz8.m5 ЃM"Hh-Y '@F=RvTxxl>':%r76;r4 x]qyM &l|x@#l]<a@ 1Ր5ر_ j;p rQl6ʋfSR6x/RFgc?t*csgl OAcۍ0Ul((fl|_֋~q(MUltU{%׶ 0Ƒ ľvν0- Uل wkh9֋ ƩV5 q\f@p6fx'FꚄ~dK s\*UZs*DQddzY*%o(Zwnb0F60}ÊJˆmCM"jef6U# :J(Шa,cӧKlXռ2l^#[Wvel?S|i1 [m!KRϥvJ|nl^ 8̴5dL}挍:Jm{jRl&s1r'] A_CKzImpCkdl pD*ka,sovN0׼N:it}Slq9Sр0fi %9\Bflv ]foe3=o& QW~XDNUz=E+/W1x;C8r>)x>I@2"l82묺EؤۧMcmA߱qq-JY y_NV'9ǸG6&^/عޢ >pƦlF*8q]u/4/zl=gS31k0e)_+6]O@7-d zǝh yze|d62-a/*16c)jFt?=M@ܓ8 S4R*o65՚/q:@K% LuC?]:ٸ8"/Souđ~ލKh"9ñZ19$N VTJ5e/g=8\/7WClQy{(Gd $# F4 NO?L<8 [mcNw*l֊=S~R!xX&O0dNXxNO6rdg\ 26ׁ. 10bgrlG^M\;sAjE39l}/>>5tn?TW]G8 0'~\ D,cCk.r9Cme}8|v|nFu He8 ypTT?se2Ljx I\TSkHK~Ύl@%Zu;qȻaq4'ChQO.7ΨMh,yxZ ]J/'+"Q0s4[u^c*(U+}~ .)~y%gzdiٰFFش];'Y&^9lrg6VeyaR+Q7'ꒂu!{4qrο`o+b`5F>sQYǰK S/z85l^i*W9ߠrРVМNzȧƾɡ༣ܼ'k0+Ztx$Hw&D uؒX<'JdK"P8l梅`iv*7@;iK>p#j7¦>) z= f*}w_;hVnmf] d".^dO$fck&l2_Q n7 %O|i)34H'~M ߇'|LWaeMd$D`a )VE8%6"MM˻^ {Nސ!ňR$ 2ـKWwc#t,8=㋬nPzr[Mn6v:~ )8rP:hU!O:AVSݏ}yآnK6ۻWG~%ll&wjw$U;&Ҫy_}vlXrx~Ѩ<3e9 `C;u׮߈ 6YƸϴ`h[̎#R hV);YX XشJ{.0Cz2pB2VdP!g&c{76c5ìk+G&Ai ZSpKwZ٭WQtXjӫ(x,HL4㦫OJ)lkַ;O,ZV \0f\gmCcÐ_k9)Y ͣ ROy>!_74 l2suSHF˦)kl|=|Py JdVJKh2G,XrpʆxfN…nSиi6ClNaZ=;&?rfTixgio",E'&`o Y:APnd9,Sz=# uG6Iê4ǟ'?wqxe(Mzl'|>K:ۿT~bM1׸q `0FHn`("0 uW͇6;<q̮;DvKyglXej썰a> ltAF[ƟOdch`I֔ EJFhwTOӛeu/M$Cconyc_yx`opX6~&wYlG6KQ4lRZKhlkxp6)%iI4y M׫pYٙ1/|}zf8cS,H%N2ulmp8 @j P~fRC.FȬUp TRa ^ ؁dΏ"7gÞ+iiR*M[ghuv,z)_J؈%&l $r 7fѬM|B9?pjaU?$>bA_tk(`h؀Tp&eA?8=G6+:)-ccyyxy)H mR]HTpfA"lF[r6z'QR]lē+ecO&Nl7_ wHף0tv3FAa=lFsqul $v̶[X,dASS+M%;P*-c(iZ߽9+^1 k @̇\~&F'$SlF"7ZՌݒQ\'L:й=Ӻd̙ -c9Q`C0UɭP&`<eWnMnXᜋJ4ȍsX}RU#*MY}f{ 6 B+1u8 (MC5I뛄k[ߠaƄ  &Hd)4v3M@#1zX`|k6y%N:z:D5tEy60e ?ttD2}k,=!zr6O[,M!͈;bsI67f^ֺ 9tCo? FTT(]ߛo.fvEz ¦։Z)#飆jtDz>4ɅZ`?hte`NFGb T8d,zM*?:65󝱑F . h]!28$f/+66X&Cަe1 `@_(>9%@&AnTJ̽"Υ 'XyQoe6R5|z޸[qqv&+r;aF kCet: IWydPl% xA=tqPt,܇&~aPF4DerSЀVU.4vX"G ~ihcl|x[ddFism:PផdsX b{E|y4 f"Hq䒶ֻEIg9$M/PlM SUl~[-d7Kzp9l30;׃FѢSnkp;@.flhVyy"؄)) /śrlgW-)IsU9Q\L!1!)$.8 &-0&l4ioڭUřw!]˒yg|:H%W) V-$ΔWmZBb.٦qp"~ 5T 'z€hINP}ҳdc-9[<}Abs= γ]3HjCʃ 6Ogƴ`uUYQ~Z]:FTc[a'")]CtoTOZ'n -n҆+G߿0;޽z_*7d$#u/|쩃qFT2 jFѬBcd ՚(ǕJ)v OF,5`͂ /I`ݏ%6]y!64iP<&z!QVu hibX96N2B  1 9iY АyN'맫OW?y/zr{8Q".gs=N&5lr.o`4E٪@ln4q!ԕ&]%M p3&o_rݩp %C=k5N T0Bp'*ߪ*2Q^iN;뤖YISiaSba 1Bd69xE5w2ڄd<} Jh "fpB `hN#)S) cP4Bb1rl>g3>")oo /زlL!u3oE78&G44fMpOZpcpt4 ǔ86 4>!!$6:'_os"N.UK~n'(Z4iژd8қz=x30Ά ĝ[77E@l2݄f5:`P6Yġv>*]`}m%yalU6\KV1YߦȵG\Fj?' ^ w{)XӢzdC=&xt3*RP.j@}*x6M"Gd) C;jr:,18EݻFL+/\qjҚ6JVpO eiT} ltdmQ$n%{thd@,qɨRMwDZF0N"7'e 5iM@,{,Eb7s n=q vJ9h0IDAT1-x '* H^v<\ T\ lBD啳&sTLe/{!F/pVBCyV Um x  1vk;`2r!jcdhN̽gp 3n3k'8e6pD.`_xS8>#AlXQPy'!+-P!{RfuV6.Ъ +(kq.9ZaXqS-O؀Nd$nث>"q-BfC8rJ<)_mFfDCH'j=q !i8FEƕiHaӗ =脛K)dk8/jzl|1JA6k( Lv[93{& ay)s.4 M9~1KMJºoT,ApSPh]dM2ڱYסw$R:LO"-f$ IXdU9?*r\Wqٙ~"t&/ a"=4>+|M൱-H^)} ȪN%ս#tl`)T}v\Vø:PX1YM6w賉ݏn6z6Ilh6[X(~ExħmaVS;7xp3K'y5}lK?f@"cwT6P&z .^وqkJn)uc;;'Ahtq#+^MCDu,!MGw3Q<T9wftt4ޔyn^`U}9_vifLL$cSJ~RqX Hz87Ǝ%(zhKoClvc:9;˽o;A2`VY67Hsak}6.XƻnҜ֖.x8fo4<,# _avt%&v>QWny3pȩ&v5^r6 asD91YÆh N»9:Z7$66rͦ-Oi)) >s;4e5.VeĦxw;Ɔ8sTbC Y#\6=amX sG}>^ݑjy8=6mL.2vj4 k`Gރ D6 q+|Q'"z$7ۆWoIu3h$-SS^ϽX pS5'r~RPbp^Uo@v̵Z}6tk"7lNMVĦEi lxM5.*rcE1˾_~{W;{+ͫt3|3|Wr'jplCI?rs9\}4FУLa{q6E-l ( 2V>s3= gi)Fn3敧lf%6353Ǧ/Dm>I8'وA٘9wzMlQ(ƿzTq7^fS-@/kxb3>6㌻' MQ _ G⍾3?ԷGeg 32#~/l$ :}be.ewњ}|KesW̦9FsH|3uFdf3U_>hUw.g\Cc_ 8>/go:Ɔ D)r巚n><͞@RHl47FH_f۶`wKf9)PX8:cBWSiAxĆ ; %8"9,'VD If+n}Q=ODԨ(-=X YS2Uc#8+w ́;>_)pM6q6$Mf=4vZP+q)h4@p.M :>0g}K3zF3N: &( `N.V8tFX1#N،aalal U:HSz^KM52,48&Ndr6Z %i5{"H%6Tp6heA߉YPDndqѩC˲r̲-Y#lڵGu4Zxs'69 8߆MZG%rc[\ lt|LښJ.{EPׇZNk_*v^s\ڝW,Z2 ;/>w~yg\E|5x5A}`s?W[-_=xoS4-v2D 86 L }1͵!;bl-OrF D%) &4ˡXYHWB,!` 5%0bSh$# 72LX5O͓f3p4JKtR ٹ_Mv~/pgo1 H];o<7O)>6ݑJ)sl4Z,ɼO[|6a6h 4 @Wҝ'jQe_-6A sL *BϓȽ`-B:i>,"ĝ~%_VVgoxecdCX~Gdrf?4+&r"V&PP$t $y(-.gY(%cl 57EhlPDT#zSBSZF.=+1MUS|ټ&#q؄C()*1><1&i($"GQlֽM3#@<@BDFиiA LTuj@_7a3 l6#<)\ce*p>k"3P\+AIl]'>졕6qk6$KL$8IRp'bCP[r&r#"& QsĘHq6oeKFj0ZI`9ʣ IENDB`mirage-0.7.2/mirage.pro000066400000000000000000000063551407747233600150000ustar00rootroot00000000000000# vim: ft=qmake # Custom functions defineReplace(glob_filenames) { for(pattern, ARGS) { results *= $$files(src/$${pattern}, true) } return($$results) } # Base configuration # widgets: Make native file dialogs available to QML (must use QApplication) QT = quick quickcontrols2 widgets DEFINES += QT_DEPRECATED_WARNINGS CONFIG += warn_off c++11 release TEMPLATE = app BUILD_DIR = build MOC_DIR = $$BUILD_DIR/moc OBJECTS_DIR = $$BUILD_DIR/obj RCC_DIR = $$BUILD_DIR/rcc QRC_FILE = $$BUILD_DIR/resources.qrc RESOURCES += $$QRC_FILE HEADERS += $$glob_filenames(*.h) submodules/hsluv-c/src/hsluv.h SOURCES += $$glob_filenames(*.cpp) submodules/hsluv-c/src/hsluv.c TARGET = mirage unix:!macx { LIBS += -lX11 -lXss } # Custom CONFIG options dev { # Enable debugging and don't use the Qt Resource System to compile faster CONFIG -= warn_off release CONFIG += debug qml_debug declarative_debug RESOURCES -= $$QRC_FILE warning(make install cannot be used with the dev CONFIG option.) } no-x11 { # Compile without X11-specific features (auto-away) DEFINES += NO_X11 LIBS -= -lX11 -lXss } # Files to copy for `make install` command !dev:unix { isEmpty(PREFIX) { PREFIX = /usr/local } executables.path = $$PREFIX/bin executables.files = $$TARGET shortcuts.path = $$PREFIX/share/applications shortcuts.files = packaging/mirage.desktop icons256.path = $$PREFIX/share/icons/hicolor/256x256/apps icons256.files = packaging/mirage.png examples.path = $$PREFIX/share/examples/mirage examples.files = src/config/settings.py INSTALLS += executables shortcuts icons256 examples } !dev:win32 { executables.path = "C:/Program Files" executables.files = $$TARGET INSTALLS += executables } # Add `make test` command test.commands = find src/gui -type f -name '*.qml' -exec qmllint '{}' + && test.commands += flake8 src/backend && test.commands += mypy --pretty src/backend QMAKE_EXTRA_TARGETS += test # Add stuff to `make clean` command # Allow cleaning folders instead of just files win32:QMAKE_DEL_FILE = rmdir /q /s !win32:QMAKE_DEL_FILE = rm -rf for(file, $$list($$glob_filenames(*.py))) { PYCACHE_DIRS *= $$dirname(file)/__pycache__ PYCACHE_DIRS *= $$dirname(file)/.mypy_cache } QMAKE_CLEAN *= $$MOC_DIR $$OBJECTS_DIR $$RCC_DIR $$PYCACHE_DIRS $$QRC_FILE QMAKE_CLEAN *= $$BUILD_DIR $$TARGET Makefile mirage.pro.user .qmake.stash QMAKE_CLEAN *= $$glob_filenames(*.pyc, *.qmlc, *.jsc, *.egg-info) QMAKE_CLEAN *= packaging/flatpak/flatpak-env QMAKE_CLEAN *= packaging/flatpak/flatpak-pip-generator QMAKE_CLEAN *= packaging/flatpak/flatpak-env-requirements.txt QMAKE_CLEAN *= packaging/flatpak/flatpak-pip.json .flatpak-builder # Generate resource file RESOURCE_FILES *= $$glob_filenames(qmldir, *.qml, *.qpl, *.js, *.py) RESOURCE_FILES *= $$glob_filenames( *.jpg, *.jpeg, *.png, *.svg, *.wav) file_content += '' file_content += '' file_content += '' file_content += '' for(file, RESOURCE_FILES) { file_content += ' ../$$file' } file_content += '' file_content += '' write_file($$QRC_FILE, file_content) mirage-0.7.2/packaging/000077500000000000000000000000001407747233600147255ustar00rootroot00000000000000mirage-0.7.2/packaging/appimage/000077500000000000000000000000001407747233600165105ustar00rootroot00000000000000mirage-0.7.2/packaging/appimage/AppRun.sh000077500000000000000000000007061407747233600202570ustar00rootroot00000000000000#!/usr/bin/env sh set -e here="$(dirname "$(readlink -f "$0")")" export RESTORE_LD_LIBRARY_PATH="$LD_LIBRARY_PATH" export RESTORE_PYTHONHOME="$PYTHONHOME" export RESTORE_PYTHONUSERBASE="$PYTHONUSERBASE" export SSL_CERT_FILE="$here/usr/lib/python$PY_XY/site-packages/certifi/cacert.pem" export LD_LIBRARY_PATH="$here/usr/lib:$LD_LIBRARY_PATH" export PYTHONHOME="$here/usr" export PYTHONUSERBASE="$here/usr" cd "$here" exec "$here/usr/bin/mirage" "$@" mirage-0.7.2/packaging/appimage/README.md000066400000000000000000000016241407747233600177720ustar00rootroot00000000000000# AppImage building The image must be built on Ubuntu 16.04 Xenial, to ensure compatibility with older systems. LXD can be used to setup a suitable container from any distro. If not done already (all default settings are usually fine): lxd init Initialize a new container named `ubuntu`: lxc launch images:ubuntu/xenial/amd64 ubuntu Now, you can either clone the repo from inside the container...: lxc exec ubuntu -- apt install -y git lxc exec ubuntu -- git pull https://github.com/mirukana/mirage ...or directly copy a repository from your local filesystem inside: lxc exec ubuntu -- /bin/mkdir -p /root/mirage lxc file push -vr /* ubuntu/root/mirage Run the build script inside the container: lxc exec ubuntu -- /root/mirage/packaging/appimage/build.sh You can also start a shell inside (e.g. if something goes wrong): lxc exec ubuntu -- /bin/bash mirage-0.7.2/packaging/appimage/build.sh000077500000000000000000000143031407747233600201470ustar00rootroot00000000000000#!/usr/bin/env bash set -eo pipefail HERE="$(dirname "$(readlink -f "$0")")" MIRAGE_REPO_URL='https://github.com/mirukana/mirage' PY_XYZ=3.9.6 PY_XY="$(cut -d . -f 1-2 <<< "$PY_XYZ")" check_distro() { if grep -q '^\s*Ubuntu\s*16.04' /etc/issue; then return; fi echo "Not running on expected distribution or version, aborting!" >&2 echo "See /packaging/appimage/README.md for more info." >&2 exit 99 } parse_cli_arguments() { if [ "$1" = --skip-install-prerequisites ] || [ "$1" = -s ]; then skip_pre=true else skip_pre=false fi } setup_dns() { if ! grep -q 'dns-nameservers 9.9.9.9' /etc/network/interfaces; then sed -i '/iface eth0 inet dhcp/a dns-nameservers 9.9.9.9' \ /etc/network/interfaces invoke-rc.d networking restart fi } install_apt_packages() { apt install -y software-properties-common add-apt-repository -y ppa:beineri/opt-qt-5.12.9-xenial add-apt-repository -y ppa:beineri/opt-qt-5.12.9-xenial apt update -y apt install -y \ qt512base qt512declarative qt512graphicaleffects \ qt512imageformats qt512quickcontrols2 qt512svg \ zip git wget cmake ccache \ build-essential mesa-common-dev libglu1-mesa-dev freeglut3-dev \ libglfw3-dev libgles2-mesa-dev libssl-dev \ python3-dev python3-setuptools python3-pip libgdbm-dev libc6-dev \ libsqlite3-dev libffi-dev openssl libreadline-dev \ libjpeg-turbo8-dev zlib1g-dev \ libtiff5-dev liblcms2-dev libwebp-dev libopenjp2-7-dev \ libx11-dev libxss-dev libasound2-dev \ pkg-config libdbus-1-dev libglib2.0-dev \ appstream-util desktop-file-utils # for appimage-lint.sh /usr/sbin/update-ccache-symlinks } setup_env() { set +euo pipefail # shellcheck disable=SC1091 source /opt/qt512/bin/qt512-env.sh set -euo pipefail export PATH="/usr/lib/ccache:$PATH" export LD_LIBRARY_PATH="$HOME/.local/lib/python$PY_XY/site-packages/PIL/.libs/:$HOME/.local/lib/python$PY_XY/site-packages/.libs_cffi_backend/:/usr/lib/x86_64-linux-gnu/:/usr/lib:$LD_LIBRARY_PATH" export PREFIX=/usr export CFLAGS="-march=x86-64 -O2 -pipe -fPIC" export CXXFLAGS="$CFLAGS" export MAKEFLAGS="-j$(($(nproc) + 1))" } install_python() { cd ~ if ! [ -d ~/.pyenv ]; then wget -O - https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash fi export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" set +euo pipefail eval "$(pyenv init --path)" set -euo pipefail export PYTHON_CFLAGS="$CFLAGS" export PYTHON_CONFIGURE_OPTS='--enable-shared --enable-optimizations --with-lto' pyenv update pyenv install --verbose --skip-existing $PY_XYZ pyenv global $PY_XYZ } install_olm() { cd ~ rm -rf olm-master.tar.gz olm-master wget 'https://gitlab.matrix.org/matrix-org/olm/-/archive/master/olm-master.tar.gz' tar xf olm-master.tar.gz cd olm-master make clean cmake . -Bbuild cmake --build build make install } install_pyotherside() { cd ~ if ! [ -f 1.5.9.tar.gz ]; then wget 'https://github.com/thp/pyotherside/archive/1.5.9.tar.gz' fi tar xf 1.5.9.tar.gz cd pyotherside-1.5.9 make clean find . -name Makefile -delete qmake make install } get_app_and_pip_dependencies() { cd ~ if ! [ -d mirage ]; then git clone --recursive "$MIRAGE_REPO_URL" fi cd mirage if pip3 show Pillow; then pip3 uninstall Pillow --yes; fi pip3 install Pillow --no-binary :all: pip3 install --user -Ur requirements.txt pip3 install --user -U certifi } initialize_appdir() { cd ~/mirage rm -rf .qmake.stash Makefile build qmake mirage.pro PREFIX=/usr make install INSTALL_ROOT=build/appdir } complete_appdir() { cd ~/mirage/build cp -r ~/.pyenv/versions/$PY_XYZ/* appdir/usr cp -r "$HOME/.local/lib/python$PY_XY/site-packages/"* \ "appdir/usr/lib/python$PY_XY/site-packages" cd ~/mirage/build/appdir/usr/lib ln -s "python$PY_XY/site-packages/Pillow.libs/"* . cd ~/mirage/build if ! [ -f ~/linuxdeployqt.AppImage ]; then wget 'https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage' \ -O ~/linuxdeployqt.AppImage fi chmod +x ~/linuxdeployqt.AppImage ~/linuxdeployqt.AppImage appdir/usr/share/applications/mirage.desktop \ -bundle-non-qt-libs -qmldir=../src/gui mkdir -p appdir/usr/share/metainfo cp ~/mirage/packaging/mirage.appdata.xml appdir/usr/share/metainfo cp /opt/qt512/qml/io/thp/pyotherside/qmldir \ appdir/usr/qml/io/thp/pyotherside # Remove useless heavy test data rm -rf "appdir/usr/lib/python$PY_XY/test" rm -rf "appdir/usr/lib/python$PY_XY/site-packages/Crypto/SelfTest/" # Remove python cache files find appdir -name '*.pyc' -delete } fix_apprun_launcher() { cd ~/mirage/build/appdir rm -f AppRun # because it's a symlink sed "s/\\\$PY_XY/$PY_XY/" "$HERE/AppRun.sh" > AppRun chmod +x AppRun } generate_appimage() { cd ~/mirage/build if ! [ -f ~/appimagetool.AppImage ]; then wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" \ -O ~/appimagetool.AppImage fi chmod +x ~/appimagetool.AppImage ~/appimagetool.AppImage --no-appstream appdir } lint_appdir() { cd ~ cat << 'EOF' > /usr/local/bin/mimetype #!/usr/bin/env sh file --mime-type "$@" | tr -d ';' EOF chmod +x /usr/local/bin/mimetype if ! [ -d pkg2appimage ]; then git clone https://github.com/AppImage/pkg2appimage fi chmod +x pkg2appimage/appdir-lint.sh cd ~/mirage/build echo -e "\e[34m\nAppDir linting result:\n\e[0m" ~/pkg2appimage/appdir-lint.sh appdir } check_distro parse_cli_arguments "$@" setup_dns if [ "$skip_pre" = false ]; then install_apt_packages; fi setup_env if [ "$skip_pre" = false ]; then install_python install_olm install_pyotherside get_app_and_pip_dependencies fi initialize_appdir complete_appdir fix_apprun_launcher generate_appimage lint_appdir mirage-0.7.2/packaging/flatpak/000077500000000000000000000000001407747233600163475ustar00rootroot00000000000000mirage-0.7.2/packaging/flatpak/README.md000066400000000000000000000041611407747233600176300ustar00rootroot00000000000000# Flatpak packaging ## Building Flatpak To build the Flatpak package, you will need `flatpak`, `flatpak-builder`, and KDE 5.14 runtime with SDK. `flatpak-builder` is usually available from the same repository as `flatpak`. See the [Flatpak setup instructions](https://flatpak.org/setup/) for your system. To install the runtimes (remove the `--user` flag and run as root if you prefer system-wide installation): ```sh flatpak install --user flathub org.kde.Platform//5.14 org.kde.Sdk//5.14 ``` If the download fails for some reason, run `flatpak repair` before retrying. To build, create a bundle and install it, run from the root of the project: ```sh make clean flatpak-builder --repo=build/flatpak/repo --force-clean build/flatpak/build packaging/flatpak/mirage.flatpak.yaml flatpak build-bundle build/flatpak/repo build/mirage.flatpak io.github.mirukana.mirage flatpak install --user build/mirage.flatpak ``` To run the installed bundle, either use your desktop environment or command line: ```sh flatpak run io.github.mirukana.mirage ``` ## Manifest The manifest can be created using the included scripts, as described below. Please note it is intended to be done by the maintainers only. In particular, there could be some unexpected issues exposed by updates in Python modules that have to be resolved. The manifest has to be updated by maintainers when the dependencies of Mirage change, or some updates in used Python modules are desired. The Flatpak packaging manifest is generated by running [generate-flatpak-script.sh](generate-flatpak-script.sh), which uses [mirage.flatpak.base.yaml](mirage.flatpak.base.yaml) and replaces the marked placeholder with Python dependencies. This script requires `libolm` to be installed on the development PC, as it will create Python virtual environment and install all the requirements in it. Note that the Python dependencies are taken from [requirements.flatpak.txt](requirements.flatpak.txt) and the project root's [requirements.txt](../../requirements.txt). In addition, the list of ignored packages is in [generate-flatpak-script.sh](generate-flatpak-script.sh). mirage-0.7.2/packaging/flatpak/collector.py000066400000000000000000000016341407747233600207130ustar00rootroot00000000000000import json import yaml with open("mirage.flatpak.base.yaml") as f: base = yaml.load(f, Loader=yaml.FullLoader) with open("flatpak-pip.json") as f: modules = json.load(f)["modules"] # set some modules in front as dependencies and dropping matrix-nio # which is declared separately front = [] back = [] for m in modules: n = m["name"] if n.startswith("python3-") and \ n[len("python3-"):] in ["cffi", "importlib-metadata", "multidict", "pytest-runner", "setuptools-scm"]: front.append(m) else: back.append(m) # replace placeholder with modules phold = None for i in range(len(base["modules"])): if base["modules"][i]["name"] == "PLACEHOLDER PYTHON DEPENDENCIES": phold = i break base["modules"] = base["modules"][:i] + front + back + base["modules"][i+1:] with open("mirage.flatpak.yaml", "w") as f: f.write(yaml.dump(base, sort_keys=False, indent=2)) mirage-0.7.2/packaging/flatpak/generate-flatpak-script.sh000077500000000000000000000013221407747233600234200ustar00rootroot00000000000000#!/usr/bin/env sh set -e dir="$(dirname "$(readlink -f "$0")")" pip_generator_url='https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/pip/flatpak-pip-generator' cd "$dir" python3 -m venv flatpak-env export PATH="$dir/flatpak-env/bin:$PATH" pip3 install -Ur requirements.flatpak.txt pip3 install -Ur ../../requirements.txt # Freeze requirements, ignore blacklisted packages pip3 freeze | grep -v six= | grep -v matrix-nio > flatpak-env-requirements.txt # Generate flatpak requirements pip3 install requirements-parser [ ! -f flatpak-pip-generator ] && wget "$pip_generator_url" python3 flatpak-pip-generator -r flatpak-env-requirements.txt -o flatpak-pip pip3 install PyYAML python3 collector.py mirage-0.7.2/packaging/flatpak/mirage.flatpak.base.yaml000066400000000000000000000055221407747233600230350ustar00rootroot00000000000000id: io.github.mirukana.mirage runtime: org.kde.Platform sdk: org.kde.Sdk runtime-version: "5.14" command: mirage finish-args: - --share=ipc - --share=network - --socket=x11 - --socket=wayland - --socket=pulseaudio - --device=dri - --filesystem=host - --talk-name=org.freedesktop.Notifications rename-icon: mirage rename-desktop-file: mirage.desktop cleanup: - /app/include - /app/usr/tests - /app/lib/cmake - /app/bin/cairosvg - /app/bin/chardetect - /app/bin/futurize - /app/bin/jsonschema - /app/bin/pasteurize - /app/bin/pwiz.py - /app/bin/watchgod modules: - name: pyotherside buildsystem: qmake make-install-args: - INSTALL_ROOT=/app post-install: - mkdir -p /app/lib/qml - ln -s /app/usr/lib/qml/io /app/lib/qml sources: - type: archive url: https://github.com/thp/pyotherside/archive/1.5.3.tar.gz sha256: 00049d5f42cac448368bc2a521edb8de36bb6d2a624e195b7f1004236758b805 - name: olm buildsystem: cmake-ninja sources: - type: git url: https://gitlab.matrix.org/matrix-org/olm.git tag: 3.2.2 commit: 3745ea57bbce319ac2f190e02062e45a46d23471 disable-shallow-clone: true config-opts: - -DCMAKE_BUILD_TYPE=Release - name: libzen subdir: Project/GNU/Library config-opts: - --enable-shared - --disable-static cleanup: - /bin - /include - /lib/pkgconfig - /lib/*.la sources: - type: archive url: https://mediaarea.net/download/source/libzen/0.4.37/libzen_0.4.37.tar.xz sha256: 38c0a68b715b55d6685d2759eecda040adf37bd066955d79a5d01f91977bd9a0 - name: libmediainfo subdir: Project/GNU/Library config-opts: - --enable-shared - --disable-static - --with-libcurl cleanup: - /bin - /include - /lib/pkgconfig - /lib/*.la sources: - type: archive url: https://mediaarea.net/download/source/libmediainfo/19.09/libmediainfo_19.09.tar.xz sha256: ff06e1a449dfbe6f2c51f27ae1187d3e72386cb54476fbb189ffaacf845f478e # Python dependencies - name: PLACEHOLDER PYTHON DEPENDENCIES # matrix-nio separate - name: python3-matrix-nio buildsystem: simple build-commands: - pip3 install --prefix=${FLATPAK_DEST} . # network access required for poetry build-options: build-args: - --share=network sources: - type: git url: https://github.com/mirukana/matrix-nio.git commit: f36ae4902e6b3773256fbe438a189e064436d0b5 - name: mirage buildsystem: qmake sources: - type: dir path: ../.. skip: - build - .git - .flatpak-builder # - name: mirage # buildsystem: qmake # sources: # - type: git # url: https://github.com/mirukana/mirage.git # tag: v0.4.3 mirage-0.7.2/packaging/flatpak/mirage.flatpak.yaml000066400000000000000000001163641407747233600221330ustar00rootroot00000000000000id: io.github.mirukana.mirage runtime: org.kde.Platform sdk: org.kde.Sdk runtime-version: '5.14' command: mirage finish-args: - --share=ipc - --share=network - --socket=x11 - --socket=wayland - --socket=pulseaudio - --device=dri - --filesystem=host - --talk-name=org.freedesktop.Notifications rename-icon: mirage rename-desktop-file: mirage.desktop cleanup: - /app/include - /app/usr/tests - /app/lib/cmake - /app/bin/cairosvg - /app/bin/chardetect - /app/bin/futurize - /app/bin/jsonschema - /app/bin/pasteurize - /app/bin/pwiz.py modules: - name: pyotherside buildsystem: qmake make-install-args: - INSTALL_ROOT=/app post-install: - mkdir -p /app/lib/qml - ln -s /app/usr/lib/qml/io /app/lib/qml sources: - type: archive url: https://github.com/thp/pyotherside/archive/1.5.3.tar.gz sha256: 00049d5f42cac448368bc2a521edb8de36bb6d2a624e195b7f1004236758b805 - name: olm buildsystem: cmake-ninja sources: - type: git url: https://gitlab.matrix.org/matrix-org/olm.git tag: 3.2.2 commit: 3745ea57bbce319ac2f190e02062e45a46d23471 disable-shallow-clone: true config-opts: - -DCMAKE_BUILD_TYPE=Release - name: libzen subdir: Project/GNU/Library config-opts: - --enable-shared - --disable-static cleanup: - /bin - /include - /lib/pkgconfig - /lib/*.la sources: - type: archive url: https://mediaarea.net/download/source/libzen/0.4.37/libzen_0.4.37.tar.xz sha256: 38c0a68b715b55d6685d2759eecda040adf37bd066955d79a5d01f91977bd9a0 - name: libmediainfo subdir: Project/GNU/Library config-opts: - --enable-shared - --disable-static - --with-libcurl cleanup: - /bin - /include - /lib/pkgconfig - /lib/*.la sources: - type: archive url: https://mediaarea.net/download/source/libmediainfo/19.09/libmediainfo_19.09.tar.xz sha256: ff06e1a449dfbe6f2c51f27ae1187d3e72386cb54476fbb189ffaacf845f478e - name: python3-cffi buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "cffi==1.14.3" sources: - type: file url: https://files.pythonhosted.org/packages/ae/e7/d9c3a176ca4b02024debf82342dab36efadfc5776f9c8db077e8f6e71821/pycparser-2.20-py2.py3-none-any.whl sha256: 7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 - type: file url: https://files.pythonhosted.org/packages/cb/ae/380e33d621ae301770358eb11a896a34c34f30db188847a561e8e39ee866/cffi-1.14.3.tar.gz sha256: f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591 - name: python3-importlib-metadata buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "importlib-metadata==1.7.0" sources: - type: file url: https://files.pythonhosted.org/packages/c4/79/3b770d51254a31bb85ba56ea70d7428d0c2c659a233cc9722352e028b539/zipp-3.2.0-py3-none-any.whl sha256: 43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6 - type: file url: https://files.pythonhosted.org/packages/8e/58/cdea07eb51fc2b906db0968a94700866fc46249bdc75cac23f9d13168929/importlib_metadata-1.7.0-py2.py3-none-any.whl sha256: dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070 - name: python3-multidict buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "multidict==4.5.2" sources: - type: file url: https://files.pythonhosted.org/packages/7f/8f/b3c8c5b062309e854ce5b726fc101195fbaa881d306ffa5c2ba19efa3af2/multidict-4.5.2.tar.gz sha256: 024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f - name: python3-pytest-runner buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pytest-runner==5.2" sources: - type: file url: https://files.pythonhosted.org/packages/16/45/81b5262c0efc08882bdf183b788e6d28e3d684863990996d8b60967d48da/pytest_runner-5.2-py2.py3-none-any.whl sha256: 5534b08b133ef9a5e2c22c7886a8f8508c95bb0b0bdc6cc13214f269c3c70d51 - name: python3-setuptools-scm buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "setuptools-scm==4.1.2" sources: - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/ad/d3/e54f8b4cde0f6fb4f231629f570c1a33ded18515411dee6df6fe363d976f/setuptools_scm-4.1.2-py2.py3-none-any.whl sha256: 69258e2eeba5f7ce1ed7a5f109519580fa3578250f8e4d6684859f86d1b15826 - name: python3-aiofiles buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "aiofiles==0.4.0" sources: - type: file url: https://files.pythonhosted.org/packages/cf/f2/a67a23bc0bb61d88f82aa7fb84a2fb5f278becfbdc038c5cbb36c31feaf1/aiofiles-0.4.0-py3-none-any.whl sha256: 1e644c2573f953664368de28d2aa4c89dfd64550429d0c27c4680ccd3aa4985d - name: python3-aiohttp buildsystem: simple build-commands: - pip3 install --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} aiohttp==3.6.2 sources: - type: file url: https://files.pythonhosted.org/packages/cb/19/57503b5de719ee45e83472f339f617b0c01ad75cba44aba1e4c97c2b0abd/idna-2.9.tar.gz sha256: 7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb - type: file url: https://files.pythonhosted.org/packages/d6/67/6e2507586eb1cfa6d55540845b0cd05b4b77c414f6bca8b00b45483b976e/yarl-1.4.2.tar.gz sha256: 58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b - type: file url: https://files.pythonhosted.org/packages/a1/78/aae1545aba6e87e23ecab8d212b58bb70e72164b67eb090b81bb17ad38e3/async-timeout-3.0.1.tar.gz sha256: 0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f - type: file url: https://files.pythonhosted.org/packages/65/d4/fabdcc5ee4451c8a8e177e27ddfd131a53a82ecc5a3b68468b7e9f8d70b4/multidict-4.7.6.tar.gz sha256: fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430 - type: file url: https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz sha256: 84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae - type: file url: https://files.pythonhosted.org/packages/98/c3/2c227e66b5e896e15ccdae2e00bbc69aa46e9a8ce8869cc5fa96310bf612/attrs-19.3.0.tar.gz sha256: f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 - type: file url: https://files.pythonhosted.org/packages/00/94/f9fa18e8d7124d7850a5715a0b9c0584f7b9375d331d35e157cee50f27cc/aiohttp-3.6.2.tar.gz sha256: 259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326 - type: file url: https://files.pythonhosted.org/packages/89/e3/afebe61c546d18fb1709a61bee788254b40e736cff7271c7de5de2dc4128/idna-2.9-py2.py3-none-any.whl sha256: a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa - type: file url: https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl sha256: fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 - type: file url: https://files.pythonhosted.org/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl sha256: 08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c - type: file url: https://files.pythonhosted.org/packages/e1/1e/5a4441be21b0726c4464f3f23c8b19628372f606755a9d2e46c187e65ec4/async_timeout-3.0.1-py3-none-any.whl sha256: 4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 - name: python3-appdirs buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "appdirs==1.4.4" sources: - type: file url: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl sha256: a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 - name: python3-async-generator buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "async-generator==1.10" sources: - type: file url: https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl sha256: 01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b - name: python3-async-timeout buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "async-timeout==3.0.1" sources: - type: file url: https://files.pythonhosted.org/packages/e1/1e/5a4441be21b0726c4464f3f23c8b19628372f606755a9d2e46c187e65ec4/async_timeout-3.0.1-py3-none-any.whl sha256: 4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3 - name: python3-atomicwrites buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "atomicwrites==1.4.0" sources: - type: file url: https://files.pythonhosted.org/packages/2c/a0/da5f49008ec6e9a658dbf5d7310a4debd397bce0b4db03cf8a410066bb87/atomicwrites-1.4.0-py2.py3-none-any.whl sha256: 6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197 - name: python3-attrs buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "attrs==20.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/14/df/479736ae1ef59842f512548bacefad1abed705e400212acba43f9b0fa556/attrs-20.2.0-py2.py3-none-any.whl sha256: fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc - name: python3-beautifulsoup4 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "beautifulsoup4==4.9.1" sources: - type: file url: https://files.pythonhosted.org/packages/6f/8f/457f4a5390eeae1cc3aeab89deb7724c965be841ffca6cfca9197482e470/soupsieve-2.0.1-py3-none-any.whl sha256: 1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55 - type: file url: https://files.pythonhosted.org/packages/66/25/ff030e2437265616a1e9b25ccc864e0371a0bc3adb7c5a404fd661c6f4f6/beautifulsoup4-4.9.1-py3-none-any.whl sha256: a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8 - name: python3-cachetools buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "cachetools==4.1.1" sources: - type: file url: https://files.pythonhosted.org/packages/cd/5c/f3aa86b6d5482f3051b433c7616668a9b96fbe49a622210e2c9781938a5c/cachetools-4.1.1-py3-none-any.whl sha256: 513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98 - name: python3-cairocffi buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "cairocffi==1.1.0" sources: - type: file url: https://files.pythonhosted.org/packages/ae/e7/d9c3a176ca4b02024debf82342dab36efadfc5776f9c8db077e8f6e71821/pycparser-2.20-py2.py3-none-any.whl sha256: 7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/cb/ae/380e33d621ae301770358eb11a896a34c34f30db188847a561e8e39ee866/cffi-1.14.3.tar.gz sha256: f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591 - type: file url: https://files.pythonhosted.org/packages/f7/99/b3a2c6393563ccbe081ffcceb359ec27a6227792c5169604c1bd8128031a/cairocffi-1.1.0.tar.gz sha256: f1c0c5878f74ac9ccb5d48b2601fcc75390c881ce476e79f4cfedd288b1b05db - name: python3-CairoSVG buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "CairoSVG==2.4.2" sources: - type: file url: https://files.pythonhosted.org/packages/ae/e7/d9c3a176ca4b02024debf82342dab36efadfc5776f9c8db077e8f6e71821/pycparser-2.20-py2.py3-none-any.whl sha256: 7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 - type: file url: https://files.pythonhosted.org/packages/cb/ae/380e33d621ae301770358eb11a896a34c34f30db188847a561e8e39ee866/cffi-1.14.3.tar.gz sha256: f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591 - type: file url: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/06/74/9b387472866358ebc08732de3da6dc48e44b0aacd2ddaa5cb85ab7e986a2/defusedxml-0.6.0-py2.py3-none-any.whl sha256: 6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 - type: file url: https://files.pythonhosted.org/packages/94/2c/4e501f9c351343c8ba10d70b5a7ca97cdab2690af043a6e52ada65b85b6b/tinycss2-1.0.2-py3-none-any.whl sha256: 9fdacc0e22d344ddd2ca053837c133900fe820ae1222f63b79617490a498507a - type: file url: https://files.pythonhosted.org/packages/f7/99/b3a2c6393563ccbe081ffcceb359ec27a6227792c5169604c1bd8128031a/cairocffi-1.1.0.tar.gz sha256: f1c0c5878f74ac9ccb5d48b2601fcc75390c881ce476e79f4cfedd288b1b05db - type: file url: https://files.pythonhosted.org/packages/72/bb/9ad85eacc5f273b08bd5203a1d587479a93f27df9056e4e5f63276f4fd0e/cssselect2-0.3.0-py3-none-any.whl sha256: 97d7d4234f846f9996d838964d38e13b45541c18143bc55cf00e4bc1281ace76 - type: file url: https://files.pythonhosted.org/packages/3e/02/b09732ca4b14405ff159c470a612979acfc6e8645dc32f83ea0129709f7a/Pillow-7.2.0.tar.gz sha256: 97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626 - type: file url: https://files.pythonhosted.org/packages/8e/3a/762f9272c20db092f4d537aaf364dd0770ecf8f7101b58c4e933e99ee2f6/CairoSVG-2.4.2-py3-none-any.whl sha256: 9cb1df7e9bc60f75fb87f67940a8fb805aad544337a67a40b67c05cfe33711a2 - name: python3-chardet buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "chardet==3.0.4" sources: - type: file url: https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl sha256: fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 - name: python3-cssselect2 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "cssselect2==0.3.0" sources: - type: file url: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/94/2c/4e501f9c351343c8ba10d70b5a7ca97cdab2690af043a6e52ada65b85b6b/tinycss2-1.0.2-py3-none-any.whl sha256: 9fdacc0e22d344ddd2ca053837c133900fe820ae1222f63b79617490a498507a - type: file url: https://files.pythonhosted.org/packages/72/bb/9ad85eacc5f273b08bd5203a1d587479a93f27df9056e4e5f63276f4fd0e/cssselect2-0.3.0-py3-none-any.whl sha256: 97d7d4234f846f9996d838964d38e13b45541c18143bc55cf00e4bc1281ace76 - name: python3-dataclasses buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "dataclasses==0.6" sources: - type: file url: https://files.pythonhosted.org/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none-any.whl sha256: 454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f - name: python3-dbus-python buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "dbus-python==1.2.16" sources: - type: file url: https://files.pythonhosted.org/packages/62/7e/d4fb56a1695fa65da0c8d3071855fa5408447b913c58c01933c2f81a269a/dbus-python-1.2.16.tar.gz sha256: 11238f1d86c995d8aed2e22f04a1e3779f0d70e587caffeab4857f3c662ed5a4 - name: python3-defusedxml buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "defusedxml==0.6.0" sources: - type: file url: https://files.pythonhosted.org/packages/06/74/9b387472866358ebc08732de3da6dc48e44b0aacd2ddaa5cb85ab7e986a2/defusedxml-0.6.0-py2.py3-none-any.whl sha256: 6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 - name: python3-filetype buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "filetype==1.0.7" sources: - type: file url: https://files.pythonhosted.org/packages/b4/6b/7bc015da1a576ac037582ae0c5acb675371de9e017e860931e97a428ee31/filetype-1.0.7-py2.py3-none-any.whl sha256: 353369948bb1c09b8b3ea3d78390b5586e9399bff9aab894a1dff954e31a66f6 - name: python3-future buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "future==0.18.2" sources: - type: file url: https://files.pythonhosted.org/packages/45/0b/38b06fd9b92dc2b68d58b75f900e97884c45bedd2ff83203d933cf5851c9/future-0.18.2.tar.gz sha256: b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d - name: python3-h11 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "h11==0.9.0" sources: - type: file url: https://files.pythonhosted.org/packages/5a/fd/3dad730b0f95e78aeeb742f96fa7bbecbdd56a58e405d3da440d5bfb90c6/h11-0.9.0-py2.py3-none-any.whl sha256: 4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1 - name: python3-h2 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "h2==3.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/8a/cc/e53517f4a1e13f74776ca93271caef378dadec14d71c61c949d759d3db69/hpack-3.0.0-py2.py3-none-any.whl sha256: 0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89 - type: file url: https://files.pythonhosted.org/packages/19/0c/bf88182bcb5dce3094e2f3e4fe20db28a9928cb7bd5b08024030e4b140db/hyperframe-5.2.0-py2.py3-none-any.whl sha256: 5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40 - type: file url: https://files.pythonhosted.org/packages/25/de/da019bcc539eeab02f6d45836f23858ac467f584bfec7a526ef200242afe/h2-3.2.0-py2.py3-none-any.whl sha256: 61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5 - name: python3-hpack buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "hpack==3.0.0" sources: - type: file url: https://files.pythonhosted.org/packages/8a/cc/e53517f4a1e13f74776ca93271caef378dadec14d71c61c949d759d3db69/hpack-3.0.0-py2.py3-none-any.whl sha256: 0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89 - name: python3-html-sanitizer buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "html-sanitizer==1.9.1" sources: - type: file url: https://files.pythonhosted.org/packages/6f/8f/457f4a5390eeae1cc3aeab89deb7724c965be841ffca6cfca9197482e470/soupsieve-2.0.1-py3-none-any.whl sha256: 1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55 - type: file url: https://files.pythonhosted.org/packages/2c/4d/3ec1ea8512a7fbf57f02dee3035e2cce2d63d0e9c0ab8e4e376e01452597/lxml-4.5.2.tar.gz sha256: cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6 - type: file url: https://files.pythonhosted.org/packages/66/25/ff030e2437265616a1e9b25ccc864e0371a0bc3adb7c5a404fd661c6f4f6/beautifulsoup4-4.9.1-py3-none-any.whl sha256: a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8 - type: file url: https://files.pythonhosted.org/packages/f1/e0/44b212c96567b5ec3a3de47443f2934fa0308daedf9b1b8cf6234263caf1/html_sanitizer-1.9.1-py2.py3-none-any.whl sha256: faec7e9d91e9224cf27ce12ab07a588753ce0813df1367aa56c221e5fa3738f3 - name: python3-hyperframe buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "hyperframe==5.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/19/0c/bf88182bcb5dce3094e2f3e4fe20db28a9928cb7bd5b08024030e4b140db/hyperframe-5.2.0-py2.py3-none-any.whl sha256: 5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40 - name: python3-idna buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "idna==2.10" sources: - type: file url: https://files.pythonhosted.org/packages/a2/38/928ddce2273eaa564f6f50de919327bf3a00f091b5baba8dfa9460f3a8a8/idna-2.10-py2.py3-none-any.whl sha256: b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 - name: python3-idna-ssl buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "idna-ssl==1.1.0" sources: - type: file url: https://files.pythonhosted.org/packages/a2/38/928ddce2273eaa564f6f50de919327bf3a00f091b5baba8dfa9460f3a8a8/idna-2.10-py2.py3-none-any.whl sha256: b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 - type: file url: https://files.pythonhosted.org/packages/46/03/07c4894aae38b0de52b52586b24bf189bb83e4ddabfe2e2c8f2419eec6f4/idna-ssl-1.1.0.tar.gz sha256: a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c - name: python3-jsonschema buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jsonschema==3.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/c4/79/3b770d51254a31bb85ba56ea70d7428d0c2c659a233cc9722352e028b539/zipp-3.2.0-py3-none-any.whl sha256: 43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6 - type: file url: https://files.pythonhosted.org/packages/14/df/479736ae1ef59842f512548bacefad1abed705e400212acba43f9b0fa556/attrs-20.2.0-py2.py3-none-any.whl sha256: fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/ee/ff/48bde5c0f013094d729fe4b0316ba2a24774b3ff1c52d924a8a4cb04078a/six-1.15.0-py2.py3-none-any.whl sha256: 8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced - type: file url: https://files.pythonhosted.org/packages/4d/70/fd441df751ba8b620e03fd2d2d9ca902103119616f0f6cc42e6405035062/pyrsistent-0.17.3.tar.gz sha256: 2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e - type: file url: https://files.pythonhosted.org/packages/6d/6d/f4bb28424bc677bce1210bc19f69a43efe823e294325606ead595211f93e/importlib_metadata-2.0.0-py2.py3-none-any.whl sha256: cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3 - type: file url: https://files.pythonhosted.org/packages/c5/8f/51e89ce52a085483359217bc72cdbf6e75ee595d5b1d4b5ade40c7e018b8/jsonschema-3.2.0-py2.py3-none-any.whl sha256: 4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163 - name: python3-Logbook buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "Logbook==1.5.3" sources: - type: file url: https://files.pythonhosted.org/packages/2f/d9/16ac346f7c0102835814cc9e5b684aaadea101560bb932a2403bd26b2320/Logbook-1.5.3.tar.gz sha256: 66f454ada0f56eae43066f604a222b09893f98c1adc18df169710761b8f32fe8 - name: python3-lxml buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "lxml==4.5.2" sources: - type: file url: https://files.pythonhosted.org/packages/2c/4d/3ec1ea8512a7fbf57f02dee3035e2cce2d63d0e9c0ab8e4e376e01452597/lxml-4.5.2.tar.gz sha256: cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6 - name: python3-mistune buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "mistune==0.8.4" sources: - type: file url: https://files.pythonhosted.org/packages/09/ec/4b43dae793655b7d8a25f76119624350b4d65eb663459eb9603d7f1f0345/mistune-0.8.4-py2.py3-none-any.whl sha256: 88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4 - name: python3-peewee buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "peewee==3.13.3" sources: - type: file url: https://files.pythonhosted.org/packages/e1/3e/a21e7268fa39756cdbd6d86af78ff1c0a92b84d6dbfadff431e9e3b9e1d3/peewee-3.13.3.tar.gz sha256: 1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369 - name: python3-Pillow buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "Pillow==7.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/3e/02/b09732ca4b14405ff159c470a612979acfc6e8645dc32f83ea0129709f7a/Pillow-7.2.0.tar.gz sha256: 97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626 - name: python3-plyer buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "plyer==1.4.3" sources: - type: file url: https://files.pythonhosted.org/packages/89/3f/9153265abcaa93418abee8b49284941330426d5afb2f72d75d067462af84/plyer-1.4.3-py2.py3-none-any.whl sha256: 6192a5c15f2cc8fe42de5a7ab898ed2d2cd47315a88dd7e0f6fd90ec972b87a9 - name: python3-pycparser buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pycparser==2.20" sources: - type: file url: https://files.pythonhosted.org/packages/ae/e7/d9c3a176ca4b02024debf82342dab36efadfc5776f9c8db077e8f6e71821/pycparser-2.20-py2.py3-none-any.whl sha256: 7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 - name: python3-pycryptodome buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pycryptodome==3.9.8" sources: - type: file url: https://files.pythonhosted.org/packages/4c/2b/eddbfc56076fae8deccca274a5c70a9eb1e0b334da0a33f894a420d0fe93/pycryptodome-3.9.8.tar.gz sha256: 0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4 - name: python3-pyfastcopy buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyfastcopy==1.0.3" sources: - type: file url: https://files.pythonhosted.org/packages/43/80/535d6b3de415e26d0a1cb774c6895dd07aa5986d2f8bde200393bd916790/pyfastcopy-1.0.3.tar.gz sha256: ed4620f1087a8949888973e315d3d59fbe9b8cc4ca5df553d76d2f21d2748999 - name: python3-pymediainfo buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pymediainfo==4.2.1" sources: - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/52/0a/26b306633acd86cf3b28ee8c08f13b8a033ccc94aeacda4d17fdc6d1cabf/pymediainfo-4.2.1.tar.gz sha256: 392d99d6bf74046ebaa2f7036d92d5327611d27532a384540e9310a62b8be26d - name: python3-pyrsistent buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyrsistent==0.17.3" sources: - type: file url: https://files.pythonhosted.org/packages/4d/70/fd441df751ba8b620e03fd2d2d9ca902103119616f0f6cc42e6405035062/pyrsistent-0.17.3.tar.gz sha256: 2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e - name: python3-python-olm buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "python-olm==3.1.3" sources: - type: file url: https://files.pythonhosted.org/packages/ae/e7/d9c3a176ca4b02024debf82342dab36efadfc5776f9c8db077e8f6e71821/pycparser-2.20-py2.py3-none-any.whl sha256: 7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 - type: file url: https://files.pythonhosted.org/packages/45/0b/38b06fd9b92dc2b68d58b75f900e97884c45bedd2ff83203d933cf5851c9/future-0.18.2.tar.gz sha256: b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d - type: file url: https://files.pythonhosted.org/packages/cb/ae/380e33d621ae301770358eb11a896a34c34f30db188847a561e8e39ee866/cffi-1.14.3.tar.gz sha256: f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591 - type: file url: https://files.pythonhosted.org/packages/d4/a4/1face47e65118d7c52726dfa305410a96bc4a0c6f3f99c90bc7104aebf21/python-olm-3.1.3.tar.gz sha256: 9a6c6133ce3777788c88e3f18b13c5b36a2f76ed1a0e774d1a48adf542fee871 - name: python3-soupsieve buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "soupsieve==2.0.1" sources: - type: file url: https://files.pythonhosted.org/packages/6f/8f/457f4a5390eeae1cc3aeab89deb7724c965be841ffca6cfca9197482e470/soupsieve-2.0.1-py3-none-any.whl sha256: 1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55 - name: python3-sortedcontainers buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "sortedcontainers==2.2.2" sources: - type: file url: https://files.pythonhosted.org/packages/23/8c/22a47a4bf8c5289e4ed946d2b0e4df62bca385b9599cc1e46878f2e2529c/sortedcontainers-2.2.2-py2.py3-none-any.whl sha256: c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f - name: python3-watchgod buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "watchgod==0.7" sources: - type: file url: https://files.pythonhosted.org/packages/57/35/9a8da3fb6681e6eba662b2d249eea58cebf575e392271efac3344c172c5f/watchgod-0.7-py3-none-any.whl sha256: d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7 - name: python3-hsluv buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "hsluv==5.0.2" sources: - type: file url: https://files.pythonhosted.org/packages/ea/85/027dfe4fe20fdfef78373050764948ff8d78d9a17c95c20f2dfd478091a4/hsluv-5.0.2-py2.py3-none-any.whl sha256: f31a72317926a985fee9c08cf18d26acd1fc3983b47c042c4da2eb4d7b57fa09 - name: python3-simpleaudio buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "simpleaudio==1.0.4" sources: - type: file url: https://files.pythonhosted.org/packages/94/1b/4dc29653733202b68c09d9c6ca085cf67ac54859ee860647ef21ac1ff3dc/simpleaudio-1.0.4.tar.gz sha256: 691c88649243544db717e7edf6a9831df112104e1aefb5f6038a5d071e8cf41d - name: python3-rply buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "rply==0.7.8" sources: - type: file url: https://files.pythonhosted.org/packages/c0/7c/f66be9e75485ae6901ae77d8bdbc3c0e99ca748ab927b3e18205759bde09/rply-0.7.8-py2.py3-none-any.whl sha256: 28ffd11d656c48aeb8c508eb382acd6a0bd906662624b34388751732a27807e7 - name: python3-baron buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "baron==0.9" sources: - type: file url: https://files.pythonhosted.org/packages/90/1e/74a712ce3d559411ad640415f0bc75b17d5ea61d63b99a529712e8f02a49/baron-0.9-py2.py3-none-any.whl sha256: 05bac85913c1ebc44986f6915bf6a043ce5ce1e5c4d392a209f73845181e9452 - name: python3-redbaron buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "redbaron==0.9.2" sources: - type: file url: https://files.pythonhosted.org/packages/d8/06/c1c97efe5d30593337721923c5813b3b4eaffcffb706e523acf3d3bc9e8c/redbaron-0.9.2-py2.py3-none-any.whl sha256: d01032b6a848b5521a8d6ef72486315c2880f420956870cdd742e2b5a09b9bab - name: python3-tinycss2 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "tinycss2==1.0.2" sources: - type: file url: https://files.pythonhosted.org/packages/44/a6/7fb6e8b3f4a6051e72e4e2218889351f0ee484b9ee17e995f5ccff780300/setuptools-50.3.0-py3-none-any.whl sha256: c77b3920663a435c9450d9d971c48f5a7478fca8881b2cd2564e59f970f03536 - type: file url: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - type: file url: https://files.pythonhosted.org/packages/94/2c/4e501f9c351343c8ba10d70b5a7ca97cdab2690af043a6e52ada65b85b6b/tinycss2-1.0.2-py3-none-any.whl sha256: 9fdacc0e22d344ddd2ca053837c133900fe820ae1222f63b79617490a498507a - name: python3-typing-extensions buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "typing-extensions==3.7.4.3" sources: - type: file url: https://files.pythonhosted.org/packages/60/7a/e881b5abb54db0e6e671ab088d079c57ce54e8a01a3ca443f561ccadb37e/typing_extensions-3.7.4.3-py3-none-any.whl sha256: 7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 - name: python3-unpaddedbase64 buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "unpaddedbase64==1.1.0" sources: - type: file url: https://files.pythonhosted.org/packages/96/da/2ebf30d2fdf0f4dc949b4935e408aaa9cca948963e55ea3c99730b1f74c0/unpaddedbase64-1.1.0-py2.py3-none-any.whl sha256: 81cb4eaaa28cc6a282dd3f2c3855eaa1fbaafa736b5ee64df69889e20540a339 - name: python3-webencodings buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "webencodings==0.5.1" sources: - type: file url: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - name: python3-yarl buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "yarl==1.4.2" sources: - type: file url: https://files.pythonhosted.org/packages/89/e3/afebe61c546d18fb1709a61bee788254b40e736cff7271c7de5de2dc4128/idna-2.9-py2.py3-none-any.whl sha256: a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa - type: file url: https://files.pythonhosted.org/packages/7f/8f/b3c8c5b062309e854ce5b726fc101195fbaa881d306ffa5c2ba19efa3af2/multidict-4.5.2.tar.gz sha256: 024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f - type: file url: https://files.pythonhosted.org/packages/d6/67/6e2507586eb1cfa6d55540845b0cd05b4b77c414f6bca8b00b45483b976e/yarl-1.4.2.tar.gz sha256: 58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b - name: python3-zipp buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "zipp==3.2.0" sources: - type: file url: https://files.pythonhosted.org/packages/c4/79/3b770d51254a31bb85ba56ea70d7428d0c2c659a233cc9722352e028b539/zipp-3.2.0-py3-none-any.whl sha256: 43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6 - name: python3-socks buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "python-socks==1.2.2" sources: - type: file url: https://files.pythonhosted.org/packages/f7/39/ce05e2772d9ec266644484ced4f9f6207b488b7670adcc9ddacf8ca78f91/python_socks-1.2.2-py3-none-any.whl sha256: 41100508d6a3723c2e9884d3330d8bd022a7ef0a953e826637833bf882550fc7 - name: python3-aiohttp-socks buildsystem: simple build-commands: - pip3 install --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "aiohttp-socks==0.6.0" sources: - type: file url: https://files.pythonhosted.org/packages/9a/6c/d302e5a8097fee1e83b9f8e9da10d7752fbf27c74db18b3cc9528b3479be/aiohttp_socks-0.6.0-py3-none-any.whl sha256: db7aa48c0758ee45d7dbc1fde499912ec6fb77eab77a6e2808825d1f41d4e300 - name: python3-matrix-nio buildsystem: simple build-commands: - pip3 install --prefix=${FLATPAK_DEST} . build-options: build-args: - --share=network sources: - type: file url: https://files.pythonhosted.org/packages/a0/56/d7923fb39395c662bab9e6044e290458a77204ea3cafc3b1ea88e27b8f4c/poetry_core-1.0.2-py2.py3-none-any.whl sha256: ee0ed4164440eeab27d1b01bc7b9b3afdc3124f68d4ea28d0821a402a9c7c044 - type: git url: https://github.com/mirukana/matrix-nio.git commit: f36ae4902e6b3773256fbe438a189e064436d0b5 - name: mirage buildsystem: qmake sources: - type: dir path: ../.. skip: - build - .git - .flatpak-builder mirage-0.7.2/packaging/flatpak/requirements.flatpak.txt000066400000000000000000000000671407747233600232570ustar00rootroot00000000000000multidict == 4.5.2 pytest-runner setuptools-scm mirage-0.7.2/packaging/mirage.appdata.xml000066400000000000000000000065041407747233600203310ustar00rootroot00000000000000 io.github.mirakuna.mirage Mirage Customizable and keyboard-operable Matrix client

A fancy, customizable, keyboard-operable Qt/QML and Python Matrix chat client for encrypted and decentralized communication.

Network Chat InstantMessaging Qt intense intense https://github.com/mirukana/mirage https://github.com/mirukana/mirage/issues io.github.mirukana.mirage mirage LGPL-3.0-or-later Chatting with Mirage https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/01-chat.png Login screen https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/02-sign-in.png Account settings https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/03-account-settings.png Creating a room https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/04-create-room.png Conversation list on a narrow screen https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/05-main-pane-small.png Chatting on a narrow screen https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/06-chat-small.png Viewing the list of rooms on a narrow screen https://raw.githubusercontent.com/mirukana/mirage/master/docs/screenshots/07-room-pane-small.png FSFAP workstation mobile
mirage-0.7.2/packaging/mirage.desktop000066400000000000000000000002661407747233600175700ustar00rootroot00000000000000[Desktop Entry] Type=Application Name=Mirage GenericName=Matrix Chat Client Exec=mirage Icon=mirage Terminal=false Categories=Network;Chat;InstantMessaging;Qt; StartupWMClass=mirage mirage-0.7.2/packaging/mirage.png000066400000000000000000000037601407747233600167050ustar00rootroot00000000000000PNG  IHDR\rfsBIT|d pHYsyyCaMtEXtSoftwarewww.inkscape.org<mIDATxݿo]w{c凄ԁ 3#M m 5S" )qmg/DK"g8 ,dcal)I%!RC}^?ܳ(7ecw$uY~>~? $&@b $&@b $&@b $&@b $&"Z={x69MBrhB0{o]gxn f@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@bКk },:VK5.0 1HL 1HL 1HL 1HL 1бR˽Zj}@.3;w/R; $&@b $&@b $&@b $&@b $&@b RZ"=R˕.Zg߾^ZwyY[v |&7>僈Z9|wkOQk3S:}{OFfUp'?-c8iL' xL9N~pW8,ud2N~x*]L8E)F>"iOÍw^?0Uzn쇽GZ+\j{]?DxΣF?}Lo=ʂ=x~G)߸N/Gi=y8ѕ7{/iw}*+ڃ-?>@wG) t gg\s3x0'?M0NOK\s&'p2ќ4Os '?q<'Of/'?e "cJ ӿG.F=Cy:}&e (NZF?;}h݊SY)[凣|-?8aM'?̮ J0O'?ohpC7Н!]N~PX Y  pp%[8O9h헂e?:e|xZ2>k`%Ƶx~!Z6v? 0t%"_7 `.@KLSZt?5 N&4n'Zf@hIp  ` %@6&sL @.$@&7>,JD\)yT9&>Q&M-|-xSJ2[}MJax(qFx!";p7k_Gė~E8g`l}$^o+õ/|Dwi0]6R8">$08&AwA2 ! Ip6`GhIp:@sL M2 f#4$8 l'$XUK`{WbDm;yIENDB`mirage-0.7.2/packaging/update-appdata-releases.py000077500000000000000000000016141407747233600217770ustar00rootroot00000000000000#!/usr/bin/env python3 import html import re from pathlib import Path root = Path(__file__).resolve().parent.parent title_pattern = re.compile(r"## (\d+\.\d+\.\d+) \((\d{4}-\d\d-\d\d)\)") release_lines = [" "] for line in (root / "docs" / "CHANGELOG.md").read_text().splitlines(): match = title_pattern.match(line) if match: args = (html.escape(match.group(1)), html.escape(match.group(2))) release_lines.append(' ' % args) appdata = root / "packaging" / "mirage.appdata.xml" in_releases = False final_lines = [] for line in appdata.read_text().splitlines(): if line == " ": in_releases = True final_lines += release_lines elif line == " ": in_releases = False if not in_releases: final_lines.append(line) appdata.write_text("\n".join(final_lines)) mirage-0.7.2/requirements-dev.txt000066400000000000000000000007661407747233600170520ustar00rootroot00000000000000remote_pdb >= 2.0.0, < 3 pdbpp >= 0.10.2, < 0.11 devtools >= 0.4.0, < 0.5 mypy >= 0.812, < 0.900 flake8 >= 3.8.4, < 4 flake8-isort >= 4.0.0, < 5 flake8-bugbear >= 20.1.4, < 21 flake8-commas >= 2.0.0, < 3 flake8-comprehensions >= 3.3.0, < 4 flake8-executable >= 2.0.4, < 3 flake8-logging-format >= 0.6.0, < 0.7 flake8-pie >= 0.6.1, < 0.7 flake8-quotes >= 3.2.0, < 4 flake8-colors >= 0.1.6, < 0.2 mirage-0.7.2/requirements.txt000066400000000000000000000014411407747233600162650ustar00rootroot00000000000000Pillow >= 7.0.0, < 9 aiofiles >= 0.4.0, < 0.7 appdirs >= 1.4.4, < 2 cairosvg >= 2.4.2, < 3 filetype >= 1.0.7, < 2 html_sanitizer >= 1.9.1, < 2 lxml >= 4.5.1, < 5 mistune >= 0.8.4, < 0.9 pymediainfo >= 4.2.1, < 5 plyer >= 1.4.3, < 2 sortedcontainers >= 2.2.2, < 3 watchgod >= 0.7, < 0.8 redbaron >= 0.9.2, < 1 hsluv >= 5.0.0, < 6 simpleaudio >= 1.0.4, < 2 dbus-python >= 1.2.16, < 2; platform_system == "Linux" async_generator >= 1.10, < 2; python_version < "3.7" dataclasses >= 0.6, < 0.7; python_version < "3.7" pyfastcopy >= 1.0.3, < 2; python_version < "3.8" git+https://github.com/mirukana/matrix-nio#egg-matrix-nio[e2e] mirage-0.7.2/src/000077500000000000000000000000001407747233600135705ustar00rootroot00000000000000mirage-0.7.2/src/backend/000077500000000000000000000000001407747233600151575ustar00rootroot00000000000000mirage-0.7.2/src/backend/__init__.py000066400000000000000000000010111407747233600172610ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """This package provides Mirage's backend side that can interact with the UI. To learn more about how this package works, you might want to check the documentation in the following modules first: - `qml_bridge` - `backend` - `matrix_client` - `nio_callbacks` """ __app_name__ = "mirage" __display_name__ = "Mirage" __reverse_dns__ = "io.github.mirukana.mirage" __version__ = "0.7.2" mirage-0.7.2/src/backend/backend.py000066400000000000000000000522621407747233600171270ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later import asyncio import io import logging as log import os import re import sys import time import traceback import wave from datetime import datetime, timedelta from pathlib import Path from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union from urllib.parse import urlparse import aiohttp import nio import plyer import pyotherside import simpleaudio from appdirs import AppDirs from nio.client.async_client import client_session from . import __app_name__ from .errors import MatrixError, MatrixInvalidAccessToken from .matrix_client import MatrixClient from .media_cache import MediaCache from .models import SyncId from .models.filters import FieldStringFilter from .models.items import Account, Event, Homeserver, PingStatus from .models.model import Model from .models.model_store import ModelStore from .presence import Presence from .sso_server import SSOServer from .user_files import ( Accounts, History, NewTheme, Pre070Settings, Settings, Theme, UIState, ) # Logging configuration log.getLogger().setLevel(log.INFO) nio.logger_group.level = nio.log.logbook.WARNING nio.log.logbook.StreamHandler(sys.stderr).push_application() class Backend: """Manage matrix clients and provide other useful general methods. Attributes: saved_accounts: User config file for saved matrix account. settings: User config file for general UI and backend settings. ui_state: User data file for saving/restoring QML UI state. history: User data file for saving/restoring text typed into QML components. models: A mapping containing our data models that are synchronized between the Python backend and the QML UI. The models should only ever be modified from the backend. If a non-existent key is accessed, it is created and an associated `Model` and returned. The mapping keys are the `Model`'s synchronization ID, which a strings or tuple of strings. Currently used sync ID throughout the code are: - `"accounts"`: logged-in accounts; - `("", "pushrules")`: push rules configured for our account `user_id`. - `("", "rooms")`: rooms our account `user_id` is part of; - `("", "transfers")`: ongoing or failed file uploads/downloads for our account `user_id`; - `("", "", "members")`: members in the room `room_id` that our account `user_id` is part of; - `("", "", "events")`: state events and messages in the room `room_id` that our account `user_id` is part of. Special models: - `"all_rooms"`: See `models.special_models.AllRooms` docstring - `"matching_accounts"` See `models.special_models.MatchingAccounts` docstring - `("", "", "filtered_members")`: See `models.special_models.FilteredMembers` docstring - `("filtered_homeservers")`: See `models.special_models.FilteredHomeservers` docstring clients: A `{user_id: MatrixClient}` dict for the logged-in clients we managed. Every client is logged to one matrix account. media_cache: A matrix media cache for downloaded files. presences: A `{user_id: Presence}` dict for storing presence info about matrix users registered on Mirage. mxc_events: A dict storing media `Event` model items for any account that have the same mxc URI """ def __init__(self) -> None: self.appdirs = AppDirs(appname=__app_name__, roaming=True) self.models = ModelStore() self.saved_accounts = Accounts(self) self.settings = Settings(self) self.ui_state = UIState(self) self.history = History(self) self.theme = Theme(self, self.settings.General.theme) # self.new_theme = NewTheme(self, self.settings.General.new_theme) self.new_theme = NewTheme(self, ".new.py") # TODO Pre070Settings(self) self.clients: Dict[str, MatrixClient] = {} self._sso_server: Optional[SSOServer] = None self._sso_server_task: Optional[asyncio.Future] = None self.profile_cache: Dict[str, nio.ProfileGetResponse] = {} self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \ DefaultDict(asyncio.Lock) # {user_id: lock} self.send_locks: DefaultDict[str, asyncio.Lock] = \ DefaultDict(asyncio.Lock) # {room_id: lock} cache_dir = Path( os.environ.get("MIRAGE_CACHE_DIR") or self.appdirs.user_cache_dir, ) self.media_cache: MediaCache = MediaCache(self, cache_dir) self.presences: Dict[str, Presence] = {} self.concurrent_get_presence_limit = asyncio.BoundedSemaphore(8) self.mxc_events: DefaultDict[str, List[Event]] = DefaultDict(list) self.notification_avatar_cache: Dict[str, Path] = {} # {mxc: path} self.notifications_working: bool = True self.audio_working: bool = True def __repr__(self) -> str: return f"{type(self).__name__}(clients={self.clients!r})" # Clients management async def server_info(self, homeserver: str) -> Tuple[str, List[str]]: """Return server's real URL and supported login flows. Retrieving the real URL uses the `.well-known` API. Possible login methods include `m.login.password` or `m.login.sso`. """ if not re.match(r"https?://", homeserver): homeserver = f"http://{homeserver}" client = MatrixClient(self, homeserver=homeserver) http_re = re.compile("^http://") is_local = urlparse(client.homeserver).netloc.split(":")[0] in ( "localhost", "127.0.0.1", "::1", ) try: client.homeserver = (await client.discovery_info()).homeserver_url except MatrixError: # This is either already the real URL, or an invalid URL. pass try: try: login_response = await client.login_info() except (asyncio.TimeoutError, MatrixError): # Maybe we still have a http URL and server only supports https client.homeserver = http_re.sub("https://", client.homeserver) login_response = await client.login_info() # If we still have a http URL and server redirected to https if login_response.transport_response.real_url.scheme == "https": client.homeserver = http_re.sub("https://", client.homeserver) # If we still have a http URL and server accept both http and https if http_re.match(client.homeserver) and not is_local: original = client.homeserver client.homeserver = http_re.sub("https://", client.homeserver) try: await asyncio.wait_for(client.login_info(), timeout=6) except (asyncio.TimeoutError, MatrixError): client.homeserver = original return (client.homeserver, login_response.flows) finally: await client.close() async def password_auth( self, user: str, password: str, homeserver: str, ) -> str: """Create & register a `MatrixClient`, login using the password and return the user ID we get. """ client = MatrixClient(self, user=user, homeserver=homeserver) return await self._do_login(client, password=password) async def start_sso_auth(self, homeserver: str) -> str: """Start SSO server and return URL to open in the user's browser. See the `sso_server.SSOServer` class documentation. Once the returned URL has been opened in the user's browser (done from QML), `MatrixClient.continue_sso_auth()` should be called. """ server = SSOServer(homeserver) self._sso_server = server self._sso_server_task = asyncio.ensure_future(server.wait_for_token()) return server.url_to_open async def continue_sso_auth(self) -> str: """Wait for the started SSO server to get a token, then login. `MatrixClient.start_sso_auth()` must be called first. Creates and register a `MatrixClient` for logging in. Returns the user ID we get from logging in. """ if not self._sso_server or not self._sso_server_task: raise RuntimeError("Must call Backend.start_sso_auth() first") await self._sso_server_task homeserver = self._sso_server.for_homeserver token = self._sso_server_task.result() self._sso_server_task = None self._sso_server = None client = MatrixClient(self, homeserver=homeserver) return await self._do_login(client, token=token) async def _do_login(self, client: MatrixClient, **login_kwargs) -> str: """Login on a client. If successful, register it and return user ID.""" try: await client.login(**login_kwargs) except MatrixError: await client.close() raise if client.user_id in self.clients: await client.logout() return client.user_id self.clients[client.user_id] = client return client.user_id async def resume_client( self, user_id: str, token: str, device_id: str, homeserver: str, state: str = "online", status_msg: str = "", ) -> None: """Create and register a `MatrixClient` with known account details.""" client = MatrixClient( self, user=user_id, homeserver=homeserver, device_id=device_id, ) self.clients[user_id] = client await client.resume(user_id, token, device_id, state, status_msg) async def load_saved_accounts(self) -> List[str]: """Call `resume_client` for all saved accounts in user config.""" async def resume(user_id: str, info: Dict[str, Any]) -> str: # Get or create account model self.models["accounts"].setdefault( user_id, Account(user_id, info.get("order", -1)), ) await self.resume_client( user_id = user_id, token = info["token"], device_id = info["device_id"], homeserver = info["homeserver"], state = info.get("presence", "online"), status_msg = info.get("status_msg", ""), ) return user_id return await asyncio.gather(*( resume(user_id, info) for user_id, info in self.saved_accounts.items() if info.get("enabled", True) )) async def logout_client(self, user_id: str) -> None: """Log a `MatrixClient` out and unregister it from our models.""" client = self.clients.pop(user_id, None) if client: try: await client.logout() except MatrixInvalidAccessToken: pass self.models["accounts"].pop(user_id, None) self.models["matching_accounts"].pop(user_id, None) self.models[user_id, "transfers"].clear() for room_id in self.models[user_id, "rooms"]: self.models["all_rooms"].pop(room_id, None) self.models[user_id, room_id, "members"].clear() self.models[user_id, room_id, "events"].clear() self.models[user_id, room_id, "filtered_members"].clear() self.models[user_id, "rooms"].clear() await self.saved_accounts.forget(user_id) async def terminate_clients(self) -> None: """Call every `MatrixClient`'s `terminate()` method.""" log.info("Setting clients offline...") tasks = [client.terminate() for client in self.clients.values()] await asyncio.gather(*tasks) async def get_client(self, user_id: str, _debug_info=None) -> MatrixClient: """Wait until a `MatrixClient` is registered in model and return it.""" failures = 0 while True: if user_id in self.clients: return self.clients[user_id] if failures and failures % 100 == 0: # every 10s except first time log.warning( "Client %r not found after %ds, _debug_info:\n%r", user_id, failures / 10, _debug_info, ) await asyncio.sleep(0.1) failures += 1 # Multi-client Matrix functions async def update_room_read_marker( self, room_id: str, event_id: str, ) -> None: """Update room's read marker to an event for all accounts part of it. """ async def update(client: MatrixClient) -> None: room = self.models[client.user_id, "rooms"].get(room_id) account = self.models["accounts"][client.user_id] if room: room.set_fields(unreads=0, highlights=0, local_unreads=False) await client.update_account_unread_counts() if account.presence not in [ Presence.State.invisible, Presence.State.offline, ]: await client.update_receipt_marker(room_id, event_id) await asyncio.gather(*[update(c) for c in self.clients.values()]) async def verify_device( self, user_id: str, device_id: str, ed25519_key: str, ) -> None: """Mark a device as verified on all our accounts.""" for client in self.clients.values(): try: device = client.device_store[user_id][device_id] except KeyError: continue if device.ed25519 == ed25519_key: client.verify_device(device) async def blacklist_device( self, user_id: str, device_id: str, ed25519_key: str, ) -> None: """Mark a device as blacklisted on all our accounts.""" for client in self.clients.values(): try: # This won't include the client's current device, as expected device = client.device_store[user_id][device_id] except KeyError: continue if device.ed25519 == ed25519_key: client.blacklist_device(device) # General functions async def get_config_dir(self) -> Path: return Path(self.appdirs.user_config_dir) async def get_theme_dir(self) -> Path: path = Path(self.appdirs.user_data_dir) / "themes" path.mkdir(parents=True, exist_ok=True) return path async def get_settings(self) -> Tuple[dict, UIState, History, str, dict]: """Return parsed user config files for QML.""" return ( self.settings.qml_data, self.ui_state.qml_data, self.history.qml_data, self.theme.qml_data, self.new_theme.qml_data, ) async def set_string_filter( self, model_id: Union[SyncId, List[str]], value: str, ) -> None: """Set a FieldStringFilter (or derived class) model's filter property. This should only be called from QML. """ if isinstance(model_id, list): # QML can't pass tuples model_id = tuple(model_id) model = Model.proxies[model_id] if not isinstance(model, FieldStringFilter): raise TypeError("model_id must point to a FieldStringFilter") model.filter = value async def set_account_collapse(self, user_id: str, collapse: bool) -> None: """Call `set_account_collapse()` on the `all_rooms` model. This should only be called from QML. """ self.models["all_rooms"].set_account_collapse(user_id, collapse) async def _ping_homeserver( self, session: aiohttp.ClientSession, homeserver_url: str, ) -> None: """Ping a homeserver present in our model and set its `ping` field.""" item = self.models["homeservers"][homeserver_url] times = [] for i in range(16): start = time.time() try: await session.get(f"{homeserver_url}/_matrix/client/versions") except aiohttp.ClientError as err: log.warning("Failed pinging %s: %r", homeserver_url, err) item.status = PingStatus.Failed return times.append(round((time.time() - start) * 1000)) if i == 7 or i == 15: item.set_fields( ping=sum(times) // len(times), status=PingStatus.Done, ) def _get_homeserver_stability( self, logs: List[Dict[str, Any]], ) -> Tuple[float, List[timedelta]]: """Return server stability % and a list of downtime durations.""" stability = 100.0 durations = [] for period in logs: started_at = datetime.fromtimestamp(period["datetime"]) time_since_now = datetime.now() - started_at if time_since_now.days > 30 or period["type"] != 1: # 1 = downtime continue lasted_minutes = period["duration"] / 60 durations.append(timedelta(seconds=period["duration"])) stability -= ( (lasted_minutes * stability / 1000) / max(1, time_since_now.days / 3) ) return (stability, durations) async def fetch_homeservers(self) -> None: """Retrieve a list of public homeservers and add them to our model.""" @client_session # need to trigger this decorator for creation async def have_session_be_created(*_): pass # We just want that client's aiohttp session, that way we don't have # to depend ourselves on aiohttp + aiohttp-socks proxy = self.settings.General.proxy client = nio.AsyncClient(homeserver="", proxy=proxy) await have_session_be_created(client) session = client.client_session # aiohttp only has "timeout" in 3.7.0+ timeout = getattr(session, "timeout", session._timeout) session = type(session)( raise_for_status = True, timeout = type(timeout)(total=20), connector = session.connector, ) api_list = "https://publiclist.anchel.nl/publiclist.json" response = await session.get(api_list) coros = [] for server in (await response.json()): homeserver_url = server["homeserver"] if homeserver_url.startswith("http://"): # insecure server continue if not re.match(r"^https?://.+", homeserver_url): homeserver_url = f"https://{homeserver_url}" if server["country"] == "USA": server["country"] = "United States" stability, durations = \ self._get_homeserver_stability(server["monitor"]["logs"]) self.models["homeservers"][homeserver_url] = Homeserver( id = homeserver_url, name = server["name"], site_url = server["url"], country = server["country"], stability = stability, downtimes_ms = [d.total_seconds() * 1000 for d in durations], ) coros.append(self._ping_homeserver(session, homeserver_url)) await asyncio.gather(*coros) await session.close() async def desktop_notify( self, title: str, body: str = "", image: Union[Path, str] = "", ) -> None: # XXX: images on windows must be .ICO try: plyer.notification.notify( title = title, message = body, app_name = __app_name__, app_icon = str(image), timeout = 10, toast = False, ) self.notifications_working = True except Exception: # noqa if self.notifications_working: trace = traceback.format_exc().rstrip() log.error("Sending desktop notification failed\n%s", trace) self.notifications_working = False async def sound_notify(self) -> None: path = self.settings.Notifications.default_sound path = str(Path(path).expanduser()) if path == "default.wav": path = "src/sounds/default.wav" try: content = pyotherside.qrc_get_file_contents(path) except ValueError: sa = simpleaudio.WaveObject.from_wave_file(path) else: wave_read = wave.open(io.BytesIO(content)) sa = simpleaudio.WaveObject.from_wave_read(wave_read) try: sa.play() self.audio_working = True except Exception as e: # noqa if self.audio_working: trace = traceback.format_exc().rstrip() log.error("Playing audio failed\n%s", trace) self.audio_working = False mirage-0.7.2/src/backend/color.py000066400000000000000000000377261407747233600166660ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Provide the `Color` class and functions to easily construct a `Color`.""" import builtins import colorsys from copy import copy from dataclasses import InitVar, dataclass, field from enum import Enum from typing import Optional, Tuple, Union from hsluv import hex_to_hsluv, hsluv_to_hex, hsluv_to_rgb, rgb_to_hsluv ColorTuple = Tuple[float, float, float, float] @dataclass(repr=False) class Color: """A color manipulable in HSLuv, HSL, RGB, hexadecimal and by SVG name. The `Color` object constructor accepts hexadecimal string ("#RGB", "#RRGGBB" or "#RRGGBBAA") or another `Color` to copy. Attributes representing the color in HSLuv, HSL, RGB, hexadecimal and SVG name formats can be accessed and modified on these `Color` objects. The `hsluv()`/`hsluva()`, `hsl()`/`hsla()` and `rgb()`/`rgba()` functions in this module are provided to create an object by specifying a color in other formats. Copies of objects with modified attributes can be created with the with the `Color.but()`, `Color.plus()` and `Copy.times()` methods. If the `hue` is outside of the normal 0-359 range, the number is interpreted as `hue % 360`, e.g. `360` is `0`, `460` is `100`, or `-20` is `340`. """ # The saturation and luv are properties due to the need for a setter # capping the value between 0-100, as hsluv handles numbers outside # this range incorrectly. color_or_hex: InitVar[str] = "#00000000" hue: float = field(init=False, default=0) _saturation: float = field(init=False, default=0) _luv: float = field(init=False, default=0) alpha: float = field(init=False, default=1) def __post_init__(self, color_or_hex: Union["Color", str]) -> None: if isinstance(color_or_hex, Color): hsluva = color_or_hex.hsluva self.hue, self.saturation, self.luv, self.alpha = hsluva else: self.hex = color_or_hex # HSLuv @property def hsluva(self) -> ColorTuple: return (self.hue, self.saturation, self.luv, self.alpha) @hsluva.setter def hsluva(self, value: ColorTuple) -> None: self.hue, self.saturation, self.luv, self.alpha = value @property def saturation(self) -> float: return self._saturation @saturation.setter def saturation(self, value: float) -> None: self._saturation = max(0, min(100, value)) @property def luv(self) -> float: return self._luv @luv.setter def luv(self, value: float) -> None: self._luv = max(0, min(100, value)) # HSL @property def hsla(self) -> ColorTuple: r, g, b = (self.red / 255, self.green / 255, self.blue / 255) h, l, s = colorsys.rgb_to_hls(r, g, b) return (h * 360, s * 100, l * 100, self.alpha) @hsla.setter def hsla(self, value: ColorTuple) -> None: h, s, l = (value[0] / 360, value[1] / 100, value[2] / 100) # noqa r, g, b = colorsys.hls_to_rgb(h, l, s) self.rgba = (r * 255, g * 255, b * 255, value[3]) @property def light(self) -> float: return self.hsla[2] @light.setter def light(self, value: float) -> None: self.hsla = (self.hue, self.saturation, value, self.alpha) # RGB @property def rgba(self) -> ColorTuple: r, g, b = hsluv_to_rgb(self.hsluva) return r * 255, g * 255, b * 255, self.alpha @rgba.setter def rgba(self, value: ColorTuple) -> None: r, g, b = (value[0] / 255, value[1] / 255, value[2] / 255) self.hsluva = rgb_to_hsluv((r, g, b)) + (self.alpha,) @property def red(self) -> float: return self.rgba[0] @red.setter def red(self, value: float) -> None: self.rgba = (value, self.green, self.blue, self.alpha) @property def green(self) -> float: return self.rgba[1] @green.setter def green(self, value: float) -> None: self.rgba = (self.red, value, self.blue, self.alpha) @property def blue(self) -> float: return self.rgba[2] @blue.setter def blue(self, value: float) -> None: self.rgba = (self.red, self.green, value, self.alpha) # Hexadecimal @property def hex(self) -> str: rgb = hsluv_to_hex(self.hsluva) alpha = builtins.hex(int(self.alpha * 255))[2:] alpha = f"0{alpha}" if len(alpha) == 1 else alpha return f"{alpha if self.alpha < 1 else ''}{rgb}".lower() @hex.setter def hex(self, value: str) -> None: if len(value) == 4: template = "#{r}{r}{g}{g}{b}{b}" value = template.format(r=value[1], g=value[2], b=value[3]) alpha = int(value[-2:] if len(value) == 9 else "ff", 16) / 255 self.hsluva = hex_to_hsluv(value) + (alpha,) # name color @property def name(self) -> Optional[str]: try: return SVGColor(self.hex).name except ValueError: return None @name.setter def name(self, value: str) -> None: self.hex = SVGColor[value.lower()].value.hex # Other methods def __repr__(self) -> str: r, g, b = int(self.red), int(self.green), int(self.blue) h, s, luv = int(self.hue), int(self.saturation), int(self.luv) l = int(self.light) # noqa a = self.alpha block = f"\x1b[38;2;{r};{g};{b}m█████\x1b[0m" sep = "\x1b[1;33m/\x1b[0m" end = f" {sep} {self.name}" if self.name else "" # Need a terminal with true color support to render the block! return ( f"{block} hsluva({h}, {s}, {luv}, {a}) {sep} " f"hsla({h}, {s}, {l}, {a}) {sep} rgba({r}, {g}, {b}, {a}) {sep} " f"{self.hex}{end}" ) def but( self, hue: Optional[float] = None, saturation: Optional[float] = None, luv: Optional[float] = None, alpha: Optional[float] = None, *, hsluva: Optional[ColorTuple] = None, hsla: Optional[ColorTuple] = None, rgba: Optional[ColorTuple] = None, hex: Optional[str] = None, name: Optional[str] = None, light: Optional[float] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, ) -> "Color": """Return a copy of this `Color` with overriden attributes. Example: >>> first = Color(100, 50, 50) >>> second = c.but(hue=20, saturation=100) >>> second.hsluva (20, 50, 100, 1) """ new = copy(self) for arg, value in locals().items(): if arg not in ("new", "self") and value is not None: setattr(new, arg, value) return new def plus( self, hue: Optional[float] = None, saturation: Optional[float] = None, luv: Optional[float] = None, alpha: Optional[float] = None, *, light: Optional[float] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, ) -> "Color": """Return a copy of this `Color` with values added to attributes. Example: >>> first = Color(100, 50, 50) >>> second = c.plus(hue=10, saturation=-20) >>> second.hsluva (110, 30, 50, 1) """ new = copy(self) for arg, value in locals().items(): if arg not in ("new", "self") and value is not None: setattr(new, arg, getattr(self, arg) + value) return new def times( self, hue: Optional[float] = None, saturation: Optional[float] = None, luv: Optional[float] = None, alpha: Optional[float] = None, *, light: Optional[float] = None, red: Optional[float] = None, green: Optional[float] = None, blue: Optional[float] = None, ) -> "Color": """Return a copy of this `Color` with multiplied attributes. Example: >>> first = Color(100, 50, 50, 0.8) >>> second = c.times(luv=2, alpha=0.5) >>> second.hsluva (100, 50, 100, 0.4) """ new = copy(self) for arg, value in locals().items(): if arg not in ("new", "self") and value is not None: setattr(new, arg, getattr(self, arg) * value) return new class SVGColor(Enum): """Standard SVG/HTML/CSS colors, with the addition of `transparent`.""" aliceblue = Color("#f0f8ff") antiquewhite = Color("#faebd7") aqua = Color("#00ffff") aquamarine = Color("#7fffd4") azure = Color("#f0ffff") beige = Color("#f5f5dc") bisque = Color("#ffe4c4") black = Color("#000000") blanchedalmond = Color("#ffebcd") blue = Color("#0000ff") blueviolet = Color("#8a2be2") brown = Color("#a52a2a") burlywood = Color("#deb887") cadetblue = Color("#5f9ea0") chartreuse = Color("#7fff00") chocolate = Color("#d2691e") coral = Color("#ff7f50") cornflowerblue = Color("#6495ed") cornsilk = Color("#fff8dc") crimson = Color("#dc143c") cyan = Color("#00ffff") darkblue = Color("#00008b") darkcyan = Color("#008b8b") darkgoldenrod = Color("#b8860b") darkgray = Color("#a9a9a9") darkgreen = Color("#006400") darkgrey = Color("#a9a9a9") darkkhaki = Color("#bdb76b") darkmagenta = Color("#8b008b") darkolivegreen = Color("#556b2f") darkorange = Color("#ff8c00") darkorchid = Color("#9932cc") darkred = Color("#8b0000") darksalmon = Color("#e9967a") darkseagreen = Color("#8fbc8f") darkslateblue = Color("#483d8b") darkslategray = Color("#2f4f4f") darkslategrey = Color("#2f4f4f") darkturquoise = Color("#00ced1") darkviolet = Color("#9400d3") deeppink = Color("#ff1493") deepskyblue = Color("#00bfff") dimgray = Color("#696969") dimgrey = Color("#696969") dodgerblue = Color("#1e90ff") firebrick = Color("#b22222") floralwhite = Color("#fffaf0") forestgreen = Color("#228b22") fuchsia = Color("#ff00ff") gainsboro = Color("#dcdcdc") ghostwhite = Color("#f8f8ff") gold = Color("#ffd700") goldenrod = Color("#daa520") gray = Color("#808080") green = Color("#008000") greenyellow = Color("#adff2f") grey = Color("#808080") honeydew = Color("#f0fff0") hotpink = Color("#ff69b4") indianred = Color("#cd5c5c") indigo = Color("#4b0082") ivory = Color("#fffff0") khaki = Color("#f0e68c") lavender = Color("#e6e6fa") lavenderblush = Color("#fff0f5") lawngreen = Color("#7cfc00") lemonchiffon = Color("#fffacd") lightblue = Color("#add8e6") lightcoral = Color("#f08080") lightcyan = Color("#e0ffff") lightgoldenrodyellow = Color("#fafad2") lightgray = Color("#d3d3d3") lightgreen = Color("#90ee90") lightgrey = Color("#d3d3d3") lightpink = Color("#ffb6c1") lightsalmon = Color("#ffa07a") lightseagreen = Color("#20b2aa") lightskyblue = Color("#87cefa") lightslategray = Color("#778899") lightslategrey = Color("#778899") lightsteelblue = Color("#b0c4de") lightyellow = Color("#ffffe0") lime = Color("#00ff00") limegreen = Color("#32cd32") linen = Color("#faf0e6") magenta = Color("#ff00ff") maroon = Color("#800000") mediumaquamarine = Color("#66cdaa") mediumblue = Color("#0000cd") mediumorchid = Color("#ba55d3") mediumpurple = Color("#9370db") mediumseagreen = Color("#3cb371") mediumslateblue = Color("#7b68ee") mediumspringgreen = Color("#00fa9a") mediumturquoise = Color("#48d1cc") mediumvioletred = Color("#c71585") midnightblue = Color("#191970") mintcream = Color("#f5fffa") mistyrose = Color("#ffe4e1") moccasin = Color("#ffe4b5") navajowhite = Color("#ffdead") navy = Color("#000080") oldlace = Color("#fdf5e6") olive = Color("#808000") olivedrab = Color("#6b8e23") orange = Color("#ffa500") orangered = Color("#ff4500") orchid = Color("#da70d6") palegoldenrod = Color("#eee8aa") palegreen = Color("#98fb98") paleturquoise = Color("#afeeee") palevioletred = Color("#db7093") papayawhip = Color("#ffefd5") peachpuff = Color("#ffdab9") peru = Color("#cd853f") pink = Color("#ffc0cb") plum = Color("#dda0dd") powderblue = Color("#b0e0e6") purple = Color("#800080") rebeccapurple = Color("#663399") red = Color("#ff0000") rosybrown = Color("#bc8f8f") royalblue = Color("#4169e1") saddlebrown = Color("#8b4513") salmon = Color("#fa8072") sandybrown = Color("#f4a460") seagreen = Color("#2e8b57") seashell = Color("#fff5ee") sienna = Color("#a0522d") silver = Color("#c0c0c0") skyblue = Color("#87ceeb") slateblue = Color("#6a5acd") slategray = Color("#708090") slategrey = Color("#708090") snow = Color("#fffafa") springgreen = Color("#00ff7f") steelblue = Color("#4682b4") tan = Color("#d2b48c") teal = Color("#008080") thistle = Color("#d8bfd8") tomato = Color("#ff6347") transparent = Color("#00000000") # not standard but exists in QML turquoise = Color("#40e0d0") violet = Color("#ee82ee") wheat = Color("#f5deb3") white = Color("#ffffff") whitesmoke = Color("#f5f5f5") yellow = Color("#ffff00") yellowgreen = Color("#9acd32") def hsluva( hue: float = 0, saturation: float = 0, luv: float = 0, alpha: float = 1, ) -> Color: """Return a `Color` from `(0-359, 0-100, 0-100, 0-1)` HSLuv arguments.""" return Color().but(hue, saturation, luv, alpha) def hsla( hue: float = 0, saturation: float = 0, light: float = 0, alpha: float = 1, ) -> Color: """Return a `Color` from `(0-359, 0-100, 0-100, 0-1)` HSL arguments.""" return Color().but(hue, saturation, light=light, alpha=alpha) def rgba( red: float = 0, green: float = 0, blue: float = 0, alpha: float = 1, ) -> Color: """Return a `Color` from `(0-255, 0-255, 0-255, 0-1)` RGB arguments.""" return Color().but(red=red, green=green, blue=blue, alpha=alpha) # Aliases color = Color hsluv = hsluva hsl = hsla rgb = rgba mirage-0.7.2/src/backend/errors.py000066400000000000000000000054771407747233600170620ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Custom exception definitions.""" from dataclasses import dataclass, field from typing import Optional import nio # Matrix Errors @dataclass class MatrixError(Exception): """An error returned by a Matrix server.""" http_code: int = 400 m_code: Optional[str] = None message: Optional[str] = None content: str = "" @classmethod async def from_nio(cls, response: nio.ErrorResponse) -> "MatrixError": """Return a `MatrixError` subclass from a nio `ErrorResponse`.""" http_code = response.transport_response.status m_code = response.status_code message = response.message content = await response.transport_response.text() for subcls in cls.__subclasses__(): if subcls.m_code and subcls.m_code == m_code: return subcls(http_code, m_code, message, content) # If error doesn't have a M_CODE, look for a generic http error class for subcls in cls.__subclasses__(): if not subcls.m_code and subcls.http_code == http_code: return subcls(http_code, m_code, message, content) return cls(http_code, m_code, message, content) @dataclass class MatrixUnrecognized(MatrixError): http_code: int = 400 m_code: str = "M_UNRECOGNIZED" @dataclass class MatrixInvalidAccessToken(MatrixError): http_code: int = 401 m_code: str = "M_UNKNOWN_TOKEN" @dataclass class MatrixUnauthorized(MatrixError): http_code: int = 401 m_code: str = "M_UNAUTHORIZED" @dataclass class MatrixForbidden(MatrixError): http_code: int = 403 m_code: str = "M_FORBIDDEN" @dataclass class MatrixBadJson(MatrixError): http_code: int = 403 m_code: str = "M_BAD_JSON" @dataclass class MatrixNotJson(MatrixError): http_code: int = 403 m_code: str = "M_NOT_JSON" @dataclass class MatrixUserDeactivated(MatrixError): http_code: int = 403 m_code: str = "M_USER_DEACTIVATED" @dataclass class MatrixNotFound(MatrixError): http_code: int = 404 m_code: str = "M_NOT_FOUND" @dataclass class MatrixTooLarge(MatrixError): http_code: int = 413 m_code: str = "M_TOO_LARGE" @dataclass class MatrixBadGateway(MatrixError): http_code: int = 502 m_code: Optional[str] = None # Client errors @dataclass class InvalidUserId(Exception): user_id: str = field() @dataclass class InvalidUserInContext(Exception): user_id: str = field() @dataclass class UserFromOtherServerDisallowed(Exception): user_id: str = field() @dataclass class UneededThumbnail(Exception): pass @dataclass class BadMimeType(Exception): wanted: str = field() got: str = field() mirage-0.7.2/src/backend/html_markdown.py000066400000000000000000000455001407747233600204030ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """HTML and Markdown processing tools.""" import re from contextlib import suppress from typing import Dict, List, Optional, Tuple from urllib.parse import unquote import html_sanitizer.sanitizer as sanitizer import lxml.html # nosec import mistune import nio from html_sanitizer.sanitizer import Sanitizer from lxml.html import HtmlElement, etree # nosec from .color import SVGColor class MarkdownInlineGrammar(mistune.InlineGrammar): """Markdown inline elements syntax modifications for the Mistune parser. - Disable underscores for bold/italics (e.g. `__bold__`) - Add syntax for coloring text: `(text)`, e.g. `(Lorem ipsum)` or `<#000040>(sit dolor amet...)` """ escape = re.compile(r"^\\([\\`*{}\[\]()#+\-.!_<>~|])") # Add < emphasis = re.compile(r"^\*((?:\*\*|[^\*])+?)\*(?!\*)") double_emphasis = re.compile(r"^\*{2}([\s\S]+?)\*{2}(?!\*)") # test string: r"(x) (x) \
b>(x) b>(x) (\(z) (foo\)xyz)" color = re.compile( r"^<(.+?)>" # capture the color in `` r"\((.+?)" # capture text in `(text` r"(? str: """Render given text with a color using ``.""" # This may be a SVG color name, try to get a #hex code from it: with suppress(KeyError): color = SVGColor[color.lower().replace(" ", "")].value.hex return f'{text}' class HTMLProcessor: """Provide HTML filtering and conversion from Markdown. Filtering sanitizes HTML and ensures it complies both with the Matrix specification: https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes and the supported Qt HTML subset for usage in QML: https://doc.qt.io/qt-5/richtext-html-subset.html Some methods take an `outgoing` argument, specifying if the HTML is intended to be sent to matrix servers or used locally in our application. For local usage, extra transformations are applied: - Wrap text lines starting with a `>` in `` with a `quote` class. This allows them to be styled appropriately from QML. Some methods take an `inline` argument, which return text appropriate for UI elements restricted to display a single line, e.g. the room last message subtitles in QML or notifications. In inline filtered HTML, block tags are stripped or substituted and newlines are turned into ⏎ symbols (U+23CE). """ inline_tags = { "span", "font", "a", "sup", "sub", "b", "i", "s", "u", "code", "mx-reply", } block_tags = { "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "ul", "ol", "li", "hr", "br", "img", "table", "thead", "tbody", "tr", "th", "td", "pre", "mx-reply", } opaque_id = r"[a-zA-Z\d._-]+?" user_id_localpart = r"[\x21-\x39\x3B-\x7E]+?" user_id_regex = re.compile( rf"(?P@{user_id_localpart}:(?P[a-zA-Z\d.:-]*[a-zA-Z\d]))", ) room_id_regex = re.compile( rf"(?P!{opaque_id}:(?P[a-zA-Z\d.:-]*[a-zA-Z\d]))", ) room_alias_regex = re.compile( r"(?=^|\W)(?P#\S+?:(?P[a-zA-Z\d.:-]*[a-zA-Z\d]))", ) link_regexes = [re.compile(r, re.IGNORECASE) if isinstance(r, str) else r for r in [ # Normal :// URLs (r"(?P[a-z\d]+://(?P[a-z\d._-]+(?:\:\d+)?)" r"(?:/[/\-.,\w#%&?:;=~!$*+^@']*)?(?:\([/\-_.,a-z\d#%&?;=~]*\))?)"), # mailto: and tel: r"mailto:(?P[a-z0-9._-]+@(?P[a-z0-9.:-]*[a-z\d]))", r"tel:(?P[0-9+-]+)(?P)", # magnet: r"(?Pmagnet:\?xt=urn:[a-z0-9]+:.+)(?P)", user_id_regex, room_id_regex, room_alias_regex, ]] matrix_to_regex = re.compile(r"^https?://matrix.to/#/", re.IGNORECASE) link_is_matrix_to_regex = re.compile( r"https?://matrix.to/#/\S+", re.IGNORECASE, ) link_is_user_id_regex = re.compile( r"https?://matrix.to/#/@\S+", re.IGNORECASE, ) link_is_room_id_regex = re.compile( r"https?://matrix.to/#/!\S+", re.IGNORECASE, ) link_is_room_alias_regex = re.compile( r"https?://matrix.to/#/#\S+", re.IGNORECASE, ) link_is_message_id_regex = re.compile( r"https?://matrix.to/#/[!#]\S+/\$\S+", re.IGNORECASE, ) inline_quote_regex = re.compile(r"(^|⏎|>)(\s*>[^⏎\n]*)", re.MULTILINE) quote_regex = re.compile( r"(^||

|
||)" r"(\s*>.*?)" r"(||
||
|$)", re.MULTILINE, ) extra_newlines_regex = re.compile(r"\n(\n*)") def __init__(self) -> None: # The whitespace remover doesn't take

 into account
        sanitizer.normalize_overall_whitespace = lambda html, *args, **kw: html
        sanitizer.normalize_whitespace_in_text_or_tail = \
            lambda el, *args, **kw: el

        # hard_wrap: convert all \n to 
without required two spaces # escape: escape HTML characters in the input string, e.g. tags self._markdown_to_html = mistune.Markdown( hard_wrap = True, escape = True, inline = MarkdownInlineLexer, block = MarkdownBlockLexer, renderer = MarkdownRenderer(), ) self._markdown_to_html.block.default_rules = [ rule for rule in self._markdown_to_html.block.default_rules if rule != "block_quote" ] def mentions_in_html(self, html: str) -> List[Tuple[str, str]]: """Return list of (text, href) tuples for all mention links in html.""" if not html.strip(): return [] return [ (a_tag.text, href) for a_tag, _, href, _ in lxml.html.iterlinks(html) if a_tag.text and self.link_is_matrix_to_regex.match(unquote(href.strip())) ] def from_markdown( self, text: str, inline: bool = False, outgoing: bool = False, display_name_mentions: Optional[Dict[str, str]] = None, ) -> str: """Return filtered HTML from Markdown text.""" return self.filter( self._markdown_to_html(text), inline, outgoing, display_name_mentions, ) def filter( self, html: str, inline: bool = False, outgoing: bool = False, display_name_mentions: Optional[Dict[str, str]] = None, ) -> str: """Filter and return HTML.""" mentions = display_name_mentions sanit = Sanitizer(self.sanitize_settings(inline, outgoing, mentions)) html = sanit.sanitize(html).rstrip("\n") if not html.strip(): return html tree = etree.fromstring( html, parser=etree.HTMLParser(encoding="utf-8"), ) for a_tag in tree.iterdescendants("a"): self._mentions_to_matrix_to_links(a_tag, mentions, outgoing) if not outgoing: self._matrix_to_links_add_classes(a_tag) html = etree.tostring(tree, encoding="utf-8", method="html").decode() html = sanit.sanitize(html).rstrip("\n") if outgoing: return html # Client-side modifications html = self.quote_regex.sub(r'\1\2\3', html) if not inline: return html return self.inline_quote_regex.sub( r'\1\2', html, ) def sanitize_settings( self, inline: bool = False, outgoing: bool = False, display_name_mentions: Optional[Dict[str, str]] = None, ) -> dict: """Return an html_sanitizer configuration.""" # https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes inline_tags = self.inline_tags all_tags = inline_tags | self.block_tags inlines_attributes = { "font": {"color"}, "a": {"href", "class", "data-mention"}, "code": {"class"}, } attributes = {**inlines_attributes, **{ "ol": {"start"}, "hr": {"width"}, "span": {"data-mx-color"}, "img": { "data-mx-emote", "src", "alt", "title", "width", "height", }, }} username_link_regexes = [re.compile(r) for r in [ rf"(?{re.escape(name or user_id)})(?!\w)(?P)" for user_id, name in (display_name_mentions or {}).items() ]] return { "tags": inline_tags if inline else all_tags, "attributes": inlines_attributes if inline else attributes, "empty": {} if inline else {"hr", "br", "img"}, "separate": {"a"} if inline else { "a", "p", "li", "table", "tr", "th", "td", "br", "hr", "img", }, "whitespace": {}, "keep_typographic_whitespace": True, "add_nofollow": False, "autolink": { "link_regexes": self.link_regexes + username_link_regexes, # type: ignore "avoid_hosts": [], }, "sanitize_href": lambda href: href, "element_preprocessors": [ sanitizer.bold_span_to_strong, sanitizer.italic_span_to_em, sanitizer.tag_replacer("strong", "b"), sanitizer.tag_replacer("em", "i"), sanitizer.tag_replacer("strike", "s"), sanitizer.tag_replacer("del", "s"), sanitizer.tag_replacer("form", "p"), sanitizer.tag_replacer("div", "p"), sanitizer.tag_replacer("caption", "p"), sanitizer.target_blank_noopener, self._span_color_to_font if not outgoing else lambda el: el, self._img_to_a, self._remove_extra_newlines, self._newlines_to_return_symbol if inline else lambda el: el, self._reply_to_inline if inline else lambda el: el, ], "element_postprocessors": [ self._font_color_to_span if outgoing else lambda el: el, self._hr_to_dashes if not outgoing else lambda el: el, ], "is_mergeable": lambda e1, e2: e1.attrib == e2.attrib, } @staticmethod def _span_color_to_font(el: HtmlElement) -> HtmlElement: """Convert HTML ``.""" if el.tag not in ("span", "font"): return el color = el.attrib.pop("data-mx-color", None) if color: el.tag = "font" el.attrib["color"] = color return el @staticmethod def _font_color_to_span(el: HtmlElement) -> HtmlElement: """Convert HTML `` to ` HtmlElement: """Linkify images by wrapping `` tags in `
`.""" if el.tag != "img": return el src = el.attrib.get("src", "") width = el.attrib.get("width") height = el.attrib.get("height") is_emote = "data-mx-emote" in el.attrib if src.startswith("mxc://"): el.attrib["src"] = nio.Api.mxc_to_http(src) if is_emote and not width and not height: el.attrib["width"] = 32 el.attrib["height"] = 32 elif is_emote and width and not height: el.attrib["height"] = width elif is_emote and height and not width: el.attrib["width"] = height elif not is_emote and (not width or not height): el.tag = "a" el.attrib["href"] = el.attrib.pop("src", "") el.text = el.attrib.pop("alt", None) or el.attrib["href"] return el def _remove_extra_newlines(self, el: HtmlElement) -> HtmlElement: r"""Remove excess `\n` characters from HTML elements. This is done to avoid additional blank lines when the CSS directive `white-space: pre` is used. Text inside `
` tags is ignored, except for the final newlines.
        """

        pre_parent = any(parent.tag == "pre" for parent in el.iterancestors())

        if el.tag != "pre" and not pre_parent:
            if el.text:
                el.text = self.extra_newlines_regex.sub(r"\1", el.text)
            if el.tail:
                el.tail = self.extra_newlines_regex.sub(r"\1", el.tail)
        else:
            if el.text and el.text.endswith("\n"):
                el.text = el.text[:-1]
            if el.tail and el.tail.endswith("\n"):
                el.tail = el.tail[:-1]

        return el


    def _newlines_to_return_symbol(self, el: HtmlElement) -> HtmlElement:
        """Turn newlines into unicode return symbols (⏎, U+23CE).

        The symbol is added to blocks with siblings (e.g. a `

` followed by another `

`) and `
` tags. The `
` themselves will be removed by the inline sanitizer. """ is_block_with_siblings = (el.tag in self.block_tags and next(el.itersiblings(), None) is not None) if el.tag == "br" or is_block_with_siblings: el.tail = f" ⏎ {el.tail or ''}" # Replace left \n in text/tail of

 content by the return symbol.
        if el.text:
            el.text = re.sub(r"\n", r" ⏎ ", el.text)

        if el.tail:
            el.tail = re.sub(r"\n", r" ⏎ ", el.tail)

        return el


    def _reply_to_inline(self, el: HtmlElement) -> HtmlElement:
        """Shorten  to only include the replied to event's sender."""

        if el.tag != "mx-reply":
            return el

        try:
            user_id = el.find("blockquote").findall("a")[1].text
            text    = f"↩ {user_id[1: ].split(':')[0]}: "  # U+21A9 arrow
            tail    = el.tail.rstrip().rstrip("⏎")
        except (AttributeError, IndexError):
            return el

        el.clear()
        el.text = text
        el.tail = tail
        return el


    def _mentions_to_matrix_to_links(
        self,
        el:                    HtmlElement,
        display_name_mentions: Optional[Dict[str, str]] = None,
        outgoing:              bool                     = False,
    ) -> HtmlElement:
        """Turn user ID, usernames and room ID/aliases into matrix.to URL.

        After the HTML sanitizer autolinks these, the links's hrefs are the
        link text, e.g. `@foo:bar.com`.
        We turn them into proper matrix.to URL in this function.
        """

        if el.tag != "a" or not el.attrib.get("href"):
            return el

        id_regexes = (
            self.user_id_regex, self.room_id_regex, self.room_alias_regex,
        )

        for regex in id_regexes:
            if regex.match(unquote(el.attrib["href"])):
                el.attrib["href"] = f"https://matrix.to/#/{el.attrib['href']}"
                return el

        for user_id, name in (display_name_mentions or {}).items():
            if unquote(el.attrib["href"]) == (name or user_id):
                el.attrib["href"] = f"https://matrix.to/#/{user_id}"
                return el

        return el


    def _matrix_to_links_add_classes(self, el: HtmlElement) -> HtmlElement:
        """Add special CSS classes to matrix.to mention links."""

        href = unquote(el.attrib.get("href", ""))

        if not href or not el.text:
            return el


        el.text = self.matrix_to_regex.sub("", el.text or "")

        # This must be first, or link will be mistaken by room ID/alias regex
        if self.link_is_message_id_regex.match(href):
            el.attrib["class"]        = "mention message-id-mention"
            el.attrib["data-mention"] = el.text.strip()

        elif self.link_is_user_id_regex.match(href):
            if el.text.strip().startswith("@"):
                el.attrib["class"] = "mention user-id-mention"
            else:
                el.attrib["class"] = "mention username-mention"

            el.attrib["data-mention"] = el.text.strip()

        elif self.link_is_room_id_regex.match(href):
            el.attrib["class"]        = "mention room-id-mention"
            el.attrib["data-mention"] = el.text.strip()

        elif self.link_is_room_alias_regex.match(href):
            el.attrib["class"]        = "mention room-alias-mention"
            el.attrib["data-mention"] = el.text.strip()

        return el


    def _hr_to_dashes(self, el: HtmlElement) -> HtmlElement:
        if el.tag != "hr":
            return el

        el.tag             = "p"
        el.attrib["class"] = "ruler"
        el.text            = "─" * 19
        return el


HTML_PROCESSOR = HTMLProcessor()
mirage-0.7.2/src/backend/matrix_client.py000066400000000000000000002606271407747233600204100ustar00rootroot00000000000000# Copyright Mirage authors & contributors 
# SPDX-License-Identifier: LGPL-3.0-or-later

"""Matrix client to interact with a homeserver and other related classes."""

import asyncio
import html
import io
import logging as log
import os
import platform
import re
import textwrap
import traceback
from contextlib import suppress
from copy import deepcopy
from datetime import datetime, timedelta
from functools import partial
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import (
    TYPE_CHECKING, Any, Callable, ClassVar, Coroutine, DefaultDict, Dict, List,
    NamedTuple, Optional, Set, Tuple, Type, Union,
)
from urllib.parse import urlparse
from uuid import UUID, uuid4

import cairosvg
import nio
from nio.crypto import AsyncDataT as UploadData
from nio.crypto import async_generator_from_data
from PIL import Image as PILImage
from pymediainfo import MediaInfo

from . import __display_name__, __reverse_dns__, utils
from .errors import (
    BadMimeType, InvalidUserId, InvalidUserInContext, MatrixBadGateway,
    MatrixError, MatrixForbidden, MatrixInvalidAccessToken, MatrixNotFound,
    MatrixTooLarge, MatrixUnauthorized, MatrixUnrecognized, UneededThumbnail,
    UserFromOtherServerDisallowed,
)
from .html_markdown import HTML_PROCESSOR as HTML
from .media_cache import Media, Thumbnail
from .models.items import (
    ZERO_DATE, Account, Event, Member, PushRule, Room,
    RoomNotificationOverride, Transfer, TransferStatus, TypeSpecifier,
)
from .models.model_store import ModelStore
from .nio_callbacks import NioCallbacks
from .presence import Presence
from .pyotherside_events import (
    InvalidAccessToken, LoopException, NotificationRequested,
)

if TYPE_CHECKING:
    from .backend import Backend

PushAction    = Union[Dict[str, Any], nio.PushAction]
PushCondition = Union[Dict[str, Any], nio.PushCondition]
CryptDict     = Dict[str, Any]
PathCallable  = Union[
    str, Path, Callable[[], Coroutine[None, None, Union[str, Path]]],
]

IS_WINDOWS = platform.system() == "Windows"

MATRIX_TO = "https://matrix.to/#"

REPLY_FALLBACK = (
    ""
        "
" 'In reply to ' '{user_id}' "
" "{content}" "
" "
" "{reply_content}" ) class SyncFilterIds(NamedTuple): """Uploaded filter IDs for various API.""" first: str others: str class UploadReturn(NamedTuple): """Details for an uploaded file.""" mxc: str mime: str decryption_dict: Dict[str, Any] class MatrixImageInfo(NamedTuple): """Image informations to be passed for Matrix file events.""" width: int height: int mime: str size: int def as_dict(self) -> Dict[str, Union[int, str]]: """Return a dict ready to be included in a Matrix file events.""" return { "w": self.width, "h": self.height, "mimetype": self.mime, "size": self.size, } class MatrixClient(nio.AsyncClient): """A client for an account to interact with a matrix homeserver.""" user_id_regex = re.compile(r"^@.+:.+") room_id_or_alias_regex = re.compile(r"^[#!].+:.+") http_s_url_regex = re.compile(r"^https?://") lazy_load_filter: ClassVar[Dict[str, Any]] = { "room": { "ephemeral": {"lazy_load_members": True}, "state": {"lazy_load_members": True}, "timeline": {"lazy_load_members": True}, "account_data": {"lazy_load_members": True}, }, } low_limit_filter: ClassVar[Dict[str, Any]] = { "room": { "ephemeral": {"limit": 1}, "timeline": { "limit": 5, # This kind says another event was redacted, but we wouldn't # have it in our model, so nothing would be shown "not_types": ["m.room.redaction"], }, }, } no_unknown_events_filter: ClassVar[Dict[str, Any]] = { "room": { "timeline": { "not_types": [ "m.room.message.feedback", "m.room.pinned_events", "m.call.*", "m.room.third_party_invite", "m.room.tombstone", "m.reaction", ], }, }, } def __init__( self, backend, user: str = "", homeserver: str = "https://matrix.org", device_id: Optional[str] = None, ) -> None: store = Path(backend.appdirs.user_data_dir) / "encryption" store.mkdir(parents=True, exist_ok=True) proxy: Optional[str] proxy = os.environ.get("http_proxy", backend.settings.General.proxy) host = re.sub(r":\d+$", "", urlparse(homeserver).netloc) if host in ("127.0.0.1", "localhost", "::1"): proxy = None super().__init__( homeserver = homeserver, user = user, device_id = device_id, store_path = store, proxy = proxy, config = nio.AsyncClientConfig( max_timeout_retry_wait_time = 10, # TODO: pass a custom encryption DB pickle key? ), ) self.backend: "Backend" = backend self.models: ModelStore = self.backend.models self.profile_task: Optional[asyncio.Future] = None self.server_config_task: Optional[asyncio.Future] = None self.sync_task: Optional[asyncio.Future] = None self.start_task: Optional[asyncio.Future] = None self.transfer_monitors: Dict[UUID, nio.TransferMonitor] = {} self.transfer_tasks: Dict[UUID, asyncio.Task] = {} self.send_message_tasks: Dict[UUID, asyncio.Task] = {} self._presence: str = "" self._sync_filter_ids: Optional[SyncFilterIds] = None self._sync_filter_ids_lock: asyncio.Lock = asyncio.Lock() self.first_sync_done: asyncio.Event = asyncio.Event() self.first_sync_date: Optional[datetime] = None self.last_sync_error: Optional[Exception] = None self.last_set_presence: datetime = datetime.now() self.past_tokens: Dict[str, str] = {} # {room_id: token} self.fully_loaded_rooms: Set[str] = set() # {room_id} self.loaded_once_rooms: Set[str] = set() # {room_id} self.cleared_events_rooms: Set[str] = set() # {room_id} self.ignored_rooms: Set[str] = set() # {room_id} self.event_to_echo_ids: Dict[str, str] = {} # {(room_id, user_id): event_id} self.unassigned_member_last_read_event: Dict[Tuple[str, str], str] = {} # {event_id: {user_id: server_timestamp}} self.unassigned_event_last_read_by: DefaultDict[str, Dict[str, int]] =\ DefaultDict(dict) self.push_rules: nio.PushRulesEvent = nio.PushRulesEvent() self.ignored_user_ids: Set[str] = set() # {room_id: event} self.power_level_events: Dict[str, nio.PowerLevelsEvent] = {} self.invalid_disconnecting: bool = False self.nio_callbacks = NioCallbacks(self) def __repr__(self) -> str: return "%s(user_id=%r, homeserver=%r, device_id=%r)" % ( type(self).__name__, self.user_id, self.homeserver, self.device_id, ) @property def default_device_name(self) -> str: """Device name to set at login if the user hasn't set a custom one.""" os_name = platform.system() if not os_name: # unknown OS return __display_name__ # On Linux, the kernel version is returned, so for a one-time-set # device name it would quickly be outdated. os_ver = platform.release() if os_name == "Windows" else "" return f"{__display_name__} on {os_name} {os_ver}".rstrip() async def _send(self, *args, **kwargs) -> nio.Response: """Raise a `MatrixError` subclass for any `nio.ErrorResponse`. This function is called by `nio.AsyncClient`'s methods to send requests to the server. Return normal responses, but catch any `ErrorResponse` to turn them into `MatrixError` exceptions we raise. """ response = await super()._send(*args, **kwargs) if isinstance(response, nio.ErrorResponse): try: raise await MatrixError.from_nio(response) except MatrixInvalidAccessToken: if not self.invalid_disconnecting: self.invalid_disconnecting = True InvalidAccessToken(self.user_id) await self.backend.logout_client(self.user_id) raise return response async def login( self, password: Optional[str] = None, token: Optional[str] = None, ) -> None: """Login to server using `m.login.password` or `m.login.token` flows. Login can be done with the account's password (if the server supports this flow) OR a token obtainable through various means. One of the way to obtain a token is to follow the `m.login.sso` flow first, see `Backend.start_sso_auth()` & `Backend.continue_sso_auth()`. """ await super().login(password, self.default_device_name, token) order = 0 saved_accounts = self.backend.saved_accounts if saved_accounts: order = max( account.get("order", i) for i, account in enumerate(saved_accounts.values()) ) + 1 # We need to create account model item here, because _start() needs it item = self.models["accounts"].setdefault( self.user_id, Account(self.user_id, order), ) # TODO: be able to set presence before logging in item.set_fields(presence=Presence.State.online, connecting=True) self._presence = "online" self.start_task = asyncio.ensure_future(self._start()) async def resume( self, user_id: str, access_token: str, device_id: str, state: str = "online", status_msg: str = "", ) -> None: """Restore a previous login to the server with a saved access token.""" self.restore_login(user_id, device_id, access_token) account = self.models["accounts"][user_id] self._presence = "offline" if state == "invisible" else state account.set_fields( presence=Presence.State(state), status_msg=status_msg, ) if state != "offline": account.connecting = True self.start_task = asyncio.ensure_future(self._start()) async def logout(self) -> None: """Logout from the server. This will delete the device.""" await self._stop() await super().logout() await self.close() async def terminate(self) -> None: """Stop tasks, Set our presence offline and close HTTP connections.""" await self._stop() if self._presence != "offline": try: await asyncio.wait_for( self.set_presence("offline", save=False), timeout = 10, ) except asyncio.TimeoutError: log.warn("%s timed out", self.user_id) await self.close() async def _start(self) -> None: """Fetch our user profile, server config and enter the sync loop.""" def on_server_config_response(future) -> None: """Update our model `Account` with the received config details.""" if future.cancelled(): # Account logged out return try: account.max_upload_size = future.result() or 0 except MatrixError: trace = traceback.format_exc().rstrip() log.warn( "On %s server config retrieval: %s", self.user_id, trace, ) self.server_config_task = asyncio.ensure_future( self.get_server_config(), ) self.server_config_task.add_done_callback( on_server_config_response, ) account = self.models["accounts"][self.user_id] presence = self.backend.presences.setdefault(self.user_id, Presence()) presence.account = account presence.presence = Presence.State(self._presence) self.profile_task = asyncio.ensure_future(self.update_own_profile()) self.server_config_task = asyncio.ensure_future( self.get_server_config(), ) self.server_config_task.add_done_callback(on_server_config_response) await self.auto_verify_all_other_accounts() while True: try: sync_filter_ids = await self.sync_filter_ids() self.sync_task = asyncio.ensure_future(self.sync_forever( timeout = 10_000, loop_sleep_time = 1000, first_sync_filter = sync_filter_ids.first, sync_filter = sync_filter_ids.others, )) await self.sync_task self.last_sync_error = None break # task cancelled except Exception as err: # noqa self.last_sync_error = err trace = traceback.format_exc().rstrip() if isinstance(err, MatrixError) and err.http_code >= 500: log.warning( "Server failure during sync for %s:\n%s", self.user_id, trace, ) else: LoopException(str(err), err, trace) await asyncio.sleep(5) async def _stop(self) -> None: """Stop client tasks. Will prevent client to receive further events.""" # Remove account model from presence update presence = self.backend.presences.get(self.user_id, None) if presence: presence.account = None tasks = ( self.profile_task, self.sync_task, self.server_config_task, self.start_task, ) for task in tasks: if task: task.cancel() with suppress(asyncio.CancelledError): await task self.first_sync_done.clear() async def get_profile( self, user_id: str, use_cache: bool = True, ) -> nio.ProfileGetResponse: """Cache and return the matrix profile of `user_id`.""" async with self.backend.get_profile_locks[user_id]: if use_cache and user_id in self.backend.profile_cache: return self.backend.profile_cache[user_id] response = await super().get_profile(user_id) self.backend.profile_cache[user_id] = response return response async def update_own_profile(self) -> None: """Fetch our profile from server and Update our model `Account`.""" resp = await self.get_profile(self.user_id, use_cache=False) account = self.models["accounts"][self.user_id] account.set_fields( profile_updated = datetime.now(), display_name = resp.displayname or "", avatar_url = resp.avatar_url or "", ) async def get_server_config(self) -> int: """Return the maximum upload size on this server""" return (await self.content_repository_config()).upload_size async def sync_filter_ids(self) -> SyncFilterIds: """Return our sync/messages filter IDs, upload them if needed.""" async with self._sync_filter_ids_lock: if self._sync_filter_ids: return self._sync_filter_ids others = deepcopy(self.lazy_load_filter) first = deepcopy(others) utils.dict_update_recursive(first, self.low_limit_filter) if not self.backend.settings.Chat.show_unknown_events: first["room"]["timeline"]["not_types"].extend( self.no_unknown_events_filter ["room"]["timeline"]["not_types"], ) others_id = (await self.upload_filter(**others)).filter_id first_id = others_id if others != first: resp = await self.upload_filter(**first) first_id = resp.filter_id self._sync_filter_ids = SyncFilterIds(first_id, others_id) return self._sync_filter_ids async def pause_while_offline(self) -> None: """Block until our account is online.""" account = self.models["accounts"][self.user_id] while account.presence == Presence.State.offline: await asyncio.sleep(0.2) async def set_ignored_users(self, *user_ids: str) -> None: previous_ignored = self.ignored_user_ids now_ignored = set(user_ids) no_longer_ignored = previous_ignored - now_ignored path = ["user", self.user_id, "account_data", "m.ignored_user_list"] params = {"access_token": self.access_token} await self._send( nio.responses.EmptyResponse, "PUT", nio.Api._build_path(path, params), nio.Api.to_json({"ignored_users": {u: {} for u in now_ignored}}), ) # Invites and messages from ignored users won't be returned anymore on # syncs, thus will be absent on client restart. # Clean up immediatly, and also update Member.ignored fields: room_model = self.models[self.user_id, "rooms"] with room_model.batch_remove(): for room_id, room in room_model.copy().items(): if room.inviter_id in now_ignored: self.ignored_rooms.add(room_id) del room_model[room_id] self.models.pop((self.user_id, room_id, "events"), None) self.models.pop((self.user_id, room_id, "members"), None) continue event_model = self.models[self.user_id, room_id, "events"] member_model = self.models[self.user_id, room_id, "members"] for user_id in now_ignored: if user_id in member_model: member_model[user_id].ignored = True for user_id in no_longer_ignored: if user_id in member_model: member_model[user_id].ignored = False with event_model.batch_remove(): for event_id, event in event_model.copy().items(): if event.sender_id in now_ignored: del event_model[event_id] await self.update_account_unread_counts() async def ignore_user(self, user_id: str, ignore: bool) -> None: current = self.ignored_user_ids new = current | {user_id} if ignore else current - {user_id} await self.set_ignored_users(*new) async def can_kick(self, room_id: str, target_user_id: str) -> bool: """Return whether we can kick a certain user in a room.""" levels = self.all_rooms[room_id].power_levels return levels.can_user_kick(self.user_id, target_user_id) async def can_ban(self, room_id: str, target_user_id: str) -> bool: """Return whether we can ban/unbun a certain user in a room.""" levels = self.all_rooms[room_id].power_levels return levels.can_user_ban(self.user_id, target_user_id) @property def all_rooms(self) -> Dict[str, nio.MatrixRoom]: """Return dict containing both our joined and invited rooms.""" return {**self.invited_rooms, **self.rooms} async def send_text( self, room_id: str, text: str, display_name_mentions: Optional[Dict[str, str]] = None, # {id: name} reply_to_event_id: Optional[str] = None, ) -> None: """Send a markdown `m.text` or `m.notice` (with `/me`) message .""" from_md = partial( HTML.from_markdown, display_name_mentions=display_name_mentions, ) escape = False if text.startswith("//") or text.startswith(r"\/"): escape = True text = text[1:] content: Dict[str, Any] if text.startswith("/me ") and not escape: event_type = nio.RoomMessageEmote text = text[len("/me "):] content = {"body": text, "msgtype": "m.emote"} to_html = from_md(text, inline=True, outgoing=True) echo_body = from_md(text, inline=True) else: event_type = nio.RoomMessageText content = {"body": text, "msgtype": "m.text"} to_html = from_md(text, outgoing=True) echo_body = from_md(text) if to_html not in (html.escape(text), f"

{html.escape(text)}

"): content["format"] = "org.matrix.custom.html" content["formatted_body"] = to_html if reply_to_event_id: to: Event = \ self.models[self.user_id, room_id, "events"][reply_to_event_id] source_body = getattr(to.source, "body", "") content["format"] = "org.matrix.custom.html" plain_source_body = "\n".join( f"> <{to.sender_id}> {line}" if i == 0 else f"> {line}" for i, line in enumerate(source_body.splitlines()) ) content["body"] = f"{plain_source_body}\n\n{text}" to_html = REPLY_FALLBACK.format( matrix_to = MATRIX_TO, room_id = room_id, event_id = to.event_id, user_id = to.sender_id, content = getattr(to.source, "formatted_body", "") or source_body or html.escape(to.source.source["type"] if to.source else ""), reply_content = to_html, ) echo_body = HTML.filter(to_html) content["formatted_body"] = HTML.filter(to_html, outgoing=True) content["m.relates_to"] = { "m.in_reply_to": {"event_id": to.event_id}, } # Can't use the standard Matrix transaction IDs; they're only visible # to the sender so our other accounts wouldn't be able to replace # local echoes by real messages. tx_id = uuid4() content[f"{__reverse_dns__}.transaction_id"] = str(tx_id) mentions = HTML.mentions_in_html(echo_body) await self._local_echo( room_id, tx_id, event_type, content = echo_body, mentions = mentions, ) await self.pause_while_offline() await self._send_message(room_id, content, tx_id) async def toggle_pause_transfer( self, room_id: str, uuid: Union[str, UUID], ) -> None: if isinstance(uuid, str): uuid = UUID(uuid) pause = not self.transfer_monitors[uuid].pause self.transfer_monitors[uuid].pause = pause self.models[room_id, "transfers"][str(uuid)].paused = pause async def cancel_transfer(self, uuid: Union[str, UUID]) -> None: if isinstance(uuid, str): uuid = UUID(uuid) self.transfer_tasks[uuid].cancel() async def send_clipboard_image( self, room_id: str, image: bytes, reply_to_event_id: Optional[str] = None, ) -> None: """Send a clipboard image passed from QML as a `m.image` message.""" prefix = datetime.now().strftime("%Y%m%d-%H%M%S.") with NamedTemporaryFile(prefix=prefix, suffix=".png") as temp: async def get_path() -> Path: # optimize is too slow for large images compressed = await utils.compress_image(image, optimize=False) async with utils.aiopen(temp.name, "wb") as file: await file.write(compressed) return Path(temp.name) await self.send_file(room_id, get_path, reply_to_event_id) async def send_file( self, room_id: str, path: PathCallable, reply_to_event_id: Optional[str] = None, ) -> None: """Send a `m.file`, `m.image`, `m.audio` or `m.video` message. The Matrix client-server API states that media messages can't have a reply attached. Thus, if a `reply_to_event_id` is passed, we send a pseudo-reply as two events: a `m.text` one with the reply but an empty body, then the actual media. """ item_uuid = uuid4() try: await self._send_file(item_uuid, room_id, path, reply_to_event_id) except (nio.TransferCancelledError, asyncio.CancelledError): self.transfer_monitors.pop(item_uuid, None) self.transfer_tasks.pop(item_uuid, None) self.models[room_id, "transfers"].pop(str(item_uuid), None) async def _send_file( self, item_uuid: UUID, room_id: str, path: PathCallable, reply_to_event_id: Optional[str] = None, ) -> None: """Upload and monitor a file + thumbnail and send the built event(s)""" # TODO: this function is way too complex, and most of it should be # refactored into nio. self.transfer_tasks[item_uuid] = utils.current_task() # type: ignore transfer = Transfer(item_uuid, is_upload=True) self.models[room_id, "transfers"][str(item_uuid)] = transfer transaction_id = uuid4() path = Path(await path() if callable(path) else path) encrypt = room_id in self.encrypted_rooms thumb_crypt_dict: Dict[str, Any] = {} crypt_dict: Dict[str, Any] = {} try: size = path.resolve().stat().st_size except (PermissionError, FileNotFoundError): # This error will be caught again by the try block later below size = 0 transfer.set_fields( status=TransferStatus.Transfering, filepath=path, total_size=size, ) monitor = nio.TransferMonitor(size) self.transfer_monitors[item_uuid] = monitor def on_transferred(transferred: int) -> None: transfer.transferred = transferred def on_speed_changed(speed: float) -> None: transfer.set_fields( speed = speed, time_left = monitor.remaining_time or timedelta(0), ) monitor.on_transferred = on_transferred monitor.on_speed_changed = on_speed_changed await self.pause_while_offline() try: url, mime, crypt_dict = await self.upload( lambda *_: path, filename = path.name, filesize = size, encrypt = encrypt, monitor = monitor, ) # FIXME: nio might not catch the cancel in time if monitor.cancel: raise nio.TransferCancelledError() except (MatrixError, OSError) as err: transfer.set_fields( status = TransferStatus.Error, error = type(err), error_args = err.args, ) # Wait for cancellation from UI, see parent send_file() method while True: await asyncio.sleep(0.1) transfer.status = TransferStatus.Caching local_media = await Media.from_existing_file( self.backend.media_cache, self.user_id, url, path, ) kind = (mime or "").split("/")[0] thumb_url: str = "" thumb_info: Optional[MatrixImageInfo] = None content: dict = { f"{__reverse_dns__}.transaction_id": str(transaction_id), "body": path.name, "info": { "mimetype": mime, "size": transfer.total_size, }, } if encrypt: content["file"] = {"url": url, **crypt_dict} else: content["url"] = url if kind == "image": is_svg = mime == "image/svg+xml" event_type = \ nio.RoomEncryptedImage if encrypt else nio.RoomMessageImage content["msgtype"] = "m.image" content["info"]["w"], content["info"]["h"] = ( await utils.svg_dimensions(path) if is_svg else PILImage.open(path).size ) try: thumb_data, thumb_info = await self.generate_thumbnail( path, is_svg=is_svg, ) except UneededThumbnail: pass except Exception: # noqa trace = traceback.format_exc().rstrip() log.warning("Failed thumbnailing %s:\n%s", path, trace) else: thumb_ext = "png" if thumb_info.mime == "image/png" else "jpg" thumb_name = f"{path.stem}_thumbnail.{thumb_ext}" transfer.set_fields( status = TransferStatus.Transfering, filepath = Path(thumb_name), total_size = len(thumb_data), ) try: transfer.total_size = thumb_info.size monitor = nio.TransferMonitor(thumb_info.size) monitor.on_transferred = on_transferred monitor.on_speed_changed = on_speed_changed self.transfer_monitors[item_uuid] = monitor thumb_url, _, thumb_crypt_dict = await self.upload( lambda *_: thumb_data, filename = f"{path.stem}_sample{path.suffix}", filesize = thumb_info.size, encrypt = encrypt, monitor = monitor, ) # FIXME: nio might not catch the cancel in time if monitor.cancel: raise nio.TransferCancelledError() except MatrixError as err: log.warning(f"Failed uploading thumbnail {path}: {err}") else: transfer.status = TransferStatus.Caching await Thumbnail.from_bytes( self.backend.media_cache, self.user_id, thumb_url, path.name, thumb_data, wanted_size = (content["info"]["w"], content["info"]["h"]), ) if encrypt: content["info"]["thumbnail_file"] = { "url": thumb_url, **thumb_crypt_dict, } else: content["info"]["thumbnail_url"] = thumb_url content["info"]["thumbnail_info"] = thumb_info.as_dict() elif kind == "audio": event_type = \ nio.RoomEncryptedAudio if encrypt else nio.RoomMessageAudio content["msgtype"] = "m.audio" content["info"]["duration"] = getattr( MediaInfo.parse(path).tracks[0], "duration", 0, ) or 0 elif kind == "video": event_type = \ nio.RoomEncryptedVideo if encrypt else nio.RoomMessageVideo content["msgtype"] = "m.video" tracks = MediaInfo.parse(path).tracks content["info"]["duration"] = \ getattr(tracks[0], "duration", 0) or 0 content["info"]["w"] = max( getattr(t, "width", 0) or 0 for t in tracks ) content["info"]["h"] = max( getattr(t, "height", 0) or 0 for t in tracks ) else: event_type = \ nio.RoomEncryptedFile if encrypt else nio.RoomMessageFile content["msgtype"] = "m.file" content["filename"] = path.name del self.transfer_monitors[item_uuid] del self.transfer_tasks[item_uuid] del self.models[room_id, "transfers"][str(transfer.id)] if reply_to_event_id: await self.send_text( room_id=room_id, text="", reply_to_event_id=reply_to_event_id, ) await self._local_echo( room_id, transaction_id, event_type, inline_content = content["body"], media_url = url, media_http_url = await self.mxc_to_http(url), media_title = path.name, media_width = content["info"].get("w", 0), media_height = content["info"].get("h", 0), media_duration = content["info"].get("duration", 0), media_size = content["info"]["size"], media_mime = content["info"]["mimetype"], media_crypt_dict = crypt_dict, media_local_path = await local_media.get_local(), thumbnail_url = thumb_url, thumbnail_crypt_dict = thumb_crypt_dict, thumbnail_width = content["info"].get("thumbnail_info", {}).get("w", 0), thumbnail_height = content["info"].get("thumbnail_info", {}).get("h", 0), thumbnail_mime = content["info"].get("thumbnail_info", {}).get("mimetype", ""), ) await self._send_message(room_id, content, transaction_id) async def _local_echo( self, room_id: str, transaction_id: UUID, event_type: Type[nio.Event], **event_fields, ) -> None: """Register a local model `Event` while waiting for the server. When the user sends a message, we want to show instant feedback in the UI timeline without waiting for the servers to receive our message and retransmit it to us. The event will be locally echoed for all our accounts that are members of the `room_id` room. This allows sending messages from other accounts within the same composer without having to go to another page in the UI, and getting direct feedback for these accounts in the timeline. When we do get the real event retransmited by the server, it will replace the local one we registered. """ our_info = self.models["accounts"][self.user_id] content = event_fields.get("content", "").strip() if content and "inline_content" not in event_fields: event_fields["inline_content"] = HTML.filter(content, inline=True) event = Event( id = f"echo-{transaction_id}", event_id = "", event_type = event_type, date = datetime.now(), sender_id = self.user_id, sender_name = our_info.display_name, sender_avatar = our_info.avatar_url, is_local_echo = True, links = Event.parse_links(content), **event_fields, ) for user_id in self.models["accounts"]: if user_id in self.models[self.user_id, room_id, "members"]: key = f"echo-{transaction_id}" self.models[user_id, room_id, "events"][key] = deepcopy(event) await self.set_room_last_event(room_id, event) async def _send_message( self, room_id: str, content: dict, transaction_id: UUID, ) -> None: """Send a message event with `content` dict to a room.""" self.send_message_tasks[transaction_id] = \ utils.current_task() # type: ignore async with self.backend.send_locks[room_id]: await self.room_send( room_id = room_id, message_type = "m.room.message", content = content, ignore_unverified_devices = True, ) async def load_all_room_members(self, room_id: str) -> None: """Request a room's full member list if it hasn't already been loaded. Member lazy-loading is used to accelerate the initial sync with the server. This method will be called from QML to load a room's entire member list when the user is currently viewing the room. """ # Room may be gone by the time this is called due to room_forget() room = self.all_rooms.get(room_id) if room and not room.members_synced: await super().joined_members(room_id) await self.register_nio_room(room, force_register_members=True) async def load_past_events(self, room_id: str) -> bool: """Ask the server for previous events of the room. If it's the first time that the room is being loaded, 10 events will be requested (to give the user something to read quickly), else 100 events will be requested. Events from before the client was started will be requested and registered into our models. Returns whether there are any messages left to load. """ if room_id in self.fully_loaded_rooms or \ room_id in self.invited_rooms or \ room_id in self.cleared_events_rooms or \ self.models[self.user_id, "rooms"][room_id].left: return False await self.first_sync_done.wait() while not self.past_tokens.get(room_id): # If a new room was added, wait for onSyncResponse to set the token await asyncio.sleep(0.1) response = await self.room_messages( room_id = room_id, start = self.past_tokens[room_id], limit = 100 if room_id in self.loaded_once_rooms else 10, message_filter = self.lazy_load_filter, ) self.loaded_once_rooms.add(room_id) more_to_load = True self.past_tokens[room_id] = response.end for event in response.chunk: if isinstance(event, nio.RoomCreateEvent): self.fully_loaded_rooms.add(room_id) more_to_load = False for cb in self.event_callbacks: if (cb.filter is None or isinstance(event, cb.filter)): await cb.func(self.all_rooms[room_id], event) return more_to_load async def new_direct_chat(self, invite: str, encrypt: bool = False) -> str: """Create a room and invite a single user in it for a direct chat.""" if invite == self.user_id: raise InvalidUserInContext(invite) if not self.user_id_regex.match(invite): raise InvalidUserId(invite) # Raise MatrixNotFound if profile doesn't exist await self.get_profile(invite) response = await super().room_create( invite = [invite], is_direct = True, visibility = nio.RoomVisibility.private, initial_state = [nio.EnableEncryptionBuilder().as_dict()] if encrypt else [], ) return response.room_id async def new_group_chat( self, name: Optional[str] = None, topic: Optional[str] = None, public: bool = False, encrypt: bool = False, federate: bool = True, ) -> str: """Create a new matrix room with the purpose of being a group chat.""" response = await super().room_create( name = name or None, topic = topic or None, federate = federate, visibility = nio.RoomVisibility.public if public else nio.RoomVisibility.private, initial_state = [nio.EnableEncryptionBuilder().as_dict()] if encrypt else [], ) return response.room_id async def room_join(self, alias_or_id_or_url: str) -> str: """Join an existing matrix room.""" string = alias_or_id_or_url.strip() if self.http_s_url_regex.match(string): for part in urlparse(string).fragment.split("/"): if self.room_id_or_alias_regex.match(part): string = part break else: raise ValueError(f"No alias or room id found in url {string}") if not self.room_id_or_alias_regex.match(string): raise ValueError("Not an alias or room id") response = await super().join(string) return response.room_id async def toggle_room_pin(self, room_id: str) -> None: room = self.models[self.user_id, "rooms"][room_id] room.pinned = not room.pinned settings = self.backend.settings pinned = settings.RoomList.Pinned user_pinned = pinned.setdefault(self.user_id, []) if room.pinned and room_id not in user_pinned: user_pinned.append(room_id) while not room.pinned and room_id in user_pinned: user_pinned.remove(room_id) # Changes inside dicts/lists aren't monitored, need to reassign settings.RoomList.Pinned[self.user_id] = user_pinned self.backend.settings.save() async def room_forget(self, room_id: str) -> None: """Leave a joined room (or decline an invite) and forget its history. If all the members of a room leave and forget it, that room will be marked as suitable for destruction by the server. """ self.ignored_rooms.add(room_id) self.models[self.user_id, "rooms"].pop(room_id, None) self.models.pop((self.user_id, room_id, "events"), None) self.models.pop((self.user_id, room_id, "members"), None) await self.update_account_unread_counts() try: await super().room_leave(room_id) except MatrixError as e: # room was already left if e.http_code != 404: raise await super().room_forget(room_id) async def room_mass_invite( self, room_id: str, *user_ids: str, ) -> Tuple[List[str], List[Tuple[str, Exception]]]: """Invite users to a room in parallel. Returns a tuple with: - A list of users we successfully invited - A list of `(user_id, Exception)` tuples for those failed to invite. """ user_ids = tuple( uid for uid in user_ids # Server would return a 403 forbidden for users already in the room if uid not in self.all_rooms[room_id].users ) async def invite(user_id: str): if not self.user_id_regex.match(user_id): return InvalidUserId(user_id) if not self.rooms[room_id].federate: _, user_server = user_id.split(":", maxsplit=1) _, room_server = room_id.split(":", maxsplit=1) user_server = re.sub(r":443$", "", user_server) room_server = re.sub(r":443$", "", room_server) if user_server != room_server: return UserFromOtherServerDisallowed(user_id) try: await self.get_profile(user_id) except (MatrixNotFound, MatrixBadGateway) as err: return err return await self.room_invite(room_id, user_id) coros = [invite(uid) for uid in user_ids] successes = [] errors: list = [] responses = await asyncio.gather(*coros) for user_id, response in zip(user_ids, responses): if isinstance(response, nio.RoomInviteError): errors.append((user_id, await MatrixError.from_nio(response))) elif isinstance(response, Exception): errors.append((user_id, response)) else: successes.append(user_id) return (successes, errors) async def room_put_state_builder( self, room_id: str, builder: nio.EventBuilder, ) -> str: """Send state event to room based from a `nio.EventBuilder` object.""" dct = builder.as_dict() response = await self.room_put_state( room_id = room_id, event_type = dct["type"], content = dct["content"], state_key = dct["state_key"], ) return response.event_id async def room_set( self, room_id: str, name: Optional[str] = None, topic: Optional[str] = None, encrypt: Optional[bool] = None, require_invite: Optional[bool] = None, forbid_guests: Optional[bool] = None, ) -> None: """Send setting state events for arguments that aren't `None`.""" builders: List[nio.EventBuilder] = [] if name is not None: builders.append(nio.ChangeNameBuilder(name=name)) if topic is not None: builders.append(nio.ChangeTopicBuilder(topic=topic)) if encrypt is False: raise ValueError("Cannot disable encryption in a E2E room") if encrypt is True: builders.append(nio.EnableEncryptionBuilder()) if require_invite is not None: builders.append(nio.ChangeJoinRulesBuilder( rule="invite" if require_invite else "public", )) if forbid_guests is not None: builders.append(nio.ChangeGuestAccessBuilder( access = "forbidden" if forbid_guests else "can_join", )) await asyncio.gather(*[ self.room_put_state_builder(room_id, b) for b in builders ]) async def room_set_member_power( self, room_id: str, user_id: str, level: int, ) -> None: """Set a room member's power level.""" while room_id not in self.power_level_events: await asyncio.sleep(0.2) content = deepcopy(self.power_level_events[room_id].source["content"]) content.setdefault("users", {})[user_id] = level await self.room_put_state(room_id, "m.room.power_levels", content) async def room_typing( self, room_id: str, typing_state: bool = True, timeout: int = 5000, ): """Set typing notice to the server.""" if not utils.config_get_account_room_rule( rules = self.backend.settings.Chat.Composer.TypingNotifications, user_id = self.user_id, room_id = room_id, ): return if self.models["accounts"][self.user_id].presence not in [ Presence.State.invisible, Presence.State.offline, ]: await super().room_typing(room_id, typing_state, timeout) async def get_redacted_event_content( self, nio_type: Type[nio.Event], redacter: str, sender: str, reason: str = "", ) -> str: """Get content to be displayed in place of a redacted event.""" content = "%1 removed this message" if redacter == sender else \ "%1's message was removed by %2" if reason: content = f"{content}, reason: {reason}" return content async def room_mass_redact( self, room_id: str, reason: str, *event_client_ids: str, ) -> List[nio.RoomRedactResponse]: """Redact events from a room in parallel.""" tasks = [] for user_id in self.backend.clients: for client_id in event_client_ids: event = self.models[user_id, room_id, "events"].get(client_id) if not event: continue if event.is_local_echo: if user_id == self.user_id: uuid = UUID(event.id.replace("echo-", "")) self.send_message_tasks[uuid].cancel() event.is_local_echo = False else: if user_id == self.user_id: tasks.append( self.room_redact(room_id, event.event_id, reason), ) event.is_local_echo = True event.set_fields( content = await self.get_redacted_event_content( event.event_type, self.user_id, event.sender_id, reason, ), event_type = nio.RedactedEvent, mentions = [], type_specifier = TypeSpecifier.Unset, media_url = "", media_http_url = "", media_title = "", media_local_path = "", thumbnail_url = "", ) await self.pause_while_offline() return await asyncio.gather(*tasks) async def generate_thumbnail( self, data: UploadData, is_svg: bool = False, ) -> Tuple[bytes, MatrixImageInfo]: """Create a thumbnail from an image, return the bytes and info.""" png_modes = ("1", "L", "P", "RGBA") data = b"".join([c async for c in async_generator_from_data(data)]) is_svg = await utils.guess_mime(data) == "image/svg+xml" if is_svg: svg_width, svg_height = await utils.svg_dimensions(data) data = cairosvg.svg2png( bytestring = data, parent_width = svg_width, parent_height = svg_height, ) thumb = PILImage.open(io.BytesIO(data)) small = thumb.width <= 800 and thumb.height <= 600 is_jpg_png = thumb.format in ("JPEG", "PNG") jpgable_png = thumb.format == "PNG" and thumb.mode not in png_modes if small and is_jpg_png and not jpgable_png and not is_svg: raise UneededThumbnail() if not small: thumb.thumbnail((800, 600)) if thumb.mode in png_modes: thumb_data = await utils.compress_image(thumb) mime = "image/png" else: thumb = thumb.convert("RGB") thumb_data = await utils.compress_image(thumb, "JPEG") mime = "image/jpeg" thumb_size = len(thumb_data) if thumb_size >= len(data) and is_jpg_png and not is_svg: raise UneededThumbnail() info = MatrixImageInfo(thumb.width, thumb.height, mime, thumb_size) return (thumb_data, info) async def upload( self, data_provider: nio.DataProvider, filename: Optional[str] = None, filesize: Optional[int] = None, mime: Optional[str] = None, encrypt: bool = False, monitor: Optional[nio.TransferMonitor] = None, ) -> UploadReturn: """Upload a file to the matrix homeserver.""" max_size = self.models["accounts"][self.user_id].max_upload_size if max_size and filesize > max_size: raise MatrixTooLarge() mime = mime or await utils.guess_mime(data_provider(0, 0)) response, decryption_dict = await super().upload( data_provider = data_provider, content_type = "application/octet-stream" if encrypt else mime, filename = filename, encrypt = encrypt, monitor = monitor, filesize = filesize, ) return UploadReturn(response.content_uri, mime, decryption_dict or {}) async def set_avatar_from_file(self, path: Union[Path, str]) -> None: """Upload an image to the homeserver and set it as our avatar.""" path = Path(path) mime = await utils.guess_mime(path) if mime.split("/")[0] != "image": raise BadMimeType(wanted="image/*", got=mime) mxc, *_ = await self.upload( data_provider = lambda *_: path, filename = path.name, filesize = path.resolve().stat().st_size, mime = mime, ) await self.set_avatar(mxc) async def get_offline_presence(self, user_id: str) -> None: """Get a offline room member's presence and set it on model item. This is called by QML when a member list delegate or profile that is offline is displayed. Since we don't get last seen times for offline in users in syncs, we have to fetch those manually. """ if self.backend.presences.get(user_id): return if not self.models["accounts"][self.user_id].presence_support: return try: async with self.backend.concurrent_get_presence_limit: resp = await self.get_presence(user_id) except (MatrixForbidden, MatrixUnrecognized): return await self.nio_callbacks.onPresenceEvent(nio.PresenceEvent( user_id = resp.user_id, presence = resp.presence, last_active_ago = resp.last_active_ago, currently_active = resp.currently_active, status_msg = resp.status_msg, )) async def set_presence( self, presence: str, status_msg: Optional[str] = None, save: bool = True, ) -> None: """Set presence state for this account.""" account = self.models["accounts"][self.user_id] call_presence_api = True self._presence = "offline" if presence == "invisible" else presence if status_msg is None: status_msg = account.status_msg # Starting/stopping client if current/new presence is offline if presence == "offline": if account.presence == Presence.State.offline: return await self._stop() # We stop syncing, so update the account manually account.set_fields( presence = Presence.State.offline, status_msg = "", currently_active = False, ) elif account.presence == Presence.State.offline: # We might receive a recent status_msg set from another client on # startup, so don't try to set a new one immediatly. # Presence though will be sent on first sync. call_presence_api = False account.connecting = True self.start_task = asyncio.ensure_future(self._start()) # Update our account model item's presence if ( Presence.State(presence) != account.presence and presence != "offline" ): account.presence = Presence.State(presence) # Saving new details in accounts.json if save: account.save_presence = True await self.backend.saved_accounts.set( self.user_id, presence=presence, status_msg=status_msg, ) else: account.save_presence = False # Update our presence/status on the server if call_presence_api: account.status_msg = status_msg await super().set_presence(self._presence, status_msg) async def import_keys(self, infile: str, passphrase: str) -> None: """Import decryption keys from a file, then retry decrypting events.""" await super().import_keys(infile, passphrase) await self.retry_decrypting_events() async def export_keys(self, outfile: str, passphrase: str) -> None: """Export our decryption keys to a file.""" path = Path(outfile) path.parent.mkdir(parents=True, exist_ok=True) # The QML dialog asks the user if he wants to overwrite before this if path.exists(): path.unlink() await super().export_keys(outfile, passphrase) async def retry_decrypting_events(self) -> None: """Retry decrypting room `Event`s in our model we failed to decrypt.""" for sync_id, model in self.models.items(): if not (isinstance(sync_id, tuple) and len(sync_id) == 3 and sync_id[0] == self.user_id and sync_id[2] == "events"): continue _, room_id, _ = sync_id with model.write_lock: for ev in model.values(): room = self.all_rooms[room_id] if isinstance(ev.source, nio.MegolmEvent): try: decrypted = self.decrypt_event(ev.source) if not decrypted: raise nio.EncryptionError() except nio.EncryptionError: continue for callback in self.event_callbacks: filter_ = callback.filter if not filter_ or isinstance(decrypted, filter_): coro = asyncio.coroutine(callback.func) await coro(room, decrypted) async def clear_events(self, room_id: str) -> None: """Remove every `Event` of a room we registered in our model. The events will be gone from the UI, until the client is restarted. """ self.cleared_events_rooms.add(room_id) model = self.models[self.user_id, room_id, "events"] if model: model.clear() self.models[self.user_id, "rooms"][room_id].last_event_date = \ ZERO_DATE async def devices_info(self) -> List[Dict[str, Any]]: """Get sorted list of devices and their info for our user.""" def get_type(device_id: str) -> str: # Return "current", "no_keys", "verified", "blacklisted", # "ignored" or "unset" if device_id == self.device_id: return "current" if device_id not in self.device_store[self.user_id]: return "no_keys" trust = self.device_store[self.user_id][device_id].trust_state return trust.name def get_ed25519(device_id: str) -> str: key = "" if device_id == self.device_id: key = self.olm.account.identity_keys["ed25519"] elif device_id in self.device_store[self.user_id]: key = self.device_store[self.user_id][device_id].ed25519 return " ".join(textwrap.wrap(key, 4)) devices = [ { "id": device.id, "display_name": device.display_name or "", "last_seen_ip": (device.last_seen_ip or "").strip(" -"), "last_seen_date": device.last_seen_date or ZERO_DATE, "last_seen_country": "", "type": get_type(device.id), "ed25519_key": get_ed25519(device.id), } for device in (await self.devices()).devices ] # Reversed due to sorted(reverse=True) call below types_order = { "current": 5, "unset": 4, "no_keys": 3, "verified": 2, "ignored": 1, "blacklisted": 0, } # Sort by type, then by descending date return sorted( devices, key = lambda d: (types_order[d["type"]], d["last_seen_date"]), reverse = True, ) async def member_devices(self, user_id: str) -> List[Dict[str, Any]]: """Get list of E2E-aware devices for a user we share a room with.""" devices = [ # types: "verified", "blacklisted", "ignored" or "unset" { "id": device.id, "display_name": device.display_name or "", "type": device.trust_state.name, "ed25519_key": " ".join(textwrap.wrap(device.ed25519, 4)), } for device in self.device_store.active_user_devices(user_id) ] types_order = { "unset": 0, "verified": 1, "ignored": 2, "blacklisted": 3, } # Sort by type, then by display name, then by ID return sorted( devices, key = lambda d: (types_order[d["type"]], d["display_name"], d["id"]), ) async def rename_device(self, device_id: str, name: str) -> bool: """Rename one of our device, return `False` if it doesn't exist.""" try: await self.update_device(device_id, {"display_name": name}) return True except MatrixNotFound: return False async def auto_verify_all_other_accounts(self) -> None: """Automatically verify/blacklist our other accounts's devices.""" for client in self.backend.clients.values(): await self.auto_verify_account(client) async def auto_verify_account(self, client: "MatrixClient") -> None: """Automatically verify/blacklist one of our accounts's devices.""" if client.user_id == self.user_id: return for device in self.device_store.active_user_devices(client.user_id): if device.device_id != client.device_id: continue if device.verified or device.blacklisted: continue if device.ed25519 == client.olm.account.identity_keys["ed25519"]: self.verify_device(device) else: self.blacklist_device(device) async def delete_devices_with_password( self, device_ids: List[str], password: str, ) -> None: """Delete devices, authentifying using the account's password.""" auth = { "type": "m.login.password", "user": self.user_id, "password": password, } resp = await super().delete_devices(device_ids, auth) if isinstance(resp, nio.DeleteDevicesAuthResponse): raise MatrixUnauthorized() async def edit_pushrule( self, kind: Union[nio.PushRuleKind, str], rule_id: str, old_kind: Union[None, nio.PushRuleKind, str] = None, old_rule_id: Optional[str] = None, move_before_rule_id: Optional[str] = None, move_after_rule_id: Optional[str] = None, enable: Optional[bool] = None, conditions: Optional[List[PushCondition]] = None, pattern: Optional[str] = None, actions: Optional[List[PushAction]] = None, ) -> None: """Create or edit an existing non-builtin pushrule. For builtin server ("default") rules, only actions can be edited. """ # Convert arguments that were passed as basic types (usually from QML) if isinstance(old_kind, str): old_kind = nio.PushRuleKind[old_kind] kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind conditions = [ nio.PushCondition.from_dict(c) if isinstance(c, dict) else c for c in conditions ] if isinstance(conditions, list) else None actions = [ nio.PushAction.from_dict(a) if isinstance(a, (str, dict)) else a for a in actions ] if isinstance(actions, list) else None # Now edit the rule old: Optional[PushRule] = None key = (old_kind.value if old_kind else None, old_rule_id) if None not in key: old = self.models[self.user_id, "pushrules"].get(key) kind_change = old and old_kind and old_kind != kind rule_id_change = old and old_rule_id and old_rule_id != rule_id explicit_move = move_before_rule_id or move_after_rule_id if old and not kind_change and not explicit_move: # If user edits a rule without specifying a new position, # the server would move it to the first position move_after_rule_id = old.rule_id if old and actions is None: # Matrix API forces us to always pass a non-null actions paramater actions = [nio.PushAction.from_dict(a) for a in old.actions] if old and old.default: await self.set_pushrule_actions("global", kind, rule_id, actions) else: await self.set_pushrule( scope = "global", kind = kind, rule_id = rule_id, before = move_before_rule_id, after = move_after_rule_id, actions = actions or [], conditions = conditions, pattern = pattern, ) # If we're editing an existing rule but its kind or ID is changed, # set_pushrule creates a new rule, thus we must delete the old one if kind_change or rule_id_change: await self.delete_pushrule("global", old_kind, old_rule_id) if enable is not None and (old.enabled if old else True) != enable: await self.enable_pushrule("global", kind, rule_id, enable) elif kind_change or rule_id_change and old and not old.enabled: await self.enable_pushrule("global", kind, rule_id, False) async def tweak_pushrule_actions( self, kind: Union[nio.PushRuleKind, str], rule_id: str, notify: Optional[bool] = None, highlight: Optional[bool] = None, bubble: Optional[bool] = None, sound: Optional[str] = None, urgency_hint: Optional[bool] = None, ) -> None: """Edit individual actions for any existing push rule.""" kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind current: PushRule = \ self.models[self.user_id, "pushrules"][kind.value, rule_id] actions: List[nio.PushAction] = [] if notify or (notify is None and current.notify): actions.append(nio.PushNotify()) if highlight or (highlight is None and current.highlight): actions.append(nio.PushSetTweak("highlight", True)) if bubble or (bubble is None and current.bubble): actions.append(nio.PushSetTweak("bubble", True)) elif bubble is False or (bubble is None and not current.bubble): actions.append(nio.PushSetTweak("bubble", False)) if sound or (sound is None and current.sound): actions.append(nio.PushSetTweak("sound", sound)) hint = urgency_hint if hint or (hint is None and current.urgency_hint): actions.append(nio.PushSetTweak("urgency_hint", True)) elif hint is False or (hint is None and not current.urgency_hint): actions.append(nio.PushSetTweak("urgency_hint", False)) await self.set_pushrule_actions("global", kind, rule_id, actions) async def mass_tweak_pushrules_actions(self, *tweaks_kwargs) -> None: coros = [self.tweak_pushrule_actions(**kws) for kws in tweaks_kwargs] await asyncio.gather(*coros) async def remove_pushrule( self, kind: Union[str, nio.PushRuleKind], rule_id: str, ) -> None: """Remove an existing non-builtin pushrule.""" kind = nio.PushRuleKind[kind] if isinstance(kind, str) else kind if (kind.value, rule_id) in self.models[self.user_id, "pushrules"]: await self.delete_pushrule("global", kind, rule_id) def _rule_overrides_room(self, rule: PushRule) -> Optional[str]: override = rule.kind is nio.PushRuleKind.override one_cnd = len(rule.conditions) == 1 if not one_cnd: return None cnd = nio.PushCondition.from_dict(rule.conditions[0]) ev_match = isinstance(cnd, nio.PushEventMatch) if override and ev_match and cnd.key == "room_id": return cnd.pattern return None async def _remove_room_override_rule(self, room_id: str) -> None: for rule in self.models[self.user_id, "pushrules"].values(): if self._rule_overrides_room(rule) == room_id: await self.remove_pushrule(rule.kind, rule.rule_id) async def room_pushrule_use_default(self, room_id: str) -> None: await self._remove_room_override_rule(room_id) await self.remove_pushrule(nio.PushRuleKind.room, room_id) async def room_pushrule_all_events(self, room_id: str) -> None: await self._remove_room_override_rule(room_id) await self.edit_pushrule( kind = nio.PushRuleKind.room, rule_id = room_id, actions = [nio.PushNotify(), nio.PushSetTweak("sound", "default")], ) async def room_pushrule_highlights_only(self, room_id: str) -> None: await self._remove_room_override_rule(room_id) await self.edit_pushrule(nio.PushRuleKind.room, room_id, actions=[]) async def room_pushrule_ignore_all(self, room_id: str) -> None: await self._remove_room_override_rule(room_id) await self.remove_pushrule(nio.PushRuleKind.room, room_id) cnd = nio.PushEventMatch("room_id", room_id) await self.edit_pushrule( nio.PushRuleKind.override, room_id, conditions=[cnd], actions=[], ) # Functions to register/modify data into models async def update_account_unread_counts(self) -> None: """Recalculate total unread notifications/highlights for our account""" unreads = 0 highlights = 0 local_unreads = False for room in self.models[self.user_id, "rooms"].values(): unreads += room.unreads highlights += room.highlights if room.local_unreads: local_unreads = True account = self.models["accounts"][self.user_id] account.set_fields( total_unread = unreads, total_highlights = highlights, local_unreads = local_unreads, ) async def event_is_past(self, ev: Union[nio.Event, Event]) -> bool: """Return whether an event was created before this client started.""" if not self.first_sync_date: return True if isinstance(ev, Event): return ev.date < self.first_sync_date date = datetime.fromtimestamp(ev.server_timestamp / 1000) return date < self.first_sync_date async def set_room_last_event(self, room_id: str, item: Event) -> None: """Set the `last_event` for a `Room` using data in our `Event` model. The `last_event` is notably displayed in the UI room subtitles. """ room = self.models[self.user_id, "rooms"][room_id] if item.date > room.last_event_date: room.last_event_date = item.date async def lock_room_position(self, room_id: str, lock: bool) -> None: """Set wheter a room should try to hold its current sort position.""" room = self.models[self.user_id, "rooms"].get(room_id) if not room: return if not lock: room._sort_overrides = {} return for k in ("last_event_date", "unreads", "highlights", "local_unreads"): room._sort_overrides[k] = getattr(room, k) room.notify_change("_sort_overrides") async def register_nio_room( self, room: nio.MatrixRoom, left: bool = False, force_register_members: bool = False, ) -> None: """Register/update a `nio.MatrixRoom` as a `models.items.Room`.""" self.ignored_rooms.discard(room.room_id) inviter = getattr(room, "inviter", "") or "" levels = room.power_levels can_send_state = partial(levels.can_user_send_state, self.user_id) can_send_msg = partial(levels.can_user_send_message, self.user_id) try: registered = self.models[self.user_id, "rooms"][room.room_id] except KeyError: registered = None sort_overrides = {} last_event_date = datetime.fromtimestamp(0) typing_members = [] local_unreads = False update_account_unread_counts = True unverified_devices = ( False if isinstance(room, nio.MatrixInvitedRoom) else self.room_contains_unverified(room.room_id) ) else: sort_overrides = registered._sort_overrides last_event_date = registered.last_event_date typing_members = registered.typing_members local_unreads = registered.local_unreads update_account_unread_counts = ( registered.unreads != room.unread_notifications or registered.highlights != room.unread_highlights ) unverified_devices = registered.unverified_devices notification_setting = RoomNotificationOverride.UseDefaultSettings for rule in self.models[self.user_id, "pushrules"].values(): overrides = self._rule_overrides_room(rule) == room.room_id is_room_kind = rule.kind is nio.PushRuleKind.room room_kind_match = is_room_kind and rule.rule_id == room.room_id if overrides and not rule.actions: notification_setting = RoomNotificationOverride.IgnoreEvents break elif overrides: notification_setting = RoomNotificationOverride.AllEvents break elif room_kind_match and not rule.actions: notification_setting = RoomNotificationOverride.HighlightsOnly break elif room_kind_match: notification_setting = RoomNotificationOverride.AllEvents break pinned = self.backend.settings.RoomList.Pinned room_item = Room( id = room.room_id, for_account = self.user_id, given_name = room.name or "", display_name = room.display_name or "", avatar_url = room.gen_avatar_url or "", plain_topic = room.topic or "", topic = HTML.filter( utils.plain2html(room.topic or ""), inline = True, ), inviter_id = inviter, inviter_name = room.user_name(inviter) if inviter else "", inviter_avatar = (room.avatar_url(inviter) or "") if inviter else "", left = left, typing_members = typing_members, encrypted = room.encrypted, unverified_devices = unverified_devices, invite_required = room.join_rule == "invite", guests_allowed = room.guest_access == "can_join", default_power_level = levels.defaults.users_default, own_power_level = levels.get_user_level(self.user_id), can_invite = levels.can_user_invite(self.user_id), can_kick = levels.can_user_kick(self.user_id), can_redact_all = levels.can_user_redact(self.user_id), can_send_messages = can_send_msg(), can_set_name = can_send_state("m.room.name"), can_set_topic = can_send_state("m.room.topic"), can_set_avatar = can_send_state("m.room.avatar"), can_set_encryption = can_send_state("m.room.encryption"), can_set_join_rules = can_send_state("m.room.join_rules"), can_set_guest_access = can_send_state("m.room.guest_access"), can_set_power_levels = can_send_state("m.room.power_levels"), last_event_date = last_event_date, unreads = room.unread_notifications, highlights = room.unread_highlights, local_unreads = local_unreads, notification_setting = notification_setting, lexical_sorting = self.backend.settings.RoomList.lexical_sort, pinned = room.room_id in pinned.get(self.user_id, []), _sort_overrides = sort_overrides, ) self.models[self.user_id, "rooms"][room.room_id] = room_item if not registered or force_register_members: model = self.models[self.user_id, room.room_id, "members"] # The members we initially get from lazy sync may be outdated # and contain members that already left. # tuple() used to avoid "dict changed size during iteration". for member_id in tuple(model): if member_id not in room.users: await self.remove_member(room, member_id) for user_id in room.users: await self.add_member(room, user_id) if update_account_unread_counts: await self.update_account_unread_counts() async def add_member(self, room: nio.MatrixRoom, user_id: str) -> None: """Register/update a room member into our models.""" room_id = room.room_id member_model = self.models[self.user_id, room_id, "members"] member = room.users[user_id] presence = self.backend.presences.get(user_id, None) try: registered = member_model[user_id] except KeyError: last_read_event = self.unassigned_member_last_read_event\ .pop((room_id, user_id), "") else: last_read_event = registered.last_read_event member_item = Member( id = user_id, display_name = room.user_name(user_id) # disambiguated if member.display_name else "", avatar_url = member.avatar_url or "", typing = user_id in room.typing_users, ignored = user_id in self.ignored_user_ids, power_level = member.power_level, invited = member.invited, last_read_event = last_read_event, ) # Associate presence with member, if it exists if presence: presence.members[room_id] = member_item # And then update presence fields presence.update_members() member_model[user_id] = member_item async def remove_member(self, room: nio.MatrixRoom, user_id: str) -> None: """Remove a room member from our models.""" self.models[self.user_id, room.room_id, "members"].pop(user_id, None) room_item = self.models[self.user_id, "rooms"].get(room.room_id) if room_item: room_item.unverified_devices = \ self.room_contains_unverified(room.room_id) async def get_event_profiles(self, room_id: str, event_id: str) -> None: """Fetch from network an event's sender, target and remover's profile. This should be called from QML, see `MatrixClient.get_member_profile`'s docstring. """ ev: Event = self.models[self.user_id, room_id, "events"][event_id] if not ev.fetch_profile: return get_profile = partial( self.get_member_profile, room_id, can_fetch_from_network=True, ) if not ev.sender_name and not ev.sender_avatar: sender_name, sender_avatar, _ = await get_profile(ev.sender_id) ev.set_fields(sender_name=sender_name, sender_avatar=sender_avatar) if ev.target_id and not ev.target_name and not ev.target_avatar: target_name, target_avatar, _ = await get_profile(ev.target_id) ev.set_fields(target_name=target_name, target_avatar=target_avatar) if ev.redacter_id and not ev.redacter_name: redacter_name, _, _ = await get_profile(ev.target_id) ev.redacter_name = redacter_name ev.fetch_profile = False async def get_member_profile( self, room_id: str, user_id: str, can_fetch_from_network: bool = False, ) -> Tuple[str, str, bool]: """Return a room member's (display_name, avatar, should_lazy_fetch) The returned tuple's last element tells whether `MatrixClient.get_event_profiles()` should be called by QML with `can_fetch_from_network = True` when appropriate, e.g. when this message comes in the user's view. If the member isn't found in the room (e.g. they left) and `can_fetch_from_network` is `True`, their profile is retrieved using `MatrixClient.get_profile()`. """ try: member = self.models[self.user_id, room_id, "members"][user_id] return (member.display_name, member.avatar_url, False) except KeyError: # e.g. member is not in the room anymore if not can_fetch_from_network: return ("", "", True) try: info = await self.get_profile(user_id) return (info.displayname or "", info.avatar_url or "", False) except MatrixError: return ("", "", False) async def get_notification_avatar(self, mxc: str, user_id: str) -> Path: """Get the path to an avatar for desktop notifications.""" # TODO: test this function on windows if mxc in self.backend.notification_avatar_cache: return self.backend.notification_avatar_cache[mxc] avatar_size = (48, 48) avatar_path = await Thumbnail( cache = self.backend.media_cache, client_user_id = self.user_id, mxc = mxc, title = f"user_{user_id}.notification", wanted_size = avatar_size, ).get() image_data = None create = False async with utils.aiopen(avatar_path, "rb") as file: if await utils.is_svg(file): await file.seek(0, 0) create = True image_data = cairosvg.svg2png( bytestring = await file.read(), parent_width = avatar_size[0], parent_height = avatar_size[1], ) else: await file.seek(0, 0) image_data = await file.read() pil_image = PILImage.open(io.BytesIO(image_data)) if pil_image.size != avatar_size: create = True pil_image.thumbnail(avatar_size) if IS_WINDOWS and pil_image.format != "ICO": create = True if not create: self.backend.notification_avatar_cache[mxc] = avatar_path return avatar_path out = io.BytesIO() if IS_WINDOWS: pil_image.save(out, "ICO", sizes=[avatar_size]) else: pil_image.save(out, "PNG") thumb = await Thumbnail.from_bytes( cache = self.backend.media_cache, client_user_id = self.user_id, mxc = mxc, filename = f"user_{user_id}.notification", overwrite = True, data = out.getvalue(), wanted_size = avatar_size, ) path = await thumb.get() self.backend.notification_avatar_cache[mxc] = path return path async def register_nio_event( self, room: nio.MatrixRoom, ev: Union[nio.Event, nio.BadEvent], event_id: str = "", override_fetch_profile: Optional[bool] = None, **fields, ) -> Event: """Register/update a `nio.Event` as a `models.items.Event` object.""" await self.register_nio_room(room) sender_name, sender_avatar, must_fetch_sender = \ await self.get_member_profile(room.room_id, ev.sender) target_id = getattr(ev, "state_key", "") or "" target_name, target_avatar, must_fetch_target = \ await self.get_member_profile(room.room_id, target_id) \ if target_id else ("", "", False) content = fields.get("content", "").strip() if content and "inline_content" not in fields: fields["inline_content"] = HTML.filter(content, inline=True) event_model = self.models[self.user_id, room.room_id, "events"] try: registered = event_model[event_id or ev.event_id] except KeyError: last_read_by = self.unassigned_event_last_read_by.pop( event_id or ev.event_id, {}, ) else: last_read_by = registered.last_read_by # Create Event ModelItem item = Event( id = event_id or ev.event_id, event_id = ev.event_id, event_type = type(ev), source = ev, date = datetime.fromtimestamp(ev.server_timestamp / 1000), sender_id = ev.sender, sender_name = sender_name, sender_avatar = sender_avatar, target_id = target_id, target_name = target_name, target_avatar = target_avatar, links = Event.parse_links(content), last_read_by = last_read_by, read_by_count = len(last_read_by), fetch_profile = (must_fetch_sender or must_fetch_target) if override_fetch_profile is None else override_fetch_profile, **fields, ) # Add the Event to model model = self.models[self.user_id, room.room_id, "events"] tx_id = ev.source.get("content", {}).get( f"{__reverse_dns__}.transaction_id", ) from_us = ev.sender in self.backend.clients if from_us and tx_id and f"echo-{tx_id}" in model: item.id = f"echo-{tx_id}" self.event_to_echo_ids[ev.event_id] = item.id model[item.id] = item await self.set_room_last_event(room.room_id, item) if from_us: return item if await self.event_is_past(ev): await self.update_account_unread_counts() return item if self.backend.settings.RoomList.local_unread_markers: room_item = self.models[self.user_id, "rooms"][room.room_id] room_item.local_unreads = True await self.update_account_unread_counts() # Alerts & notifications name = self.models["accounts"][self.user_id].display_name nio_rule = self.push_rules.global_rules.matching_rule(ev, room, name) if not nio_rule: return item model = self.models[self.user_id, "pushrules"] rule = model[nio_rule.kind.value, nio_rule.id] if not rule.notify and not rule.highlight: return item if must_fetch_sender: sender_name, sender_avatar, _ = await self.get_member_profile( room.room_id, ev.sender, can_fetch_from_network=True, ) item.set_fields( sender_name=sender_name, sender_avatar=sender_avatar, ) sender = item.sender_name or item.sender_id is_linux = platform.system() == "Linux" use_html = is_linux and self.backend.settings.Notifications.use_html content = item.inline_content if use_html else item.plain_content if isinstance(ev, nio.RoomMessageEmote) and use_html: body = f"{sender} {content}" elif isinstance(ev, nio.RoomMessageEmote): body = f"{sender} {content}" elif not isinstance(ev, nio.RoomMessage): body = content.replace( "%1", item.sender_name or item.sender_id, ).replace( "%2", item.target_name or item.target_id, ) elif room.member_count == 2 and room.display_name == sender: body = content else: body = f"{sender}: {content}" NotificationRequested( id = item.id, critical = rule.highlight, bubble = rule.bubble, sound = rule.sound, urgency_hint = rule.urgency_hint, title = room.display_name, body = body.replace(" ⏎ ", "
") .replace(" ⏎⏎ ", f"
{'─' * 24}
"), image = await self.get_notification_avatar( mxc=item.sender_avatar, user_id=item.sender_id, ) if item.sender_avatar else "", ) return item mirage-0.7.2/src/backend/media_cache.py000066400000000000000000000270761407747233600177470ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Matrix media downloading, caching and retrieval.""" import asyncio import functools import io import re import shutil import sys from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, DefaultDict, Dict, Optional from urllib.parse import urlparse from uuid import uuid4 import nio from PIL import Image as PILImage from .models.items import Transfer, TransferStatus from .models.model import Model from .utils import Size, atomic_write, current_task if TYPE_CHECKING: from .backend import Backend if sys.version_info < (3, 8): import pyfastcopy # noqa CONCURRENT_DOWNLOADS_LIMIT = asyncio.BoundedSemaphore(8) ACCESS_LOCKS: DefaultDict[str, asyncio.Lock] = DefaultDict(asyncio.Lock) @dataclass class MediaCache: """Matrix downloaded media cache.""" backend: "Backend" = field() base_dir: Path = field() def __post_init__(self) -> None: self.thumbs_dir = self.base_dir / "thumbnails" self.downloads_dir = self.base_dir / "downloads" self.thumbs_dir.mkdir(parents=True, exist_ok=True) self.downloads_dir.mkdir(parents=True, exist_ok=True) async def get_media(self, *args) -> Path: """Return `Media(self, ...).get()`'s result. Intended for QML.""" return await Media(self, *args).get() async def get_thumbnail(self, width: float, height: float, *args) -> Path: """Return `Thumbnail(self, ...).get()`'s result. Intended for QML.""" # QML sometimes pass float sizes, which matrix API doesn't like. size = (round(width), round(height)) return await Thumbnail( self, *args, wanted_size=size, # type: ignore ).get() @dataclass class Media: """A matrix media file that is downloaded or has yet to be. If the `room_id` is not set, no `Transfer` model item will be registered while this media is being downloaded. """ cache: "MediaCache" = field() client_user_id: str = field() mxc: str = field() title: str = field() room_id: Optional[str] = None filesize: Optional[int] = None crypt_dict: Optional[Dict[str, Any]] = field(default=None, repr=False) def __post_init__(self) -> None: self.mxc = re.sub(r"#auto$", "", self.mxc) if not re.match(r"^mxc://.+/.+", self.mxc): raise ValueError(f"Invalid mxc URI: {self.mxc}") @property def local_path(self) -> Path: """The path where the file either exists or should be downloaded. The returned paths are in this form: ``` // _.` ``` e.g. `~/.cache/mirage/downloads/matrix.org/foo_Hm24ar11i768b0el.png`. """ parsed = urlparse(self.mxc) mxc_id = parsed.path.lstrip("/") title = Path(self.title) filename = f"{title.stem}_{mxc_id}{title.suffix}" return self.cache.downloads_dir / parsed.netloc / filename async def get(self) -> Path: """Return the cached file's path, downloading it first if needed.""" async with ACCESS_LOCKS[self.mxc]: try: return await self.get_local() except FileNotFoundError: return await self.create() async def get_local(self) -> Path: """Return a cached local existing path for this media or raise.""" if not self.local_path.exists(): raise FileNotFoundError() return self.local_path async def create(self) -> Path: """Download and cache the media file to disk.""" async with CONCURRENT_DOWNLOADS_LIMIT: data = await self._get_remote_data() self.local_path.parent.mkdir(parents=True, exist_ok=True) async with atomic_write(self.local_path, binary=True) as (file, done): await file.write(data) done() if type(self) is Media: for event in self.cache.backend.mxc_events[self.mxc]: event.media_local_path = self.local_path return self.local_path async def _get_remote_data(self) -> bytes: """Return the file's data from the matrix server, decrypt if needed.""" client = self.cache.backend.clients[self.client_user_id] transfer: Optional[Transfer] = None model: Optional[Model] = None if self.room_id: model = self.cache.backend.models[self.room_id, "transfers"] transfer = Transfer( id = uuid4(), is_upload = False, filepath = self.local_path, total_size = self.filesize or 0, status = TransferStatus.Transfering, ) assert model is not None client.transfer_tasks[transfer.id] = current_task() # type: ignore model[str(transfer.id)] = transfer try: parsed = urlparse(self.mxc) resp = await client.download( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), ) except (nio.TransferCancelledError, asyncio.CancelledError): if transfer and model: del model[str(transfer.id)] del client.transfer_tasks[transfer.id] raise if transfer and model: del model[str(transfer.id)] del client.transfer_tasks[transfer.id] return await self._decrypt(resp.body) async def _decrypt(self, data: bytes) -> bytes: """Decrypt an encrypted file's data.""" if not self.crypt_dict: return data func = functools.partial( nio.crypto.attachments.decrypt_attachment, data, self.crypt_dict["key"]["k"], self.crypt_dict["hashes"]["sha256"], self.crypt_dict["iv"], ) # Run in a separate thread return await asyncio.get_event_loop().run_in_executor(None, func) @classmethod async def from_existing_file( cls, cache: "MediaCache", client_user_id: str, mxc: str, existing: Path, overwrite: bool = False, **kwargs, ) -> "Media": """Copy an existing file to cache and return a `Media` for it.""" media = cls( cache = cache, client_user_id = client_user_id, mxc = mxc, title = existing.name, filesize = existing.stat().st_size, **kwargs, ) media.local_path.parent.mkdir(parents=True, exist_ok=True) if not media.local_path.exists() or overwrite: func = functools.partial(shutil.copy, existing, media.local_path) await asyncio.get_event_loop().run_in_executor(None, func) return media @classmethod async def from_bytes( cls, cache: "MediaCache", client_user_id: str, mxc: str, filename: str, data: bytes, overwrite: bool = False, **kwargs, ) -> "Media": """Create a cached file from bytes data and return a `Media` for it.""" media = cls( cache, client_user_id, mxc, filename, filesize=len(data), **kwargs, ) media.local_path.parent.mkdir(parents=True, exist_ok=True) if not media.local_path.exists() or overwrite: path = media.local_path async with atomic_write(path, binary=True) as (file, done): await file.write(data) done() return media @dataclass class Thumbnail(Media): """A matrix media's thumbnail, which is downloaded or has yet to be.""" wanted_size: Size = (800, 600) server_size: Optional[Size] = field(init=False, repr=False, default=None) @staticmethod def normalize_size(size: Size) -> Size: """Return standard `(width, height)` matrix thumbnail dimensions. The Matrix specification defines a few standard thumbnail dimensions for homeservers to store and return: 32x32, 96x96, 320x240, 640x480, and 800x600. This method returns the best matching size for a `size` without upscaling, e.g. passing `(641, 480)` will return `(800, 600)`. """ if size[0] > 640 or size[1] > 480: return (800, 600) if size[0] > 320 or size[1] > 240: return (640, 480) if size[0] > 96 or size[1] > 96: return (320, 240) if size[0] > 32 or size[1] > 32: return (96, 96) return (32, 32) @property def local_path(self) -> Path: """The path where the thumbnail either exists or should be downloaded. The returned paths are in this form: ``` /// _.` ``` e.g. `~/.cache/mirage/thumbnails/matrix.org/32x32/foo_Hm24ar11i768b0el.png`. """ size = self.normalize_size(self.server_size or self.wanted_size) size_dir = f"{size[0]}x{size[1]}" parsed = urlparse(self.mxc) mxc_id = parsed.path.lstrip("/") title = Path(self.title) filename = f"{title.stem}_{mxc_id}{title.suffix}" return self.cache.thumbs_dir / parsed.netloc / size_dir / filename async def get_local(self) -> Path: """Return an existing thumbnail path or raise `FileNotFoundError`. If we have a bigger size thumbnail downloaded than the `wanted_size` for the media, return it instead of asking the server for a smaller thumbnail. """ if self.local_path.exists(): return self.local_path try_sizes = ((32, 32), (96, 96), (320, 240), (640, 480), (800, 600)) parts = list(self.local_path.parts) size = self.normalize_size(self.server_size or self.wanted_size) for width, height in try_sizes: if width < size[0] or height < size[1]: continue parts[-2] = f"{width}x{height}" path = Path("/".join(parts)) if path.exists(): return path raise FileNotFoundError() async def _get_remote_data(self) -> bytes: """Return the (decrypted) media file's content from the server.""" parsed = urlparse(self.mxc) client = self.cache.backend.clients[self.client_user_id] if self.crypt_dict: # Matrix makes encrypted thumbs only available through the download # end-point, not the thumbnail one resp = await client.download( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), ) else: resp = await client.thumbnail( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), width = self.wanted_size[0], height = self.wanted_size[1], ) decrypted = await self._decrypt(resp.body) with io.BytesIO(decrypted) as img: # The server may return a thumbnail bigger than what we asked for self.server_size = PILImage.open(img).size return decrypted mirage-0.7.2/src/backend/models/000077500000000000000000000000001407747233600164425ustar00rootroot00000000000000mirage-0.7.2/src/backend/models/__init__.py000066400000000000000000000004201407747233600205470ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Provide classes related to data models shared between Python and QML.""" from typing import Tuple, Union SyncId = Union[str, Tuple[str, ...]] mirage-0.7.2/src/backend/models/filters.py000066400000000000000000000135751407747233600204770ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from typing import ( TYPE_CHECKING, Any, Callable, Collection, Dict, List, Optional, Tuple, ) from . import SyncId from .model import Model from .proxy import ModelProxy if TYPE_CHECKING: from .model_item import ModelItem class ModelFilter(ModelProxy): """Filter data from one or more source models.""" def __init__(self, sync_id: SyncId) -> None: self.filtered_out: Dict[Tuple[Optional[SyncId], str], "ModelItem"] = {} self.items_changed_callbacks: List[Callable[[], None]] = [] super().__init__(sync_id) def accept_item(self, item: "ModelItem") -> bool: """Return whether an item should be present or filtered out.""" return True def source_item_set( self, source: Model, key, value: "ModelItem", _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: with self.write_lock: if self.accept_source(source): value = self.convert_item(value) if self.accept_item(value): self.__setitem__( (source.sync_id, key), value, _changed_fields, ) self.filtered_out.pop((source.sync_id, key), None) else: self.filtered_out[source.sync_id, key] = value self.pop((source.sync_id, key), None) for callback in self.items_changed_callbacks: callback() def source_item_deleted(self, source: Model, key) -> None: with self.write_lock: if self.accept_source(source): try: del self[source.sync_id, key] except KeyError: del self.filtered_out[source.sync_id, key] for callback in self.items_changed_callbacks: callback() def source_cleared(self, source: Model) -> None: with self.write_lock: if self.accept_source(source): for source_sync_id, key in self.copy(): if source_sync_id == source.sync_id: try: del self[source.sync_id, key] except KeyError: del self.filtered_out[source.sync_id, key] for callback in self.items_changed_callbacks: callback() def refilter( self, only_if: Optional[Callable[["ModelItem"], bool]] = None, ) -> None: """Recheck every item to decide if they should be filtered out.""" with self.write_lock: take_out = [] bring_back = [] for key, item in sorted(self.items(), key=lambda kv: kv[1]): if only_if and not only_if(item): continue if not self.accept_item(item): take_out.append(key) for key, item in self.filtered_out.items(): if only_if and not only_if(item): continue if self.accept_item(item): bring_back.append(key) with self.batch_remove(): for key in take_out: self.filtered_out[key] = self.pop(key) for key in bring_back: self[key] = self.filtered_out.pop(key) if take_out or bring_back: for callback in self.items_changed_callbacks: callback() class FieldStringFilter(ModelFilter): """Filter source models based on if their fields matches a string. This is used for filter fields in QML: the user enters some text and only items with a certain field (typically `display_name`) that starts with the entered text will be shown. Matching is done using "smart case": insensitive if the filter text is all lowercase, sensitive otherwise. """ def __init__( self, sync_id: SyncId, fields: Collection[str], no_filter_accept_all_items: bool = True, ) -> None: self.fields = fields self.no_filter_accept_all_items = no_filter_accept_all_items self._filter: str = "" super().__init__(sync_id) @property def filter(self) -> str: return self._filter @filter.setter def filter(self, value: str) -> None: if value != self._filter: self._filter = value self.refilter() def accept_item(self, item: "ModelItem") -> bool: if not self.filter: return self.no_filter_accept_all_items fields = {f: getattr(item, f) for f in self.fields} filtr = self.filter lowercase = filtr.lower() if lowercase == filtr: # Consider case only if filter isn't all lowercase filtr = lowercase fields = {name: value.lower() for name, value in fields.items()} return self.match(fields, filtr) def match(self, fields: Dict[str, str], filtr: str) -> bool: for value in fields.values(): if value.startswith(filtr): return True return False class FieldSubstringFilter(FieldStringFilter): """Fuzzy-like alternative to `FieldStringFilter`. All words in the filter string must fully or partially match words in the item field values, e.g. "red l" can match "red light", "tired legs", "light red" (order of the filter words doesn't matter), but not just "red" or "light" by themselves. """ def match(self, fields: Dict[str, str], filtr: str) -> bool: text = " ".join(fields.values()) for word in filtr.split(): if word and word not in text: return False return True mirage-0.7.2/src/backend/models/items.py000066400000000000000000000354761407747233600201540ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """`ModelItem` subclasses definitions.""" import json from dataclasses import asdict, dataclass, field from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union from uuid import UUID import lxml # nosec import nio from ..presence import Presence from ..utils import AutoStrEnum, auto, strip_html_tags from .model_item import ModelItem OptionalExceptionType = Union[Type[None], Type[Exception]] ZERO_DATE = datetime.fromtimestamp(0) class TypeSpecifier(AutoStrEnum): """Enum providing clarification of purpose for some matrix events.""" Unset = auto() ProfileChange = auto() MembershipChange = auto() class PingStatus(AutoStrEnum): """Enum for the status of a homeserver ping operation.""" Done = auto() Pinging = auto() Failed = auto() class RoomNotificationOverride(AutoStrEnum): """Possible per-room notification override settings, as displayed in the left sidepane's context menu when right-clicking a room. """ UseDefaultSettings = auto() AllEvents = auto() HighlightsOnly = auto() IgnoreEvents = auto() @dataclass(eq=False) class Homeserver(ModelItem): """A homeserver we can connect to. The `id` field is the server's URL.""" id: str = field() name: str = field() site_url: str = field() country: str = field() ping: int = -1 status: PingStatus = PingStatus.Pinging stability: float = -1 downtimes_ms: List[float] = field(default_factory=list) def __lt__(self, other: "Homeserver") -> bool: return (self.name.lower(), self.id) < (other.name.lower(), other.id) @dataclass(eq=False) class Account(ModelItem): """A logged in matrix account.""" id: str = field() order: int = -1 display_name: str = "" avatar_url: str = "" max_upload_size: int = 0 profile_updated: datetime = ZERO_DATE connecting: bool = False total_unread: int = 0 total_highlights: int = 0 local_unreads: bool = False ignored_users: Set[str] = field(default_factory=set) # For some reason, Account cannot inherit Presence, because QML keeps # complaining type error on unknown file presence_support: bool = False save_presence: bool = True presence: Presence.State = Presence.State.offline currently_active: bool = False last_active_at: datetime = ZERO_DATE status_msg: str = "" def __lt__(self, other: "Account") -> bool: return (self.order, self.id) < (other.order, other.id) @dataclass(eq=False) class PushRule(ModelItem): """A push rule configured for one of our account.""" id: Tuple[str, str] = field() # (kind.value, rule_id) kind: nio.PushRuleKind = field() rule_id: str = field() order: int = field() default: bool = field() enabled: bool = True conditions: List[Dict[str, Any]] = field(default_factory=list) pattern: str = "" actions: List[Dict[str, Any]] = field(default_factory=list) notify: bool = False highlight: bool = False bubble: bool = False sound: str = "" # usually "default" when set urgency_hint: bool = False def __lt__(self, other: "PushRule") -> bool: return ( self.kind is nio.PushRuleKind.underride, self.kind is nio.PushRuleKind.sender, self.kind is nio.PushRuleKind.room, self.kind is nio.PushRuleKind.content, self.kind is nio.PushRuleKind.override, self.order, self.id, ) < ( other.kind is nio.PushRuleKind.underride, other.kind is nio.PushRuleKind.sender, other.kind is nio.PushRuleKind.room, other.kind is nio.PushRuleKind.content, other.kind is nio.PushRuleKind.override, other.order, other.id, ) @dataclass class Room(ModelItem): """A matrix room we are invited to, are or were member of.""" id: str = field() for_account: str = "" given_name: str = "" display_name: str = "" main_alias: str = "" avatar_url: str = "" plain_topic: str = "" topic: str = "" inviter_id: str = "" inviter_name: str = "" inviter_avatar: str = "" left: bool = False typing_members: List[str] = field(default_factory=list) federated: bool = True encrypted: bool = False unverified_devices: bool = False invite_required: bool = True guests_allowed: bool = True default_power_level: int = 0 own_power_level: int = 0 can_invite: bool = False can_kick: bool = False can_redact_all: bool = False can_send_messages: bool = False can_set_name: bool = False can_set_topic: bool = False can_set_avatar: bool = False can_set_encryption: bool = False can_set_join_rules: bool = False can_set_guest_access: bool = False can_set_power_levels: bool = False last_event_date: datetime = ZERO_DATE unreads: int = 0 highlights: int = 0 local_unreads: bool = False notification_setting: RoomNotificationOverride = \ RoomNotificationOverride.UseDefaultSettings lexical_sorting: bool = False pinned: bool = False # Allowed keys: "last_event_date", "unreads", "highlights", "local_unreads" # Keys in this dict will override their corresponding item fields for the # __lt__ method. This is used when we want to lock a room's position, # e.g. to avoid having the room move around when it is focused in the GUI _sort_overrides: Dict[str, Any] = field(default_factory=dict) def _sorting(self, key: str) -> Any: return self._sort_overrides.get(key, getattr(self, key)) def __lt__(self, other: "Room") -> bool: by_activity = not self.lexical_sorting return ( self.for_account, other.pinned, self.left, # Left rooms may have an inviter_id, check them first bool(other.inviter_id), bool(by_activity and other._sorting("highlights")), bool(by_activity and other._sorting("unreads")), bool(by_activity and other._sorting("local_unreads")), other._sorting("last_event_date") if by_activity else ZERO_DATE, (self.display_name or self.id).lower(), self.id, ) < ( other.for_account, self.pinned, other.left, bool(self.inviter_id), bool(by_activity and self._sorting("highlights")), bool(by_activity and self._sorting("unreads")), bool(by_activity and self._sorting("local_unreads")), self._sorting("last_event_date") if by_activity else ZERO_DATE, (other.display_name or other.id).lower(), other.id, ) @dataclass(eq=False) class AccountOrRoom(Account, Room): """The left sidepane in the GUI lists a mixture of accounts and rooms giving a tree view illusion. Since all items in a QML ListView must have the same available properties, this class inherits both `Account` and `Room` to fulfill that purpose. """ type: Union[Type[Account], Type[Room]] = Account account_order: int = -1 def __lt__(self, other: "AccountOrRoom") -> bool: # type: ignore by_activity = not self.lexical_sorting return ( self.account_order, self.id if self.type is Account else self.for_account, other.type is Account, other.pinned, self.left, bool(other.inviter_id), bool(by_activity and other._sorting("highlights")), bool(by_activity and other._sorting("unreads")), bool(by_activity and other._sorting("local_unreads")), other._sorting("last_event_date") if by_activity else ZERO_DATE, (self.display_name or self.id).lower(), self.id, ) < ( other.account_order, other.id if other.type is Account else other.for_account, self.type is Account, self.pinned, other.left, bool(self.inviter_id), bool(by_activity and self._sorting("highlights")), bool(by_activity and self._sorting("unreads")), bool(by_activity and self._sorting("local_unreads")), self._sorting("last_event_date") if by_activity else ZERO_DATE, (other.display_name or other.id).lower(), other.id, ) @dataclass(eq=False) class Member(ModelItem): """A member in a matrix room.""" id: str = field() display_name: str = "" avatar_url: str = "" typing: bool = False power_level: int = 0 invited: bool = False ignored: bool = False profile_updated: datetime = ZERO_DATE last_read_event: str = "" presence: Presence.State = Presence.State.offline currently_active: bool = False last_active_at: datetime = ZERO_DATE status_msg: str = "" def __lt__(self, other: "Member") -> bool: return ( self.invited, other.power_level, self.ignored, Presence.State.offline if self.ignored else self.presence, (self.display_name or self.id[1:]).lower(), self.id, ) < ( other.invited, self.power_level, other.ignored, Presence.State.offline if other.ignored else other.presence, (other.display_name or other.id[1:]).lower(), other.id, ) class TransferStatus(AutoStrEnum): """Enum describing the status of an upload operation.""" Preparing = auto() Transfering = auto() Caching = auto() Error = auto() @dataclass(eq=False) class Transfer(ModelItem): """Represent a running or failed file upload/download operation.""" id: UUID = field() is_upload: bool = field() filepath: Path = Path("-") total_size: int = 0 transferred: int = 0 speed: float = 0 time_left: timedelta = timedelta(0) paused: bool = False status: TransferStatus = TransferStatus.Preparing error: OptionalExceptionType = type(None) error_args: Tuple[Any, ...] = () start_date: datetime = field(init=False, default_factory=datetime.now) def __lt__(self, other: "Transfer") -> bool: return (self.start_date, self.id) > (other.start_date, other.id) @dataclass(eq=False) class Event(ModelItem): """A matrix state event or message.""" id: str = field() event_id: str = field() event_type: Type[nio.Event] = field() date: datetime = field() sender_id: str = field() sender_name: str = field() sender_avatar: str = field() fetch_profile: bool = False content: str = "" inline_content: str = "" reason: str = "" links: List[str] = field(default_factory=list) mentions: List[Tuple[str, str]] = field(default_factory=list) type_specifier: TypeSpecifier = TypeSpecifier.Unset target_id: str = "" target_name: str = "" target_avatar: str = "" redacter_id: str = "" redacter_name: str = "" # {user_id: server_timestamp} - QML can't parse dates from JSONified dicts last_read_by: Dict[str, int] = field(default_factory=dict) read_by_count: int = 0 is_local_echo: bool = False source: Optional[nio.Event] = None media_url: str = "" media_http_url: str = "" media_title: str = "" media_width: int = 0 media_height: int = 0 media_duration: int = 0 media_size: int = 0 media_mime: str = "" media_crypt_dict: Dict[str, Any] = field(default_factory=dict) media_local_path: Union[str, Path] = "" thumbnail_url: str = "" thumbnail_mime: str = "" thumbnail_width: int = 0 thumbnail_height: int = 0 thumbnail_crypt_dict: Dict[str, Any] = field(default_factory=dict) def __lt__(self, other: "Event") -> bool: return (self.date, self.id) > (other.date, other.id) @property def plain_content(self) -> str: """Plaintext version of the event's content.""" if isinstance(self.source, nio.RoomMessageText): return self.source.body return strip_html_tags(self.content) @staticmethod def parse_links(text: str) -> List[str]: """Return list of URLs (`` tags) present in the content.""" ignore = [] if "" in text or "mention" in text: parser = lxml.html.etree.HTMLParser() tree = lxml.etree.fromstring(text, parser) ignore = [ lxml.etree.tostring(matching_element) for ugly_disgusting_xpath in [ # Match mx-reply > blockquote > second a (user ID link) "//mx-reply/blockquote/a[count(preceding-sibling::*)<=1]", # Match tags with a mention class '//a[contains(concat(" ",normalize-space(@class)," ")' '," mention ")]', ] for matching_element in tree.xpath(ugly_disgusting_xpath) ] if not text.strip(): return [] return [ url for el, attrib, url, pos in lxml.html.iterlinks(text) if lxml.etree.tostring(el) not in ignore ] def serialized_field(self, field: str) -> Any: if field == "source": source_dict = asdict(self.source) if self.source else {} return json.dumps(source_dict) return super().serialized_field(field) mirage-0.7.2/src/backend/models/model.py000066400000000000000000000145501407747233600201210ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later import itertools from contextlib import contextmanager from threading import RLock from typing import ( TYPE_CHECKING, Any, Dict, Iterator, List, MutableMapping, Optional, Tuple, ) from sortedcontainers import SortedList from ..pyotherside_events import ModelCleared, ModelItemDeleted, ModelItemSet from ..utils import serialize_value_for_qml from . import SyncId if TYPE_CHECKING: from .model_item import ModelItem from .proxy import ModelProxy # noqa class Model(MutableMapping): """A mapping of `{ModelItem.id: ModelItem}` synced between Python & QML. From the Python side, the model is usable like a normal dict of `ModelItem` subclass objects. Different types of `ModelItem` must not be mixed in the same model. When items are added, replaced, removed, have field value changes, or the model is cleared, corresponding `PyOtherSideEvent` are fired to inform QML of the changes so that it can keep its models in sync. Items in the model are kept sorted using the `ModelItem` subclass `__lt__`. """ instances: Dict[SyncId, "Model"] = {} proxies: Dict[SyncId, "ModelProxy"] = {} def __init__(self, sync_id: Optional[SyncId]) -> None: self.sync_id: Optional[SyncId] = sync_id self.write_lock: RLock = RLock() self._data: Dict[Any, "ModelItem"] = {} self._sorted_data: SortedList["ModelItem"] = SortedList() self.take_items_ownership: bool = True # [(index, item.id), ...] self._active_batch_removed: Optional[List[Tuple[int, Any]]] = None if self.sync_id: self.instances[self.sync_id] = self def __repr__(self) -> str: """Provide a full representation of the model and its content.""" return "%s(sync_id=%s, %s)" % ( type(self).__name__, self.sync_id, self._data, ) def __str__(self) -> str: """Provide a short ": items" representation.""" return f"{self.sync_id}: {len(self)} items" def __getitem__(self, key): return self._data[key] def __setitem__( self, key, value: "ModelItem", _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: with self.write_lock: existing = self._data.get(key) new = value # Collect changed fields changed_fields = _changed_fields or {} if not changed_fields: for field in new.__dataclass_fields__: # type: ignore if field.startswith("_"): continue changed = True if existing: changed = \ getattr(new, field) != getattr(existing, field) if changed: changed_fields[field] = new.serialized_field(field) # Set parent model on new item if self.sync_id and self.take_items_ownership: new.parent_model = self # Insert into sorted data index_then = None if existing: index_then = self._sorted_data.index(existing) del self._sorted_data[index_then] self._sorted_data.add(new) index_now = self._sorted_data.index(new) # Insert into dict data self._data[key] = new # Callbacks for sync_id, proxy in self.proxies.items(): if sync_id != self.sync_id: proxy.source_item_set(self, key, value) # Emit PyOtherSide event if self.sync_id and (index_then != index_now or changed_fields): ModelItemSet( self.sync_id, index_then, index_now, changed_fields, ) def __delitem__(self, key) -> None: with self.write_lock: item = self._data[key] if self.sync_id and self.take_items_ownership: item.parent_model = None del self._data[key] index = self._sorted_data.index(item) del self._sorted_data[index] for sync_id, proxy in self.proxies.items(): if sync_id != self.sync_id: proxy.source_item_deleted(self, key) if self.sync_id: if self._active_batch_removed is None: i = serialize_value_for_qml(item.id, json_list_dicts=True) ModelItemDeleted(self.sync_id, index, 1, (i,)) else: self._active_batch_removed.append((index, item.id)) def __iter__(self) -> Iterator: return iter(self._data) def __len__(self) -> int: return len(self._data) def __lt__(self, other: "Model") -> bool: """Sort `Model` objects lexically by `sync_id`.""" return str(self.sync_id) < str(other.sync_id) def clear(self) -> None: super().clear() if self.sync_id: ModelCleared(self.sync_id) def copy(self, sync_id: Optional[SyncId] = None) -> "Model": new = type(self)(sync_id=sync_id) new.update(self) return new @contextmanager def batch_remove(self): """Context manager that accumulates item removal events. When the context manager exits, sequences of removed items are grouped and one `ModelItemDeleted` pyotherside event is fired per sequence. """ with self.write_lock: try: self._active_batch_removed = [] yield None finally: batch = self._active_batch_removed groups = [ list(group) for item, group in itertools.groupby(batch, key=lambda x: x[0]) ] def serialize_id(id_): return serialize_value_for_qml(id_, json_list_dicts=True) for group in groups: ModelItemDeleted( self.sync_id, index = group[0][0], count = len(group), ids = [serialize_id(item[1]) for item in group], ) self._active_batch_removed = None mirage-0.7.2/src/backend/models/model_item.py000066400000000000000000000103641407747233600211360ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Dict, Optional from ..pyotherside_events import ModelItemSet from ..utils import serialize_value_for_qml if TYPE_CHECKING: from .model import Model @dataclass(eq=False) class ModelItem: """Base class for items stored inside a `Model`. This class must be subclassed and not used directly. All subclasses must use the `@dataclass(eq=False)` decorator. Subclasses are also expected to implement `__lt__()`, to provide support for comparisons with the `<`, `>`, `<=`, `=>` operators and thus allow a `Model` to keep its data sorted. Make sure to respect SortedList requirements when implementing `__lt__()`: http://www.grantjenks.com/docs/sortedcontainers/introduction.html#caveats """ id: Any = field() def __new__(cls, *_args, **_kwargs) -> "ModelItem": cls.parent_model: Optional[Model] = None return super().__new__(cls) def __setattr__(self, name: str, value) -> None: self.set_fields(**{name: value}) def __delattr__(self, name: str) -> None: raise NotImplementedError() @property def serialized(self) -> Dict[str, Any]: """Return this item as a dict ready to be passed to QML.""" return { name: self.serialized_field(name) for name in self.__dataclass_fields__ # type: ignore if not name.startswith("_") } def serialized_field(self, field: str) -> Any: """Return a field's value in a form suitable for passing to QML.""" value = getattr(self, field) return serialize_value_for_qml(value, json_list_dicts=True) def set_fields(self, _force: bool = False, **fields: Any) -> None: """Set one or more field's value and call `ModelItem.notify_change`. For efficiency, to change multiple fields, this method should be used rather than setting them one after another with `=` or `setattr`. """ parent = self.parent_model # If we're currently being created or haven't been put in a model yet: if not parent: for name, value in fields.items(): super().__setattr__(name, value) return with parent.write_lock: qml_changes = {} changes = { name: value for name, value in fields.items() if _force or getattr(self, name) != value } if not changes: return # To avoid corrupting the SortedList, we have to take out the item, # apply the field changes, *then* add it back in. index_then = parent._sorted_data.index(self) del parent._sorted_data[index_then] for name, value in changes.items(): super().__setattr__(name, value) is_field = name in self.__dataclass_fields__ # type: ignore if is_field and not name.startswith("_"): qml_changes[name] = self.serialized_field(name) parent._sorted_data.add(self) index_now = parent._sorted_data.index(self) index_change = index_then != index_now # Now, inform QML about changed dataclass fields if any. if not parent.sync_id or (not qml_changes and not index_change): return ModelItemSet(parent.sync_id, index_then, index_now, qml_changes) # Inform any proxy connected to the parent model of the field changes for sync_id, proxy in parent.proxies.items(): if sync_id != parent.sync_id: proxy.source_item_set(parent, self.id, self, qml_changes) def notify_change(self, *fields: str) -> None: """Notify the parent model that fields of this item have changed. The model cannot automatically detect changes inside object fields, such as list or dicts having their data modified. In these cases, this method should be called. """ kwargs = {name: getattr(self, name) for name in fields} kwargs["_force"] = True self.set_fields(**kwargs) mirage-0.7.2/src/backend/models/model_store.py000066400000000000000000000044471407747233600213410ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from collections import UserDict from dataclasses import dataclass, field from typing import Dict, List, Union from . import SyncId from .model import Model from .special_models import ( AllRooms, AutoCompletedMembers, FilteredHomeservers, FilteredMembers, MatchingAccounts, ) @dataclass(frozen=True) class ModelStore(UserDict): """Dict of sync ID keys and `Model` values. The dict keys must be the sync ID of `Model` values. If a non-existent key is accessed, a corresponding `Model` will be created, put into the internal `data` dict and returned. """ data: Dict[SyncId, Model] = field(default_factory=dict) def __missing__(self, key: SyncId) -> Model: """When accessing a non-existent model, create and return it. Special models rather than a generic `Model` object may be returned depending on the passed key. """ is_tuple = isinstance(key, tuple) model: Model if key == "all_rooms": model = AllRooms(self["accounts"]) elif key == "matching_accounts": model = MatchingAccounts(self["all_rooms"]) elif key == "filtered_homeservers": model = FilteredHomeservers() elif is_tuple and len(key) == 3 and key[2] == "filtered_members": model = FilteredMembers(user_id=key[0], room_id=key[1]) elif is_tuple and len(key) == 3 and key[2] == "autocompleted_members": model = AutoCompletedMembers(user_id=key[0], room_id=key[1]) else: model = Model(sync_id=key) self.data[key] = model return model def __str__(self) -> str: """Provide a nice overview of stored models when `print()` called.""" return "%s(\n %s\n)" % ( type(self).__name__, "\n ".join(sorted(str(v) for v in self.values())), ) async def ensure_exists_from_qml( self, sync_id: Union[SyncId, List[str]], ) -> None: """Create model if it doesn't exist. Should only be called by QML.""" if isinstance(sync_id, list): # QML can't pass tuples sync_id = tuple(sync_id) self[sync_id] # will call __missing__ if needed mirage-0.7.2/src/backend/models/proxy.py000066400000000000000000000045721407747233600202050ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from copy import copy from typing import TYPE_CHECKING, Any, Dict, Optional from . import SyncId from .model import Model if TYPE_CHECKING: from .model_item import ModelItem class ModelProxy(Model): """Proxies data from one or more `Model` objects.""" def __init__(self, sync_id: SyncId) -> None: super().__init__(sync_id) self.take_items_ownership = False Model.proxies[sync_id] = self with self.write_lock: for sync_id, model in Model.instances.items(): if sync_id != self.sync_id and self.accept_source(model): for key, item in model.items(): self.source_item_set(model, key, item) def accept_source(self, source: Model) -> bool: """Return whether passed `Model` should be proxied by this proxy.""" return True def convert_item(self, item: "ModelItem") -> "ModelItem": """Take a source `ModelItem`, return an appropriate one for proxy. By default, this returns the passed item unchanged. Due to QML `ListModel` restrictions, if multiple source models containing different subclasses of `ModelItem` are proxied, they should be converted to a same `ModelItem` subclass by overriding this function. """ return copy(item) def source_item_set( self, source: Model, key, value: "ModelItem", _changed_fields: Optional[Dict[str, Any]] = None, ) -> None: """Called when a source model item is added or changed.""" if self.accept_source(source): value = self.convert_item(value) self.__setitem__((source.sync_id, key), value, _changed_fields) def source_item_deleted(self, source: Model, key) -> None: """Called when a source model item is removed.""" if self.accept_source(source): del self[source.sync_id, key] def source_cleared(self, source: Model) -> None: """Called when a source model is cleared.""" if self.accept_source(source): with self.batch_remove(): for source_sync_id, key in self.copy(): if source_sync_id == source.sync_id: del self[source_sync_id, key] mirage-0.7.2/src/backend/models/special_models.py000066400000000000000000000112431407747233600220000ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import asdict from typing import Dict, Set from .filters import FieldStringFilter, FieldSubstringFilter, ModelFilter from .items import Account, AccountOrRoom, Room from .model import Model from .model_item import ModelItem class AllRooms(FieldSubstringFilter): """Flat filtered list of all accounts and their rooms.""" def __init__(self, accounts: Model) -> None: self.accounts = accounts self._collapsed: Set[str] = set() super().__init__(sync_id="all_rooms", fields=("display_name",)) self.items_changed_callbacks.append(self.refilter_accounts) def set_account_collapse(self, user_id: str, collapsed: bool) -> None: """Set whether the rooms for an account should be filtered out.""" def only_if(item): return item.type is Room and item.for_account == user_id if collapsed and user_id not in self._collapsed: self._collapsed.add(user_id) self.refilter(only_if) if not collapsed and user_id in self._collapsed: self._collapsed.remove(user_id) self.refilter(only_if) def accept_source(self, source: Model) -> bool: return source.sync_id == "accounts" or ( isinstance(source.sync_id, tuple) and len(source.sync_id) == 2 and source.sync_id[1] == "rooms" ) def convert_item(self, item: ModelItem) -> AccountOrRoom: return AccountOrRoom( **asdict(item), type = type(item), # type: ignore account_order = item.order if isinstance(item, Account) else self.accounts[item.for_account].order, # type: ignore ) def accept_item(self, item: ModelItem) -> bool: assert isinstance(item, AccountOrRoom) # nosec if not self.filter and \ item.type is Room and \ item.for_account in self._collapsed: return False matches_filter = super().accept_item(item) if item.type is not Account or not self.filter: return matches_filter return next( (i for i in self.values() if i.for_account == item.id), False, ) def refilter_accounts(self) -> None: self.refilter(lambda i: i.type is Account) # type: ignore class MatchingAccounts(ModelFilter): """List of our accounts in `AllRooms` with at least one matching room if a `filter` is set, else list of all accounts. """ def __init__(self, all_rooms: AllRooms) -> None: self.all_rooms = all_rooms self.all_rooms.items_changed_callbacks.append(self.refilter) super().__init__(sync_id="matching_accounts") def accept_source(self, source: Model) -> bool: return source.sync_id == "accounts" def accept_item(self, item: ModelItem) -> bool: if not self.all_rooms.filter: return True return next( (i for i in self.all_rooms.values() if i.id == item.id), False, ) class FilteredMembers(FieldSubstringFilter): """Filtered list of members for a room.""" def __init__(self, user_id: str, room_id: str) -> None: self.user_id = user_id self.room_id = room_id sync_id = (user_id, room_id, "filtered_members") super().__init__(sync_id=sync_id, fields=("display_name",)) def accept_source(self, source: Model) -> bool: return source.sync_id == (self.user_id, self.room_id, "members") class AutoCompletedMembers(FieldStringFilter): """Filtered list of mentionable members for tab-completion.""" def __init__(self, user_id: str, room_id: str) -> None: self.user_id = user_id self.room_id = room_id sync_id = (user_id, room_id, "autocompleted_members") super().__init__( sync_id = sync_id, fields = ("display_name", "id"), no_filter_accept_all_items = False, ) def accept_source(self, source: Model) -> bool: return source.sync_id == (self.user_id, self.room_id, "members") def match(self, fields: Dict[str, str], filtr: str) -> bool: fields["id"] = fields["id"][1:] # remove leading @ return super().match(fields, filtr) class FilteredHomeservers(FieldSubstringFilter): """Filtered list of public Matrix homeservers.""" def __init__(self) -> None: super().__init__(sync_id="filtered_homeservers", fields=("id", "name")) def accept_source(self, source: Model) -> bool: return source.sync_id == "homeservers" mirage-0.7.2/src/backend/nio_callbacks.py000066400000000000000000001025701407747233600203220ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later import json import logging as log from dataclasses import dataclass, field from datetime import datetime, timedelta from html import escape from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union from urllib.parse import quote import nio from .html_markdown import HTML_PROCESSOR from .media_cache import Media from .models.items import PushRule, TypeSpecifier from .presence import Presence from .pyotherside_events import DevicesUpdated from .utils import classes_defined_in, plain2html if TYPE_CHECKING: from .matrix_client import MatrixClient @dataclass class NioCallbacks: """Register callbacks for nio's request responses and events. For every class defined in the `nio.responses` and `nio.events` modules, this class can have a method named `on` (e.g. `onRoomMessageText`) that will automatically be registered in the `client`'s callbacks. For room event content strings, the `%1` and `%2` placeholders refer to the event's sender and who this event targets (`state_key`) or the redactor of this event. These are processed from QML, to allow for future translations of the strings. """ client: "MatrixClient" = field() def __post_init__(self) -> None: """Register our methods as callbacks.""" self.models = self.client.models for name, response_class in classes_defined_in(nio.responses).items(): method = getattr(self, f"on{name}", None) if method: self.client.add_response_callback(method, response_class) for name, ev_class in classes_defined_in(nio.events).items(): method = getattr(self, f"on{name}", None) if not method: continue if issubclass(ev_class, nio.EphemeralEvent): self.client.add_ephemeral_callback(method, ev_class) elif issubclass(ev_class, nio.ToDeviceEvent): self.client.add_to_device_callback(method, ev_class) elif issubclass(ev_class, nio.AccountDataEvent): self.client.add_global_account_data_callback(method, ev_class) elif issubclass(ev_class, nio.PresenceEvent): self.client.add_presence_callback(method, ev_class) else: self.client.add_event_callback(method, ev_class) @property def user_id(self) -> str: return self.client.user_id # Response callbacks async def onSyncResponse(self, resp: nio.SyncResponse) -> None: for room_id in resp.rooms.invite: await self.client.register_nio_room(self.client.all_rooms[room_id]) for room_id, info in resp.rooms.join.items(): await self.client.register_nio_room(self.client.rooms[room_id]) if room_id not in self.client.past_tokens: self.client.past_tokens[room_id] = info.timeline.prev_batch for ev in info.state: if isinstance(ev, nio.PowerLevelsEvent): stored = self.client.power_level_events.get(room_id) time = ev.server_timestamp if not stored or time > stored.server_timestamp: self.client.power_level_events[room_id] = ev # TODO: way of knowing if a nio.MatrixRoom is left for room_id, info in resp.rooms.leave.items(): # We forgot this room or rejected an invite and ignored the sender if room_id in self.client.ignored_rooms: continue # TODO: handle in nio, these are rooms that were left before # starting the client. if room_id not in self.client.all_rooms: continue # TODO: handle left events in nio async client for ev in info.timeline.events: if isinstance(ev, nio.RoomMemberEvent): await self.onRoomMemberEvent( self.client.all_rooms[room_id], ev, ) await self.client.register_nio_room( self.client.all_rooms[room_id], left=True, ) account = self.models["accounts"][self.user_id] account.connecting = False if not self.client.first_sync_done.is_set(): self.client.first_sync_done.set() self.client.first_sync_date = datetime.now() async def onKeysQueryResponse(self, resp: nio.KeysQueryResponse) -> None: refresh_rooms = {} clients = self.client.backend.clients for user_id in resp.changed: for room in self.client.rooms.values(): if user_id in room.users: refresh_rooms[room.room_id] = room if user_id != self.user_id and user_id in clients: await self.client.auto_verify_account(clients[user_id]) for room_id, room in refresh_rooms.items(): room_item = self.models[self.user_id, "rooms"].get(room_id) if room_item: room_item.unverified_devices = \ self.client.room_contains_unverified(room_id) else: await self.client.register_nio_room(room) DevicesUpdated(self.user_id) # Room events, invite events and misc events callbacks async def onRoomMessageText( self, room: nio.MatrixRoom, ev: nio.RoomMessageText, ) -> None: co = HTML_PROCESSOR.filter( ev.formatted_body if ev.format == "org.matrix.custom.html" else plain2html(ev.body), ) mention_list = HTML_PROCESSOR.mentions_in_html(co) await self.client.register_nio_event( room, ev, content=co, mentions=mention_list, ) async def onRoomMessageNotice( self, room: nio.MatrixRoom, ev: nio.RoomMessageNotice, ) -> None: await self.onRoomMessageText(room, ev) async def onRoomMessageEmote( self, room: nio.MatrixRoom, ev: nio.RoomMessageEmote, ) -> None: await self.onRoomMessageText(room, ev) async def onRoomMessageUnknown( self, room: nio.MatrixRoom, ev: nio.RoomMessageUnknown, ) -> None: co = f"%1 sent an unsupported {escape(ev.msgtype)} message" await self.client.register_nio_event(room, ev, content=co) async def onRoomMessageMedia( self, room: nio.MatrixRoom, ev: nio.RoomMessageMedia, ) -> None: info = ev.source["content"].get("info", {}) media_crypt_dict = ev.source["content"].get("file", {}) thumb_info = info.get("thumbnail_info", {}) thumb_crypt_dict = info.get("thumbnail_file", {}) try: media_local_path: Union[Path, str] = await Media( cache = self.client.backend.media_cache, client_user_id = self.user_id, mxc = ev.url, title = ev.body, room_id = room.room_id, filesize = info.get("size") or 0, crypt_dict = media_crypt_dict, ).get_local() except FileNotFoundError: media_local_path = "" item = await self.client.register_nio_event( room, ev, content = "", inline_content = ev.body, media_url = ev.url, media_http_url = await self.client.mxc_to_http(ev.url), media_title = ev.body, media_width = info.get("w") or 0, media_height = info.get("h") or 0, media_duration = info.get("duration") or 0, media_size = info.get("size") or 0, media_mime = info.get("mimetype") or "", media_crypt_dict = media_crypt_dict, media_local_path = media_local_path, thumbnail_url = info.get("thumbnail_url") or thumb_crypt_dict.get("url") or "", thumbnail_width = thumb_info.get("w") or 0, thumbnail_height = thumb_info.get("h") or 0, thumbnail_mime = thumb_info.get("mimetype") or "", thumbnail_crypt_dict = thumb_crypt_dict, ) self.client.backend.mxc_events[ev.url].append(item) async def onRoomEncryptedMedia( self, room: nio.MatrixRoom, ev: nio.RoomEncryptedMedia, ) -> None: await self.onRoomMessageMedia(room, ev) async def onRedactionEvent( self, room: nio.MatrixRoom, ev: nio.RedactionEvent, ) -> None: model = self.models[self.user_id, room.room_id, "events"] event = None for existing in model._sorted_data: if existing.event_id == ev.redacts: event = existing break if not ( event and (event.event_type is not nio.RedactedEvent or event.is_local_echo) ): await self.client.register_nio_room(room) return event.source.source["content"] = {} event.source.source["unsigned"] = { "redacted_by": ev.event_id, "redacted_because": ev.source, } await self.onRedactedEvent( room, nio.RedactedEvent.from_dict(event.source.source), event_id = event.id, ) async def onRedactedEvent( self, room: nio.MatrixRoom, ev: nio.RedactedEvent, event_id: str = "", ) -> None: redacter_name, _, must_fetch_redacter = \ await self.client.get_member_profile(room.room_id, ev.redacter) \ if ev.redacter else ("", "", False) await self.client.register_nio_event( room, ev, event_id = event_id, reason = ev.reason or "", content = await self.client.get_redacted_event_content( type(ev), ev.redacter, ev.sender, ev.reason, ), mentions = [], type_specifier = TypeSpecifier.Unset, media_url = "", media_http_url = "", media_title = "", media_local_path = "", thumbnail_url = "", redacter_id = ev.redacter or "", redacter_name = redacter_name, override_fetch_profile = True, ) async def onRoomCreateEvent( self, room: nio.MatrixRoom, ev: nio.RoomCreateEvent, ) -> None: co = "%1 allowed users on other matrix servers to join this room" \ if ev.federate else \ "%1 blocked users on other matrix servers from joining this room" await self.client.register_nio_event(room, ev, content=co) async def onRoomGuestAccessEvent( self, room: nio.MatrixRoom, ev: nio.RoomGuestAccessEvent, ) -> None: allowed = "allowed" if ev.guest_access == "can_join" else "forbad" co = f"%1 {allowed} guests to join the room" await self.client.register_nio_event(room, ev, content=co) async def onRoomJoinRulesEvent( self, room: nio.MatrixRoom, ev: nio.RoomJoinRulesEvent, ) -> None: access = "public" if ev.join_rule == "public" else "invite-only" co = f"%1 made the room {access}" await self.client.register_nio_event(room, ev, content=co) async def onRoomHistoryVisibilityEvent( self, room: nio.MatrixRoom, ev: nio.RoomHistoryVisibilityEvent, ) -> None: if ev.history_visibility == "shared": to = "all room members" elif ev.history_visibility == "world_readable": to = "any member or outsider" elif ev.history_visibility == "joined": to = "all room members, since the time they joined" elif ev.history_visibility == "invited": to = "all room members, since the time they were invited" else: to = "???" log.warning("Invalid visibility - %s", json.dumps(vars(ev), indent=4)) co = f"%1 made future room history visible to {to}" await self.client.register_nio_event(room, ev, content=co) async def onPowerLevelsEvent( self, room: nio.MatrixRoom, ev: nio.PowerLevelsEvent, ) -> None: levels = ev.power_levels stored = self.client.power_level_events.get(room.room_id) if not stored or ev.server_timestamp > stored.server_timestamp: self.client.power_level_events[room.room_id] = ev try: previous = ev.source["unsigned"]["prev_content"] except KeyError: previous = {} users_previous = previous.get("users", {}) events_previous = previous.get("events", {}) changes: List[Tuple[str, int, int]] = [] event_changes: List[Tuple[str, int, int]] = [] user_changes: List[Tuple[str, int, int]] = [] def lvl(level: int) -> str: return ( f"Admin ({level})" if level == 100 else f"Moderator ({level})" if level >= 50 else f"User ({level})" if level >= 0 else f"Muted ({level})" ) def format_defaults_dict( levels: Dict[str, Union[int, dict]], previous: Dict[str, Union[int, dict]], prefix: str = "", ) -> None: default_0 = ("users_default", "events_default", "invite") for name in set({**levels, **previous}): if not prefix and name in ("users", "events"): continue old_level = previous.get( name, 0 if not prefix and name in default_0 else 50, ) level = levels.get( name, 0 if not prefix and name in default_0 else 50, ) if isinstance(level, dict): if not isinstance(old_level, dict): old_level = {} format_defaults_dict(level, old_level, f"{prefix}{name}.") continue if not isinstance(old_level, int): old_level = 50 if old_level != level or not previous: changes.append((f"{prefix}{name}", old_level, level)) format_defaults_dict(ev.source["content"], previous) # Minimum level to send event changes for ev_type in set({**levels.events, **events_previous}): old_level = events_previous.get( ev_type, levels.defaults.state_default if ev_type.startswith("m.room.") else levels.defaults.events_default, ) level = levels.events.get( ev_type, levels.defaults.state_default if ev_type.startswith("m.room.") else levels.defaults.events_default, ) if old_level != level or not previous: event_changes.append((ev_type, old_level, level)) # User level changes for user_id in set({**levels.users, **users_previous}): old_level = \ users_previous.get(user_id, levels.defaults.users_default) level = levels.users.get(user_id, levels.defaults.users_default) if old_level != level or not previous: user_changes.append((user_id, old_level, level)) if user_id in room.users: await self.client.add_member(room, user_id) # Gather and format changes if changes or event_changes or user_changes: changes.sort(key=lambda c: (c[2], c[0])) event_changes.sort(key=lambda c: (c[2], c[0])) user_changes.sort(key=lambda c: (c[2], c[0])) all_changes = changes + event_changes + user_changes if len(all_changes) == 1: co = HTML_PROCESSOR.from_markdown( "%%1 changed the level for **%s**: %s → %s " % ( all_changes[0][0], lvl(all_changes[0][1]).lower(), lvl(all_changes[0][2]).lower(), ), inline = True, ) else: co = HTML_PROCESSOR.from_markdown("\n".join([ "%1 changed the room's permissions", "", "Change | Previous | Current ", "--- | --- | ---", *[ f"{name} | {lvl(old)} | {lvl(now)}" for name, old, now in all_changes ], ])) else: co = "%1 didn't change the room's permissions" await self.client.register_nio_event(room, ev, content=co) async def process_room_member_event( self, room: nio.MatrixRoom, ev: nio.RoomMemberEvent, ) -> Optional[Tuple[TypeSpecifier, str]]: """Return a `TypeSpecifier` and string describing a member event. Matrix member events can represent many actions: a user joined the room, a user banned another, a user changed their display name, etc. """ if ev.prev_content == ev.content: return None prev = ev.prev_content now = ev.content membership = ev.membership prev_membership = ev.prev_membership ev_date = datetime.fromtimestamp(ev.server_timestamp / 1000) member_change = TypeSpecifier.MembershipChange # Membership changes if not prev or membership != prev_membership: if not self.client.backend.settings.Chat.show_membership_events: return None reason = escape( f", reason: {now['reason']}" if now.get("reason") else "", ) if membership == "join": return ( member_change, "%1 accepted their invitation" if prev and prev_membership == "invite" else "%1 joined the room", ) if membership == "invite": return (member_change, "%1 invited %2 to the room") if membership == "leave": if ev.state_key == ev.sender: return ( member_change, f"%1 declined their invitation{reason}" if prev and prev_membership == "invite" else f"%1 left the room{reason}", ) return ( member_change, f"%1 withdrew %2's invitation{reason}" if prev and prev_membership == "invite" else f"%1 unbanned %2 from the room{reason}" if prev and prev_membership == "ban" else f"%1 kicked %2 out from the room{reason}", ) if membership == "ban": return (member_change, f"%1 banned %2 from the room{reason}") # Profile changes changed = [] if prev and now.get("avatar_url") != prev.get("avatar_url"): changed.append("profile picture") # TODO: s if prev and now.get("displayname") != prev.get("displayname"): changed.append('display name from "{}" to "{}"'.format( escape(prev.get("displayname") or ev.state_key), escape(now.get("displayname") or ev.state_key), )) if changed: # Update our account profile if the event is newer than last update if ev.state_key == self.user_id: account = self.models["accounts"][self.user_id] if account.profile_updated < ev_date: account.set_fields( profile_updated = ev_date, display_name = now.get("displayname") or "", avatar_url = now.get("avatar_url") or "", ) if not self.client.backend.settings.Chat.show_profile_changes: return None return ( TypeSpecifier.ProfileChange, "%1 changed their {}".format(" and ".join(changed)), ) # log.warning("Unknown member ev.: %s", json.dumps(vars(ev), indent=4)) return None async def onRoomMemberEvent( self, room: nio.MatrixRoom, ev: nio.RoomMemberEvent, ) -> None: # The event can be a past event, don't trust it to update the model # room's current state. if ev.state_key in room.users: await self.client.add_member(room, user_id=ev.state_key) else: await self.client.remove_member(room, user_id=ev.state_key) type_and_content = await self.process_room_member_event(room, ev) if type_and_content is not None: type_specifier, content = type_and_content await self.client.register_nio_event( room, ev, content=content, type_specifier=type_specifier, ) else: # Normally, register_nio_event() will call register_nio_room(). # but in this case we don't have any event we want to register. await self.client.register_nio_room(room) async def onRoomAliasEvent( self, room: nio.MatrixRoom, ev: nio.RoomAliasEvent, ) -> None: if ev.canonical_alias: url = f"https://matrix.to/#/{quote(ev.canonical_alias)}" link = f"{escape(ev.canonical_alias)}" co = f"%1 set the room's main address to {link}" else: co = "%1 removed the room's main address" await self.client.register_nio_event(room, ev, content=co) async def onRoomNameEvent( self, room: nio.MatrixRoom, ev: nio.RoomNameEvent, ) -> None: if ev.name: co = f"%1 changed the room's name to \"{escape(ev.name)}\"" else: co = "%1 removed the room's name" await self.client.register_nio_event(room, ev, content=co) async def onRoomAvatarEvent( self, room: nio.MatrixRoom, ev: nio.RoomAvatarEvent, ) -> None: if ev.avatar_url: co = "%1 changed the room's picture" else: co = "%1 removed the room's picture" http = await self.client.mxc_to_http(ev.avatar_url) await self.client.register_nio_event( room, ev, content=co, media_url=ev.avatar_url, media_http_url=http, ) async def onRoomTopicEvent( self, room: nio.MatrixRoom, ev: nio.RoomTopicEvent, ) -> None: if ev.topic: topic = HTML_PROCESSOR.filter(plain2html(ev.topic), inline=True) co = f"%1 changed the room's topic to \"{topic}\"" else: co = "%1 removed the room's topic" await self.client.register_nio_event(room, ev, content=co) async def onRoomEncryptionEvent( self, room: nio.MatrixRoom, ev: nio.RoomEncryptionEvent, ) -> None: co = "%1 turned on encryption for this room" await self.client.register_nio_event(room, ev, content=co) async def onMegolmEvent( self, room: nio.MatrixRoom, ev: nio.MegolmEvent, ) -> None: co = "%1 sent an undecryptable message" await self.client.register_nio_event(room, ev, content=co) async def onBadEvent( self, room: nio.MatrixRoom, ev: nio.BadEvent, ) -> None: co = f"%1 sent a malformed {escape(ev.type)} event" await self.client.register_nio_event(room, ev, content=co) async def onUnknownEvent( self, room: nio.MatrixRoom, ev: nio.UnknownEvent, ) -> None: if not self.client.backend.settings.Chat.show_unknown_events: await self.client.register_nio_room(room) return co = f"%1 sent an unsupported {escape(ev.type)} event" await self.client.register_nio_event(room, ev, content=co) async def onUnknownEncryptedEvent( self, room: nio.MatrixRoom, ev: nio.UnknownEncryptedEvent, ) -> None: co = ( f"%1 sent an {escape(ev.type)} event encrypted with " f"unsupported {escape(ev.algorithm)} algorithm" ) await self.client.register_nio_event(room, ev, content=co) async def onInviteEvent( self, room: nio.MatrixRoom, ev: nio.InviteEvent, ) -> None: await self.client.register_nio_room(room) # Ephemeral event callbacks async def onTypingNoticeEvent( self, room: nio.MatrixRoom, ev: nio.TypingNoticeEvent, ) -> None: # Prevent recent past typing notices from being shown for a split # second on client startup: if not self.client.first_sync_done.is_set(): return await self.client.register_nio_room(room) room_id = room.room_id room_item = self.models[self.user_id, "rooms"][room_id] room_item.typing_members = sorted( room.user_name(user_id) or user_id for user_id in ev.users if user_id not in self.client.backend.clients ) async def onReceiptEvent( self, room: nio.MatrixRoom, ev: nio.ReceiptEvent, ) -> None: member_model = self.models[self.user_id, room.room_id, "members"] event_model = self.models[self.user_id, room.room_id, "events"] unassigned_mems = self.client.unassigned_member_last_read_event unassigned_evs = self.client.unassigned_event_last_read_by recount_markers = [] for receipt in ev.receipts: if receipt.user_id in self.client.backend.clients: continue if receipt.receipt_type != "m.read": continue echo_id = self.client.event_to_echo_ids.get(receipt.event_id) read_event = event_model.get(echo_id or receipt.event_id) timestamp = receipt.timestamp if read_event: recount_markers.append(read_event) read_event.last_read_by[receipt.user_id] = timestamp read_event.notify_change("last_read_by") else: # We haven't received the read event from the server yet unassigned_evs[receipt.event_id][receipt.user_id] = timestamp if receipt.user_id not in member_model: # We haven't loaded the member yet (lazy loading), or they left unassigned_mems[room.room_id, receipt.user_id] = \ echo_id or receipt.event_id continue member = member_model[receipt.user_id] previous_read_event = event_model.get(member.last_read_event) if previous_read_event: # Remove the read marker from the previous last read event recount_markers.append(previous_read_event) previous_read_event.last_read_by.pop(receipt.user_id, None) previous_read_event.notify_change("last_read_by") member.last_read_event = echo_id or receipt.event_id for ev in recount_markers: ev.read_by_count = len(ev.last_read_by) # Account data callbacks async def onPushRulesEvent(self, ev: nio.PushRulesEvent) -> None: async def update_affected_room(rule: PushRule) -> None: affects_room: Optional[str] if rule.kind == nio.PushRuleKind.room: affects_room = rule.rule_id else: affects_room = self.client._rule_overrides_room(rule) if affects_room in self.client.rooms: nio_room = self.client.rooms[affects_room] await self.client.register_nio_room(nio_room) model = self.models[self.user_id, "pushrules"] kinds: Dict[nio.PushRuleKind, List[nio.PushRule]] = { kind: getattr(ev.global_rules, kind.value) for kind in nio.PushRuleKind } # Remove from model rules that are now deleted. # MUST be done first to avoid having rules sharing the same kind+order. new_keys: Set[Tuple[str, str]] = set() for kind, rules in kinds.items(): for rule in rules: new_keys.add((kind.value, rule.id)) with model.batch_remove(): for key, rule in list(model.items()): if key not in new_keys: del model[key] await update_affected_room(rule) # Then, add new rules/modify changed existing ones for kind, rules in kinds.items(): for order, rule in enumerate(rules): tweaks = { action.tweak: action.value for action in rule.actions if isinstance(action, nio.PushSetTweak) } # Note: The `dont_notify` action does nothing. # As of now (sept 2020), `coalesce` is just a `notify` synonym. notify = any( isinstance(action, (nio.PushNotify, nio.PushCoalesce)) for action in rule.actions ) high = tweaks.get("highlight", False) is not False bubble = tweaks.get("bubble", notify) is not False sound = str(tweaks.get("sound") or "") hint = tweaks.get("urgency_hint", bool(sound)) is not False rule_item = PushRule( id = (kind.value, rule.id), kind = kind, rule_id = rule.id, order = order, default = rule.default, enabled = rule.enabled, conditions = [c.as_value for c in rule.conditions], pattern = rule.pattern, actions = [a.as_value for a in rule.actions], notify = notify, highlight = high, bubble = bubble, sound = sound, urgency_hint = hint, ) model[kind.value, rule.id] = rule_item await update_affected_room(rule_item) self.client.push_rules = ev async def onUnknownAccountDataEvent( self, ev: nio.UnknownAccountDataEvent, ) -> None: if ev.type == "m.ignored_user_list": users = set(ev.content.get("ignored_users", {})) self.client.ignored_user_ids = users self.models["accounts"][self.client.user_id].ignored_users = users # Presence event callbacks async def onPresenceEvent( self, ev: Union[nio.PresenceEvent, nio.PresenceGetResponse], ) -> None: # Servers that send presence events support presence self.models["accounts"][self.client.user_id].presence_support = True account = self.models["accounts"].get(ev.user_id) presence = self.client.backend.presences.get(ev.user_id, Presence()) invisible = False if account: invisible = account.presence == Presence.State.invisible client = self.client.backend.clients[ev.user_id] # Synapse is stupid enough to return an older presence state on # sync, which then causes a never-ending loop of presence cycling. # Let's hope they didn't screw up the get_presence API too: ev = await client.get_presence(ev.user_id) if ev.presence == "offline" and not invisible: to_set = account.presence.value await client.set_presence(to_set, account.status_msg) return elif not (invisible and ev.presence != "offline"): client._presence = ev.presence if invisible and ev.presence == "offline": presence.presence = Presence.State.invisible else: presence.presence = Presence.State(ev.presence) presence.currently_active = ev.currently_active or False # Restore status msg lost from server due to e.g. getting offline if account and account.status_msg and not ev.status_msg: if invisible: presence.status_msg = account.status_msg else: await client.set_presence(ev.presence, account.status_msg) else: presence.status_msg = ev.status_msg or "" if ev.last_active_ago: presence.last_active_at = datetime.now() - timedelta( milliseconds=ev.last_active_ago, ) else: presence.last_active_at = datetime.fromtimestamp(0) # Add all existing members related to this presence for room_id in self.models[self.user_id, "rooms"]: members = self.models[self.user_id, room_id, "members"] if ev.user_id in members: presence.members[room_id] = members[ev.user_id] presence.update_members() if not account: self.client.backend.presences[ev.user_id] = presence return client = self.client.backend.clients[ev.user_id] # Save the presence to be restored next time we restart application if account.save_presence: status_msg = presence.status_msg state = presence.presence await self.client.backend.saved_accounts.set( user_id = ev.user_id, status_msg = status_msg, presence = state.value, ) presence.update_account() mirage-0.7.2/src/backend/pcn/000077500000000000000000000000001407747233600157375ustar00rootroot00000000000000mirage-0.7.2/src/backend/pcn/__init__.py000066400000000000000000000002741407747233600200530ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Parse and operate on PCN (Python Config Notation) files.""" mirage-0.7.2/src/backend/pcn/globals_dict.py000066400000000000000000000022641407747233600207430ustar00rootroot00000000000000from collections import UserDict from typing import TYPE_CHECKING, Any, Dict, Iterator if TYPE_CHECKING: from .section import Section from .. import color PCN_GLOBALS: Dict[str, Any] = { "color": color.Color, "hsluv": color.hsluv, "hsluva": color.hsluva, "hsl": color.hsl, "hsla": color.hsla, "rgb": color.rgb, "rgba": color.rgba, } class GlobalsDict(UserDict): def __init__(self, section: "Section") -> None: super().__init__() self.section = section @property def full_dict(self) -> Dict[str, Any]: return { **PCN_GLOBALS, **(self.section.root if self.section.root else {}), **(self.section.root.globals if self.section.root else {}), "self": self.section, "parent": self.section.parent, "root": self.section.parent, **self.data, } def __getitem__(self, key: str) -> Any: return self.full_dict[key] def __iter__(self) -> Iterator[str]: return iter(self.full_dict) def __len__(self) -> int: return len(self.full_dict) def __repr__(self) -> str: return repr(self.full_dict) mirage-0.7.2/src/backend/pcn/property.py000066400000000000000000000024731407747233600202030ustar00rootroot00000000000000import re from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, Dict, Type if TYPE_CHECKING: from .section import Section TYPE_PROCESSORS: Dict[str, Callable[[Any], Any]] = { "tuple": lambda v: tuple(v), "set": lambda v: set(v), } class Unset: pass @dataclass class Property: name: str = field() annotation: str = field() expression: str = field() section: "Section" = field() value_override: Any = Unset def __get__(self, obj: "Section", objtype: Type["Section"]) -> Any: if not obj: return self if self.value_override is not Unset: return self.value_override env = obj.globals result = eval(self.expression, dict(env), env) # nosec return process_value(self.annotation, result) def __set__(self, obj: "Section", value: Any) -> None: self.value_override = value obj._edited[self.name] = value def process_value(annotation: str, value: Any) -> Any: annotation = re.sub(r"\[.*\]$", "", annotation) if annotation in TYPE_PROCESSORS: return TYPE_PROCESSORS[annotation](value) if annotation.lower() in TYPE_PROCESSORS: return TYPE_PROCESSORS[annotation.lower()](value) return value mirage-0.7.2/src/backend/pcn/section.py000066400000000000000000000344051407747233600177630ustar00rootroot00000000000000import re import textwrap from collections import OrderedDict from collections.abc import MutableMapping from contextlib import suppress from dataclasses import dataclass, field from operator import attrgetter from pathlib import Path from typing import ( Any, Callable, ClassVar, Dict, Generator, List, Optional, Set, Tuple, Type, Union, ) import pyotherside import redbaron as red from .globals_dict import GlobalsDict from .property import Property, process_value # TODO: docstrings, error handling, support @property, non-section classes BUILTINS_DIR: Path = Path(__file__).parent.parent.parent assert BUILTINS_DIR.name == "src" @dataclass(repr=False, eq=False) class Section(MutableMapping): sections: ClassVar[Set[str]] = set() methods: ClassVar[Set[str]] = set() properties: ClassVar[Set[str]] = set() order: ClassVar[Dict[str, None]] = OrderedDict() source_path: Optional[Path] = None root: Optional["Section"] = None parent: Optional["Section"] = None builtins_path: Path = BUILTINS_DIR included: List[Path] = field(default_factory=list) globals: GlobalsDict = field(init=False) _edited: Dict[str, Any] = field(init=False, default_factory=dict) def __init_subclass__(cls, **kwargs) -> None: # Make these attributes not shared between Section and its subclasses cls.sections = set() cls.methods = set() cls.properties = set() cls.order = OrderedDict() for parent_class in cls.__bases__: if not issubclass(parent_class, Section): continue cls.sections |= parent_class.sections # union operator cls.methods |= parent_class.methods cls.properties |= parent_class.properties cls.order.update(parent_class.order) super().__init_subclass__(**kwargs) # type: ignore def __post_init__(self) -> None: self.globals = GlobalsDict(self) def __getattr__(self, name: str) -> Union["Section", Any]: # This method signature tells mypy about the dynamic attribute types # we can access. The body is run for attributes that aren't found. return super().__getattribute__(name) def __setattr__(self, name: str, value: Any) -> None: # This method tells mypy about the dynamic attribute types we can set. # The body is also run when setting an existing or new attribute. if name in self.__dataclass_fields__: super().__setattr__(name, value) return if name in self.properties: value = process_value(getattr(type(self), name).annotation, value) if self[name] == value: return getattr(type(self), name).value_override = value self._edited[name] = value return if name in self.sections or isinstance(value, Section): raise NotImplementedError(f"cannot set section {name!r}") if name in self.methods or callable(value): raise NotImplementedError(f"cannot set method {name!r}") self._set_property(name, "Any", "None") getattr(type(self), name).value_override = value self._edited[name] = value def __delattr__(self, name: str) -> None: raise NotImplementedError(f"cannot delete existing attribute {name!r}") def __getitem__(self, key: str) -> Any: try: return getattr(self, key) except AttributeError as err: raise KeyError(str(err)) def __setitem__(self, key: str, value: Union["Section", str]) -> None: setattr(self, key, value) def __delitem__(self, key: str) -> None: delattr(self, key) def __iter__(self) -> Generator[str, None, None]: for attr_name in self.order: yield attr_name def __len__(self) -> int: return len(self.order) def __eq__(self, obj: Any) -> bool: if not isinstance(obj, Section): return False if self.globals.data != obj.globals.data or self.order != obj.order: return False return not any(self[attr] != obj[attr] for attr in self.order) def __repr__(self) -> str: name: str = type(self).__name__ children: List[str] = [] content: str = "" newline: bool = False for attr_name in self.order: value = getattr(self, attr_name) if attr_name in self.sections: before = "\n" if children else "" newline = True try: children.append(f"{before}{value!r},") except RecursionError as err: name = type(value).__name__ children.append(f"{before}{name}(\n {err!r}\n),") pass elif attr_name in self.methods: before = "\n" if children else "" newline = True children.append(f"{before}def {value.__name__}(…),") elif attr_name in self.properties: before = "\n" if newline else "" newline = False try: children.append(f"{before}{attr_name} = {value!r},") except RecursionError as err: children.append(f"{before}{attr_name} = {err!r},") else: newline = False if children: content = "\n%s\n" % textwrap.indent("\n".join(children), " " * 4) return f"{name}({content})" def children(self) -> Tuple[Tuple[str, Union["Section", Any]], ...]: """Return pairs of (name, value) for child sections and properties.""" return tuple((name, getattr(self, name)) for name in self) @classmethod def _register_set_attr(cls, name: str, add_to_set_name: str) -> None: cls.methods.discard(name) cls.properties.discard(name) cls.sections.discard(name) getattr(cls, add_to_set_name).add(name) cls.order[name] = None for subclass in cls.__subclasses__(): subclass._register_set_attr(name, add_to_set_name) def _set_section(self, section: "Section") -> None: name = type(section).__name__ if hasattr(self, name) and name not in self.order: raise AttributeError(f"{name!r}: forbidden name") if name in self.sections: self[name].deep_merge(section) return self._register_set_attr(name, "sections") setattr(type(self), name, section) def _set_method(self, name: str, method: Callable) -> None: if hasattr(self, name) and name not in self.order: raise AttributeError(f"{name!r}: forbidden name") self._register_set_attr(name, "methods") setattr(type(self), name, method) def _set_property( self, name: str, annotation: str, expression: str, ) -> None: if hasattr(self, name) and name not in self.order: raise AttributeError(f"{name!r}: forbidden name") prop = Property(name, annotation, expression, self) self._register_set_attr(name, "properties") setattr(type(self), name, prop) def deep_merge(self, section2: "Section") -> None: self.included += section2.included for key in section2: if key in self.sections and key in section2.sections: self.globals.data.update(section2.globals.data) self[key].deep_merge(section2[key]) elif key in section2.sections: self.globals.data.update(section2.globals.data) new_type = type(key, (Section,), {}) instance = new_type( source_path = self.source_path, root = self.root or self, parent = self, builtins_path = self.builtins_path, ) self._set_section(instance) instance.deep_merge(section2[key]) elif key in section2.methods: self._set_method(key, section2[key]) else: prop2 = getattr(type(section2), key) self._set_property(key, prop2.annotation, prop2.expression) def include_file(self, path: Union[Path, str]) -> None: path = Path(path) if not path.is_absolute() and self.source_path: path = self.source_path.parent / path with suppress(ValueError): self.included.remove(path) self.included.append(path) self.deep_merge(Section.from_file(path)) def include_builtin(self, relative_path: Union[Path, str]) -> None: path = self.builtins_path / relative_path with suppress(ValueError): self.included.remove(path) self.included.append(path) self.deep_merge(Section.from_file(path)) def as_dict(self, _section: Optional["Section"] = None) -> Dict[str, Any]: dct = {} section = self if _section is None else _section for key, value in section.items(): if isinstance(value, Section): dct[key] = self.as_dict(value) else: dct[key] = value return dct def edits_as_dict( self, _section: Optional["Section"] = None, ) -> Dict[str, Any]: warning = ( "This file is generated when settings are changed from the GUI, " "and properties in it override the ones in the corresponding " "PCN user config file. " "If a property is gets changed in the PCN file, any corresponding " "property override here is removed." ) if _section is None: section = self dct = {"__comment": warning, "set": section._edited.copy()} add_to = dct["set"] else: section = _section dct = { prop_name: ( getattr(type(section), prop_name).expression, value_override, ) for prop_name, value_override in section._edited.items() } add_to = dct for name in section.sections: edits = section.edits_as_dict(section[name]) if edits: add_to[name] = edits # type: ignore return dct def deep_merge_edits( self, edits: Dict[str, Any], has_expressions: bool = True, ) -> bool: changes = False if not self.parent: # this is Root edits = edits.get("set", {}) for name, value in edits.copy().items(): if isinstance(self.get(name), Section) and isinstance(value, dict): if self[name].deep_merge_edits(value, has_expressions): changes = True elif not has_expressions: self[name] = value elif isinstance(value, (tuple, list)): user_expression, gui_value = value if not hasattr(type(self), name): self[name] = gui_value elif getattr(type(self), name).expression == user_expression: self[name] = gui_value else: # If user changed their config file, discard the GUI edit del edits[name] changes = True return changes @property def all_includes(self) -> Generator[Path, None, None]: yield from self.included for sub in self.sections: yield from self[sub].all_includes @classmethod def from_source_code( cls, code: str, path: Optional[Path] = None, builtins: Optional[Path] = None, *, inherit: Tuple[Type["Section"], ...] = (), node: Union[None, red.RedBaron, red.ClassNode] = None, name: str = "Root", root: Optional["Section"] = None, parent: Optional["Section"] = None, ) -> "Section": builtins = builtins or BUILTINS_DIR section: Type["Section"] = type(name, inherit or (Section,), {}) instance: Section = section(path, root, parent, builtins) node = node or red.RedBaron(code) for child in node.node_list: if isinstance(child, red.ClassNode): root_arg = instance if root is None else root child_inherit = [] for name in child.inherit_from.dumps().split(","): name = name.strip() if name: child_inherit.append(type(attrgetter(name)(root_arg))) instance._set_section(section.from_source_code( code = code, path = path, builtins = builtins, inherit = tuple(child_inherit), node = child, name = child.name, root = root_arg, parent = instance, )) elif isinstance(child, red.AssignmentNode): if isinstance(child.target, red.NameNode): name = child.target.value else: name = str(child.target.to_python()) instance._set_property( name, child.annotation.dumps() if child.annotation else "", child.value.dumps(), ) else: env = instance.globals exec(child.dumps(), dict(env), env) # nosec if isinstance(child, red.DefNode): instance._set_method(child.name, env[child.name]) return instance @classmethod def from_file( cls, path: Union[str, Path], builtins: Union[str, Path] = BUILTINS_DIR, ) -> "Section": path = Path(re.sub(r"^qrc:/", "", str(path))) try: content = pyotherside.qrc_get_file_contents(str(path)).decode() except ValueError: # App was compiled without QRC content = path.read_text() return Section.from_source_code(content, path, Path(builtins)) mirage-0.7.2/src/backend/presence.py000066400000000000000000000066641407747233600173510ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import dataclass, field from datetime import datetime from typing import TYPE_CHECKING, Dict, Optional from .utils import AutoStrEnum, auto if TYPE_CHECKING: from .models.items import Account, Member ORDER: Dict[str, int] = { "online": 0, "unavailable": 1, "invisible": 2, "offline": 3, } @dataclass class Presence: """Represents a single matrix user's presence fields. These objects are stored in `Backend.presences`, indexed by user ID. It must only be instanced when receiving a `PresenceEvent` or registering an `Account` model item. When receiving a `PresenceEvent`, we get or create a `Presence` object in `Backend.presences` for the targeted user. If the user is registered in any room, add its `Member` model item to `members`. Finally, update every `Member` presence fields inside `members`. When a room member is registered, we try to find a `Presence` in `Backend.presences` for that user ID. If found, the `Member` item is added to `members`. When an Account model is registered, we create a `Presence` in `Backend.presences` for the accountu's user ID whether the server supports presence or not (we cannot know yet at this point), and assign that `Account` to the `Presence.account` field. Special attributes: members: A `{room_id: Member}` dict for storing room members related to this `Presence`. As each room has its own `Member`s objects, we have to keep track of their presence fields. `Member`s are indexed by room ID. account: `Account` related to this `Presence`, if any. Should be assigned when client starts (`MatrixClient._start()`) and cleared when client stops (`MatrixClient._start()`). """ class State(AutoStrEnum): offline = auto() # can mean offline, invisible or unknwon unavailable = auto() online = auto() invisible = auto() def __lt__(self, other: "Presence.State") -> bool: return ORDER[self.value] < ORDER[other.value] presence: State = State.offline currently_active: bool = False last_active_at: datetime = datetime.fromtimestamp(0) status_msg: str = "" members: Dict[str, "Member"] = field(default_factory=dict) account: Optional["Account"] = None def update_members(self) -> None: """Update presence fields of every `Member` in `members`. Currently it is only called when receiving a `PresenceEvent` and when registering room members. """ for member in self.members.values(): member.set_fields( presence = self.presence, status_msg = self.status_msg, last_active_at = self.last_active_at, currently_active = self.currently_active, ) def update_account(self) -> None: """Update presence fields of `Account` related to this `Presence`.""" if self.account: self.account.set_fields( presence = self.presence, status_msg = self.status_msg, last_active_at = self.last_active_at, currently_active = self.currently_active, ) mirage-0.7.2/src/backend/pyotherside_events.py000066400000000000000000000064301407747233600214570ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Type, Union import pyotherside from .utils import serialize_value_for_qml if TYPE_CHECKING: from .models import SyncId from .user_files import UserFile @dataclass class PyOtherSideEvent: """Event that will be sent on instanciation to QML by PyOtherSide.""" def __post_init__(self) -> None: # XXX: CPython 3.6 or any Python implemention >= 3.7 is required for # correct __dataclass_fields__ dict order. args = [ serialize_value_for_qml(getattr(self, field)) for field in self.__dataclass_fields__ # type: ignore if field != "callbacks" ] pyotherside.send(type(self).__name__, *args) @dataclass class NotificationRequested(PyOtherSideEvent): """Request a notification bubble, sound or window urgency hint. Urgency hints usually flash or highlight the program's icon in a taskbar, dock or panel. """ id: str = field() critical: bool = False bubble: bool = False sound: bool = False urgency_hint: bool = False # Bubble parameters title: str = "" body: str = "" image: Union[Path, str] = "" @dataclass class CoroutineDone(PyOtherSideEvent): """Indicate that an asyncio coroutine finished.""" uuid: str = field() result: Any = None exception: Optional[Exception] = None traceback: Optional[str] = None @dataclass class LoopException(PyOtherSideEvent): """Indicate an uncaught exception occurance in the asyncio loop.""" message: str = field() exception: Optional[Exception] = field() traceback: Optional[str] = None @dataclass class Pre070SettingsDetected(PyOtherSideEvent): """Warn that a pre-0.7.0 settings.json file exists.""" path: Path = field() @dataclass class UserFileChanged(PyOtherSideEvent): """Indicate that a config or data file changed on disk.""" type: Type["UserFile"] = field() new_data: Any = field() @dataclass class ModelEvent(PyOtherSideEvent): """Base class for model change events.""" sync_id: "SyncId" = field() @dataclass class ModelItemSet(ModelEvent): """Indicate `ModelItem` insert or field changes in a `Backend` `Model`.""" index_then: Optional[int] = field() index_now: int = field() fields: Dict[str, Any] = field() @dataclass class ModelItemDeleted(ModelEvent): """Indicate the removal of a `ModelItem` from a `Backend` `Model`.""" index: int = field() count: int = 1 ids: Sequence[Any] = () @dataclass class ModelCleared(ModelEvent): """Indicate that a `Backend` `Model` was cleared.""" @dataclass class DevicesUpdated(PyOtherSideEvent): """Indicate changes in devices for us or users we share a room with.""" our_user_id: str = field() @dataclass class InvalidAccessToken(PyOtherSideEvent): """Indicate one of our account's access token is invalid or revoked.""" user_id: str = field() mirage-0.7.2/src/backend/qml_bridge.py000066400000000000000000000140401407747233600176350ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later # WARNING: make sure to not top-level import the media_cache module here, # directly or indirectly via another module import (e.g. backend). # See https://stackoverflow.com/a/55918049 """Provide `BRIDGE`, main object accessed by QML to interact with Python. PyOtherSide, the library that handles interaction between our Python backend and QML UI, will access the `BRIDGE` object and call its methods directly. The `BRIDGE` object should be the only existing instance of the `QMLBridge` class. """ import asyncio import logging as log import os import traceback from concurrent.futures import Future from operator import attrgetter from threading import Thread from typing import Coroutine, Dict, Sequence, Set import pyotherside from .pyotherside_events import CoroutineDone, LoopException class QMLBridge: """Setup asyncio and provide methods to call coroutines from QML. A thread is created to run the asyncio loop in, to ensure all calls from QML return instantly. Synchronous methods are provided for QML to call coroutines using PyOtherSide, which doesn't have this ability out of the box. Attributes: backend: The `backend.Backend` object containing general coroutines for QML and that manages `MatrixClient` objects. """ def __init__(self) -> None: try: self._loop = asyncio.get_event_loop() except RuntimeError: self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._loop.set_exception_handler(self._loop_exception_handler) from .backend import Backend self.backend: Backend = Backend() self._running_futures: Dict[str, Future] = {} self._cancelled_early: Set[str] = set() Thread(target=self._start_asyncio_loop).start() def _loop_exception_handler( self, loop: asyncio.AbstractEventLoop, context: dict, ) -> None: if "exception" in context: err = context["exception"] trace = "".join( traceback.format_exception(type(err), err, err.__traceback__), ) LoopException(context["message"], err, trace) loop.default_exception_handler(context) def _start_asyncio_loop(self) -> None: asyncio.set_event_loop(self._loop) self._loop.run_forever() def _call_coro(self, coro: Coroutine, uuid: str) -> None: """Schedule a coroutine to run in our thread and return a `Future`.""" if uuid in self._cancelled_early: self._cancelled_early.remove(uuid) return def on_done(future: Future) -> None: """Send a PyOtherSide event with the coro's result/exception.""" result = exception = trace = None try: result = future.result() except Exception as err: # noqa exception = err trace = traceback.format_exc().rstrip() CoroutineDone(uuid, result, exception, trace) del self._running_futures[uuid] future = asyncio.run_coroutine_threadsafe(coro, self._loop) self._running_futures[uuid] = future future.add_done_callback(on_done) def call_backend_coro( self, name: str, uuid: str, args: Sequence[str] = (), ) -> None: """Schedule a coroutine from the `QMLBridge.backend` object.""" if uuid in self._cancelled_early: self._cancelled_early.remove(uuid) else: self._call_coro(attrgetter(name)(self.backend)(*args), uuid) def call_client_coro( self, user_id: str, name: str, uuid: str, args: Sequence[str] = (), ) -> None: """Schedule a coroutine from a `QMLBridge.backend.clients` client.""" if uuid in self._cancelled_early: self._cancelled_early.remove(uuid) else: client = self.backend.clients[user_id] self._call_coro(attrgetter(name)(client)(*args), uuid) def cancel_coro(self, uuid: str) -> None: """Cancel a couroutine scheduled by the `QMLBridge` methods.""" if uuid in self._running_futures: self._running_futures[uuid].cancel() else: self._cancelled_early.add(uuid) def pdb(self, extra_data: Sequence = (), remote: bool = False) -> None: """Call the python debugger, defining some conveniance variables.""" ad = extra_data # noqa ba = self.backend # noqa mo = self.backend.models # noqa cl = self.backend.clients gcl = lambda user: cl[f"@{user}"] # noqa rc = lambda c: asyncio.run_coroutine_threadsafe(c, self._loop) # noqa try: from devtools import debug # noqa d = debug # noqa except ModuleNotFoundError: log.warning("Module python-devtools not found, can't use debug()") if remote: # Run `socat readline tcp:127.0.0.1:4444` in a terminal to connect import remote_pdb remote_pdb.RemotePdb("127.0.0.1", 4444).set_trace() else: import pdb pdb.set_trace() def exit(self) -> None: try: asyncio.run_coroutine_threadsafe( self.backend.terminate_clients(), self._loop, ).result() except Exception as e: # noqa print(e) # The AppImage AppRun script overwrites some environment path variables to # correctly work, and sets RESTORE_ equivalents with the original values. # If the app is launched from an AppImage, now restore the original values # to prevent problems like QML Qt.openUrlExternally() failing because # the external launched program is affected by our AppImage-specific variables. for var in ("LD_LIBRARY_PATH", "PYTHONHOME", "PYTHONUSERBASE"): if f"RESTORE_{var}" in os.environ: os.environ[var] = os.environ[f"RESTORE_{var}"] BRIDGE = QMLBridge() pyotherside.atexit(BRIDGE.exit) mirage-0.7.2/src/backend/sso_server.py000066400000000000000000000065221407747233600177300ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later import asyncio from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, quote, urlparse from . import __display_name__ _SUCCESS_HTML_PAGE = """ """ + __display_name__ + """
""" class _SSORequestHandler(BaseHTTPRequestHandler): def do_GET(self) -> None: self.server: "SSOServer" redirect = "%s/_matrix/client/r0/login/sso/redirect?redirectUrl=%s" % ( self.server.for_homeserver, quote(self.server.url_to_open), ) parameters = parse_qs(urlparse(self.path).query) if "loginToken" in parameters: self.server._token = parameters["loginToken"][0] self.send_response(200) # OK self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(_SUCCESS_HTML_PAGE.encode()) else: self.send_response(308) # Permanent redirect, same method only self.send_header("Location", redirect) self.end_headers() self.close_connection = True class SSOServer(HTTPServer): """Local HTTP server to retrieve a SSO login token. Call `SSOServer.wait_for_token()` in a background task to start waiting for a SSO login token from the Matrix homeserver. Once the task is running, the user must open `SSOServer.url_to_open` in their browser, where they will be able to complete the login process. Once they are done, the homeserver will call us back with a login token and the `SSOServer.wait_for_token()` task will return. """ def __init__(self, for_homeserver: str) -> None: self.for_homeserver: str = for_homeserver self._token: str = "" # Pick the first available port super().__init__(("127.0.0.1", 0), _SSORequestHandler) @property def url_to_open(self) -> str: """URL for the user to open in their browser, to do the SSO process.""" return f"http://{self.server_address[0]}:{self.server_port}" async def wait_for_token(self) -> str: """Wait until the homeserver gives us a login token and return it.""" loop = asyncio.get_event_loop() while not self._token: await loop.run_in_executor(None, self.handle_request) return self._token mirage-0.7.2/src/backend/theme_parser.py000066400000000000000000000052341407747233600202130ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """QPL (Qt Property Language) theme files to QML converter. QPL is a custom configuration format currently used for theme files. This is a big hack and will be replaced in the future by a standard language. """ import re from typing import Generator PROPERTY_TYPES = {"bool", "double", "int", "list", "real", "string", "url", "var", "date", "point", "rect", "size", "color"} def _add_property(line: str) -> str: """Return a QML property declaration line from a QPL property line.""" if re.match(r"^\s*[a-zA-Z\d_]+\s*:$", line): return re.sub(r"^(\s*)(\S*\s*):$", r"\1readonly property QtObject \2: QtObject", line) types = "|".join(PROPERTY_TYPES) if re.match(fr"^\s*({types}) [a-zA-Z\d_]+\s*:", line): return re.sub(r"^(\s*)(\S*)", r"\1property \2", line) return line def _process_lines(content: str) -> Generator[str, None, None]: """Yield lines of real QML from lines of QPL.""" skip = False indent = " " * 4 current_indent = 0 for line in content.split("\n"): line = line.rstrip() if not line.strip() or line.strip().startswith("//"): continue start_space_list = re.findall(r"^ +", line) start_space = start_space_list[0] if start_space_list else "" line_indents = len(re.findall(indent, start_space)) if not skip: if line_indents > current_indent: yield "%s{" % (indent * current_indent) current_indent = line_indents while line_indents < current_indent: current_indent -= 1 yield "%s}" % (indent * current_indent) line = _add_property(line) yield line skip = any((line.endswith(e) for e in "([{+\\,?:")) while current_indent: current_indent -= 1 yield "%s}" % (indent * current_indent) def convert_to_qml(theme_content: str) -> str: """Return valid QML code with imports from QPL content.""" theme_content = theme_content.replace("\t", " ") lines = [ "import QtQuick 2.12", 'import "../Base"', "QtObject {", " function hsluv(h, s, l, a) { return utils.hsluv(h, s, l, a) }", " function hsl(h, s, l) { return utils.hsl(h, s, l) }", " function hsla(h, s, l, a) { return utils.hsla(h, s, l, a) }", " id: theme", ] lines += [f" {line}" for line in _process_lines(theme_content)] lines += ["}"] return "\n".join(lines) mirage-0.7.2/src/backend/user_files.py000066400000000000000000000403121407747233600176710ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """User data and configuration files definitions.""" import asyncio import json import os import traceback from collections.abc import MutableMapping from dataclasses import dataclass, field from pathlib import Path from typing import ( TYPE_CHECKING, Any, ClassVar, Dict, Iterator, Optional, Tuple, ) import pyotherside from watchgod import Change, awatch from .pcn.section import Section from .pyotherside_events import ( LoopException, Pre070SettingsDetected, UserFileChanged, ) from .theme_parser import convert_to_qml from .utils import ( aiopen, atomic_write, deep_serialize_for_qml, dict_update_recursive, flatten_dict_keys, ) if TYPE_CHECKING: from .backend import Backend @dataclass class UserFile: """Base class representing a user config or data file.""" create_missing: ClassVar[bool] = True backend: "Backend" = field(repr=False) filename: str = field() parent: Optional["UserFile"] = None children: Dict[Path, "UserFile"] = field(default_factory=dict) data: Any = field(init=False, default_factory=dict) _need_write: bool = field(init=False, default=False) _mtime: Optional[float] = field(init=False, default=None) _reader: Optional[asyncio.Future] = field(init=False, default=None) _writer: Optional[asyncio.Future] = field(init=False, default=None) def __post_init__(self) -> None: self.data = self.default_data self._need_write = self.create_missing if self.path.exists(): try: text = self.path.read_text() self.data, self._need_write = self.deserialized(text) except Exception as err: # noqa LoopException(str(err), err, traceback.format_exc().rstrip()) self._reader = asyncio.ensure_future(self._start_reader()) self._writer = asyncio.ensure_future(self._start_writer()) @property def path(self) -> Path: """Full path of the file to read, can exist or not exist.""" raise NotImplementedError() @property def write_path(self) -> Path: """Full path of the file to write, can exist or not exist.""" return self.path @property def default_data(self) -> Any: """Default deserialized content to use if the file doesn't exist.""" raise NotImplementedError() @property def qml_data(self) -> Any: """Data converted for usage in QML.""" return self.data def deserialized(self, data: str) -> Tuple[Any, bool]: """Return parsed data from file text and whether to call `save()`.""" return (data, False) def serialized(self) -> str: """Return text from `UserFile.data` that can be written to disk.""" raise NotImplementedError() def save(self) -> None: """Inform the disk writer coroutine that the data has changed.""" self._need_write = True def stop_watching(self) -> None: """Stop watching the on-disk file for changes.""" if self._reader: self._reader.cancel() if self._writer: self._writer.cancel() for child in self.children.values(): child.stop_watching() async def set_data(self, data: Any) -> None: """Set `data` and call `save()`, conveniance method for QML.""" self.data = data self.save() async def update_from_file(self) -> None: """Read file at `path`, update `data` and call `save()` if needed.""" if not self.path.exists(): self.data = self.default_data self._need_write = self.create_missing return async with aiopen(self.path) as file: self.data, self._need_write = self.deserialized(await file.read()) async def _start_reader(self) -> None: """Disk reader coroutine, watches for file changes to update `data`.""" while not self.path.exists(): await asyncio.sleep(1) async for changes in awatch(self.path): try: ignored = 0 for change in changes: if change[0] in (Change.added, Change.modified): mtime = self.path.stat().st_mtime if mtime == self._mtime: ignored += 1 continue await self.update_from_file() self._mtime = mtime elif change[0] == Change.deleted: self._mtime = None self.data = self.default_data self._need_write = self.create_missing if changes and ignored < len(changes): UserFileChanged(type(self), self.qml_data) parent = self.parent while parent: await parent.update_from_file() UserFileChanged(type(parent), parent.qml_data) parent = parent.parent while not self.path.exists(): # Prevent error spam after file gets deleted await asyncio.sleep(0.5) except Exception as err: # noqa LoopException(str(err), err, traceback.format_exc().rstrip()) async def _start_writer(self) -> None: """Disk writer coroutine, update the file with a 1 second cooldown.""" if self.write_path.parts[0] == "qrc:": return self.write_path.parent.mkdir(parents=True, exist_ok=True) while True: await asyncio.sleep(1) try: if self._need_write: async with atomic_write(self.write_path) as (new, done): await new.write(self.serialized()) done() self._need_write = False self._mtime = self.write_path.stat().st_mtime except Exception as err: # noqa self._need_write = False LoopException(str(err), err, traceback.format_exc().rstrip()) @dataclass class ConfigFile(UserFile): """A file that goes in the configuration directory, e.g. ~/.config/app.""" @property def path(self) -> Path: return Path( os.environ.get("MIRAGE_CONFIG_DIR") or self.backend.appdirs.user_config_dir, ) / self.filename @dataclass class UserDataFile(UserFile): """A file that goes in the user data directory, e.g. ~/.local/share/app.""" @property def path(self) -> Path: return Path( os.environ.get("MIRAGE_DATA_DIR") or self.backend.appdirs.user_data_dir, ) / self.filename @dataclass class MappingFile(MutableMapping, UserFile): """A file manipulable like a dict. `data` must be a mutable mapping.""" def __getitem__(self, key: Any) -> Any: return self.data[key] def __setitem__(self, key: Any, value: Any) -> None: self.data[key] = value def __delitem__(self, key: Any) -> None: del self.data[key] def __iter__(self) -> Iterator: return iter(self.data) def __len__(self) -> int: return len(self.data) def __getattr__(self, key: Any) -> Any: try: return self.data[key] except KeyError: return super().__getattribute__(key) def __setattr__(self, key: Any, value: Any) -> None: if key in self.__dataclass_fields__: super().__setattr__(key, value) return self.data[key] = value def __delattr__(self, key: Any) -> None: del self.data[key] @dataclass class JSONFile(MappingFile): """A file stored on disk in the JSON format.""" @property def default_data(self) -> dict: return {} def deserialized(self, data: str) -> Tuple[dict, bool]: """Return parsed data from file text and whether to call `save()`. If the file has missing keys, the missing data will be merged to the returned dict and the second tuple item will be `True`. """ loaded = json.loads(data) all_data = self.default_data.copy() dict_update_recursive(all_data, loaded) return (all_data, loaded != all_data) def serialized(self) -> str: data = self.data return json.dumps(data, indent=4, ensure_ascii=False, sort_keys=True) @dataclass class PCNFile(MappingFile): """File stored in the PCN format, with machine edits in a separate JSON.""" create_missing = False path_override: Optional[Path] = None @property def path(self) -> Path: return self.path_override or super().path @property def write_path(self) -> Path: """Full path of file where programatically-done edits are stored.""" return self.path.with_suffix(".gui.json") @property def qml_data(self) -> Dict[str, Any]: return deep_serialize_for_qml(self.data.as_dict()) # type: ignore @property def default_data(self) -> Section: return Section() def deserialized(self, data: str) -> Tuple[Section, bool]: root = Section.from_source_code(data, self.path) edits = "{}" if self.write_path.exists(): edits = self.write_path.read_text() includes_now = list(root.all_includes) for path, pcn in self.children.copy().items(): if path not in includes_now: pcn.stop_watching() del self.children[path] for path in includes_now: if path not in self.children: self.children[path] = PCNFile( self.backend, filename = path.name, parent = self, path_override = path, ) return (root, root.deep_merge_edits(json.loads(edits))) def serialized(self) -> str: edits = self.data.edits_as_dict() return json.dumps(edits, indent=4, ensure_ascii=False) async def set_data(self, data: Dict[str, Any]) -> None: self.data.deep_merge_edits({"set": data}, has_expressions=False) self.save() @dataclass class Accounts(ConfigFile, JSONFile): """Config file for saved matrix accounts: user ID, access tokens, etc""" filename: str = "accounts.json" async def any_saved(self) -> bool: """Return for QML whether there are any accounts saved on disk.""" return bool(self.data) async def add(self, user_id: str) -> None: """Add an account to the config and write it on disk. The account's details such as its access token are retrieved from the corresponding `MatrixClient` in `backend.clients`. """ client = self.backend.clients[user_id] account = self.backend.models["accounts"][user_id] self.update({ client.user_id: { "homeserver": client.homeserver, "token": client.access_token, "device_id": client.device_id, "enabled": True, "presence": account.presence.value.replace("echo_", ""), "status_msg": account.status_msg, "order": account.order, }, }) self.save() async def set( self, user_id: str, enabled: Optional[str] = None, presence: Optional[str] = None, order: Optional[int] = None, status_msg: Optional[str] = None, ) -> None: """Update an account if found in the config file and write to disk.""" if user_id not in self: return if enabled is not None: self[user_id]["enabled"] = enabled if presence is not None: self[user_id]["presence"] = presence if order is not None: self[user_id]["order"] = order if status_msg is not None: self[user_id]["status_msg"] = status_msg self.save() async def forget(self, user_id: str) -> None: """Delete an account from the config and write it on disk.""" self.pop(user_id, None) self.save() @dataclass class Pre070Settings(ConfigFile): """Detect and warn about the presence of a pre-0.7.0 settings.json file.""" filename: str = "settings.json" def __post_init__(self) -> None: if self.path.exists(): Pre070SettingsDetected(self.path) @dataclass class Settings(ConfigFile, PCNFile): """General config file for UI and backend settings""" filename: str = "settings.py" @property def default_data(self) -> Section: root = Section.from_file("src/config/settings.py") edits = "{}" if self.write_path.exists(): edits = self.write_path.read_text() root.deep_merge_edits(json.loads(edits)) return root def deserialized(self, data: str) -> Tuple[Section, bool]: section, save = super().deserialized(data) if self and self.General.theme != section.General.theme: if hasattr(self.backend, "theme"): self.backend.theme.stop_watching() self.backend.theme = Theme( self.backend, section.General.theme, # type: ignore ) UserFileChanged(Theme, self.backend.theme.qml_data) # if self and self.General.new_theme != section.General.new_theme: # self.backend.new_theme.stop_watching() # self.backend.new_theme = NewTheme( # self.backend, section.General.new_theme, # type: ignore # ) # UserFileChanged(Theme, self.backend.new_theme.qml_data) return (section, save) @dataclass class NewTheme(UserDataFile, PCNFile): """A theme file defining the look of QML components.""" create_missing = False @property def path(self) -> Path: data_dir = Path( os.environ.get("MIRAGE_DATA_DIR") or self.backend.appdirs.user_data_dir, ) return data_dir / "themes" / self.filename @property def qml_data(self) -> Dict[str, Any]: return flatten_dict_keys(super().qml_data, last_level=False) @dataclass class UIState(UserDataFile, JSONFile): """File used to save and restore the state of QML components.""" filename: str = "state.json" @property def default_data(self) -> dict: return { "collapseAccounts": {}, "page": "Pages/Default.qml", "pageProperties": {}, } def deserialized(self, data: str) -> Tuple[dict, bool]: dict_data, save = super().deserialized(data) for user_id, do in dict_data["collapseAccounts"].items(): self.backend.models["all_rooms"].set_account_collapse(user_id, do) return (dict_data, save) @dataclass class History(UserDataFile, JSONFile): """File to save and restore lines typed by the user in QML components.""" filename: str = "history.json" @property def default_data(self) -> dict: return {"console": []} @dataclass class Theme(UserDataFile): """A theme file defining the look of QML components.""" # Since it currently breaks at every update and the file format will be # changed later, don't copy the theme to user data dir if it doesn't exist. create_missing = False @property def path(self) -> Path: data_dir = Path( os.environ.get("MIRAGE_DATA_DIR") or self.backend.appdirs.user_data_dir, ) return data_dir / "themes" / self.filename @property def default_data(self) -> str: if self.filename in ("Midnight.qpl", "Glass.qpl"): path = f"src/themes/{self.filename}" else: path = "src/themes/Midnight.qpl" try: byte_content = pyotherside.qrc_get_file_contents(path) except ValueError: # App was compiled without QRC return convert_to_qml(Path(path).read_text()) else: return convert_to_qml(byte_content.decode()) def deserialized(self, data: str) -> Tuple[str, bool]: return (convert_to_qml(data), False) mirage-0.7.2/src/backend/utils.py000066400000000000000000000257141407747233600167020ustar00rootroot00000000000000# Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later """Various utilities that are used throughout the package.""" import asyncio import collections import html import inspect import io import json import re import sys import xml.etree.cElementTree as xml_etree from concurrent.futures import ProcessPoolExecutor from contextlib import suppress from datetime import date, datetime, time, timedelta from enum import Enum from enum import auto as autostr from pathlib import Path from tempfile import NamedTemporaryFile from types import ModuleType from typing import ( Any, AsyncIterator, Callable, Collection, Dict, Iterable, Mapping, Optional, Tuple, Type, Union, ) from uuid import UUID import aiofiles import filetype from aiofiles.threadpool.binary import AsyncBufferedIOBase from nio.crypto import AsyncDataT as File from nio.crypto import async_generator_from_data from PIL import Image as PILImage from .color import Color from .pcn.section import Section if sys.version_info >= (3, 7): from contextlib import asynccontextmanager current_task = asyncio.current_task else: from async_generator import asynccontextmanager current_task = asyncio.Task.current_task Size = Tuple[int, int] BytesOrPIL = Union[bytes, PILImage.Image] auto = autostr COMPRESSION_POOL = ProcessPoolExecutor() class AutoStrEnum(Enum): """An Enum where auto() assigns the member's name instead of an integer. Example: >>> class Fruits(AutoStrEnum): apple = auto() >>> Fruits.apple.value "apple" """ @staticmethod def _generate_next_value_(name, *_): return name def dict_update_recursive(dict1: dict, dict2: dict) -> None: """Deep-merge `dict1` and `dict2`, recursive version of `dict.update()`.""" # https://gist.github.com/angstwad/bf22d1822c38a92ec0a9 for k in dict2: if (k in dict1 and isinstance(dict1[k], dict) and isinstance(dict2[k], collections.Mapping)): dict_update_recursive(dict1[k], dict2[k]) else: dict1[k] = dict2[k] def flatten_dict_keys( source: Optional[Dict[str, Any]] = None, separator: str = ".", last_level: bool = True, _flat: Optional[Dict[str, Any]] = None, _prefix: str = "", ) -> Dict[str, Any]: """Return a flattened version of the ``source`` dict. Example: >>> dct {"content": {"body": "foo"}, "m.test": {"key": {"bar": 1}}} >>> flatten_dict_keys(dct) {"content.body": "foo", "m.test.key.bar": 1} >>> flatten_dict_keys(dct, last_level=False) {"content": {"body": "foo"}, "m.test.key": {bar": 1}} """ flat = {} if _flat is None else _flat for key, value in (source or {}).items(): if isinstance(value, dict): prefix = f"{_prefix}{key}{separator}" flatten_dict_keys(value, separator, last_level, flat, prefix) elif last_level: flat[f"{_prefix}{key}"] = value else: prefix = _prefix[:-len(separator)] # remove trailing separator flat.setdefault(prefix, {})[key] = value return flat def config_get_account_room_rule( rules: Section, user_id: str, room_id: str, ) -> Any: """Return best matching rule value for an account/room PCN free Section.""" for name, value in reversed(rules.children()): name = re.sub(r"\s+", " ", name.strip()) if name in (user_id, room_id, f"{user_id} {room_id}"): return value return rules.default async def is_svg(file: File) -> bool: """Return whether the file is a SVG (`lxml` is used for detection).""" chunks = [c async for c in async_generator_from_data(file)] with io.BytesIO(b"".join(chunks)) as file: try: _, element = next(xml_etree.iterparse(file, ("start",))) return element.tag == "{http://www.w3.org/2000/svg}svg" except (StopIteration, xml_etree.ParseError): return False async def svg_dimensions(file: File) -> Size: """Return the width and height, or viewBox width and height for a SVG. If these properties are missing (broken file), ``(256, 256)`` is returned. """ chunks = [c async for c in async_generator_from_data(file)] with io.BytesIO(b"".join(chunks)) as file: attrs = xml_etree.parse(file).getroot().attrib try: width = round(float(attrs.get("width", attrs["viewBox"].split()[3]))) except (KeyError, IndexError, ValueError, TypeError): width = 256 try: height = round(float(attrs.get("height", attrs["viewBox"].split()[4]))) except (KeyError, IndexError, ValueError, TypeError): height = 256 return (width, height) async def guess_mime(file: File) -> str: """Return the file's mimetype, or `application/octet-stream` if unknown.""" if isinstance(file, io.IOBase): file.seek(0, 0) elif isinstance(file, AsyncBufferedIOBase): await file.seek(0, 0) try: first_chunk: bytes async for first_chunk in async_generator_from_data(file): break else: return "inode/x-empty" # empty file # TODO: plaintext mime = filetype.guess_mime(first_chunk) return mime or ( "image/svg+xml" if await is_svg(file) else "application/octet-stream" ) finally: if isinstance(file, io.IOBase): file.seek(0, 0) elif isinstance(file, AsyncBufferedIOBase): await file.seek(0, 0) def plain2html(text: str) -> str: """Convert `\\n` into `
` tags and `\\t` into four spaces.""" return html.escape(text)\ .replace("\n", "
")\ .replace("\t", " " * 4) def strip_html_tags(text: str) -> str: """Remove HTML tags from text.""" return re.sub(r"<\/?[^>]+(>|$)", "", text) def serialize_value_for_qml( value: Any, json_list_dicts: bool = False, reject_unknown: bool = False, ) -> Any: """Convert a value to make it easier to use from QML. Returns: - For `bool`, `int`, `float`, `bytes`, `str`, `datetime`, `date`, `time`: the unchanged value (PyOtherSide handles these) - For `Collection` objects (includes `list` and `dict`): a JSON dump if `json_list_dicts` is `True`, else the unchanged value - If the value is an instancied object and has a `serialized` attribute or property, return that - For `Enum` members, the actual value of the member - For `Path` objects, a `file://` string - For `UUID` object: the UUID in string form - For `timedelta` objects: the delta as a number of milliseconds `int` - For `Color` objects: the color's hexadecimal value - For class types: the class `__name__` - For anything else: raise a `TypeError` if `reject_unknown` is `True`, else return the unchanged value. """ if isinstance(value, (bool, int, float, bytes, str, datetime, date, time)): return value if json_list_dicts and isinstance(value, Collection): if isinstance(value, set): value = list(value) return json.dumps(value) if not inspect.isclass(value) and hasattr(value, "serialized"): return value.serialized if isinstance(value, Iterable): return value if hasattr(value, "__class__") and issubclass(value.__class__, Enum): return value.value if isinstance(value, Path): return f"file://{value!s}" if isinstance(value, UUID): return str(value) if isinstance(value, timedelta): return value.total_seconds() * 1000 if isinstance(value, Color): return value.hex if inspect.isclass(value): return value.__name__ if reject_unknown: raise TypeError("Unknown type reject") return value def deep_serialize_for_qml(obj: Iterable) -> Union[list, dict]: """Recursively serialize lists and dict values for QML.""" if isinstance(obj, Mapping): dct = {} for key, value in obj.items(): if isinstance(value, Iterable) and not isinstance(value, str): # PyOtherSide only accept dicts with string keys dct[str(key)] = deep_serialize_for_qml(value) continue with suppress(TypeError): dct[str(key)] = \ serialize_value_for_qml(value, reject_unknown=True) return dct lst = [] for value in obj: if isinstance(value, Iterable) and not isinstance(value, str): lst.append(deep_serialize_for_qml(value)) continue with suppress(TypeError): lst.append(serialize_value_for_qml(value, reject_unknown=True)) return lst def classes_defined_in(module: ModuleType) -> Dict[str, Type]: """Return a `{name: class}` dict of all the classes a module defines.""" return { m[0]: m[1] for m in inspect.getmembers(module, inspect.isclass) if not m[0].startswith("_") and m[1].__module__.startswith(module.__name__) } @asynccontextmanager async def aiopen(*args, **kwargs) -> AsyncIterator[Any]: """Wrapper for `aiofiles.open()` that doesn't break mypy""" async with aiofiles.open(*args, **kwargs) as file: yield file @asynccontextmanager async def atomic_write( path: Union[Path, str], binary: bool = False, **kwargs, ) -> AsyncIterator[Tuple[Any, Callable[[], None]]]: """Write a file asynchronously (using aiofiles) and atomically. Yields a `(open_temporary_file, done_function)` tuple. The done function should be called after writing to the given file. When the context manager exits, the temporary file will either replace `path` if the function was called, or be deleted. Example: >>> async with atomic_write("foo.txt") as (file, done): >>> await file.write("Sample text") >>> done() """ mode = "wb" if binary else "w" path = Path(path) temp = NamedTemporaryFile(dir=path.parent, delete=False) temp_path = Path(temp.name) can_replace = False def done() -> None: nonlocal can_replace can_replace = True try: async with aiopen(temp_path, mode, **kwargs) as out: yield (out, done) finally: if can_replace: temp_path.replace(path) else: temp_path.unlink() def _compress(image: BytesOrPIL, fmt: str, optimize: bool) -> bytes: if isinstance(image, bytes): pil_image = PILImage.open(io.BytesIO(image)) else: pil_image = image with io.BytesIO() as buffer: pil_image.save(buffer, fmt, optimize=optimize) return buffer.getvalue() async def compress_image( image: BytesOrPIL, fmt: str = "PNG", optimize: bool = True, ) -> bytes: """Compress image in a separate process, without blocking event loop.""" return await asyncio.get_event_loop().run_in_executor( COMPRESSION_POOL, _compress, image, fmt, optimize, ) mirage-0.7.2/src/clipboard.h000066400000000000000000000061401407747233600157010ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later // The Clipboard class exposes system clipboard management and retrieval // to QML. #ifndef CLIPBOARD_H #define CLIPBOARD_H #include #include #include #include #include #include #include #include #include #include class Clipboard : public QObject { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QByteArray image READ image WRITE setImage NOTIFY imageChanged) Q_PROPERTY(bool hasImage READ hasImage NOTIFY hasImageChanged) Q_PROPERTY(QString selection READ selection WRITE setSelection NOTIFY selectionChanged) Q_PROPERTY(bool supportsSelection READ supportsSelection CONSTANT) public: explicit Clipboard(QObject *parent = nullptr) : QObject(parent) { connect(this->clipboard, &QClipboard::dataChanged, this, &Clipboard::mainClipboardChanged); connect(this->clipboard, &QClipboard::selectionChanged, this, &Clipboard::selectionChanged); } // Normal primary clipboard QString text() const { return this->clipboard->text(QClipboard::Clipboard); } void setText(const QString &text) const { this->clipboard->setText(text, QClipboard::Clipboard); } QImage *qimage() { QMutexLocker locker(&(this->imageRetrievalLock)); if (this->cachedImage.isNull()) this->cachedImage = this->clipboard->image(); return &(this->cachedImage); } QByteArray image() { QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); QImage *image = this->qimage(); // minimum compression, fastest to not freeze the UI image->save(&buffer, "PNG", 100); buffer.close(); return byteArray; } void setImage(const QByteArray &image) const { // TODO Q_UNUSED(image) } bool hasImage() const { return this->clipboard->mimeData()->hasImage(); } // X11 select-middle-click-paste clipboard QString selection() const { return this->clipboard->text(QClipboard::Selection); } void setSelection(const QString &text) const { if (this->clipboard->supportsSelection()) { this->clipboard->setText(text, QClipboard::Selection); } } // Info bool supportsSelection() const { return this->clipboard->supportsSelection(); } signals: void contentChanged(); void textChanged(); void imageChanged(); void hasImageChanged(); void selectionChanged(); private: QClipboard *clipboard = QGuiApplication::clipboard(); QImage cachedImage = QImage(); QMutex imageRetrievalLock; void mainClipboardChanged() { this->contentChanged(); this->cachedImage = QImage(); this->hasImage() ? this->imageChanged() : this->textChanged(); this->hasImageChanged(); }; }; #endif mirage-0.7.2/src/clipboard_image_provider.h000066400000000000000000000017371407747233600207640ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later #ifndef CLIPBOARD_IMAGE_PROVIDER_H #define CLIPBOARD_IMAGE_PROVIDER_H #include #include #include "clipboard.h" class ClipboardImageProvider : public QQuickImageProvider { public: explicit ClipboardImageProvider(Clipboard *clipboard) : QQuickImageProvider(QQuickImageProvider::Image) { this->clipboard = clipboard; } QImage requestImage( const QString &id, QSize *size, const QSize &requestSize ) { Q_UNUSED(id); QImage *image = this->clipboard->qimage(); if (size) *size = image->size(); if (requestSize.width() > 0 && requestSize.height() > 0) return image->scaled( requestSize.width(), requestSize.height(), Qt::KeepAspectRatio ); return *image; } private: Clipboard *clipboard; }; #endif mirage-0.7.2/src/config/000077500000000000000000000000001407747233600150355ustar00rootroot00000000000000mirage-0.7.2/src/config/settings.py000066400000000000000000000537001407747233600172540ustar00rootroot00000000000000# pylint: skip-file # flake8: noqa # mypy: ignore-errors class General: # When closing the window, minimize the application to system tray instead # of quitting the application. # A click on the tray icon reveals the window, middle click fully quits it # and right click opens a menu with these options. close_to_tray: bool = False # Show rooms, members and messages in way that takes less vertical space. compact: bool = False # When the window width is less than this number of pixels, switch to a # mobile-like mode where only the left main pane, center page/chat or # right room pane is visible at a time. hide_side_panes_under: int = 450 # Whether to wrap around or do nothing when using the earlier_page or # later_page keybinds and reaching the start or end of the history. wrap_history: bool = True # How many seconds the cursor must hover on buttons and other elements # to show tooltips. tooltips_delay: float = 0.7 # Application theme to use. # Can be the name of a built-in theme (Mirage.qpl or Glass.qpl), or # the name (including extension) of a file in the user theme folder, which # is "$XDG_DATA_HOME/mirage/themes" if that environment variable is set, # else "~/.local/share/mirage/themes". # For Flatpak, it is # "~/.var/app/io.github.mirukana.mirage/data/mirage/themes". theme: str = "Midnight.qpl" # Interface scale multiplier, e.g. 0.5 makes everything half-size. zoom: float = 1.0 # Adress of an HTTP or SOCKS5 proxy to pass network traffic through. # Example adresses: "socks5://localhost:9050" (TOR with default port), # "http://username:password@123.456.7.8:80" (HTTP with authentication). # The application must be restarted to apply changes for this setting. proxy: Optional[str] = None class Presence: # Automatically set your presence to unavailable after this number of # seconds without any mouse or keyboard activity. # This currently only works on Linux X11. auto_away_after: int = 60 * 10 # Number of previously set status messages to keep saved. Available for # quick access in the context menu when right-clicking an account. saved_status: int = 5 class Notifications: # Default global notification level when starting the application. # Allows muting (i.e. preventing desktop bubbles, sounds and flashes) # all notifications in the running client, overriding account settings. # Can be either: # - "enable" (notifications will work normally) # - "highlights_only" (notify only for highlights, e.g. replies, keywords) # - "mute" (don't notify for anything) start_level: str = "enable" # Use HTML formatting in notification bubbles. # This option has no effect on Windows and OSX. # Rendering is only supported by notification servers which follow the # GNOME Desktop Notification Specification, set this to `False` if you # keep seeing raw in notifications. use_html: bool = True # Default sound to play for notifications. Can be the filename # of a builtin sound (only "default.wav" currently exists), or the # absolute path to a WAV file (supports ~ user expansion). default_sound: str = "default.wav" # How long in seconds the window will flash in your dock or taskbar when # a new message, which matches a notification push rule with a # window alert/flash/lightbulb action, is posted in a room. # The effect may differ depending on your OS/desktop. # Can be set to -1 for alerts that last until the window is focused. flash_time: float = 5 # Same as flash_time, but for messages that match a push rule with a # highlight (red [+1]) action. By default this includes when your # name is mentioned, replied to, or messages containing keywords. highlight_flash_time: float = -1 class Scrolling: # Use velocity-based kinetic scrolling. # Can cause problems on laptop touchpads and some special mouse wheels. kinetic: bool = True # Maximum allowed velocity when kinetic scrolling is used. kinetic_max_speed: int = 2500 # When kinetic scrolling is used, how fast the view slows down when you # stop physically scrolling. kinetic_deceleration: int = 1500 # Multiplier for the scrolling speed when kinetic scrolling is # disabled, e.g. 1.5 is 1.5x faster than the default speed. non_kinetic_speed: float = 1.0 class RoomList: # Prevent resizing the pane below this pixel width. min_width: int = 144 # Sort rooms in alphabetical order instead of recent activity. # The application must be restarted to apply changes for this setting. lexical_sort: bool = False # When any event is received in a room, mark the room as unread with a [!], # regardless of notification push rules. This does not take into account # anything received while the client is not running. local_unread_markers: bool = False # When clicking on a room, recenter the room list on that room. click_centers: bool = False # When pressing enter in the room filter field, clear the field's text, # in addition to activating the keyboard-focused room. enter_clears_filter: bool = True # When pressing escape in the room filter field, clear the field's text. # in addition to focusing the current page or chat composer. escape_clears_filter: bool = True class Pinned: # Each property in this section is an account user ID, and the # value is a list of room ID to always keep on top. # A room's ID can be copied by right clicking on it in the room list. "@account:example.org": List[str] = ["!roomID:a.org", "!other:b.org"] class Chat: # Center the chat header (room avatar, name and topic) even when sidepanes # aren't hidden (see comment for the hide_sidepanes_under setting). always_center_header: bool = False # When the chat timeline is larger than this pixel width, # align your own messages to the left of the timeline instead of right. # Can be 0 to always show your messages on the left. own_messages_on_left_above: int = 895 # Maximum number of characters in a message line before wrapping the text # to a new line. Ignores messages containing code blocks or tables. max_messages_line_length: int = 65 # Show membership events in the timeline: when someone is invited to the # room, joins, leaves, is kicked, banned or unbanned. show_membership_events: bool = True # Show room member display name and avatar change events in the timeline. show_profile_changes: bool = False # Show a notice in the timeline for types of events that aren't recognized. show_unknown_events: bool = False # In a chat with unread messages, the messages will be marked as read # after this number of seconds. # Focusing another window or chat resets the timer. mark_read_delay: float = 0.2 class Composer: class TypingNotifications: # Rules controlling whether " is typing..." notifications # should be sent to other users in rooms. # The `default` property is required. Other properties can be # added: user IDs, room IDs, or space-separated user + room IDs. # Send typing notifications everywhere by default: default: bool = True # But don't send them for rooms under this account: "@account_1:example.org": bool = False # Neither send them in this room, regardless of the account used: "!room:example.org": bool = False # Except if it's this account and this room, then send them: "@account_2:example.org !room:example.org": bool = True class Aliases: # Each property is the user ID of an account, value is the alias. # From any chat, start a message with an alias followed by a space # to type and send as the associated account. # The account must have permission to talk in the room. # To ignore an alias when typing, prepend it with a space. "!account:example.org": str = "a" "!other_account:example.org": str = "oa" class Files: # Minimum pixel width of the file name box for files without previews. min_file_width: int = 256 # Minimum (width, height) for image thumbnails. min_thumbnail_size: Tuple[int, int] = (256, 256) # How much of the chat height image thumbnails can take at most, # e.g. 0.4 for 40% of the chat or 1 for 100%. max_thumbnail_height_ratio: float = 0.4 # Automatically play animated GIF images in the timeline. auto_play_gif: bool = True # When clicking on a file in the timeline, open it in an external # program instead of displaying it using Mirage's interface. # On Linux, the xdg-open command is called. click_opens_externally: bool = False # In the full image viewer, if the image is large enough to cover the # info bar or buttons, they will automatically hide after this number # of seconds. # Hovering on the top/bottom with a mouse or tapping on a touch screen # reveals the hidden controls. autohide_image_controls_after: float = 2.0 class Keys: # All keybind settings, unless their comment says otherwise, are list of # the possible shortcuts for an action, e.g. ["Ctrl+A", "Alt+Shift+A"]. # # The available modifiers are Ctrl, Shift, Alt and Meta. # On macOS, Ctrl corresponds to Cmd and Meta corresponds to Control. # On other systems, Meta corresponds to the Windows/Super/mod4 key. # # https://doc.qt.io/qt-5/qt.html#Key-enum lists the names of special # keys, e.g. for "Qt::Key_Space", you would use "Space" in this config. # # The Escape key by itself should not be bound, as it would conflict with # closing popups and various other actions. # # Key chords can be defined by having up to four shortcuts # separated by commas in a string, e.g. for ["Ctrl+A,B"], Ctrl+A then B # would need to be pressed. # Helper functions import platform def os_ctrl(self) -> str: # Return Meta on macOS, which corresponds to Ctrl, and Ctrl on others. return "Meta" if platform.system() == "Darwin" else "Ctrl" def alt_or_cmd(self) -> str: # Return Ctrl on macOS, which corresponds to Cmd, and Alt on others. return "Ctrl" if platform.system() == "Darwin" else "Alt" # Toggle compact interface mode. See the compact setting comment. compact = ["Alt+Ctrl+C"] # Control the interface scale. zoom_in = ["Ctrl++"] zoom_out = ["Ctrl+-"] reset_zoom = ["Ctrl+="] # Switch to the previous/next tab in pages. In chats, this controls what # the right room pane shows, e.g. member list or room settings. previous_tab = ["Alt+Shift+Left", "Alt+Shift+H"] next_tab = ["Alt+Shift+Right", "Alt+Shift+L"] # Switch to the last opened page/chat, similar to Alt+Tab on most desktops. last_page = ["Ctrl+Tab"] # Go throgh history of opened chats, # similar to the "page back" and "page forward" keys in web browsers earlier_page = ["Ctrl+H"] later_page = ["Ctrl+L"] # Toggle muting all notifications in the running client, # except highlights (e.g. replies or keywords) notifications_highlights_only = ["Ctrl+Alt+H"] # Toggle muting all notifications in the running client notifications_mute = ["Ctrl+Alt+N"] # Toggle the QML developer console. Type ". help" inside it for more info. qml_console = ["F1"] # Start the Python backend debugger. # Mirage must be connected to a terminal for this to work. python_debugger = ["Shift+F1"] # Start the Python backend debugger in remote access mode. # The remote-pdb Python package must be installed. # From any terminal, run `socat readline tcp:127.0.0.1:4444` to connect. python_remote_debugger = ["Alt+F1"] # Quit Mirage quit = [] class Scrolling: # Pages and chat timeline scrolling up = ["Alt+Up", "Alt+K"] down = ["Alt+Down", "Alt+J"] page_up = ["Alt+Ctrl+Up", "Alt+Ctrl+K", "PgUp"] page_down = ["Alt+Ctrl+Down", "Alt+Ctrl+J", "PgDown"] top = ["Alt+Ctrl+Shift+Up", "Alt+Ctrl+Shift+K", "Home"] bottom = ["Alt+Ctrl+Shift+Down", "Alt+Ctrl+Shift+J", "End"] class Accounts: # The current account is the account under which a page or chat is # opened, or the keyboard-focused one when using the room filter field. # Add a new account add = ["Alt+Shift+A"] # Collapse the current account collapse = ["Alt+O"] # Open the current account settings settings = ["Alt+A"] # Open the current account context menu menu = ["Alt+P"] # Toggle current account presence between this status and online unavailable = ["Alt+Ctrl+U", "Alt+Ctrl+A"] invisible = ["Alt+Ctrl+I"] offline = ["Alt+Ctrl+O"] # Switch to first room of the previous/next account in the room list. previous = ["Alt+Shift+N"] next = ["Alt+N"] class AtIndex: # Switch to the first room of the account number X in the list. # Each property is a list of keybinds for the account number X. # Numbers beyond the default ones can be added. 1 = [Keys.os_ctrl() + "+1"] 2 = [Keys.os_ctrl() + "+2"] 3 = [Keys.os_ctrl() + "+3"] 4 = [Keys.os_ctrl() + "+4"] 5 = [Keys.os_ctrl() + "+5"] 6 = [Keys.os_ctrl() + "+6"] 7 = [Keys.os_ctrl() + "+7"] 8 = [Keys.os_ctrl() + "+8"] 9 = [Keys.os_ctrl() + "+9"] 10 = [Keys.os_ctrl() + "+0"] class Rooms: # Add a new room (direct chat, join or create a group). add = ["Alt+C"] # Focus or clear the text of the left main pane's room filter field. # When focusing the field, use Tab/Shift+Tab or the arrows to navigate # the list, Enter to switch to focused account/room, Escape to cancel, # Menu to open the context menu. focus_filter = ["Alt+F"] clear_filter = ["Alt+Shift+F"] # Switch to the previous/next room in the list. previous = ["Alt+Shift+Up", "Alt+Shift+K"] next = ["Alt+Shift+Down", "Alt+Shift+J"] # Switch to the previous/next room with unread messages in the list. previous_unread = ["Alt+Shift+U"] next_unread = ["Alt+U"] # Switch to the room with the oldest/latest unread message. oldest_unread = ["Ctrl+Shift+U"] latest_unread = ["Ctrl+U"] # Switch to the previous/next room with highlighted messages in the # list. What causes a highlight is controlled by push rules # (editable in GUI account settings): by default, this includes # when your name is mentioned, replied to, or messages with keywords. previous_highlight = ["Alt+Shift+M"] next_highlight = ["Alt+M"] # Switch to the room with the oldest/latest unread message, # but only rooms with highlights are considered. oldest_highlight = ["Ctrl+Shift+M"] latest_highlight = ["Ctrl+M"] class AtIndex: # Switch to room number X in the current account. # Each property is a list of keybinds for the room number X: # Numbers beyond the default ones can be added. 1 = [Keys.alt_or_cmd() + "+1"] 2 = [Keys.alt_or_cmd() + "+2"] 3 = [Keys.alt_or_cmd() + "+3"] 4 = [Keys.alt_or_cmd() + "+4"] 5 = [Keys.alt_or_cmd() + "+5"] 6 = [Keys.alt_or_cmd() + "+6"] 7 = [Keys.alt_or_cmd() + "+7"] 8 = [Keys.alt_or_cmd() + "+8"] 9 = [Keys.alt_or_cmd() + "+9"] 10 = [Keys.alt_or_cmd() + "+0"] class Direct: # Switch to specific rooms with keybindings. # An unlimited number of properties can be added, where each # property maps a room to a list of keybind. # A room's ID can be copied by right clicking on it in the room list. "!roomID:example.org" = [] # If you have multiple accounts in the same room, you can also set # which account should be targeted as " ": "@account:example.org !roomID:example.org" = [] class Chat: # Keybinds specific to the current chat page. # Focus the right room pane. If the pane is currently showing the # room member list, the corresponding filter field is focused. # When focusing the field, use Tab/Shift+Tab or the arrows to navigate # the list, Enter to see the focused member's profile, Escape to cancel, # Menu to open the context menu. focus_room_pane = ["Alt+R"] # Toggle hiding the right pane. # Can also be done by clicking on current tab button at the top right. hide_room_pane = ["Alt+Ctrl+R"] # Invite new members or leave the current chat. invite = ["Alt+I"] leave = ["Alt+Escape"] # Open the file picker to upload files in the current chat. send_file = ["Alt+S"] # If your clipboard contains a file path, upload that file. send_clipboard_path = ["Alt+Shift+S"] class Messages: # Focus the previous/next message in the timeline. # Keybinds defined below in this section affect the focused message. # The Menu key can open the context menu for a focused message. previous = ["Ctrl+Up", "Ctrl+K"] next = ["Ctrl+Down", "Ctrl+J"] # Select the currently focused message, same as clicking on it. # When there are selected messages, some right click menu options # and keybinds defined below will affect these messages instead of # the focused (for keybinds) or mouse-targeted (right click menu) one. # The Menu key can open the context menu for selected messages. select = ["Ctrl+Space"] # Select all messages from point A to point B. # If used when no messages are already selected, all the messages # from the most recent in the timeline to the focused one are selected. # Otherwise, messages from the last selected to focused are selected. select_until_here = ["Ctrl+Shift+Space"] # Clear the message keyboard focus. # If no message is focused but some are selected, clear the selection. unfocus_or_deselect = ["Ctrl+D"] # Toggle display of the focused message's seen counter tooltip, # which shows which user have this message as their last seen and when # did they send that information. # When this mode is active, you can move the focus to other messages # and the tooltip will update itself. # If a message doesn't have a counter, it won't have a tooltip. seen_tooltips = ["Ctrl+S"] # Remove the selected messages if any, else the focused message if any, # else the last message you posted. remove = ["Ctrl+R", "Alt+Del"] # Reply/cancel reply to the focused message if any, # else the last message posted by someone else. # Replying can also be cancelled by pressing Escape. reply = ["Ctrl+Q"] # Open the QML developer console for the focused message if any, # and display the event source. debug = ["Ctrl+Shift+D"] # Open the files and links in selected messages if any, else the # file/links of the focused message if any, else the last # files/link in the timeline. open_links_files = ["Ctrl+O"] # Like open_links_files, but files open in external programs instead. # On Linux, this uses the xdg-open command. open_links_files_externally = ["Ctrl+Shift+O"] # Copy the paths of the downloaded files in selected messages if any, # else the file path for the focused message if any, else the # path for the last downloaded file in the timeline. copy_files_path = ["Ctrl+Shift+C"] # Clear all messages from the chat. # This does not remove anything for other users. clear_all = ["Ctrl+Shift+L"] class ImageViewer: # Close the image viewer. Escape can also be used. close = ["X", "Q"] # Toggle alternate image scaling mode: if the original image size is # smaller than the window, upscale it to fit the window. # If the original size is bigger than the window, expand the image # to show it as its real size. expand = ["E"] # Toggle fullscreen mode. fullscreen = ["F", "F11", "Alt+Return", "Alt+Enter"] # Pan/scroll the image. pan_left = ["H", "Left", "Alt+H", "Alt+Left"] pan_down = ["J", "Down", "Alt+J", "Alt+Down"] pan_up = ["K", "Up", "Alt+K", "Alt+Up"] pan_right = ["L", "Right", "Alt+L", "Alt+Right"] # Control the image zoom. Ctrl+wheel can also be used. zoom_in = ["Z", "+", "Ctrl++"] zoom_out = ["Shift+Z", "-", "Ctrl+-"] reset_zoom = ["Alt+Z", "=", "Ctrl+="] # Control the image's rotation. rotate_right = ["R"] rotate_left = ["Shift+R"] reset_rotation = ["Alt+R"] # Control the playback speed of animated GIF images. speed_up = ["S"] slow_down = ["Shift+S"] reset_speed = ["Alt+S"] # Toggle pausing of animated GIF images. pause = ["Space"] class Security: # These keybinds affect the Security tab in your account settings. # # Currently unchangable keys: # - Tab/Shift+Tab to navigate the interface # - Space to check/uncheck a focused session # - Menu to open the focused session's context menu # Refresh the list of sessions. refresh = ["Alt+R", "F5"] # Sign out checked sessions if any, else sign out all sessions. sign_out = ["Alt+S", "Delete"] mirage-0.7.2/src/gui/000077500000000000000000000000001407747233600143545ustar00rootroot00000000000000mirage-0.7.2/src/gui/ArgumentParser.qml000066400000000000000000000034251407747233600200320ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later pragma Singleton import QtQuick 2.12 QtObject { property bool startInTray: false property string loadQml: "" readonly property string help: `Usage: ${Qt.application.name} [options] Options: -t, --start-in-tray Start in the system tray, without a visible window -l, --load-qml PATH Override the file to be loaded as src/gui/UI.qml -V, --version Show the application's version and exit -h, --help Show this help and exit Environment variables: MIRAGE_CONFIG_DIR Override the default configuration folder MIRAGE_DATA_DIR Override the default application data folder MIRAGE_CACHE_DIR Override the default cache and downloads folder http_proxy Override the General.proxy setting, see settings.py ` readonly property bool ready: { const passedArguments = Qt.application.arguments.slice(1) while (passedArguments.length) { switch (passedArguments.shift()) { case "-h": case "--help": print("\n\n" + help.replace(/^ {4}/gm, "")) Qt.quit() break case "-v": case "--version": print(Qt.application.version) Qt.quit() break case "-t": case "--start-in-tray": startInTray = true break case "-l": case "--load-qml": loadQml = passedArguments.shift() break } } return true } } mirage-0.7.2/src/gui/Base/000077500000000000000000000000001407747233600152265ustar00rootroot00000000000000mirage-0.7.2/src/gui/Base/AutoDirectionLayout.qml000066400000000000000000000007631407747233600217160ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 HGridLayout { readonly property real summedImplicitWidth: utils.sumChildrenImplicitWidths(visibleChildren, columnSpacing) readonly property bool vertical: flow === HGridLayout.TopToBottom flow: width >= summedImplicitWidth ? HGridLayout.LeftToRight : HGridLayout.TopToBottom } mirage-0.7.2/src/gui/Base/Buttons/000077500000000000000000000000001407747233600166645ustar00rootroot00000000000000mirage-0.7.2/src/gui/Base/Buttons/ApplyButton.qml000066400000000000000000000003011407747233600216520ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later PositiveButton { text: qsTr("Apply") icon.name: "apply" } mirage-0.7.2/src/gui/Base/Buttons/CancelButton.qml000066400000000000000000000003031407747233600217540ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later NegativeButton { text: qsTr("Cancel") icon.name: "cancel" } mirage-0.7.2/src/gui/Base/Buttons/FieldCopyButton.qml000066400000000000000000000015041407747233600224510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." HButton { property Item textControl // HTextField or HTextArea icon.name: "copy-text" iconItem.small: true toolTip.text: qsTr("Copy") toolTip.onClosed: toolTip.text = qsTr("Copy") toolTip.label.wrapMode: HLabel.NoWrap onClicked: { const oldPosition = textControl.cursorPosition textControl.selectAll() textControl.copy() textControl.deselect() textControl.cursorPosition = oldPosition toolTip.text = qsTr("Copied!") toolTip.instantShow(2000) } onActiveFocusChanged: if (! activeFocus && toolTip.visible) toolTip.hide() Layout.fillHeight: true } mirage-0.7.2/src/gui/Base/Buttons/FieldHelpButton.qml000066400000000000000000000007101407747233600224250ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." HButton { property string helpText icon.name: "field-help" iconItem.small: true toolTip.text: helpText onClicked: toolTip.instantShow() onActiveFocusChanged: if (! activeFocus && toolTip.visible) toolTip.hide() Layout.fillHeight: true } mirage-0.7.2/src/gui/Base/Buttons/GroupButton.qml000066400000000000000000000004041407747233600216650ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick.Layouts 1.12 import ".." HButton { Layout.preferredHeight: theme.baseElementsHeight Layout.fillWidth: true } mirage-0.7.2/src/gui/Base/Buttons/MiddleButton.qml000066400000000000000000000002751407747233600217750ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later GroupButton { icon.color: theme.colors.middleBackground } mirage-0.7.2/src/gui/Base/Buttons/NegativeButton.qml000066400000000000000000000002771407747233600223430ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later GroupButton { icon.color: theme.colors.negativeBackground } mirage-0.7.2/src/gui/Base/Buttons/PositiveButton.qml000066400000000000000000000002771407747233600224030ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later GroupButton { icon.color: theme.colors.positiveBackground } mirage-0.7.2/src/gui/Base/Class.qml000066400000000000000000000002721407747233600170070ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 QtObject { property string name } mirage-0.7.2/src/gui/Base/DelegateTransitionFixer.qml000066400000000000000000000017751407747233600225360ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Timer { // Sometimes and randomly, a HListView/HGridView delegate's add/populate // Transition will stop too early, leaving a stuck invisible or tiny item. // This is a workaround for this Qt bug happening despite the neccessary // Transition precautions from the docs being applied. property Item delegate: parent readonly property HNumberAnimation opacityFixer: HNumberAnimation { target: delegate property: "opacity" from: delegate.opacity to: 1 } readonly property HNumberAnimation scaleFixer: HNumberAnimation { target: delegate property: "scale" from: delegate.scale to: 1 } interval: theme.animationDuration * 2 running: true onTriggered: { // if (delegate.opacity < 1 || delegate.scale < 1) print(delegate) if (delegate.opacity < 1) opacityFixer.start() if (delegate.scale < 1) scaleFixer.start() } } mirage-0.7.2/src/gui/Base/HAvatar.qml000066400000000000000000000070721407747233600172750ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Rectangle { id: avatar property bool compact: false property string name property alias clientUserId: avatarImage.clientUserId property alias mxc: avatarImage.mxc property alias title: avatarImage.title property alias sourceOverride: avatarImage.sourceOverride property alias fillMode: avatarImage.fillMode property alias animate: avatarImage.animate property string toolTipMxc: mxc property string toolTipSourceOverride: "" readonly property alias hovered: hoverHandler.hovered readonly property alias circleRadius: avatarImage.circleRadius implicitWidth: implicitHeight implicitHeight: compact ? theme.controls.avatar.compactSize : theme.controls.avatar.size radius: theme.controls.avatar.radius color: avatarImage.visible ? "transparent" : utils.hsluv( name ? utils.hueFrom(name) : 0, name ? theme.controls.avatar.background.saturation : 0, theme.controls.avatar.background.lightness, theme.controls.avatar.background.opacity ) Behavior on color { HColorAnimation {} } HLabel { z: 1 anchors.centerIn: parent visible: ! avatarImage.visible text: name ? name.charAt(0) : "?" font.pixelSize: parent.height / 1.4 color: utils.hsluv( name ? utils.hueFrom(name) : 0, name ? theme.controls.avatar.letter.saturation : 0, theme.controls.avatar.letter.lightness, theme.controls.avatar.letter.opacity ) Behavior on color { HColorAnimation {} } } HMxcImage { id: avatarImage anchors.fill: parent visible: Boolean(sourceOverride || mxc) z: 2 sourceSize.width: parent.width sourceSize.height: parent.height showProgressBar: false fillMode: Image.PreserveAspectCrop animatedFillMode: AnimatedImage.PreserveAspectCrop animate: false radius: parent.radius HoverHandler { id: hoverHandler } HToolTip { id: avatarToolTip readonly property int dimension: Math.min( mainUI.width / 1.25, mainUI.height / 1.25, theme.controls.avatar.hoveredImage.size + background.border.width * 2, ) visible: ! avatarImage.broken && ! window.anyMenu && avatarImage.width < dimension * 0.75 && (toolTipSourceOverride || toolTipMxc) && hoverHandler.hovered delay: 1000 backgroundColor: theme.controls.avatar.hoveredImage.background contentItem: HLoader { active: avatarToolTip.visible sourceComponent: HMxcImage { id: avatarToolTipImage fillMode: Image.PreserveAspectCrop animatedFillMode: AnimatedImage.PreserveAspectCrop clientUserId: avatarImage.clientUserId title: avatarImage.title mxc: avatar.toolTipMxc sourceOverride: avatar.toolTipSourceOverride sourceSize.width: avatarToolTip.dimension sourceSize.height: avatarToolTip.dimension width: avatarToolTip.dimension height: avatarToolTip.dimension } } } } } mirage-0.7.2/src/gui/Base/HBottomFocusLine.qml000066400000000000000000000007001407747233600211220ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HRectangleBottomBorder { id: line property bool show: false transform: Scale { origin.x: line.width / 2 origin.y: line.height / 2 xScale: line.show ? 1 : 0 Behavior on xScale { HNumberAnimation {} } } Behavior on color { HColorAnimation {} } } mirage-0.7.2/src/gui/Base/HBox.qml000066400000000000000000000011761407747233600166060ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 HColumnPage { implicitWidth: Math.min(parent.width, theme.controls.box.defaultWidth) padding: theme.spacing background: Rectangle { color: theme.controls.box.background radius: theme.controls.box.radius } HNumberAnimation on scale { running: true from: 0 to: 1 overshoot: 2 } Behavior on implicitWidth { HNumberAnimation {} } Behavior on implicitHeight { HNumberAnimation {} } } mirage-0.7.2/src/gui/Base/HBusyIndicator.qml000066400000000000000000000006311407747233600206300ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HCircleProgressBar { progress: 0.5 label.visible: false baseCircle.strokeWidth: 2 progressCircle.strokeWidth: 2 HNumberAnimation on rotation { from: 0 to: 360 loops: Animation.Infinite duration: 600 } } mirage-0.7.2/src/gui/Base/HButton.qml000066400000000000000000000043601407747233600173270ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 Button { id: button readonly property alias iconItem: contentItem.icon readonly property alias label: contentItem.label property color backgroundColor: theme.controls.button.background property color focusLineColor: Qt.colorEqual(icon.color, theme.icons.colorize) ? theme.controls.button.focusedBorder : icon.color property bool disableWhileLoading: true property bool loading: false property bool circle: false property bool padded: true property bool enableRadius: false property bool uncheckable: true property HToolTip toolTip: HToolTip { id: toolTip visible: text && hovered } enabled: ! button.loading spacing: theme.spacing topPadding: padded ? spacing * (circle ? 1 : 0.5) : 0 bottomPadding: topPadding leftPadding: padded ? spacing : 0 rightPadding: leftPadding icon.color: theme.icons.colorize // Must be explicitely set to display correctly on KDE implicitWidth: Math.max( implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding ) implicitHeight: Math.max( implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding ) // Prevent button from gaining focus and being highlighted on click focusPolicy: Qt.TabFocus background: HButtonBackground { button: button buttonTheme: theme.controls.button radius: circle ? height / 2 : enableRadius ? theme.radius : 0 color: backgroundColor } contentItem: HButtonContent { id: contentItem button: button buttonTheme: theme.controls.button } Keys.onReturnPressed: if (enabled) clicked() Keys.onEnterPressed: Keys.onReturnPressed(event) activeFocusOnTab: true Binding on enabled { when: disableWhileLoading && button.loading value: false } MouseArea { anchors.fill: parent enabled: ! parent.uncheckable && parent.checked } } mirage-0.7.2/src/gui/Base/HButtonBackground.qml000066400000000000000000000023141407747233600213240ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Rectangle { property var button property QtObject buttonTheme property bool useFocusLine: true color: buttonTheme.background opacity: button.loading ? theme.loadingElementsOpacity : enabled ? 1 : theme.disabledElementsOpacity Behavior on opacity { HNumberAnimation {} } Rectangle { anchors.fill: parent radius: parent.radius color: button.checked ? buttonTheme.checkedOverlay : button.enabled && button.pressed ? buttonTheme.pressedOverlay : button.enabled && ! useFocusLine && button.activeFocus ? buttonTheme.hoveredOverlay : button.enabled && button.hovered ? buttonTheme.hoveredOverlay : "transparent" Behavior on color { HColorAnimation { factor: 0.5 } } } HBottomFocusLine { show: useFocusLine && button.activeFocus borderHeight: useFocusLine ? buttonTheme.focusedBorderWidth : 0 color: useFocusLine ? button.focusLineColor : "transparent" } } mirage-0.7.2/src/gui/Base/HButtonContent.qml000066400000000000000000000037161407747233600206660ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 HRowLayout { id: buttonContent property var button property QtObject buttonTheme readonly property alias icon: icon readonly property alias label: label spacing: button.spacing opacity: button.loading ? theme.loadingElementsOpacity : enabled ? 1 : theme.disabledElementsOpacity Behavior on opacity { HNumberAnimation {} } Item { visible: Boolean(button.icon.name || button.loading) Layout.preferredWidth: button.loading ? busyIndicatorLoader.width : icon.width Layout.preferredHeight: button.loading ? busyIndicatorLoader.height : icon.height Layout.fillHeight: true Layout.alignment: Qt.AlignCenter HIcon { id: icon anchors.centerIn: parent width: svgName ? implicitWidth : 0 visible: width > 0 opacity: button.loading ? 0 : 1 colorize: button.icon.color svgName: button.icon.name // cache: button.icon.cache // TODO: need Qt 5.13+ Behavior on opacity { HNumberAnimation {} } } HLoader { id: busyIndicatorLoader anchors.centerIn: parent width: height height: parent.height opacity: button.loading ? 1 : 0 active: opacity > 0 sourceComponent: HBusyIndicator {} Behavior on opacity { HNumberAnimation {} } } } HLabel { id: label text: button.text visible: Boolean(text) color: buttonTheme.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight Layout.fillWidth: true Layout.fillHeight: true } } mirage-0.7.2/src/gui/Base/HCheckBox.qml000066400000000000000000000064751407747233600175530ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 CheckBox { id: box property alias mainText: mainText property alias subtitle: subtitleText property bool defaultChecked: false readonly property bool changed: checked !== defaultChecked property bool previousDefaultChecked: false // private function reset() { checked = defaultChecked } checked: defaultChecked spacing: contentItem.visible ? theme.spacing : 0 padding: 0 indicator: Rectangle { opacity: box.enabled ? 1 : theme.disabledElementsOpacity + 0.2 implicitWidth: 24 implicitHeight: implicitWidth x: box.leftPadding y: box.topPadding + box.availableHeight / 2 - height / 2 radius: theme.radius color: theme.controls.checkBox.boxBackground border.color: box.enabled && box.pressed ? theme.controls.checkBox.boxPressedBorder : (box.enabled && box.hovered) || box.activeFocus ? theme.controls.checkBox.boxHoveredBorder : theme.controls.checkBox.boxBorder Behavior on border.color { HColorAnimation { factor: 0.5 } } // FIXME: workaround for sizing bug in Security.qml ListView sections Component.onCompleted: implicitWidth = Qt.binding(() => theme.controls.checkBox.boxSize) HIcon { anchors.centerIn: parent dimension: parent.width - 2 colorize: theme.controls.checkBox.checkIconColorize svgName: box.checkState === Qt.PartiallyChecked ? "check-mark-partial" : "check-mark" scale: box.checkState === Qt.Unchecked ? 0 : 1 Behavior on scale { HNumberAnimation { overshoot: 3 easing.type: Easing.InOutBack factor: 0.5 } } } } contentItem: HColumnLayout { visible: mainText.text || subtitleText.text opacity: box.enabled ? 1 : theme.disabledElementsOpacity HLabel { id: mainText text: box.text color: theme.controls.checkBox.text // Set a width on CheckBox for wrapping to work, // e.g. by using Layout.fillWidth wrapMode: HLabel.Wrap leftPadding: box.indicator.width + box.spacing verticalAlignment: Text.AlignVCenter Layout.fillWidth: true } HLabel { id: subtitleText visible: Boolean(text) color: theme.controls.checkBox.subtitle font.pixelSize: theme.fontSize.small wrapMode: mainText.wrapMode leftPadding: mainText.leftPadding verticalAlignment: mainText.verticalAlignment Layout.fillWidth: true } } onDefaultCheckedChanged: { if (checked === previousDefaultChecked) checked = Qt.binding(() => defaultChecked) previousDefaultChecked = defaultChecked } // Break binding Component.onCompleted: previousDefaultChecked = previousDefaultChecked Behavior on opacity { HNumberAnimation { factor: 2 } } } mirage-0.7.2/src/gui/Base/HCircleProgressBar.qml000066400000000000000000000043041407747233600214250ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Shapes 1.12 Item { property real progress: 0 // 0-1 readonly property alias baseCircle: baseCircle readonly property alias progressCircle: progressCircle readonly property alias label: label implicitWidth: 96 * (theme ? theme.uiScale : 1) implicitHeight: implicitWidth layer.enabled: true layer.samples: 4 layer.smooth: true HLabel { id: label property int progressNumber: Math.floor(progress * 100) anchors.centerIn: parent text: progressNumber + "%" font.pixelSize: theme ? theme.fontSize.big : 22 Behavior on progressNumber { HNumberAnimation { factor: 2 } } } Shape { id: shape anchors.fill: parent asynchronous: true ShapePath { id: baseCircle fillColor: "transparent" strokeColor: theme.controls.circleProgressBar.background strokeWidth: theme.controls.circleProgressBar.thickness capStyle: ShapePath.RoundCap startX: shape.width / 2 startY: strokeWidth PathAngleArc { centerX: shape.width / 2 centerY: shape.height / 2 radiusX: shape.width / 2 - baseCircle.strokeWidth radiusY: shape.height / 2 - baseCircle.strokeWidth sweepAngle: 360 } } ShapePath { id: progressCircle fillColor: baseCircle.fillColor strokeColor: theme.controls.circleProgressBar.foreground strokeWidth: baseCircle.strokeWidth PathAngleArc { centerX: shape.width / 2 centerY: shape.height / 2 radiusX: shape.width / 2 - progressCircle.strokeWidth radiusY: shape.height / 2 - progressCircle.strokeWidth startAngle: 270 sweepAngle: progress * 360 Behavior on startAngle { HNumberAnimation {} } Behavior on sweepAngle { HNumberAnimation {} } } } } } mirage-0.7.2/src/gui/Base/HColorAnimation.qml000066400000000000000000000003651407747233600207730ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 ColorAnimation { property real factor: 1.0 duration: theme.animationDuration * factor } mirage-0.7.2/src/gui/Base/HColumnLayout.qml000066400000000000000000000003201407747233600204770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 ColumnLayout { spacing: 0 } mirage-0.7.2/src/gui/Base/HColumnPage.qml000066400000000000000000000007241407747233600201060ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HPage { id: page default property alias columnData: column.data property alias column: column implicitWidth: theme.controls.box.defaultWidth contentHeight: column.childrenRect.height HColumnLayout { id: column anchors.fill: parent spacing: theme.spacing * 1.5 } } mirage-0.7.2/src/gui/Base/HComboBox.qml000066400000000000000000000057551407747233600175750ustar00rootroot00000000000000import QtQuick 2.12 import QtQuick.Controls 2.12 ComboBox { id: root property bool error: false property bool bordered: true property color backgroundColor: theme.controls.textField.background property color borderColor: theme.controls.textField.border property color errorBorder: theme.controls.textField.errorBorder property color focusedBorderColor: theme.controls.textField.focusedBorder property color focusedBackgroundColor: theme.controls.textField.focusedBackground readonly property bool popupVisible: popup && popup.visible readonly property alias field: field spacing: 0 background: Rectangle { radius: theme.radius color: field.activeFocus ? focusedBackgroundColor : backgroundColor border.width: bordered ? theme.controls.textField.borderWidth : 0 border.color: borderColor HBottomFocusLine { show: field.activeFocus borderHeight: theme.controls.textField.borderWidth color: error ? errorBorder : focusedBorderColor } } contentItem: HTextField { id: field background: null text: root.displayText readOnly: ! root.editable rightPadding: (root.indicator ? root.indicator.width : 0) + theme.spacing TapHandler { enabled: field.readOnly onTapped: root.popupVisible ? root.popup.close() : root.popup.open() } } indicator: HButton { x: root.width - root.rightPadding height: root.availableHeight backgroundColor: "transparent" icon.name: "combo-box-" + (root.popupVisible ? "close" : "open") iconItem.small: true onClicked: root.popupVisible ? root.popup.close() : root.popup.open() } popup: HMenu { id: menu y: root.height width: root.width modal: false onOpened: currentIndex = root.currentIndex enter: Transition { HNumberAnimation { property: "height" from: 0 to: menu.implicitHeight easing.type: Easing.OutQuad } } exit: Transition { HNumberAnimation { property: "height" to: 0 easing.type: Easing.InQuad } } HLabel { visible: root.editable height: visible ? implicitHeight : 0 text: qsTr("Custom input accepted") color: theme.colors.dimText leftPadding: theme.spacing rightPadding: leftPadding topPadding: theme.spacing / 1.75 bottomPadding: topPadding width: menu.width wrapMode: HLabel.Wrap } Repeater { model: root.popupVisible ? root.model : null delegate: root.delegate } } delegate: HMenuItem { text: modelData onTriggered: root.currentIndex = model.index } } mirage-0.7.2/src/gui/Base/HDrawer.qml000066400000000000000000000113071407747233600172770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Drawer { id: drawer property string saveName: "" property var saveId: "ALL" property var saveProperties: ["preferredSize", "forceCollapse"] // property alias color: bg.color property int defaultSize: 300 * theme.uiScale property bool requireDefaultSize: false property int preferredSize: window.getState(this, "preferredSize", defaultSize) property int minimumSize: resizeAreaSize property int maximumSize: horizontal ? referenceSizeParent.width : referenceSizeParent.height property int snapAt: defaultSize property int snapZone: theme.spacing * 2 property bool forceCollapse: window.getState(this, "forceCollapse", false) // property Item referenceSizeParent: parent property bool collapse: (horizontal ? window.width : window.height) < window.settings.General.hide_side_panes_under * theme.uiScale property int peekSizeWhileCollapsed: (horizontal ? referenceSizeParent.width : referenceSizeParent.height) * (forceCollapse && ! collapse ? 0.5 : 1) property int resizeAreaSize: theme.spacing / 2 property int calculatedMinimumSize: requireDefaultSize ? defaultSize : minimumSize readonly property int calculatedSizeNoRequiredMinimum: normalOrForceCollapse ? peekSizeWhileCollapsed : Math.max(minimumSize, Math.min(preferredSize, maximumSize)) readonly property int calculatedSize: normalOrForceCollapse ? peekSizeWhileCollapsed : Math.max(calculatedMinimumSize, Math.min(preferredSize, maximumSize)) // readonly property bool normalOrForceCollapse: collapse || forceCollapse readonly property int visibleSize: visible ? width * position : 0 readonly property bool horizontal: edge === Qt.LeftEdge || edge === Qt.RightEdge readonly property bool vertical: ! horizontal implicitWidth: horizontal ? calculatedSize : parent.width implicitHeight: vertical ? calculatedSize : parent.height // Prevents this: open a popup, make the window small enough for the // drawer to collapse, then make it big again → popup is now behind drawer z: -1 topPadding: 0 bottomPadding: 0 leftPadding: 0 rightPadding: 0 // FIXME: https://bugreports.qt.io/browse/QTBUG-59141 // dragMargin: parent.width / 2 // interactive: normalOrForceCollapse dragMargin: 0 interactive: false position: 1 visible: ! collapse && ! forceCollapse modal: false closePolicy: Popup.NoAutoClose background: Rectangle { id: bg; color: theme.colors.strongBackground } onForceCollapseChanged: window.saveState(this) Behavior on width { enabled: horizontal && ! resizeMouseHandler.drag.active NumberAnimation { duration: 100 } } Behavior on height { enabled: vertical && ! resizeMouseHandler.drag.active NumberAnimation { duration: 100 } } Behavior on calculatedMinimumSize { HNumberAnimation { factor: 0.75 } } Item { id: resizeArea x: vertical || drawer.edge === Qt.RightEdge ? 0 : drawer.width-width y: horizontal || drawer.edge !== Qt.TopEdge ? 0 : drawer.height-height width: horizontal ? resizeAreaSize * theme.uiScale : parent.width height: vertical ? resizeAreaSize * theme.uiScale : parent.height z: 999 MouseArea { id: resizeMouseHandler function snapSize(num) { return num < snapAt + snapZone && num > snapAt - snapZone ? snapAt : num } anchors.fill: parent enabled: ! drawer.collapse acceptedButtons: Qt.LeftButton preventStealing: true hoverEnabled: true cursorShape: containsMouse || drag.active ? (horizontal ? Qt.SizeHorCursor : Qt.SizeVerCursor) : Qt.ArrowCursor onMouseXChanged: if (horizontal && pressed) { drawer.preferredSize = snapSize( drawer.calculatedSize + (drawer.edge === Qt.RightEdge ? -mouseX : mouseX) ) } onMouseYChanged: if (vertical && pressed) { drawer.preferredSize = snapSize( drawer.calculatedSize + (drawer.edge === Qt.BottomEdge ? -mouseY : mouseY) ) } onReleased: window.saveState(drawer) } } } mirage-0.7.2/src/gui/Base/HDrawerSwipeHandler.qml000066400000000000000000000026471407747233600216140ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 DragHandler { id: root property HDrawer drawer property bool swiped: false property real minimumSwipeDistance: Math.min(drawer.implicitWidth / 2, 100) * theme.uiScale readonly property HNumberAnimation hide: HNumberAnimation { target: drawer property: "position" to: 0 onStopped: root.closeRequest() } readonly property HNumberAnimation cancel: HNumberAnimation { target: drawer property: "position" to: 1 } signal closeRequest() target: null enabled: drawer.normalOrForceCollapse && drawer.visible onTranslationChanged: { if (hide.running || cancel.running) return drawer.position = drawer.edge === Qt.LeftEdge ? 1 + translation.x / implicitWidth : drawer.edge === Qt.RightEdge ? 1 - translation.x / implicitWidth : drawer.edge === Qt.TopEdge ? 1 - translation.y / implicitHeight : 1 + translation.y / implicitHeight const distance = Math.abs(translation[drawer.horizontal ? "x" : "y"]) if (distance > minimumSwipeDistance) swiped = true } onSwipedChanged: if (swiped) hide.start() onActiveChanged: if (! active) { if (! swiped) cancel.start() swiped = false } } mirage-0.7.2/src/gui/Base/HFlickable.qml000066400000000000000000000017301407747233600177260ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Flickable { id: flickable maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed flickDeceleration: window.settings.Scrolling.kinetic_deceleration ScrollBar.vertical: HScrollBar { visible: parent.interactive z: 999 flickableMoving: flickable.moving } Component.onCompleted: { kineticScrollingDisabler = Qt.createComponent( "HKineticScrollingDisabler.qml" ).createObject(flickable, {flickable}) kineticScrollingDisabler.width = Qt.binding(() => kineticScrollingDisabler.enabled ? flickable.width : 0 ) kineticScrollingDisabler.height = Qt.binding(() => kineticScrollingDisabler.enabled ? flickable.height : 0 ) } property var kineticScrollingDisabler } mirage-0.7.2/src/gui/Base/HFlickableColumnPage.qml000066400000000000000000000031551407747233600217040ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import "../ShortcutBundles" HPage { id: page default property alias columnData: column.data property alias column: column property alias flickable: flickable property alias flickShortcuts: flickShortcuts property bool enableFlickShortcuts: SwipeView && SwipeView.view ? SwipeView.isCurrentItem : true implicitWidth: theme.controls.box.defaultWidth implicitHeight: contentHeight + implicitHeaderHeight + implicitFooterHeight contentHeight: flickable.contentHeight + flickable.topMargin + flickable.bottomMargin padding: 0 HFlickable { id: flickable anchors.fill: parent contentWidth: parent.width contentHeight: column.implicitHeight flickableDirection: Flickable.VerticalFlick clip: true topMargin: theme.spacing bottomMargin: topMargin leftMargin: topMargin rightMargin: topMargin FlickShortcuts { id: flickShortcuts active: ! mainUI.debugConsole.visible && enableFlickShortcuts flickable: flickable } HColumnLayout { id: column width: flickable.width - flickable.leftMargin - flickable.rightMargin spacing: theme.spacing * 1.5 } } HKineticScrollingDisabler { flickable: flickable width: enabled ? flickable.width : 0 height: enabled ? flickable.height : 0 } } mirage-0.7.2/src/gui/Base/HFlow.qml000066400000000000000000000021031407747233600167540ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Flow { populate: Transition { id: addTrans SequentialAnimation { PropertyAction { property: "opacity"; value: 0 } PauseAnimation { duration: addTrans.ViewTransition.index * theme.animationDuration / 2 } ParallelAnimation { HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { properties: "x,y"; from: 0 } } } } add: Transition { ParallelAnimation { HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { properties: "x,y"; from: 0 } } } move: Transition { ParallelAnimation { // Ensure opacity goes to 1 if add transition is interrupted HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { properties: "x,y" } } } } mirage-0.7.2/src/gui/Base/HGridLayout.qml000066400000000000000000000003461407747233600201370ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 GridLayout { rowSpacing: 0 columnSpacing: 0 } mirage-0.7.2/src/gui/Base/HGridView.qml000066400000000000000000000075421407747233600176010ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 GridView { id: gridView property int defaultCurrentIndex: -1 property alias cursorShape: mouseArea.cursorShape property int currentItemHeight: currentItem ? currentItem.height : 0 property var checked: ({}) property int lastCheckedDelegateIndex: 0 property int selectedCount: Object.keys(checked).length function check(...indices) { for (const i of indices) { const model = gridView.model.get(i) checked[model.id] = model } lastCheckedDelegateIndex = indices.slice(-1)[0] checkedChanged() } function checkFromLastToHere(here) { const indices = utils.range(lastCheckedDelegateIndex, here) eventList.check(...indices) } function uncheck(...indices) { for (const i of indices) { const model = gridView.model.get(i) delete checked[model.id] } checkedChanged() } function uncheckAll() { checked = {} } function toggleCheck(...indices) { const checkedIndices = [] for (const i of indices) { const model = gridView.model.get(i) if (model.id in checked) { delete checked[model.id] } else { checked[model.id] = model checkedIndices.push(i) } } if (checkedIndices.length > 0) lastCheckedDelegateIndex = checkedIndices.slice(-1)[0] checkedChanged() } function getSortedChecked() { return Object.values(checked).sort( (a, b) => a.date > b.date ? 1 : -1 ) } currentIndex: defaultCurrentIndex keyNavigationWraps: true highlightMoveDuration: theme.animationDuration // Keep highlighted delegate at the center highlightRangeMode: GridView.ApplyRange preferredHighlightBegin: height / 2 - currentItemHeight / 2 preferredHighlightEnd: height / 2 + currentItemHeight / 2 maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed flickDeceleration: window.settings.Scrolling.kinetic_deceleration highlight: Rectangle { color: theme.controls.gridView.highlight } ScrollBar.vertical: HScrollBar { visible: gridView.interactive flickableMoving: gridView.moving } // property bool debug: false // https://doc.qt.io/qt-5/qml-qtquick-viewtransition.html // #handling-interrupted-animations add: Transition { // ScriptAction { script: if (gridView.debug) print("add") } HNumberAnimation { property: "opacity"; from: 0; to: 1 } HNumberAnimation { property: "scale"; from: 0; to: 1 } } move: Transition { // ScriptAction { script: if (gridView.debug) print("move") } HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { property: "scale"; to: 1 } HNumberAnimation { properties: "x,y" } } remove: Transition { // ScriptAction { script: if (gridView.debug) print("remove") } HNumberAnimation { property: "opacity"; to: 0 } HNumberAnimation { property: "scale"; to: 0 } } displaced: Transition { // ScriptAction { script: if (gridView.debug) print("displaced") } HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { property: "scale"; to: 1 } HNumberAnimation { properties: "x,y" } } onSelectedCountChanged: if (! selectedCount) lastCheckedDelegateIndex = 0 onModelChanged: { currentIndex = defaultCurrentIndex uncheckAll() } HKineticScrollingDisabler { id: mouseArea width: enabled ? parent.width : 0 height: enabled ? parent.height : 0 } } mirage-0.7.2/src/gui/Base/HIcon.qml000066400000000000000000000017601407747233600167450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtGraphicalEffects 1.12 Image { id: icon property string svgName: "" property bool small: false property int dimension: theme ? (small ? theme.icons.smallDimension : theme.icons.dimension) : (small ? 16 : 22) property color colorize: theme.icons.colorize property string iconPack: theme ? theme.icons.preferredPack : "thin" cache: true asynchronous: true fillMode: Image.PreserveAspectFit visible: Boolean(svgName) source: svgName ? `../../icons/${iconPack}/${svgName}.svg` : "" sourceSize.width: svgName ? dimension : 0 sourceSize.height: svgName ? dimension : 0 layer.enabled: ! Qt.colorEqual(colorize, "transparent") layer.effect: ColorOverlay { color: icon.colorize cached: icon.cache Behavior on color { HColorAnimation {} } } } mirage-0.7.2/src/gui/Base/HImage.qml000066400000000000000000000122261407747233600170760ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtGraphicalEffects 1.12 Image { id: image property bool circle: radius === circleRadius property bool broken: image.status === Image.Error property bool animate: true property bool animated: utils.urlExtension(image.source).toLowerCase() === "gif" property int animatedFillMode: AnimatedImage.PreserveAspectFit property alias radius: roundMask.radius property alias showProgressBar: progressBarLoader.active property bool showPauseButton: true property bool pause: ! window.settings.Chat.Files.auto_play_gif property bool forcePause: false property real speed: 1 readonly property int circleRadius: Math.ceil(Math.max(image.width, image.height)) readonly property int animatedPaintedWidth: animatedLoader.item ? animatedLoader.item.paintedWidth : 0 readonly property int animatedPaintedHeight: animatedLoader.item ? animatedLoader.item.paintedHeight : 0 readonly property int animatedImplicitWidth: animatedLoader.item ? animatedLoader.item.implicitWidth : 0 readonly property int animatedImplicitHeight: animatedLoader.item ? animatedLoader.item.implicitHeight : 0 function reload() { // Can be reimplemented in components inheriting HImage const oldSource = source source = "" source = oldSource } autoTransform: true asynchronous: true fillMode: Image.PreserveAspectFit cache: ! (animate && animated) && (sourceSize.width + sourceSize.height) <= 512 layer.enabled: radius !== 0 layer.effect: OpacityMask { maskSource: roundMask } Component { id: animatedImageComponent AnimatedImage { id: animatedImage source: image.source autoTransform: image.autoTransform asynchronous: image.asynchronous fillMode: image.animatedFillMode mirror: image.mirror mipmap: image.mipmap smooth: image.smooth horizontalAlignment: image.horizontalAlignment verticalAlignment: image.verticalAlignment // Online GIFs won't be able to loop if cache is set to false, // but caching GIFs is expansive. cache: ! Qt.resolvedUrl(source).startsWith("file://") speed: window.mainUI.debugConsole.baseGIFSpeed * image.speed paused: ! visible || window.hidden || image.pause || image.forcePause layer.enabled: image.radius !== 0 layer.effect: OpacityMask { maskSource: roundMask } // Hack to make the non-animated image behind this one // basically invisible Binding { target: image property: "fillMode" value: Image.Pad } Binding { target: image property: "sourceSize.width" value: 1 } Binding { target: image property: "sourceSize.height" value: 1 } HButton { anchors.left: parent.left anchors.bottom: parent.bottom anchors.leftMargin: theme.spacing / 2 anchors.bottomMargin: theme.spacing / 2 enableRadius: true icon.name: image.pause ? "player-play" : "player-pause" iconItem.small: true visible: image.showPauseButton && parent.width > width * 2 && parent.height > height * 2 onClicked: image.pause = ! image.pause } } } HLoader { id: animatedLoader anchors.fill: parent sourceComponent: animate && animated ? animatedImageComponent : null } HLoader { id: progressBarLoader readonly property alias progress: image.progress readonly property Component determinate: HCircleProgressBar { progress: image.progress } anchors.centerIn: parent width: Math.min( 96 * theme.uiScale, Math.min(parent.width, parent.height) * 0.5, ) height: width active: image.visible && image.opacity > 0.01 && image.status === Image.Loading sourceComponent: HBusyIndicator {} onProgressChanged: if (progress > 0 && progress < 1) sourceComponent = determinate } HIcon { anchors.centerIn: parent visible: image.broken svgName: "broken-image" colorize: theme.colors.negativeBackground } Rectangle { id: roundMask anchors.fill: parent visible: false } Timer { property int retries: 0 running: image.broken repeat: true interval: Math.min(60, 0.2 * Math.pow(2, Math.min(1000, retries) - 1)) * 1000 onTriggered: { image.reload() retries += 1 } } } mirage-0.7.2/src/gui/Base/HKineticScrollingDisabler.qml000066400000000000000000000057271407747233600227750ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 MouseArea { id: mouseArea property Flickable flickable: parent function getNewPosition(flickable, wheel) { // wheel.pixelDelta will be available on high resolution trackpads. // Otherwise use wheel.angleDelta, which is available from mouses and // low resolution trackpads. // When higher pixelDelta, more scroll will be applied const speedMultiply = Qt.styleHints.wheelScrollLines * window.settings.Scrolling.non_kinetic_speed const pixelDelta = { x: wheel.pixelDelta.x || wheel.angleDelta.x / 8 * speedMultiply, y: wheel.pixelDelta.y || wheel.angleDelta.y / 8 * speedMultiply, } // Return current position if there was not any movement if ( flickable.contentHeight < flickable.height || (! pixelDelta.x && ! pixelDelta.y) ) return {x: flickable.contentX, y: flickable.contentY} // Rotate the direction if shift is pressed if (wheel.modifiers === Qt.ShiftModifier) [pixelDelta.x, pixelDelta.y] = [pixelDelta.y, pixelDelta.x] const maxScroll = { x: flickable.contentWidth + flickable.originX - // Why subtract? flickable.rightMargin - flickable.width, y: flickable.contentHeight + flickable.originY + flickable.bottomMargin - flickable.height, } const minScroll = { x: flickable.originX - flickable.leftMargin, y: flickable.originY - flickable.topMargin, } // Avoid overscrolling return { x: Math.max( minScroll.x, Math.min(maxScroll.x, flickable.contentX - pixelDelta.x) ), y: Math.max( minScroll.y, Math.min(maxScroll.y, flickable.contentY - pixelDelta.y) ), } } enabled: ! window.settings.Scrolling.kinetic && ! utils.keyboardFlicking propagateComposedEvents: true acceptedButtons: Qt.NoButton onWheel: { // Make components below the stack notice the wheel event wheel.accepted = false if (wheel.modifiers === Qt.ControlModifier) return const pos = getNewPosition(flickable, wheel) flickable.flick(0, 0) flickable.contentX = pos.x flickable.contentY = pos.y } Binding { target: flickable property: "maximumFlickVelocity" value: mouseArea.enabled ? 0 : window.settings.Scrolling.kinetic_max_speed } Binding { target: flickable property: "flickDeceleration" value: mouseArea.enabled ? 0 : window.settings.Scrolling.kinetic_deceleration } } mirage-0.7.2/src/gui/Base/HLabel.qml000066400000000000000000000010421407747233600170650ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick.Controls 2.12 import QtQuick 2.12 Label { font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal font.pointSize: -1 textFormat: Label.PlainText horizontalAlignment: Label.AlignLeft color: theme.colors.text linkColor: theme.colors.link maximumLineCount: elide === Label.ElideNone ? Number.MAX_VALUE : 1 onLinkActivated: Qt.openUrlExternally(link) } mirage-0.7.2/src/gui/Base/HLabeledItem.qml000066400000000000000000000027751407747233600202330ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 HColumnLayout { default property alias insideData: itemHolder.data property bool loading: false property real elementsOpacity: item.opacity readonly property Item item: itemHolder.children[0] readonly property alias label: label readonly property alias errorLabel: errorLabel spacing: theme.spacing / 2 HRowLayout { spacing: theme.spacing HLabel { id: label opacity: elementsOpacity wrapMode: HLabel.Wrap Layout.fillWidth: true } HLoader { source: "HBusyIndicator.qml" active: loading visible: height > 0 Layout.preferredWidth: height Layout.preferredHeight: active ? label.height : 0 Behavior on Layout.preferredHeight { HNumberAnimation {} } } } Item { id: itemHolder // implicitWidth: childrenRect.width implicitHeight: childrenRect.height Layout.fillWidth: true } HLabel { id: errorLabel opacity: elementsOpacity visible: Layout.maximumHeight > 0 wrapMode: HLabel.Wrap color: theme.colors.errorText Layout.maximumHeight: text ? implicitHeight : 0 Layout.fillWidth: true Behavior on Layout.maximumHeight { HNumberAnimation {} } } } mirage-0.7.2/src/gui/Base/HListView.qml000066400000000000000000000110531407747233600176170ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 ListView { id: listView property int defaultCurrentIndex: -1 property int currentItemHeight: currentItem ? currentItem.height : 0 property var checked: ({}) property var checkedIndice: new Set() property int lastCheckedDelegateIndex: 0 property int selectedCount: Object.keys(checked).length readonly property point visibleStart: Qt.point(contentX - originX, contentY - originY) readonly property point visibleEnd: Qt.point(visibleStart.x + width, visibleStart.y + height) function check(...indices) { for (const i of indices) { const model = listView.model.get(i) checked[model.id] = model checkedIndice.add(i) } lastCheckedDelegateIndex = indices.slice(-1)[0] checkedChanged() checkedIndiceChanged() } function checkFromLastToHere(here) { const indices = utils.range(lastCheckedDelegateIndex, here) eventList.check(...indices) } function uncheck(...indices) { for (const i of indices) { const model = listView.model.get(i) delete checked[model.id] checkedIndice.delete(i) } checkedChanged() checkedIndiceChanged() } function uncheckAll() { checked = {} checkedIndice = new Set() } function toggleCheck(...indices) { const checkedNow = [] for (const i of indices) { const model = listView.model.get(i) if (model.id in checked) { delete checked[model.id] checkedIndice.delete(i) } else { checked[model.id] = model checkedNow.push(i) checkedIndice.add(i) } } if (checkedNow.length > 0) lastCheckedDelegateIndex = checkedNow.slice(-1)[0] checkedChanged() checkedIndiceChanged() } function getSortedChecked() { return Object.values(checked).sort( (a, b) => a.date > b.date ? 1 : -1 ) } currentIndex: defaultCurrentIndex keyNavigationWraps: true highlightMoveDuration: theme.animationDuration highlightResizeDuration: theme.animationDuration // Keep highlighted delegate at the center highlightRangeMode: ListView.ApplyRange preferredHighlightBegin: height / 2 - currentItemHeight / 2 preferredHighlightEnd: height / 2 + currentItemHeight / 2 maximumFlickVelocity: window.settings.Scrolling.kinetic_max_speed flickDeceleration: window.settings.Scrolling.kinetic_deceleration highlight: Rectangle { color: theme.controls.listView.highlight Rectangle { width: theme.controls.listView.highlightBorderThickness height: parent.height color: theme.controls.listView.highlightBorder } } ScrollBar.vertical: HScrollBar { flickableMoving: listView.moving visible: listView.interactive } // property bool debug: false // https://doc.qt.io/qt-5/qml-qtquick-viewtransition.html // #handling-interrupted-animations populate: add add: Transition { // ScriptAction { script: if (listView.debug) print("add") } HNumberAnimation { property: "opacity"; from: 0; to: 1 } HNumberAnimation { property: "scale"; from: 0; to: 1 } } move: Transition { // ScriptAction { script: if (listView.debug) print("move") } HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { property: "scale"; to: 1 } HNumberAnimation { properties: "x,y" } } remove: Transition { // ScriptAction { script: if (listView.debug) print("remove") } HNumberAnimation { property: "opacity"; to: 0 } HNumberAnimation { property: "scale"; to: 0 } } displaced: Transition { // ScriptAction { script: if (listView.debug) print("displaced") } HNumberAnimation { property: "opacity"; to: 1 } HNumberAnimation { property: "scale"; to: 1 } HNumberAnimation { properties: "x,y" } } onSelectedCountChanged: if (! selectedCount) lastCheckedDelegateIndex = 0 onModelChanged: { currentIndex = defaultCurrentIndex uncheckAll() } HKineticScrollingDisabler { width: enabled ? parent.width : 0 height: enabled ? parent.height : 0 } } mirage-0.7.2/src/gui/Base/HLoader.qml000066400000000000000000000003551407747233600172620ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Loader { id: loader asynchronous: true // visible: status === Loader.Ready } mirage-0.7.2/src/gui/Base/HMenu.qml000066400000000000000000000043231407747233600167570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import CppUtils 0.1 Menu { id: menu property bool isSubMenu: false property var previouslyFocused: null // MenuItems that open popups (or other elements taking focus when opened) // should set this to null. It will be reset to previouslyFocus when // the Menu is closed and opened again. property Item focusOnClosed: previouslyFocused readonly property string uuid: CppUtils.uuid() modal: true dim: false padding: theme.controls.menu.borderWidth implicitWidth: { let result = 0 for (let i = 0; i < count; ++i) { const item = itemAt(i) if (! item.visible) continue result = Math.max(item.implicitWidth, result) } return Math.min(result + menu.padding * 2, window.width) } background: Rectangle { color: theme.controls.menu.background border.color: theme.controls.menu.border border.width: theme.controls.menu.borderWidth radius: theme.radius // Workaround for this: when opening menu at mouse position, // cursor will be in menu's border which doesn't handle clicks TapHandler { gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: eventPoint => { const pos = eventPoint.position const border = parent.border.width if (pos.x <= border || pos.x >= parent.width - border) menu.close() if (pos.y <= border || pos.y >= parent.height - border) menu.close() } } } onAboutToShow: { if (isSubMenu) return previouslyFocused = window.activeFocusItem focusOnClosed = Qt.binding(() => previouslyFocused) } onOpened: { window.visibleMenus[uuid] = this window.visibleMenusChanged() } onClosed: { if (focusOnClosed) focusOnClosed.forceActiveFocus() delete window.visibleMenus[uuid] window.visibleMenusChanged() } Component.onDestruction: closed() } mirage-0.7.2/src/gui/Base/HMenuItem.qml000066400000000000000000000024531407747233600176000ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 MenuItem { id: menuItem readonly property alias iconItem: contentItem.icon readonly property alias label: contentItem.label spacing: theme.spacing leftPadding: spacing rightPadding: leftPadding topPadding: spacing / 1.75 bottomPadding: topPadding height: visible ? implicitHeight : 0 enabled: visible // prevent focusing invisible items by using keyboard icon.color: theme.icons.colorize background: HButtonBackground { button: menuItem buttonTheme: theme.controls.menuItem useFocusLine: false } contentItem: HButtonContent { id: contentItem button: menuItem buttonTheme: theme.controls.menuItem label.horizontalAlignment: Label.AlignLeft HIcon { visible: menuItem.checkable opacity: menuItem.checked ? 1 : 0 svgName: "menu-item-check-mark" Behavior on opacity { HNumberAnimation {} } } HIcon { visible: menuItem.subMenu svgName: "submenu-arrow" } } arrow: null indicator: null } mirage-0.7.2/src/gui/Base/HMenuItemPopupSpawner.qml000066400000000000000000000011421407747233600221560ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 HMenuItem { property var popup // url or HPopup Component property bool autoDestruct: true property var properties: ({}) onTriggered: { menu.focusOnClosed = null utils.makePopup( popup, window, utils.objectUpdate( { focusOnClosed: menu.previouslyFocused }, properties, ), null, autoDestruct, ) } } mirage-0.7.2/src/gui/Base/HMenuSeparator.qml000066400000000000000000000005001407747233600206310ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 MenuSeparator { id: separator padding: 0 contentItem: Item { implicitHeight: separator.visible ? theme.spacing : 0 } } mirage-0.7.2/src/gui/Base/HMxcImage.qml000066400000000000000000000043471407747233600175530ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HImage { id: image property string clientUserId property string mxc property string title property var roomId: undefined // undefined or string property var fileSize: undefined // undefined or int (bytes) property string sourceOverride: "" property bool thumbnail: true property var cryptDict: ({}) property string cachedPath: "" property bool canUpdate: true property bool show: ! canUpdate property string getFutureId: "" readonly property bool isMxc: mxc.startsWith("mxc://") function reload() { if (! py || ! canUpdate) return // component was destroyed const w = sourceSize.width || width const h = sourceSize.height || height if (! image.mxc || w < 1 || h < 1 ) { show = false return } if (! isMxc) { if (source !== mxc) source = mxc show = image.visible return } const method = image.thumbnail ? "get_thumbnail" : "get_media" let args = [ clientUserId, image.mxc, image.title, roomId, fileSize, cryptDict, ] if (image.thumbnail) args = [w, h, ...args] getFutureId = py.callCoro("media_cache." + method, args, path => { if (! image) return if (image.cachedPath !== path) image.cachedPath = path getFutureId = "" image.broken = Qt.binding(() => image.status === Image.Error) image.show = image.visible }, (type, args, error, traceback) => { print(`Error retrieving ${mxc} (${title}): ${type} ${args}`) if (image) image.broken = true }, ) } source: sourceOverride || (show ? cachedPath : "") showProgressBar: (isMxc && status === Image.Null) || status === Image.Loading onWidthChanged: Qt.callLater(reload) onHeightChanged: Qt.callLater(reload) onVisibleChanged: Qt.callLater(reload) onMxcChanged: Qt.callLater(reload) Component.onDestruction: if (getFutureId) py.cancelCoro(getFutureId) } mirage-0.7.2/src/gui/Base/HNoticePage.qml000066400000000000000000000020441407747233600200670ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 HRowLayout { property alias label: noticeLabel property alias text: noticeLabel.text property alias color: noticeLabel.color property alias font: noticeLabel.font property alias backgroundColor: noticeLabelBackground.color property alias radius: noticeLabelBackground.radius HLabel { id: noticeLabel horizontalAlignment: Text.AlignHCenter wrapMode: HLabel.Wrap padding: theme.spacing / 2 leftPadding: theme.spacing rightPadding: leftPadding opacity: width > 16 * theme.uiScale ? 1 : 0 background: Rectangle { id: noticeLabelBackground color: theme.controls.box.background radius: theme.controls.box.radius } Layout.alignment: Qt.AlignCenter Layout.preferredWidth: implicitWidth Layout.maximumWidth: parent.width } } mirage-0.7.2/src/gui/Base/HNumberAnimation.qml000066400000000000000000000007621407747233600211460ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 NumberAnimation { property real factor: 1.0 property real overshoot: 0.0 duration: theme.animationDuration * Math.max((1 + Math.abs(overshoot)) / 1.7, 1.0) * factor easing.type: overshoot > 0 ? Easing.OutBack : overshoot < 0 ? Easing.InBack : Easing.Linear easing.overshoot: overshoot } mirage-0.7.2/src/gui/Base/HPage.qml000066400000000000000000000014101407747233600167210ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Page { property bool useVariableSpacing: true property int currentSpacing: useVariableSpacing ? Math.min( theme.spacing * width / 400, theme.spacing * height / 400, theme.spacing, ) : theme.spacing signal keyboardAccept() signal keyboardCancel() padding: currentSpacing < theme.spacing ? 0 : currentSpacing background: null Keys.onReturnPressed: keyboardAccept() Keys.onEnterPressed: keyboardAccept() Keys.onEscapePressed: keyboardCancel() Behavior on padding { HNumberAnimation {} } } mirage-0.7.2/src/gui/Base/HPauseAnimation.qml000066400000000000000000000003651407747233600207720ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 PauseAnimation { property real factor: 1.0 duration: theme.animationDuration * factor } mirage-0.7.2/src/gui/Base/HPopup.qml000066400000000000000000000031271407747233600171570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import CppUtils 0.1 Popup { id: popup property var previouslyFocused: null property Item focusOnClosed: previouslyFocused readonly property int maximumPreferredWidth: window.width - leftMargin - rightMargin - leftInset - rightInset readonly property int maximumPreferredHeight: window.height - topMargin - bottomMargin - topInset - bottomInset readonly property string uuid: CppUtils.uuid() modal: true focus: true padding: 0 margins: theme.spacing // FIXME: Qt 5.15: `anchors.centerIn: Overlay.overlay` + transition broken x: (parent.width - width) / 2 y: (parent.height - height) / 2 enter: Transition { HNumberAnimation { property: "scale"; from: 0; to: 1; overshoot: 3 } } exit: Transition { HNumberAnimation { property: "scale"; to: 0 } } background: Rectangle { color: theme.controls.popup.background } Overlay.modal: Rectangle { color: "transparent" HColorAnimation on color { to: theme.controls.popup.windowOverlay } } onAboutToShow: previouslyFocused = window.activeFocusItem onOpened: { window.visiblePopups[uuid] = this window.visiblePopupsChanged() } onClosed: { if (focusOnClosed) focusOnClosed.forceActiveFocus() delete window.visiblePopups[uuid] window.visiblePopupsChanged() } Component.onDestruction: closed() } mirage-0.7.2/src/gui/Base/HProgressBar.qml000066400000000000000000000024601407747233600203040ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 ProgressBar { id: bar property color backgroundColor: theme.controls.progressBar.background property color foregroundColor: theme.controls.progressBar.foreground background: Rectangle { implicitWidth: 200 implicitHeight: theme.controls.progressBar.height color: backgroundColor } contentItem: Item { implicitWidth: 200 implicitHeight: theme.controls.progressBar.height Rectangle { id: indicator width: bar.indeterminate ? parent.width / 8 : bar.visualPosition * parent.width height: parent.height color: foregroundColor Behavior on color { HColorAnimation {} } HNumberAnimation on x { running: bar.visible && bar.indeterminate duration: 800 from: 0 to: bar.width - indicator.width onStopped: if (bar.indeterminate) { [from, to] = [to, from]; start() } else { indicator.x = 0 } } } } } mirage-0.7.2/src/gui/Base/HQtObject.qml000066400000000000000000000003121407747233600175600ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 QtObject { default property list data } mirage-0.7.2/src/gui/Base/HRadioButton.qml000066400000000000000000000053051407747233600203060ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 RadioButton { id: button property alias mainText: mainText property alias subtitle: subtitleText property bool defaultChecked: false readonly property bool changed: checked !== defaultChecked function reset() { checked = defaultChecked } checked: defaultChecked spacing: contentItem.visible ? theme.spacing : 0 padding: 0 indicator: Rectangle { opacity: button.enabled ? 1 : theme.disabledElementsOpacity + 0.2 implicitWidth: theme.controls.checkBox.boxSize implicitHeight: implicitWidth x: button.leftPadding y: button.topPadding + button.availableHeight / 2 - height / 2 radius: width / 2 color: theme.controls.checkBox.boxBackground border.color: button.enabled && button.pressed ? theme.controls.checkBox.boxPressedBorder : (button.enabled && button.hovered) || button.activeFocus ? theme.controls.checkBox.boxHoveredBorder : theme.controls.checkBox.boxBorder Behavior on border.color { HColorAnimation { factor: 0.5 } } Rectangle { anchors.centerIn: parent width: parent.width * 0.5 // XXX theme height: width radius: parent.radius color: theme.controls.checkBox.checkIconColorize scale: button.checked ? 1 : 0 Behavior on scale { HNumberAnimation { overshoot: 4 easing.type: Easing.InOutBack factor: 0.5 } } } } contentItem: HColumnLayout { visible: mainText.text || subtitleText.text opacity: button.enabled ? 1 : theme.disabledElementsOpacity HLabel { id: mainText text: button.text color: theme.controls.checkBox.text // Set a width on RadioButton for wrapping to work, // e.g. by using Layout.fillWidth wrapMode: HLabel.Wrap leftPadding: button.indicator.width + button.spacing verticalAlignment: Text.AlignVCenter Layout.fillWidth: true } HLabel { id: subtitleText visible: Boolean(text) color: theme.controls.checkBox.subtitle font.pixelSize: theme.fontSize.small wrapMode: mainText.wrapMode leftPadding: mainText.leftPadding verticalAlignment: mainText.verticalAlignment Layout.fillWidth: true } } Behavior on opacity { HNumberAnimation { factor: 2 } } } mirage-0.7.2/src/gui/Base/HRectangleBottomBorder.qml000066400000000000000000000012371407747233600223030ustar00rootroot00000000000000import QtQuick 2.12 Item { id: root property Rectangle rectangle: parent property alias borderHeight: clipArea.height property alias color: borderRectangle.color implicitWidth: rectangle.width implicitHeight: rectangle.height Item { id: clipArea anchors.bottom: parent ? parent.bottom : undefined width: parent ? parent.width : 0 height: 1 clip: true Rectangle { id: borderRectangle anchors.bottom: parent ? parent.bottom : undefined width: parent ? parent.width : 0 height: root.height radius: rectangle.radius } } } mirage-0.7.2/src/gui/Base/HRepeater.qml000066400000000000000000000020621407747233600176200ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick.Controls 2.12 import QtQuick 2.12 Repeater { id: repeater readonly property var childrenImplicitWidth: { const widths = [] for (let i = 0; i < repeater.count; i++) { const item = repeater.itemAt(i) if (item) widths.push(item.implicitWidth > 0 ? item.implicitWidth : 0) } return widths } readonly property var childrenWidth: { const widths = [] for (let i = 0; i < repeater.count; i++) { const item = repeater.itemAt(i) if (item) widths.push(item.width > 0 ? item.width : 0) } return widths } readonly property real summedWidth: utils.sum(childrenWidth) readonly property real summedImplicitWidth:utils.sum(childrenImplicitWidth) readonly property real thinestChild: Math.min(...childrenWidth) readonly property real widestChild: Math.max(...childrenWidth) } mirage-0.7.2/src/gui/Base/HRoomAvatar.qml000066400000000000000000000005751407747233600201330ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HAvatar { property string roomId property string displayName name: displayName[0] === "#" && displayName.length > 1 ? displayName.substring(1) : displayName title: "room_" + roomId + ".avatar" } mirage-0.7.2/src/gui/Base/HRowLayout.qml000066400000000000000000000003151407747233600200150ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 RowLayout { spacing: 0 } mirage-0.7.2/src/gui/Base/HScrollBar.qml000066400000000000000000000030741407747233600177400ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 ScrollBar { id: scrollBar property bool flickableMoving minimumSize: (Math.min(height / 1.5, 48) * theme.uiScale) / height opacity: size < 1 && (active || hovered) ? 1 : 0 padding: 0 background: Rectangle { color: theme.controls.scrollBar.track } contentItem: Item { implicitWidth: theme.controls.scrollBar.width Rectangle { anchors.fill: parent anchors.leftMargin: theme.controls.scrollBar.sliderPadding anchors.rightMargin: anchors.leftMargin radius: theme.controls.scrollBar.sliderRadius color: scrollBar.pressed ? theme.controls.scrollBar.pressedSlider : sliderHover.hovered ? theme.controls.scrollBar.hoveredSlider : theme.controls.scrollBar.slider Behavior on color { HColorAnimation {} } HoverHandler { id: sliderHover } } } // onFlickableMovingChanged: if (flickableMoving) activeOverride.when = false // Behavior on opacity { HNumberAnimation { factor: 2 } } Behavior on opacity { HNumberAnimation {} } // Binding on active { // id: activeOverride // value: initialVisibilityTimer.running // } // Timer { // id: initialVisibilityTimer // interval: window.settings.autoHideScrollBarsAfterMsec // running: scrollBar.size < 1 // } } mirage-0.7.2/src/gui/Base/HScrollView.qml000066400000000000000000000006441407747233600201460ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 ScrollView { id: scrollView ScrollBar.vertical: HScrollBar { parent: scrollView x: scrollView.mirrored ? 0 : scrollView.width - width y: scrollView.topPadding height: scrollView.availableHeight } } mirage-0.7.2/src/gui/Base/HSelectableLabel.qml000066400000000000000000000023041407747233600210530ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 TextEdit { id: label property bool enableLinkActivation: true // For rich text, selectedText returns some weird invisible characters // instead of real newlines readonly property string selectedPlainText: selectedText.replace(/[\u2028\u2029]/g, "\n") function selectWordAt(position) { label.cursorPosition = positionAt(position.x, position.y) label.selectWord() } function selectAllText() { label.selectAll() } font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal color: theme.colors.text textFormat: Label.PlainText tabStopDistance: 4 * 4 // 4 spaces horizontalAlignment: Label.AlignLeft readOnly: true activeFocusOnPress: false focus: false selectByMouse: true onLinkActivated: if (enableLinkActivation) Qt.openUrlExternally(link) MouseArea { anchors.fill: label acceptedButtons: Qt.NoButton cursorShape: label.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor } } mirage-0.7.2/src/gui/Base/HShortcut.qml000066400000000000000000000006611407747233600176670ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Shortcut { // TODO: use enabled + a Binding with restoreValue when switch to Qt 5.15 property bool active: true property bool disableIfAnyPopupOrMenu: true enabled: (! window.anyPopupOrMenu || ! disableIfAnyPopupOrMenu) && active context: Qt.ApplicationShortcut } mirage-0.7.2/src/gui/Base/HSlider.qml000066400000000000000000000043141407747233600172750ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 Slider { id: slider property bool enableRadius: true property bool fullHeight: false property color backgroundColor: theme.controls.slider.background property color foregroundColor: theme.controls.slider.foreground property alias toolTip: toolTip property alias mouseArea: mouseArea leftPadding: 0 rightPadding: leftPadding topPadding: 0 bottomPadding: topPadding background: Rectangle { color: backgroundColor x: slider.leftPadding y: slider.topPadding + slider.availableHeight / 2 - height / 2 implicitWidth: 200 implicitHeight: theme.controls.slider.height width: slider.availableWidth height: fullHeight ? slider.height : implicitHeight radius: enableRadius ? theme.controls.slider.radius : 0 Rectangle { width: slider.visualPosition * parent.width height: parent.height color: foregroundColor radius: parent.radius } } handle: Rectangle { x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width) y: slider.topPadding + slider.availableHeight / 2 - height / 2 implicitWidth: theme.controls.slider.handle.size implicitHeight: implicitWidth radius: implicitWidth / 2 color: slider.pressed ? theme.controls.slider.handle.pressedInside : theme.controls.slider.handle.inside border.color: slider.pressed ? theme.controls.slider.handle.pressedBorder : theme.controls.slider.handle.border Behavior on color { HColorAnimation {} } Behavior on border.color { HColorAnimation {} } } HToolTip { id: toolTip parent: slider.handle visible: slider.pressed && text delay: 0 } MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: slider.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor } } mirage-0.7.2/src/gui/Base/HSpacer.qml000066400000000000000000000003601407747233600172650ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 Item { Layout.fillWidth: true Layout.fillHeight: true } mirage-0.7.2/src/gui/Base/HSpinBox.qml000066400000000000000000000045151407747233600174400ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 SpinBox { id: box property var defaultValue: null readonly property bool changed: value !== (defaultValue || 0) function reset() { value = Qt.binding(() => defaultValue || 0) } value: defaultValue || 0 implicitHeight: theme.baseElementsHeight padding: 0 editable: true to: 2147483647 background: null contentItem: HRowLayout { HButton { text: qsTr("-") font.pixelSize: theme.fontSize.biggest autoRepeat: true autoRepeatInterval: 50 // Don't set enabled to false or it glitches, use opacity instead opacity: box.value > box.from ? 1 : theme.disabledElementsOpacity onPressed: if (box.value > box.from) box.decrease() Layout.fillHeight: true Behavior on opacity { HNumberAnimation {} } } HTextField { id: textField height: parent.height implicitWidth: 90 * theme.uiScale radius: 0 horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter text: box.value readOnly: ! box.editable validator: box.validator inputMethodHints: Qt.ImhFormattedNumbersOnly onTextEdited: { if (! text || text === "-") return const input = parseInt(text, 10) box.value = Math.max(box.from, Math.min(box.to, input)) } } HButton { text: qsTr("+") font.pixelSize: theme.fontSize.biggest autoRepeat: true autoRepeatInterval: 50 opacity: box.value < box.to ? 1 : theme.disabledElementsOpacity onPressed: if (box.value < box.to) box.increase() Layout.fillHeight: true Behavior on opacity { HNumberAnimation {} } } } down.indicator: null up.indicator: null MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: textField.hovered ? Qt.IBeamCursor : Qt.ArrowCursor onWheel: wheel => { wheel.angleDelta.y < 0 ? box.decrease() : box.increase() wheel.accepted = true } } } mirage-0.7.2/src/gui/Base/HStackView.qml000066400000000000000000000013561407747233600177560ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 StackView { pushEnter: Transition { HNumberAnimation { property: "scale" from: 0 to: 1 } } pushExit: Transition { HNumberAnimation { property: "opacity" from: 1 to: 0 } } popEnter: Transition { HNumberAnimation { property: "opacity" from: 0 to: 1 } } popExit: Transition { HNumberAnimation { property: "scale" from: 1 to: 0 } } } mirage-0.7.2/src/gui/Base/HSwipeView.qml000066400000000000000000000027451407747233600200030ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import "../ShortcutBundles" SwipeView { id: swipeView enum Move { ToPrevious, ToNext } property string saveName: "" property var saveId: "ALL" property var saveProperties: ["currentIndex"] // Prevent onCurrentIndexChanged from running before Component.onCompleted property bool saveEnabled: false property int previousIndex: 0 property int defaultIndex: 0 property int lastMove: HSwipeView.Move.ToNext property bool changed: currentIndex !== defaultIndex function reset() { setCurrentIndex(defaultIndex) } function incrementWrapIndex() { currentIndex === count - 1 ? setCurrentIndex(0) : incrementCurrentIndex() } function decrementWrapIndex() { currentIndex === 0 ? setCurrentIndex(count - 1) : decrementCurrentIndex() } Component.onCompleted: if (! changed) { setCurrentIndex(window.getState(this, "currentIndex", defaultIndex)) saveEnabled = true } onCurrentIndexChanged: { if (saveEnabled) window.saveState(this) if (currentIndex < previousIndex) lastMove = HSwipeView.Move.ToPrevious if (currentIndex > previousIndex) lastMove = HSwipeView.Move.ToNext previousIndex = currentIndex } TabShortcuts { container: swipeView } } mirage-0.7.2/src/gui/Base/HTabBar.qml000066400000000000000000000011471407747233600172070ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import "../ShortcutBundles" TabBar { id: tabBar property alias shortcutsEnabled: tabShortcuts.active spacing: 0 position: TabBar.Header background: Item { Rectangle { width: parent.width anchors.bottom: parent.bottom height: 2 color: theme.controls.tab.bottomLine } } TabShortcuts { id: tabShortcuts container: tabBar } } mirage-0.7.2/src/gui/Base/HTabButton.qml000066400000000000000000000032771407747233600177640ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 TabButton { id: button readonly property alias iconItem: contentItem.icon readonly property alias label: contentItem.label property color backgroundColor: TabBar.index % 2 === 0 ? theme.controls.tab.background : theme.controls.tab.alternateBackground property color focusLineColor: Qt.colorEqual(icon.color, theme.icons.colorize) ? theme.controls.tab.focusedBorder : icon.color property bool loading: false property HToolTip toolTip: HToolTip { id: toolTip visible: text && hovered } spacing: theme.spacing topPadding: spacing / 1.5 bottomPadding: topPadding leftPadding: spacing rightPadding: leftPadding icon.color: theme.icons.colorize implicitWidth: Math.max( implicitBackgroundWidth + leftInset + rightInset, // FIXME: why is *2 needed to not get ellided text in AddAccount page? implicitContentWidth + leftPadding * 2 + rightPadding * 2, ) implicitHeight: Math.max( implicitBackgroundHeight + topInset + bottomInset, implicitContentHeight + topPadding + bottomPadding, ) // Prevent button from gaining focus and being highlighted on click focusPolicy: Qt.TabFocus background: HButtonBackground { button: button buttonTheme: theme.controls.tab color: backgroundColor } contentItem: HButtonContent { id: contentItem button: button buttonTheme: theme.controls.tab } } mirage-0.7.2/src/gui/Base/HTabbedBox.qml000066400000000000000000000032311407747233600177020ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 HPage { default property alias swipeViewData: swipeView.contentData property Component tabBar: HTabBar {} property alias backButton: backButton property bool showBackButton: false readonly property alias swipeView: swipeView contentWidth: Math.max(swipeView.contentWidth, theme.controls.box.defaultWidth) header: HRowLayout { HButton { id: backButton visible: Layout.preferredWidth > 0 Layout.preferredWidth: showBackButton ? implicitWidth : 0 Behavior on Layout.preferredWidth { HNumberAnimation {} } } HLoader { id: tabBarLoader asynchronous: false sourceComponent: tabBar Layout.fillWidth: true Layout.fillHeight: true } } background: Rectangle { color: theme.controls.box.background radius: theme.controls.box.radius } HNumberAnimation on scale { running: true from: 0 to: 1 overshoot: 2 } Behavior on implicitWidth { HNumberAnimation {} } Behavior on implicitHeight { HNumberAnimation {} } Binding { target: tabBarLoader.item property: "currentIndex" value: swipeView.currentIndex } SwipeView { id: swipeView anchors.fill: parent clip: true currentIndex: tabBarLoader.item.currentIndex onCurrentItemChanged: currentItem.takeFocus() } } mirage-0.7.2/src/gui/Base/HTextArea.qml000066400000000000000000000134541407747233600175750ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import Clipboard 0.1 TextArea { id: textArea property string saveName: "" property var saveId: "ALL" property var saveProperties: ["text"] property bool error: false property alias radius: textAreaBackground.radius property bool bordered: true property var focusItemOnTab: null property bool menuKeySpawnsMenu: true property var disabledText: null property var defaultText: null // XXX test me readonly property bool changed: text !== (defaultText || "") readonly property string displayText: text + preeditText property alias backgroundColor: textAreaBackground.color property color borderColor: theme.controls.textArea.border property color errorBorder: theme.controls.textArea.errorBorder property color focusedBorderColor: theme.controls.textArea.focusedBorder property string previousDefaultText: "" // private // For rich text, selectedText returns some weird invisible characters // instead of real newlines readonly property string selectedPlainText: selectedText.replace(/[\u2028\u2029]/g, "\n") property bool enableCustomImagePaste: false signal customImagePaste() function reset() { clear() text = Qt.binding(() => defaultText || "") } function loadState() { if (! text) insertAtCursor(window.getState(this, "text", "")) } function insertAtCursor(text) { insert(cursorPosition, text) } function getWordAt(position) { return utils.getWordAtPosition(displayText, position) } function getWordBehindCursor() { return cursorPosition === 0 ? null : getWordAt(cursorPosition - 1) } text: defaultText || "" opacity: enabled ? 1 : theme.disabledElementsOpacity selectByMouse: true activeFocusOnPress: ! readOnly leftPadding: theme.spacing rightPadding: leftPadding topPadding: theme.spacing / 1.5 bottomPadding: topPadding readOnly: ! visible wrapMode: TextEdit.Wrap font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal font.pointSize: -1 placeholderTextColor: theme.controls.textArea.placeholderText color: theme.controls.textArea.text background: Rectangle { id: textAreaBackground radius: bordered ? theme.radius : 0 color: theme.controls.textArea.background opacity: textArea.opacity border.width: bordered ? theme.controls.textArea.borderWidth : 0 border.color: borderColor HBottomFocusLine { show: textArea.activeFocus borderHeight: theme.controls.textArea.borderWidth color: error ? errorBorder : focusedBorderColor } } Component.onCompleted: { // Break binding previousDefaultText = previousDefaultText loadState() } onTextChanged: window.saveState(this) onActiveFocusChanged: if (defaultText !== null) text = text // Break binding onDefaultTextChanged: if (defaultText !== null) { if (text === previousDefaultText) text = Qt.binding(() => defaultText) previousDefaultText = defaultText } onPressed: ev => { if (ev.button === Qt.RightButton) contextMenu.spawn() } onPressAndHold: ev => contextMenu.spawn() Keys.onPressed: ev => { // Prevent alt/super+any key from typing text if ( ev.modifiers & Qt.AltModifier || ev.modifiers & Qt.MetaModifier ) ev.accepted = true if ( ev.matches(StandardKey.Paste) && textArea.enableCustomImagePaste && Clipboard.hasImage ) { ev.accepted = true textArea.customImagePaste() } } Keys.onMenuPressed: if (menuKeySpawnsMenu) contextMenu.spawn(false) // Prevent leaking arrow presses to parent elements when the carret is at // the beginning or end of the text Keys.onLeftPressed: event.accepted = readOnly || (cursorPosition === 0 && ! selectedText) Keys.onRightPressed: event.accepted = readOnly || (cursorPosition === length && ! selectedText) KeyNavigation.priority: KeyNavigation.BeforeItem KeyNavigation.tab: focusItemOnTab Binding on color { value: "transparent" when: disabledText !== null && ! textArea.enabled } Binding on placeholderTextColor { value: "transparent" when: disabledText !== null && ! textArea.enabled } Binding on implicitHeight { value: disabledTextLabel.implicitHeight when: disabledText !== null && ! textArea.enabled } Behavior on opacity { HNumberAnimation {} } Behavior on color { HColorAnimation {} } Behavior on placeholderTextColor { HColorAnimation {} } HLabel { id: disabledTextLabel anchors.fill: parent visible: opacity > 0 opacity: disabledText !== null && parent.enabled ? 0 : 1 text: disabledText || "" leftPadding: parent.leftPadding rightPadding: parent.rightPadding topPadding: parent.topPadding bottomPadding: parent.bottomPadding wrapMode: parent.wrapMode === TextEdit.Wrap ? HLabel.Wrap : parent.wrapMode === TextEdit.WordWrap ? HLabel.WordWrap : parent.wrapMode === TextEdit.WrapAnywhere ? HLabel.WrapAnywhere : Text.NoWrap font.family: parent.font.family font.pixelSize: parent.font.pixelSize Behavior on opacity { HNumberAnimation {} } } HTextContextMenu { id: contextMenu enableCustomImagePaste: textArea.enableCustomImagePaste onCustomImagePaste: textArea.customImagePaste() } } mirage-0.7.2/src/gui/Base/HTextContextMenu.qml000066400000000000000000000043261407747233600211740ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Clipboard 0.1 HMenu { id: menu property Item control: parent // HTextField or HTextArea property bool enableCustomImagePaste: false property bool hadPersistentSelection: false // TODO: use a Qt 5.15 Binding signal customImagePaste() function spawn(atMousePosition=true) { hadPersistentSelection = control.persistentSelection control.persistentSelection = true atMousePosition ? popup() : popup( control.cursorRectangle.right, control.cursorRectangle.bottom + theme.spacing / 4, ) } onClosed: control.persistentSelection = hadPersistentSelection Component.onDestruction: control.persistentSelection = hadPersistentSelection HMenuItem { icon.name: "undo" text: qsTr("Undo") visible: ! control.readOnly enabled: control.canUndo onTriggered: control.undo() } HMenuItem { icon.name: "redo" text: qsTr("Redo") visible: ! control.readOnly enabled: control.canRedo onTriggered: control.redo() } HMenuSeparator { visible: ! control.readOnly } HMenuItem { icon.name: "cut-text" text: qsTr("Cut") visible: ! control.readOnly enabled: control.selectedPlainText onTriggered: control.cut() } HMenuItem { icon.name: "copy-text" text: qsTr("Copy") enabled: control.selectedPlainText onTriggered: control.copy() } HMenuItem { property bool pasteImage: menu.enableCustomImagePaste && Clipboard.hasImage icon.name: "paste-text" text: qsTr("Paste") visible: ! control.readOnly enabled: control.canPaste || pasteImage onTriggered: pasteImage ? menu.customImagePaste() : control.paste() } HMenuSeparator { visible: ! control.readOnly } HMenuItem { icon.name: "select-all-text" text: qsTr("Select all") enabled: control.length > 0 onTriggered: control.selectAll() } } mirage-0.7.2/src/gui/Base/HTextField.qml000066400000000000000000000116311407747233600177430ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 TextField { id: field property string saveName: "" property var saveId: "ALL" property var saveProperties: ["text"] property bool error: false property alias radius: textFieldBackground.radius property bool bordered: true property color backgroundColor: theme.controls.textField.background property color borderColor: theme.controls.textField.border property color errorBorder: theme.controls.textField.errorBorder property color focusedBackgroundColor: theme.controls.textField.focusedBackground property color focusedBorderColor: theme.controls.textField.focusedBorder property var disabledText: null property var defaultText: null readonly property bool changed: text !== (defaultText || "") property string previousDefaultText: "" // private // For rich text, selectedText returns some weird invisible characters // instead of real newlines readonly property string selectedPlainText: selectedText.replace(/[\u2028\u2029]/g, "\n") function reset() { clear() text = Qt.binding(() => defaultText || "") } function loadState() { if (! text) insertAtCursor(window.getState(this, "text", "")) } function insertAtCursor(text) { insert(cursorPosition, text) } text: defaultText || "" opacity: enabled ? 1 : theme.disabledElementsOpacity selectByMouse: true activeFocusOnPress: ! readOnly leftPadding: theme.spacing rightPadding: leftPadding topPadding: theme.spacing / 1.5 bottomPadding: topPadding font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal font.pointSize: -1 placeholderTextColor: theme.controls.textField.placeholderText color: activeFocus ? theme.controls.textField.focusedText : theme.controls.textField.text background: Rectangle { id: textFieldBackground radius: bordered ? theme.radius : 0 color: field.activeFocus ? focusedBackgroundColor : backgroundColor border.width: bordered ? theme.controls.textField.borderWidth : 0 border.color: borderColor HBottomFocusLine { show: field.activeFocus borderHeight: theme.controls.textField.borderWidth color: error ? errorBorder : focusedBorderColor } } Component.onCompleted: { // Break binding previousDefaultText = previousDefaultText loadState() } onTextChanged: window.saveState(this) onActiveFocusChanged: if (defaultText !== null) text = text // Break binding onDefaultTextChanged: if (defaultText !== null) { if (text === previousDefaultText) text = Qt.binding(() => defaultText) previousDefaultText = defaultText } onPressed: ev => { if (ev.button === Qt.RightButton) contextMenu.spawn() } onPressAndHold: ev => contextMenu.spawn() // Prevent alt/super+any key from typing text Keys.onPressed: if ( event.modifiers & Qt.AltModifier || event.modifiers & Qt.MetaModifier ) event.accepted = true Keys.onMenuPressed: contextMenu.spawn(false) // Prevent leaking arrow presses to parent elements when the carret is at // the beginning or end of the text Keys.onLeftPressed: event.accepted = cursorPosition === 0 && ! selectedText Keys.onRightPressed: event.accepted = cursorPosition === length && ! selectedText Binding on color { value: "transparent" when: disabledText !== null && ! field.enabled } Binding on placeholderTextColor { value: "transparent" when: disabledText !== null && ! field.enabled } Binding on implicitHeight { value: disabledTextLabel.implicitHeight when: disabledText !== null && ! field.enabled } Behavior on opacity { HNumberAnimation {} } Behavior on color { HColorAnimation {} } Behavior on placeholderTextColor { HColorAnimation {} } HLabel { id: disabledTextLabel anchors.fill: parent visible: opacity > 0 opacity: disabledText !== null && parent.enabled ? 0 : 1 text: disabledText || "" leftPadding: parent.leftPadding rightPadding: parent.rightPadding topPadding: parent.topPadding bottomPadding: parent.bottomPadding wrapMode: parent.wrapMode === TextField.Wrap ? HLabel.Wrap : parent.wrapMode === TextField.WordWrap ? HLabel.WordWrap : parent.wrapMode === TextField.WrapAnywhere ? HLabel.WrapAnywhere : Text.NoWrap font.family: parent.font.family font.pixelSize: parent.font.pixelSize Behavior on opacity { HNumberAnimation {} } } HTextContextMenu { id: contextMenu } } mirage-0.7.2/src/gui/Base/HTile/000077500000000000000000000000001407747233600162335ustar00rootroot00000000000000mirage-0.7.2/src/gui/Base/HTile/ContentRow.qml000066400000000000000000000004031407747233600210450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import ".." HRowLayout { property HTile tile spacing: tile.spacing opacity: tile.contentOpacity } mirage-0.7.2/src/gui/Base/HTile/HTile.qml000066400000000000000000000035611407747233600177600ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import ".." HButton { id: tile property bool compact: window.settings.General.compact property real contentOpacity: 1 property Component contextMenu: null property HMenu openedMenu: null signal leftClicked() signal middleClicked() signal rightClicked() signal longPressed() function openMenu(atCursor=true) { if (! contextMenu) return if (openedMenu) { openedMenu.close() return } openedMenu = contextMenu.createObject(tile) openedMenu.closed.connect(() => openedMenu.destroy()) atCursor ? openedMenu.popup() : openedMenu.popup(tile.width / 2, tile.height / 2) } function doRightClick(menuAtCursor=true) { rightClicked() openMenu(menuAtCursor) } topPadding: padded ? spacing / (compact ? 4 : 2) : 0 bottomPadding: topPadding Keys.onEnterPressed: leftClicked() Keys.onReturnPressed: leftClicked() Keys.onSpacePressed: leftClicked() Keys.onMenuPressed: doRightClick(false) Behavior on topPadding { HNumberAnimation {} } Behavior on bottomPadding { HNumberAnimation {} } TapHandler { acceptedButtons: Qt.LeftButton onTapped: leftClicked() onLongPressed: tile.longPressed() } TapHandler { acceptedButtons: Qt.MiddleButton onTapped: middleClicked() onLongPressed: tile.middleClicked() } TapHandler { acceptedButtons: Qt.RightButton acceptedPointerTypes: PointerDevice.GenericPointer | PointerDevice.Pen onTapped: doRightClick() } TapHandler { acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen onLongPressed: doRightClick() } } mirage-0.7.2/src/gui/Base/HTile/SubtitleLabel.qml000066400000000000000000000014431407747233600215030ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." HLabel { property HTile tile textFormat: Text.StyledText font.pixelSize: theme.fontSize.small verticalAlignment: Qt.AlignVCenter elide: Text.ElideRight color: theme.colors.dimText visible: Layout.maximumHeight > 0 Layout.maximumHeight: ! tile.compact && text ? implicitHeight : 0 Layout.fillWidth: true Layout.fillHeight: true Behavior on Layout.maximumHeight { HNumberAnimation {} } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } mirage-0.7.2/src/gui/Base/HTile/TitleLabel.qml000066400000000000000000000005011407747233600207630ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." HLabel { elide: Text.ElideRight verticalAlignment: Qt.AlignVCenter Layout.fillWidth: true Layout.fillHeight: true } mirage-0.7.2/src/gui/Base/HTile/TitleRightInfoLabel.qml000066400000000000000000000012131407747233600225760ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." HLabel { property HTile tile property int hideUnderWidth: 200 font.pixelSize: theme.fontSize.small verticalAlignment: Qt.AlignVCenter color: theme.colors.halfDimText opacity: Layout.maximumWidth > 0 ? 1 : 0 visible: opacity > 0 Layout.fillHeight: true Layout.maximumWidth: text && tile.width >= hideUnderWidth * theme.uiScale ? implicitWidth : 0 Behavior on Layout.maximumWidth { HNumberAnimation {} } } mirage-0.7.2/src/gui/Base/HToolTip.qml000066400000000000000000000036031407747233600174450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 ToolTip { id: toolTip property bool instant: false property alias label: label property alias backgroundColor: background.color function instantShow(timeout=-1) { if (visible) return instant = true timeout === -1 ? open() : show(text, timeout) instant = false } function instantToggle(timeout=-1) { visible ? hide() : instantShow(timeout) } delay: instant ? 0 : window.settings.General.tooltips_delay * 1000 padding: background.border.width background: Rectangle { id: background color: theme.controls.toolTip.background border.color: theme.controls.toolTip.border border.width: theme.controls.toolTip.borderWidth } contentItem: HRowLayout { HLabel { id: label color: theme.controls.toolTip.text text: toolTip.text wrapMode: HLabel.Wrap leftPadding: theme.spacing / 1.5 rightPadding: leftPadding topPadding: theme.spacing / 2 bottomPadding: topPadding Layout.maximumWidth: Math.min( window.width / 1.25, theme.fontSize.normal * 0.5 * 75, ) } } enter: Transition { HNumberAnimation { property: "opacity"; from: 0.0; to: 1.0 } } exit: Transition { HNumberAnimation { property: "opacity"; to: 0.0 } } TapHandler { onTapped: toolTip.hide() } HoverHandler { onHoveredChanged: if (! hovered) toolTip.hide() } HoverHandler { target: mainUI enabled: toolTip.visible onHoveredChanged: if (toolTip.visible && ! hovered) toolTip.hide() } } mirage-0.7.2/src/gui/Base/HUserAvatar.qml000066400000000000000000000037771407747233600201440ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 HAvatar { id: avatar property string userId property string displayName property string presence: "" property int powerLevel: 0 property int shiftMembershipIconPositionBy: -8 property bool invited: false readonly property bool admin: powerLevel >= 100 readonly property bool moderator: powerLevel >= 50 && ! admin name: displayName || userId.substring(1) // no leading @ title: "user_" + userId + ".avatar" HLoader { active: admin || moderator || invited anchors.top: parent.top anchors.left: parent.left anchors.topMargin: shiftMembershipIconPositionBy anchors.leftMargin: anchors.topMargin z: 100 Behavior on anchors.topMargin { HNumberAnimation {} } sourceComponent: HIcon { small: true svgName: invited ? "user-invited" : admin ? "user-power-100" : "user-power-50" colorize: invited ? theme.chat.roomPane.listView.member.invitedIcon : admin ? theme.chat.roomPane.listView.member.adminIcon : theme.chat.roomPane.listView.member.moderatorIcon HoverHandler { id: membershipIcon } HToolTip { visible: membershipIcon.hovered text: invited ? qsTr("Invited") : admin ? qsTr("Admin (%1 power)").arg(powerLevel) : qsTr("Moderator (%1 power)").arg(powerLevel) } } } HLoader { active: presence && presence !== "offline" anchors.bottom: parent.bottom anchors.right: parent.right anchors.bottomMargin: item ? -item.width / 2 : 0 anchors.rightMargin: item ? -item.height / 2 : 0 z: 300 sourceComponent: PresenceOrb { presence: avatar.presence } } } mirage-0.7.2/src/gui/Base/MediaPlayer/000077500000000000000000000000001407747233600174225ustar00rootroot00000000000000mirage-0.7.2/src/gui/Base/MediaPlayer/AudioPlayer.qml000066400000000000000000000011151407747233600223510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtAV 1.7 OSD { id: osd property alias source: audioPlayer.source audioOnly: true media: audioPlayer implicitWidth: osd.width implicitHeight: osd.height MediaPlayer { id: audioPlayer autoLoad: window.settings.media.autoLoad autoPlay: window.settings.media.autoPlay volume: window.settings.media.defaultVolume / 100 muted: window.settings.media.startMuted } } mirage-0.7.2/src/gui/Base/MediaPlayer/OSD.qml000066400000000000000000000211401407747233600205600ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtAV 1.7 import "../../Base" HColumnLayout { id: osd property QtObject media: parent // QtAV.Video or QtAV.MediaPlayer property bool audioOnly: false property bool showup: false property bool fullScreen: false property real savedAspectRatio: 16 / 9 property int savedDuration: 0 readonly property real aspectRatio: media.sourceAspectRatio || 0 readonly property int duration: media.duration readonly property int boundPosition: savedDuration ? Math.min(media.position, savedDuration) : media.position function togglePlay() { media.playbackState === MediaPlayer.PlayingState ? media.pause() : media.play() } function seekToPosition(pos) { // pos: 0.0 to 1.0 if (media.playbackState === MediaPlayer.StoppedState) media.play() if (media.seekable) media.seek(pos * (savedDuration || boundPosition)) } visible: osdScaleTransform.yScale > 0 transform: Scale { id: osdScaleTransform yScale: audioOnly || osdHover.hovered || media.playbackState !== MediaPlayer.PlayingState || osd.showup ? 1 : 0 origin.y: osd.height Behavior on yScale { HNumberAnimation {} } } onShowupChanged: if (showup) osdHideTimer.restart() onDurationChanged: if (duration) savedDuration = duration onAspectRatioChanged: if (aspectRatio) savedAspectRatio = aspectRatio HoverHandler { id: osdHover } Timer { id: osdHideTimer interval: window.settings.Chat.Files.autohide_image_controls_after onTriggered: osd.showup = false } HSlider { id: timeSlider topPadding: 5 z: 1 to: savedDuration || boundPosition backgroundColor: theme.mediaPlayer.progress.background enableRadius: false fullHeight: true mouseArea.hoverEnabled: true onMoved: seekToPosition(timeSlider.position) Layout.fillWidth: true Layout.preferredHeight: theme.mediaPlayer.progress.height HToolTip { id: previewToolTip readonly property int wantTimestamp: visible ? savedDuration * (timeSlider.mouseArea.mouseX / timeSlider.mouseArea.width) : -1 x: timeSlider.mouseArea.mouseX - width / 2 visible: ! audioOnly && preview.implicitWidth >= previewLabel.implicitWidth + previewLabel.padding && preview.implicitHeight >= previewLabel.implicitHeight + previewLabel.padding && ! timeSlider.pressed && timeSlider.mouseArea.containsMouse contentItem: VideoPreview { id: preview implicitHeight: Math.min( theme.mediaPlayer.hoverPreview.maxHeight, media.height - osd.height - theme.spacing ) implicitWidth: Math.min( implicitHeight * savedAspectRatio, media.width - theme.spacing, ) file: media.source HLabel { id: previewLabel anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.margins: padding / 4 text: utils.formatDuration(previewToolTip.wantTimestamp) padding: theme.spacing / 2 opacity: previewToolTip.wantTimestamp === -1 ? 0 : 1 background: Rectangle { color: theme.mediaPlayer.controls.background radius: theme.radius } } } Binding on value { value: boundPosition when: ! timeSlider.pressed } Timer { interval: 300 running: previewToolTip.visible repeat: true triggeredOnStart: true onTriggered: preview.timestamp = previewToolTip.wantTimestamp } } } Rectangle { color: theme.mediaPlayer.controls.background Layout.fillWidth: true Layout.preferredHeight: childrenRect.height HRowLayout { width: parent.width OSDButton { readonly property string mode: media.playbackState === MediaPlayer.StoppedState && savedDuration && boundPosition >= savedDuration - 500 ? "restart" : media.playbackState === MediaPlayer.PlayingState ? "pause" : "play" icon.name: "player-" + mode toolTip.text: qsTr( mode === "play" ? "Play" : mode === "pause" ? "Pause" : "Restart" ) onClicked: togglePlay() } // OSDButton { // icon.name: "player-loop" // visible: false // } OSDButton { id: volumeButton icon.name: "player-volume-" + ( media.muted ? "mute" : media.volume > 0.5 ? "high" : "low" ) text: media.muted ? "" : Math.round(media.volume * 100) toolTip.text: media.muted ? qsTr("Unmute") : qsTr("Mute") onClicked: media.muted = ! media.muted } HSlider { value: media.volume onMoved: media.volume = value visible: Layout.preferredWidth > 0 Layout.preferredWidth: ! media.muted && (hovered || pressed || volumeButton.hovered) ? theme.mediaPlayer.controls.volumeSliderWidth : 0 Layout.fillHeight: true Behavior on Layout.preferredWidth { HNumberAnimation {} } } OSDButton { id: speedButton icon.name: "player-speed" text: qsTr("%1x").arg(utils.round(media.playbackRate)) toolTip.text: qsTr("Reset speed") onClicked: media.playbackRate = 1 } HSlider { id: speedSlider from: 0.2 to: 4 value: media.playbackRate stepSize: 0.2 snapMode: HSlider.SnapAlways onMoved: media.playbackRate = value visible: Layout.preferredWidth > 0 Layout.preferredWidth: (hovered || pressed || speedButton.hovered) ? theme.mediaPlayer.controls.speedSliderWidth : 0 Layout.fillHeight: true Behavior on Layout.preferredWidth { HNumberAnimation {} } } OSDLabel { text: boundPosition && savedDuration ? qsTr("%1 / %2") .arg(utils.formatDuration(boundPosition)) .arg(utils.formatDuration(savedDuration)) : boundPosition || savedDuration ? utils.formatDuration(boundPosition || savedDuration) : "" } HSpacer {} OSDLabel { text: boundPosition && savedDuration ? qsTr("-%1").arg( utils.formatDuration(savedDuration - boundPosition) ) : "" } // OSDButton { // icon.name: "player-track-video" // } // OSDButton { // icon.name: "player-track-audio" // } // OSDButton { // icon.name: "player-track-subtitle" // } OSDButton { icon.name: "download" toolTip.text: qsTr("Download") onClicked: Qt.openUrlExternally(media.source) } OSDButton { id: fullScreenButton visible: ! audioOnly icon.name: "player-fullscreen" + (fullScreen ? "-exit" : "") toolTip.text: fullScreen ? qsTr("Exit fullscreen") : qsTr("Fullscreen") onClicked: fullScreen = ! fullScreen } } } } mirage-0.7.2/src/gui/Base/MediaPlayer/OSDButton.qml000066400000000000000000000004231407747233600217550ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../Base" HButton { backgroundColor: "transparent" iconItem.dimension: theme.mediaPlayer.controls.iconSize } mirage-0.7.2/src/gui/Base/MediaPlayer/OSDLabel.qml000066400000000000000000000004421407747233600215220ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HLabel { Layout.leftMargin: theme.spacing / 2 Layout.rightMargin: Layout.leftMargin } mirage-0.7.2/src/gui/Base/MediaPlayer/VideoPlayer.qml000066400000000000000000000035001407747233600223560ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Window 2.12 import QtAV 1.7 Video { id: video autoLoad: window.settings.media.autoLoad autoPlay: window.settings.media.autoPlay volume: window.settings.media.defaultVolume / 100 muted: window.settings.media.startMuted implicitWidth: fullScreen ? window.width : 640 implicitHeight: fullScreen ? window.height : (width / osd.savedAspectRatio) property bool hovered: false property alias fullScreen: osd.fullScreen property int oldVisibility: Window.Windowed property QtObject oldParent: video.parent onFullScreenChanged: { if (fullScreen) { oldVisibility = window.visibility window.visibility = Window.FullScreen oldParent = video.parent video.parent = mainUI.fullScreenPopup.contentItem mainUI.fullScreenPopup.open() } else { window.visibility = oldVisibility mainUI.fullScreenPopup.close() video.parent = oldParent } } Connections { target: mainUI.fullScreenPopup onClosed: fullScreen = false } TapHandler { onTapped: osd.togglePlay() onDoubleTapped: video.fullScreen = ! video.fullScreen } MouseArea { width: parent.width height: parent.height - (osd.visible ? osd.height : 0) acceptedButtons: Qt.NoButton hoverEnabled: true propagateComposedEvents: true onContainsMouseChanged: video.hovered = containsMouse onMouseXChanged: osd.showup = true onMouseYChanged: osd.showup = true } OSD { id: osd width: parent.width anchors.bottom: parent.bottom } } mirage-0.7.2/src/gui/Base/MultiviewPane.qml000066400000000000000000000025631407747233600205400ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 HDrawer { id: pane default property alias swipeViewData: swipeView.contentData property color buttonsBackgroundColor property int buttonWidth: buttonRepeater.count > 0 ? buttonRepeater.itemAt(0).implicitWidth : 0 readonly property alias contentTranslation: contentTranslation readonly property alias buttonRepeater: buttonRepeater readonly property alias swipeView: swipeView defaultSize: buttonRepeater.count * buttonWidth minimumSize: buttonWidth HColumnLayout { anchors.fill: parent transform: Translate { id: contentTranslation } Rectangle { color: buttonsBackgroundColor Layout.fillWidth: true Layout.preferredHeight: childrenRect.height HFlow { id: buttonFlow width: parent.width populate: null Repeater { id: buttonRepeater } } } HSwipeView { id: swipeView clip: true interactive: ! pane.collapsed Layout.fillWidth: true Layout.fillHeight: true } } } mirage-0.7.2/src/gui/Base/PowerLevelControl.qml000066400000000000000000000046441407747233600213760ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 AutoDirectionLayout { id: root property int defaultLevel: 0 property int maximumLevel: 100 readonly property alias changed: field.changed readonly property int uncappedLevel: parseInt(field.text || "0", 10) readonly property int level: Math.min(maximumLevel, uncappedLevel) readonly property alias fieldFocused: field.activeFocus readonly property bool fieldOverMaximum: parseInt(field.text || "0", 10) > maximumLevel readonly property alias field: field signal accepted() function reset() { field.reset() } rowSpacing: theme.spacing onActiveFocusChanged: if (activeFocus) field.forceActiveFocus() HSpacer {} HTextField { id: field radius: 0 horizontalAlignment: Qt.AlignHCenter validator: IntValidator { top: root.maximumLevel } inputMethodHints: Qt.ImhFormattedNumbersOnly maximumLength: root.level < 0 ? 16 : 3 defaultText: String(root.defaultLevel) error: root.fieldOverMaximum onAccepted: root.accepted() onActiveFocusChanged: if (! activeFocus && fieldOverMaximum) text = root.maximumLevel Layout.minimumWidth: mainUI.fontMetrics.boundingRect("-999").width + leftPadding + rightPadding Layout.alignment: Qt.AlignCenter } Row { Layout.preferredHeight: field.height Layout.alignment: Qt.AlignCenter HButton { height: parent.height icon.name: "user-power-default" toolTip.text: qsTr("Limited") checked: root.uncappedLevel < 50 uncheckable: false onClicked: field.text = 0 } HButton { height: parent.height icon.name: "user-power-50" toolTip.text: qsTr("Moderator") checked: root.uncappedLevel >= 50 && root.uncappedLevel < 100 uncheckable: false onClicked: field.text = 50 } HButton { height: parent.height icon.name: "user-power-100" toolTip.text: qsTr("Admin") checked: root.uncappedLevel >= 100 uncheckable: false onClicked: field.text = 100 } } HSpacer {} } mirage-0.7.2/src/gui/Base/PresenceOrb.qml000066400000000000000000000023431407747233600201520ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Rectangle { property string presence implicitWidth: window.settings.General.compact ? theme.controls.presence.radius * 2 : theme.controls.presence.radius * 2.5 implicitHeight: width radius: width / 2 opacity: theme.controls.presence.opacity color: presence.includes("online") ? theme.controls.presence.online : presence.includes("unavailable") ? theme.controls.presence.unavailable : theme.controls.presence.offline border.color: theme.controls.presence.border border.width: theme.controls.presence.borderWidth Behavior on color { HColorAnimation {} } Behavior on opacity { HNumberAnimation {} } HoverHandler { id: presenceHover } HToolTip { visible: presenceHover.hovered text: qsTr("%1 (%2)").arg( presence.includes("online") ? qsTr("Online") : presence.includes("unavailable") ? qsTr("Unavailable") : presence.includes("invisible") ? qsTr("Invisible") : qsTr("Offline") ).arg("unknown to server") } } mirage-0.7.2/src/gui/Base/Theme.qml000066400000000000000000000013501407747233600170020ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 QtObject { id: root property Item target default property list classes readonly property var matchablePathRegex: utils.getClassPathRegex(target) readonly property var themeRules: window.themeRules readonly property var data: { const newData = {} for (const [path, section] of Object.entries(themeRules)) if (matchablePathRegex.test(path)) for (const [name, value] of Object.entries(section)) if (! name.startsWith("_")) newData[name] = value return newData } } mirage-0.7.2/src/gui/DebugConsole.qml000066400000000000000000000262501407747233600174450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import "Base" import "ShortcutBundles" HDrawer { id: debugConsole property Item previouslyFocused: null property QtObject target: null property alias t: debugConsole.target property var history: window.history.console property int historyEntry: -1 property int maxHistoryLength: 4096 property var textBeforeHistoryNavigation: null // null or string property int selectedOutputDelegateIndex: -1 property string selectedOutputText: "" property string pythonDebugKeybind: window.settings.Keys.python_debugger[0] property string help: qsTr( `Interact with the QML code using JavaScript ES6 syntax. Useful variables: t Target item to debug for which this console was opened this The console itself py Python interpreter (${pythonDebugKeybind} to start debugger) window, mainUI, theme, settings, utils, mainPane, pageLoader Special commands: .j OBJECT, .json OBJECT Print OBJECT as human-readable JSON .t, .top Attach console to the parent window's top .b, .bottom Attach console to the parent window's bottom .l, .left Attach console to the parent window's left .r, .right Attach console to the parent window's right .h, .help Show this help`.replace(/^ {8}/gm, "") ) property bool doUselessThing: false property real baseGIFSpeed: 1.0 readonly property alias outputList: outputList function toggle(targetItem=null, js="", addToHistory=false) { if (debugConsole.visible) { debugConsole.visible = false return } debugConsole.visible = true debugConsole.target = ! targetItem && ! debugConsole.target ? mainUI : targetItem ? targetItem : debugConsole.target if (js) debugConsole.runJS(js, addToHistory) } function runJS(input, addToHistory=true) { if (addToHistory && history.slice(-1)[0] !== input) { history.push(input) while (history.length > maxHistoryLength) history.shift() window.saveHistory() } let output = "" let error = false try { if ([".h", ".help"].includes(input)) { output = debugConsole.help } else if ([".t", ".top"].includes(input)) { debugConsole.edge = Qt.TopEdge } else if ([".b", ".bottom"].includes(input)) { debugConsole.edge = Qt.BottomEdge } else if ([".l", ".left"].includes(input)) { debugConsole.edge = Qt.LeftEdge } else if ([".r", ".right"].includes(input)) { debugConsole.edge = Qt.RightEdge } else if (input.startsWith(".j ") || input.startsWith(".json ")) { output = JSON.stringify(eval(input.substring(2)), null, 4) } else { let result = eval(input) output = result instanceof Array ? "[" + String(result) + "]" : String(result) } } catch (err) { error = true output = err.toString() } outputList.model.insert(0, { input, output, error }) } objectName: "debugConsole" edge: Qt.TopEdge x: horizontal ? 0 : referenceSizeParent.width / 2 - width / 2 y: vertical ? 0 : referenceSizeParent.height / 2 - height / 2 width: horizontal ? calculatedSize : Math.min(window.width, 720) height: vertical ? calculatedSize : Math.min(window.height, 720) defaultSize: 400 z: 9999 position: 0 onTargetChanged: { outputList.model.insert(0, { input: "t = " + String(target), output: "", error: false, }) } onVisibleChanged: { if (visible) { previouslyFocused = window.activeFocusItem forceActiveFocus() } else if (previouslyFocused) { previouslyFocused.forceActiveFocus() } } onHistoryEntryChanged: { if (historyEntry === -1) { inputArea.clear() inputArea.append(textBeforeHistoryNavigation) textBeforeHistoryNavigation = null return } if (textBeforeHistoryNavigation === null) textBeforeHistoryNavigation = inputArea.text inputArea.clear() inputArea.append(history.slice(-historyEntry - 1)[0]) } HShortcut { sequences: settings.Keys.qml_console onActivated: debugConsole.toggle() } HColumnLayout { anchors.fill: parent // Fixes inputArea cursor invisible when at cursorPosition 0 anchors.leftMargin: 1 HListView { id: outputList spacing: theme.spacing topMargin: theme.spacing bottomMargin: topMargin leftMargin: theme.spacing rightMargin: leftMargin clip: true verticalLayoutDirection: ListView.BottomToTop Layout.fillWidth: true Layout.fillHeight: true model: ListModel {} delegate: HSelectableLabel { id: delegate width: outputList.width - outputList.leftMargin - outputList.rightMargin readonly property color inputColor: model.error ? theme.colors.errorText : model.output ? theme.colors.accentText : theme.colors.positiveText text: `
` + `` + utils.plain2Html(model.input) + "" + (model.input && model.output ? "
" : "") + (model.output ? utils.plain2Html(model.output) : "") + "
" leftPadding: theme.spacing textFormat: HSelectableLabel.RichText wrapMode: HLabel.Wrap font.family: theme.fontFamily.mono color: model.error ? Qt.darker(inputColor, 1.4) : theme.colors.halfDimText Layout.fillWidth: true onSelectedTextChanged: if (selectedPlainText) { selectedOutputDelegateIndex = model.index selectedOutputText = selectedPlainText } else if (selectedOutputDelegateIndex === model.index) { selectedOutputDelegateIndex = -1 selectedOutputText = "" } Connections { target: debugConsole onSelectedOutputDelegateIndexChanged: { if (selectedOutputDelegateIndex !== model.index) delegate.deselect() } } TapHandler { acceptedButtons: Qt.RightButton gesturePolicy: TapHandler.ReleaseWithinBounds acceptedPointerTypes: PointerDevice.GenericPointer | PointerDevice.Pen onTapped: menu.popup() } TapHandler { acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen onLongPressed: menu.popup() } HMenu { id: menu implicitWidth: Math.min(window.width, 150) z: 10000 HMenuItem { icon.name: "copy-text" text: qsTr("Copy") onTriggered: { if (delegate.selectedPlainText) { delegate.copy() return } delegate.selectAll() delegate.copy() delegate.deselect() } } } Rectangle { width: 1 height: parent.height color: model.error ? theme.colors.negativeBackground : model.output ? theme.colors.accentElement : theme.colors.positiveBackground } } FlickShortcuts { active: debugConsole.visible flickable: outputList } Rectangle { z: -10 anchors.fill: parent color: theme.colors.weakBackground } } HTextArea { id: inputArea function accept() { if (! text) return runJS(text) clear() historyEntry = -1 } focus: true backgroundColor: Qt.hsla(0, 0, 0, 0.85) bordered: false placeholderText: qsTr("QML/JavaScript debug console - Type .help") font.family: theme.fontFamily.mono Keys.onUpPressed: ev => { ev.accepted = cursorRectangle.top < topPadding + font.pixelSize && historyEntry + 1 < history.length if (ev.accepted) { historyEntry += 1 cursorPosition = length } } Keys.onDownPressed: ev => { ev.accepted = cursorRectangle.bottom >= height - bottomPadding - font.pixelSize && historyEntry - 1 >= -1 if (ev.accepted) historyEntry -= 1 } Keys.onTabPressed: inputArea.insertAtCursor(" ") Keys.onReturnPressed: ev => { ev.modifiers & Qt.ShiftModifier || ev.modifiers & Qt.ControlModifier || ev.modifiers & Qt.AltModifier ? inputArea.insertAtCursor("\n") : accept() } Keys.onEnterPressed: ev => Keys.returnPressed(ev) Keys.onEscapePressed: debugConsole.close() Keys.onPressed: ev => { if ( ev.matches(StandardKey.Copy) && ! inputArea.selectedPlainText && selectedOutputText ) { ev.accepted = true Clipboard.text = selectedOutputText } } Layout.fillWidth: true } } NumberAnimation { running: doUselessThing target: mainUI.mainPane.roomList property: "rotation" duration: 250 from: 360 to: 0 loops: Animation.Infinite onStopped: target.rotation = 0 } } mirage-0.7.2/src/gui/Dialogs/000077500000000000000000000000001407747233600157365ustar00rootroot00000000000000mirage-0.7.2/src/gui/Dialogs/ExportKeys.qml000066400000000000000000000022721407747233600205710ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Qt.labs.platform 1.1 import "../Popups" HFileDialogOpener { // This is used for the SignOutPopup to know when the export is done // so it can close signal done() property string userId: "" property bool exporting: false function exportKeys(file, passphrase) { exporting = true const path = file.toString().replace(/^file:\/\//, "") py.callClientCoro(userId, "export_keys", [path, passphrase], () => { exporting = false done() }) } fill: false dialog.title: qsTr("Save decryption keys file as...") dialog.fileMode: FileDialog.SaveFile onFilePicked: { exportPasswordPopup.file = file exportPasswordPopup.open() } PasswordPopup { id: exportPasswordPopup property url file: "" summary.text: qsTr("Passphrase to protect this file:") validateButton.text: qsTr("Export") validateButton.icon.name: "export-keys" onAcceptedPasswordChanged: exportKeys(file, acceptedPassword) } } mirage-0.7.2/src/gui/Dialogs/HFileDialogOpener.qml000066400000000000000000000051041407747233600217310ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Qt.labs.platform 1.1 Item { id: opener enum FileType { All, Images } property bool fill: true property alias dialog: fileDialog property string selectedFile: "" property string file: "" property var selectedFiles: [] property var files: [] property string selectSubject: dialog.fileMode === FileDialog.SaveFile ? qsTr("file") : qsTr("open") property int fileType: HFileDialogOpener.FileType.All signal filePicked(string file) signal filesPicked(var files) signal cancelled() anchors.fill: fill ? parent : undefined TapHandler { enabled: opener.enabled && fill; onTapped: fileDialog.open() } FileDialog { id: fileDialog property var filters: ({ all: qsTr("All files") + " (*)", images: qsTr("Image files") + " (*.jpg *.jpeg *.png *.gif *.bmp *.webp)" }) nameFilters: fileType === HFileDialogOpener.FileType.Images ? [filters.images, filters.all] : [filters.all] folder: StandardPaths.writableLocation( fileType === HFileDialogOpener.FileType.Images ? StandardPaths.PicturesLocation : StandardPaths.HomeLocation ) title: fileMode === FileDialog.OpenFile ? qsTr("Select a file to open") : fileMode === FileDialog.OpenFiles ? qsTr("Select files to open") : fileMode === FileDialog.SaveFile ? qsTr("Save as...") : "" modality: Qt.NonModal onVisibleChanged: if (visible) { opener.selectedFile = Qt.binding(() => Qt.resolvedUrl(currentFile)) opener.file = Qt.binding(() => Qt.resolvedUrl(file)) opener.files = Qt.binding(() => Qt.resolvedUrl(files)) opener.selectedFiles = Qt.binding(() => Qt.resolvedUrl(currentFiles)) } onAccepted: { opener.selectedFile = currentFile opener.selectedFiles = currentFiles opener.file = file opener.files = files opener.filePicked(file) opener.filesPicked(files) } onRejected: { selectedFile = "" file = "" selectedFiles = "" files = "" cancelled() } } } mirage-0.7.2/src/gui/Dialogs/ImportKeys.qml000066400000000000000000000043401407747233600205600ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import Qt.labs.platform 1.1 import "../Popups" HFileDialogOpener { property string userId: "" property string importFutureId: "" fill: false dialog.title: qsTr("Select a decryption keys file to import") onFilePicked: { importPasswordPopup.file = file importPasswordPopup.open() } PasswordPopup { id: importPasswordPopup property url file: "" function verifyPassword(pass, callback) { const call = py.callClientCoro const path = file.toString().replace(/^file:\/\//, "") importFutureId = call(userId, "import_keys", [path, pass], () => { importFutureId = "" callback(true) }, (type, args, error, traceback, uuid) => { let unknown = false importFutureId = "" callback( type === "EncryptionError" ? false : type === "ValueError" ? qsTr("Invalid file format") : type === "FileNotFoundError" ? qsTr("This file doesn't exist") : type === "IsADirectoryError" ? qsTr("A folder was given, expecting a file") : type === "PermissionError" ? qsTr("No permission to read this file") : ( unknown = true, qsTr("Unknown error: %1 - %2").arg(type).arg(args) ) ) if (unknown) py.showError(type, traceback, uuid) }) } summary.text: importFutureId ? qsTr("This might take a while...") : qsTr("Passphrase used to protect this file:") validateButton.text: qsTr("Import") validateButton.icon.name: "import-keys" onClosed: if (importFutureId) py.cancelCoro(importFutureId) Binding on closePolicy { value: Popup.CloseOnEscape when: importFutureId } } } mirage-0.7.2/src/gui/Dialogs/SendFilePicker.qml000066400000000000000000000020251407747233600212770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Qt.labs.platform 1.1 HFileDialogOpener { property string userId property string roomId property string replyToEventId: "" property bool destroyWhenDone: false signal replied() fill: false dialog.title: qsTr("Select a file to send") dialog.fileMode: FileDialog.OpenFiles onFilesPicked: { for (const file of files) { const path = Qt.resolvedUrl(file).replace(/^file:/, "") const args = [roomId, path, replyToEventId || undefined] py.callClientCoro(userId, "send_file", args, () => { if (destroyWhenDone) destroy() }, (type, args, error, traceback) => { console.error(`python:\n${traceback}`) if (destroyWhenDone) destroy() }) if (replyToUserId) replied() } } onCancelled: if (destroyWhenDone) destroy() } mirage-0.7.2/src/gui/GlobalTapHandlers.qml000066400000000000000000000013021407747233600204110ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 Item { id: root property PageLoader pageLoader // Raise our z-index as much as possible, so that mouse events go before // anything else through this item which the TapHandlers are watching z: 99999 implicitWidth: parent ? parent.width : 0 implicitHeight: parent ? parent.height : 0 TapHandler { acceptedButtons: Qt.BackButton onTapped: root.pageLoader.moveThroughHistory(1) } TapHandler { acceptedButtons: Qt.ForwardButton onTapped: root.pageLoader.moveThroughHistory(-1) } } mirage-0.7.2/src/gui/IdleManager.qml000066400000000000000000000025601407747233600172420ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import CppUtils 0.1 import "." Timer { readonly property ListModel accounts: ModelStore.get("accounts") readonly property var accountsSet: new Set() function setPresence(userId, presence) { py.callClientCoro(userId, "set_presence", [presence, undefined, false]) } interval: 1000 repeat: true running: window.settings.Presence.auto_away_after > 0 && CppUtils.idleMilliseconds() !== -1 onTriggered: { let changes = false const beUnavailable = CppUtils.idleMilliseconds() / 1000 >= window.settings.Presence.auto_away_after for (let i = 0; i < accounts.count; i++) { const account = accounts.get(i) if (! account.presence_support) continue if (beUnavailable && account.presence === "online") { setPresence(account.id, "unavailable") accountsSet.add(account.id) changes = true } else if (! beUnavailable && accountsSet.has(account.id)) { setPresence(account.id, "online") accountsSet.delete(account.id) changes = true } } if (changes) accountsSetChanged() } } mirage-0.7.2/src/gui/LoadingScreen.qml000066400000000000000000000013461407747233600176100ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "Base" Rectangle { color: utils.hsluv(0, 0, 0, 0.7) HBusyIndicator { anchors.centerIn: parent width: Math.min(160, parent.width - 16, parent.height - 16) height: width // Because the theme is not loaded at this point, we must set these // properties manually: baseCircle.strokeColor: utils.hsluv(240, 60 / 1.5 * 2, 0, 0.7) progressCircle.strokeColor: utils.hsluv(240, 60 * 1.5, 72) label.font.family: "Roboto" label.font.pixelSize: 0 label.color: "black" label.linkColor: "black" } } mirage-0.7.2/src/gui/MainPane/000077500000000000000000000000001407747233600160445ustar00rootroot00000000000000mirage-0.7.2/src/gui/MainPane/AccountBar.qml000066400000000000000000000056011407747233600206020ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import ".." import "../Base" import "../Base/HTile" Rectangle { property RoomList roomList readonly property alias accountList: accountList color: theme.mainPane.accountBar.background implicitHeight: accountList.count >= 2 ? accountList.contentHeight + accountList.topMargin + accountList.bottomMargin : 0 Behavior on implicitHeight { HNumberAnimation {} } HGridView { id: accountList anchors.centerIn: parent width: Math.min(cellWidth * count, parent.width) height: parent.height topMargin: theme.spacing / 2 bottomMargin: topMargin clip: true cellWidth: theme.controls.avatar.size + theme.spacing cellHeight: cellWidth currentIndex: roomList.count === 0 || roomList.currentIndex === -1 ? -1 : model.findIndex( roomList.model.get(roomList.currentIndex).for_account || roomList.model.get(roomList.currentIndex).id, -1, ) model: ModelStore.get("matching_accounts") delegate: AccountDelegate { width: accountList.cellWidth height: accountList.cellHeight padded: false compact: false filterActive: Boolean(roomList.filter) title.visible: false addChat.visible: false expand.visible: false onLeftClicked: roomList.goToAccount(model.id) onWentToAccountPage: roomList.currentIndex = roomList.accountIndice[model.id] } highlight: Item { readonly property alias border: border Rectangle { anchors.fill: parent color: theme.mainPane.accountBar.account.selectedBackground opacity: theme.mainPane.accountBar.account .selectedBackgroundOpacity } Rectangle { id: border anchors.bottom: parent.bottom width: parent.width height: theme.mainPane.accountBar.account.selectedBorderSize color: theme.mainPane.accountBar.account.selectedBorder } } HShortcut { sequences: window.settings.Keys.Accounts.previous onActivated: { accountList.moveCurrentIndexLeft() accountList.currentItem.leftClicked() } } HShortcut { sequences: window.settings.Keys.Accounts.next onActivated: { accountList.moveCurrentIndexRight() accountList.currentItem.leftClicked() } } } } mirage-0.7.2/src/gui/MainPane/AccountContextMenu.qml000066400000000000000000000143631407747233600223540ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import "../Base" HMenu { id: root property string userId property string presence property string statusMsg signal wentToAccountPage() function setPresence(presence, statusMsg=undefined) { py.callClientCoro(userId, "set_presence", [presence, statusMsg]) } function resizeStatusList() { if (statusRepeater.count <= window.settings.Presence.saved_status) return statusRepeater.items.length = window.settings.Presence.saved_status statusRepeater.itemsChanged() } function statusFieldApply(newStatus=null) { if (newStatus === null) newStatus = statusField.editText.trim() if (newStatus) { const existing = statusRepeater.items.indexOf(newStatus) if (existing !== -1) statusRepeater.items.splice(existing, 1) statusRepeater.items.unshift(newStatus) resizeStatusList() statusRepeater.itemsChanged() window.saveState(statusRepeater) } setPresence(presence, newStatus) close() } onOpened: statusField.forceActiveFocus() Component.onCompleted: resizeStatusList() HLabeledItem { id: statusMsgLabel enabled: presence && presence !== "offline" width: parent.width height: visible ? implicitHeight : 0 label.text: qsTr("Status message:") label.horizontalAlignment: Qt.AlignHCenter label.leftPadding: theme.spacing label.rightPadding: label.leftPadding label.topPadding: theme.spacing / 2 label.bottomPadding: label.topPadding HRowLayout { width: parent.width HComboBox { // We use a ComboBox disguised as a field for the // autosuggestion-as-we-type feature id: statusField editable: true indicator: null popup: null model: statusRepeater.model currentIndex: statusRepeater.items.indexOf( root.currentIndex !== -1 && root.itemAt(root.currentIndex).isStatus ? root.itemAt(root.currentIndex).text : root.statusMsg ) field.placeholderText: presence ? "" : "Unsupported server" field.maximumLength: 255 onAccepted: root.statusFieldApply() onActiveFocusChanged: if (activeFocus) field.selectAll() Keys.onBacktabPressed: event => Keys.upPressed(event) Keys.onTabPressed: event => Keys.downPressed(event) Keys.onUpPressed: signOutItem.forceActiveFocus() Keys.onDownPressed: (statusRepeater.itemAt(0) || onlineItem).forceActiveFocus() Layout.fillWidth: true } HButton { id: button visible: presence icon.name: "apply" icon.color: theme.colors.positiveBackground onClicked: root.statusFieldApply() Layout.fillHeight: true } } } HMenuSeparator {} Repeater { id: statusRepeater // Separate property instead of setting model directly so that we can // manipulate this as a JS list, not a QQmlModel. // If server doesn't support presence, don't show status. property var items: presence ? window.getState(this, "items", []) : [] readonly property string saveName: "lastStatus" readonly property string saveId: "ALL" readonly property var saveProperties: ["items"] model: items delegate: HMenuItem { readonly property bool isStatus: true icon.name: "previously-set-status" text: modelData onTriggered: root.statusFieldApply(text) Keys.onBacktabPressed: event => Keys.upPressed(event) Keys.onUpPressed: event => { event.accepted = index === 0 if (event.accepted) statusField.forceActiveFocus() } } } HMenuSeparator { visible: statusRepeater.count > 0 } HMenuItem { id: onlineItem icon.name: "presence-online" icon.color: theme.controls.presence.online text: qsTr("Online") onTriggered: setPresence("online") } HMenuItem { enabled: presence icon.name: "presence-busy" icon.color: theme.controls.presence.unavailable text: qsTr("Unavailable") onTriggered: setPresence("unavailable") } HMenuItem { icon.name: "presence-invisible" icon.color: theme.controls.presence.offline text: qsTr("Invisible") onTriggered: setPresence("invisible") } HMenuItem { icon.name: "presence-offline" icon.color: theme.controls.presence.offline text: qsTr("Offline") onTriggered: setPresence("offline") } HMenuSeparator { visible: statusMsgLabel.visible height: visible ? implicitHeight : 0 } HMenuItem { icon.name: "account-settings" text: qsTr("Account settings") onTriggered: { pageLoader.show( "Pages/AccountSettings/AccountSettings.qml", { "userId": userId }, ) wentToAccountPage() } } HMenuItem { icon.name: "menu-add-chat" text: qsTr("Add new chat") onTriggered: { pageLoader.show("Pages/AddChat/AddChat.qml", {userId: userId}) wentToAccountPage() } } HMenuItem { icon.name: "copy-user-id" text: qsTr("Copy user ID") onTriggered: Clipboard.text = userId } HMenuItemPopupSpawner { id: signOutItem icon.name: "sign-out" icon.color: theme.colors.negativeBackground text: qsTr("Sign out") popup: "Popups/SignOutPopup.qml" properties: { "userId": userId } Keys.onDownPressed: statusField.forceActiveFocus() } } mirage-0.7.2/src/gui/MainPane/AccountDelegate.qml000066400000000000000000000162471407747233600216200ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/HTile" HTile { id: account property bool enableKeybinds: false property bool filterActive: false readonly property bool collapsed: (window.uiState.collapseAccounts[model.id] || false) && ! filterActive readonly property alias avatar: title readonly property alias totalMessageIndicator: totalMessageIndicator readonly property alias title: title readonly property alias addChat: addChat readonly property alias expand: expand signal wentToAccountPage() function setCollapse(collapse) { window.uiState.collapseAccounts[model.id] = collapse window.saveUIState() py.callCoro("set_account_collapse", [model.id, collapse]) } function toggleCollapse() { setCollapse(! collapsed) } function togglePresence(presence) { if (model.presence === presence) presence = "online" py.callClientCoro(model.id, "set_presence", [presence]) } backgroundColor: theme.mainPane.listView.account.background contentItem: ContentRow { tile: account spacing: 0 opacity: collapsed ? theme.mainPane.listView.account.collapsedOpacity : model.presence == "offline" ? theme.mainPane.listView.offlineOpacity : 1 Behavior on opacity { HNumberAnimation {} } HUserAvatar { id: avatar clientUserId: model.id userId: model.id displayName: model.display_name mxc: model.avatar_url radius: theme.mainPane.listView.account.avatarRadius compact: account.compact presence: model.presence Layout.alignment: Qt.AlignCenter HLoader { anchors.fill: parent z: 100 opacity: model.connecting ? 1 : 0 active: opacity > 0 sourceComponent: Rectangle { radius: avatar.radius color: utils.hsluv(0, 0, 0, 0.6) HBusyIndicator { anchors.centerIn: parent width: parent.width / 2 height: width } } Behavior on opacity { HNumberAnimation {} } } MessageIndicator { id: totalMessageIndicator anchors.right: parent.right anchors.bottom: parent.bottom z: 200 indicatorTheme: theme.mainPane.accountBar.account.unreadIndicator unreads: model.total_unread highlights: model.total_highlights localUnreads: model.local_unreads } } HColumnLayout { id: title TitleLabel { text: model.display_name || model.id color: hovered ? utils.nameColor( model.display_name || model.id.substring(1), ) : theme.mainPane.listView.account.name Behavior on color { HColorAnimation {} } Layout.leftMargin: theme.spacing } SubtitleLabel { id: statusMsg tile: account textFormat: SubtitleLabel.PlainText text: model.status_msg.trim() visible: Boolean(text) font.strikeout: ! model.presence_support || model.presence.includes("offline") || model.presence.includes("invisible") Layout.leftMargin: theme.spacing } HoverHandler { id: nameHover } HToolTip { visible: nameHover.hovered text: model.id + (statusMsg.text ? " - " + model.status_msg.trim() : "") } } HButton { id: addChat iconItem.small: true icon.name: "add-chat" backgroundColor: "transparent" toolTip.text: qsTr("Add new chat") onClicked: { pageLoader.show( "Pages/AddChat/AddChat.qml", {userId: model.id}, ) account.wentToAccountPage() } leftPadding: theme.spacing rightPadding: theme.spacing / 1.75 Layout.fillHeight: true Layout.maximumWidth: account.width >= 100 * theme.uiScale ? implicitWidth : 0 HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Rooms.add onActivated: addChat.clicked() } } HButton { id: expand iconItem.small: true icon.name: "expand" backgroundColor: "transparent" toolTip.text: collapsed ? qsTr("Expand") : qsTr("Collapse") onClicked: account.toggleCollapse() leftPadding: theme.spacing / 1.75 rightPadding: theme.spacing visible: Layout.maximumWidth > 0 Layout.fillHeight: true Layout.maximumWidth: ! filterActive && account.width >= 120 * theme.uiScale ? implicitWidth : 0 iconItem.transform: Rotation { origin.x: expand.iconItem.width / 2 origin.y: expand.iconItem.height / 2 angle: expand.loading ? 0 : collapsed ? 180 : 90 Behavior on angle { HNumberAnimation {} } } Behavior on Layout.maximumWidth { HNumberAnimation {} } } } contextMenu: AccountContextMenu { userId: model.id statusMsg: model.status_msg presence: model.presence_support || model.presence === "offline" ? model.presence : null onWentToAccountPage: account.wentToAccountPage() } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.settings onActivated: leftClicked() } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.collapse onActivated: toggleCollapse() } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.menu onActivated: account.doRightClick(false) } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.unavailable onActivated: account.togglePresence("unavailable") } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.invisible onActivated: account.togglePresence("invisible") } HShortcut { enabled: enableKeybinds sequences: window.settings.Keys.Accounts.offline onActivated: account.togglePresence("offline") } DelegateTransitionFixer {} } mirage-0.7.2/src/gui/MainPane/BottomBar.qml000066400000000000000000000050301407747233600204460ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" Rectangle { property RoomList roomList readonly property alias addAccountButton: addAccountButton readonly property alias filterField: filterField // Hide filter field overflowing for a sec on size changes clip: true implicitHeight: theme.baseElementsHeight color: theme.mainPane.bottomBar.background HRowLayout { anchors.fill: parent HButton { id: addAccountButton icon.name: "add-account" toolTip.text: qsTr("Add another account") onClicked: { pageLoader.show("Pages/AddAccount/AddAccount.qml") roomList.startCorrectItemSearch() } Layout.fillHeight: true HShortcut { sequences: window.settings.Keys.Accounts.add onActivated: addAccountButton.clicked() } } HTextField { id: filterField saveName: "roomFilterField" placeholderText: qsTr("Filter rooms") backgroundColor: theme.mainPane.bottomBar.filterFieldBackground bordered: false opacity: width >= 16 * theme.uiScale ? 1 : 0 Layout.fillWidth: true Layout.fillHeight: true Keys.forwardTo: [roomList] Keys.priority: Keys.AfterItem Keys.onTabPressed: roomList.incrementCurrentIndex() Keys.onBacktabPressed: roomList.decrementCurrentIndex() Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: { roomList.showItemAtIndex() if (window.settings.RoomList.enter_clears_filter) text = "" } Keys.onMenuPressed: if (roomList.currentItem) roomList.currentItem.doRightClick(false) Keys.onEscapePressed: { mainPane.toggleFocus() if (window.settings.RoomList.escape_clears_filter) text = "" } Behavior on opacity { HNumberAnimation {} } HShortcut { sequences: window.settings.Keys.Rooms.clear_filter onActivated: filterField.text = "" } HShortcut { sequences: window.settings.Keys.Rooms.focus_filter onActivated: mainPane.toggleFocus() } } } } mirage-0.7.2/src/gui/MainPane/MainPane.qml000066400000000000000000000032761407747233600202570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" HDrawer { id: mainPane readonly property alias accountBar: accountBar readonly property alias roomList: roomList readonly property alias bottomBar: bottomBar function toggleFocus() { if (bottomBar.filterField.activeFocus) { pageLoader.takeFocus() return } mainPane.open() bottomBar.filterField.forceActiveFocus() } saveName: "mainPane" background: Rectangle { color: theme.mainPane.background } requireDefaultSize: bottomBar.filterField.activeFocus minimumSize: window.settings.RoomList.min_width * window.settings.General.zoom Behavior on opacity { HNumberAnimation {} } Binding on visible { value: false when: ! mainUI.accountsPresent } HDrawerSwipeHandler { drawer: mainPane onCloseRequest: mainPane.toggleFocus() } HColumnLayout { anchors.fill: parent TopBar { roomList: roomList Layout.fillWidth: true } AccountBar { id: accountBar roomList: roomList Layout.fillWidth: true Layout.maximumHeight: parent.height / 3 } RoomList { id: roomList clip: true filter: bottomBar.filterField.text Layout.fillWidth: true Layout.fillHeight: true } BottomBar { id: bottomBar roomList: roomList Layout.fillWidth: true } } } mirage-0.7.2/src/gui/MainPane/MessageIndicator.qml000066400000000000000000000033001407747233600217740ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" HLabel { id: root property QtObject indicatorTheme property int unreads: 0 property int highlights: 0 property bool localUnreads: false text: unreads >= 1000000 ? Math.floor(unreads / 1000000) + "M" : unreads >= 1000 ? Math.floor(unreads / 1000) + "K" : unreads ? unreads : localUnreads ? "!" : "" color: highlights ? indicatorTheme.highlightText : indicatorTheme.text font.pixelSize: theme.fontSize.small font.bold: highlights ? indicatorTheme.highlightBold : indicatorTheme.bold verticalAlignment: Qt.AlignVCenter leftPadding: theme.spacing / 3 rightPadding: leftPadding scale: text ? 1 : 0 visible: text !== "" background: Rectangle { radius: highlights ? indicatorTheme.highlightRadius : indicatorTheme.radius color: highlights ? indicatorTheme.highlightBackground : indicatorTheme.background border.width: highlights ? indicatorTheme.highlightBorderWidth : indicatorTheme.borderWidth border.color: highlights ? indicatorTheme.highlightBorder : indicatorTheme.border Behavior on radius { HColorAnimation {} } Behavior on color { HColorAnimation {} } Behavior on border.color { HColorAnimation {} } } Behavior on scale { HNumberAnimation {} } Behavior on color { HColorAnimation {} } } mirage-0.7.2/src/gui/MainPane/RoomDelegate.qml000066400000000000000000000247521407747233600211400ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import ".." import "../Base" import "../Base/HTile" HTile { id: room property string fetchProfilesFutureId: "" property string loadEventsFutureId: "" property bool moreToLoad: true readonly property bool joined: ! invited && ! parted readonly property bool invited: model.inviter_id && ! parted readonly property bool parted: model.left readonly property ListModel eventModel: ModelStore.get(model.for_account, model.id, "events") // FIXME: binding loop readonly property QtObject accountModel: ModelStore.get("accounts").find(model.for_account) readonly property QtObject lastEvent: eventModel.count > 0 ? eventModel.get(0) : null backgroundColor: theme.mainPane.listView.room.background leftPadding: theme.spacing * 2 rightPadding: theme.spacing contentItem: ContentRow { tile: room opacity: accountModel.presence === "offline" ? theme.mainPane.listView.offlineOpacity : model.left ? theme.mainPane.listView.room.leftRoomOpacity : 1 Behavior on opacity { HNumberAnimation {} } HRoomAvatar { id: avatar clientUserId: model.for_account roomId: model.id displayName: model.display_name mxc: model.avatar_url compact: room.compact radius: theme.mainPane.listView.room.avatarRadius Behavior on radius { HNumberAnimation {} } } HColumnLayout { HRowLayout { spacing: room.spacing TitleLabel { text: // U+1f4cc pushpin + force black-and-white variant (model.pinned ? "📌\ufe0e " : "") + (model.display_name || qsTr("Empty room")) color: model.unreads || model.local_unreads ? theme.mainPane.listView.room.unreadName : theme.mainPane.listView.room.name } MessageIndicator { indicatorTheme: theme.mainPane.listView.room.unreadIndicator unreads: model.unreads highlights: model.highlights localUnreads: model.local_unreads } TitleRightInfoLabel { tile: room color: theme.mainPane.listView.room.lastEventDate text: utils.smartFormatDate(model.last_event_date) } } SubtitleLabel { tile: room color: theme.mainPane.listView.room.subtitle textFormat: Text.StyledText font.italic: lastEvent && lastEvent.event_type === "RoomMessageEmote" text: { // If this is a room invite with no last event to show, // and the room name isn't just the inviter's name if ( ! lastEvent && model.inviter_id && ( ! model.display_name || model.inviter_name !== model.display_name ) ) return utils.coloredNameHtml( model.inviter_name, model.inviter_id, ) if (! lastEvent) return "" const ev_type = lastEvent.event_type const isEmote = ev_type === "RoomMessageEmote" const isMsg = ev_type.startsWith("RoomMessage") const isUnknownMsg = ev_type === "RoomMessageUnknown" const isCryptMedia = ev_type.startsWith("RoomEncrypted") // If it's a general event if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia)) return utils.processedEventText(lastEvent) const text = utils.coloredNameHtml( lastEvent.sender_name, lastEvent.sender_id ) + ": " + lastEvent.inline_content const subColor = theme.mainPane.listView.room.subtitleQuote return text.replace( /< *span +class=['"]?quote['"]? *>(.+?)<\/ *span *>/g, `$1`, ).replace( /< *mx-reply *>(.+?)<\/ *mx-reply *>/g, `$1`, ) } } } HIcon { svgName: "invite-received" colorize: theme.colors.alertBackground small: room.compact visible: invited Layout.maximumWidth: invited ? implicitWidth : 0 Behavior on Layout.maximumWidth { HNumberAnimation {} } } } contextMenu: HMenu { // This delegate is only used for nested menus delegate: HMenuItem { icon.name: "room-menu-notifications" } HMenuItem { icon.name: model.pinned ? "room-unpin": "room-pin" text: model.pinned ? qsTr("Unpin"): qsTr("Pin to top") onTriggered: py.callClientCoro( model.for_account, "toggle_room_pin", [model.id] ) } HMenu { title: qsTr("Notifications") isSubMenu: true HMenuItem { text: qsTr("Use default account settings") checkable: true checked: model.notification_setting === "UseDefaultSettings" onTriggered: py.callClientCoro( model.for_account, "room_pushrule_use_default", [model.id], ) } HMenuItem { text: qsTr("All new messages") checkable: true checked: model.notification_setting === "AllEvents" onTriggered: py.callClientCoro( model.for_account, "room_pushrule_all_events", [model.id], ) } HMenuItem { text: qsTr("Highlights only (replies, keywords...)") checkable: true checked: model.notification_setting === "HighlightsOnly" onTriggered: py.callClientCoro( model.for_account, "room_pushrule_highlights_only", [model.id], ) } HMenuItem { text: qsTr("Ignore new messages") checkable: true checked: model.notification_setting === "IgnoreEvents" onTriggered: py.callClientCoro( model.for_account, "room_pushrule_ignore_all", [model.id], ) } } HMenuItemPopupSpawner { visible: joined enabled: model.can_invite && accountModel.presence !== "offline" icon.name: "room-send-invite" text: qsTr("Invite users") popup: "Popups/InviteToRoomPopup.qml" properties: ({ userId: model.for_account, roomId: model.id, roomName: model.display_name, invitingAllowed: Qt.binding(() => model.can_invite) }) } HMenuItem { icon.name: "copy-room-id" text: qsTr("Copy room ID") onTriggered: Clipboard.text = model.id } HMenuItem { visible: invited icon.name: "invite-accept" icon.color: theme.colors.positiveBackground text: qsTr("Accept %1's invite").arg(utils.coloredNameHtml( model.inviter_name, model.inviter_id )) label.textFormat: Text.StyledText enabled: accountModel.presence !== "offline" onTriggered: py.callClientCoro( model.for_account, "join", [model.id] ) } HMenuItemPopupSpawner { enabled: accountModel.presence !== "offline" icon.color: theme.colors.negativeBackground icon.name: parted ? "room-forget" : invited ? "invite-decline" : "room-leave" text: parted ? qsTr("Forget history") : invited ? qsTr("Decline invite") : qsTr("Leave") popup: "Popups/LeaveRoomPopup.qml" properties: ({ userId: model.for_account, roomId: model.id, roomName: model.display_name, inviterId: model.inviter_id, left: model.left, }) } } Component.onDestruction: { if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId) if (loadEventsFutureId) py.cancelCoro(loadEventsFutureId) } DelegateTransitionFixer {} Timer { interval: 1000 triggeredOnStart: true running: ! accountModel.connecting && accountModel.presence !== "offline" && ! lastEvent && moreToLoad onTriggered: if (! loadEventsFutureId) { loadEventsFutureId = py.callClientCoro( model.for_account, "load_past_events", [model.id], more => { if (! room) return // delegate was destroyed loadEventsFutureId = "" moreToLoad = more } ) } } Timer { // Ensure this event stays long enough for bothering to // fetch the profile to be worth it interval: 500 running: ! accountModel.connecting && accountModel.presence !== "offline" && lastEvent && lastEvent.fetch_profile onTriggered: { if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId) fetchProfilesFutureId = py.callClientCoro( model.for_account, "get_event_profiles", [model.id, lastEvent.id], () => { if (room) fetchProfilesFutureId = "" }, ) } } } mirage-0.7.2/src/gui/MainPane/RoomList.qml000066400000000000000000000260051407747233600203320ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import Qt.labs.qmlmodels 1.0 import ".." import "../Base" HListView { id: roomList property string filter: "" property bool keepListCentered: true readonly property bool currentShouldBeAccount: window.uiState.page === "Pages/AccountSettings/AccountSettings.qml" || window.uiState.page === "Pages/AddChat/AddChat.qml" readonly property bool currentShouldBeRoom: window.uiState.page === "Pages/Chat/Chat.qml" readonly property string wantedUserId: ( window.uiState.pageProperties.userRoomId || [window.uiState.pageProperties.userId, ""] )[0] || "" readonly property string wantedRoomId: ( window.uiState.pageProperties.userRoomId || ["", window.uiState.pageProperties.roomId] )[1] || "" readonly property var accountIndice: { const accounts = {} for (let i = 0; i < model.count; i++) { if (model.get(i).type === "Account") accounts[model.get(i).id] = i } return accounts } function goToAccount(userId) { currentIndex = accountIndice[userId] showItemLimiter.restart() } function goToAccountNumber(num) { currentIndex = Object.entries(accountIndice)[num][1] showItemLimiter.restart() } function showItemAtIndex(index=currentIndex, fromClick=false) { if (index === -1) index = 0 index = Math.min(index, model.count - 1) const item = model.get(index) item.type === "Account" ? pageLoader.show( "Pages/AccountSettings/AccountSettings.qml", { "userId": item.id }, ) : pageLoader.showRoom(item.for_account, item.id) if (fromClick && ! window.settings.RoomList.click_centers) keepListCentered = false currentIndex = index if (fromClick && ! window.settings.RoomList.click_centers) keepListCentered = true } function showById(roomId, accountId=null) { // If only a room ID is passed, first account with this room is used if (accountId === null) { const roomIndex = model.findIndex(roomId) roomIndex === null ? console.warn("No account with such room ID:", roomId) : showItemAtIndex(roomIndex) return } if (! (accountId in accountIndice)) { console.warn("No such account:", accountId) return } pageLoader.showRoom(accountId, roomId) startCorrectItemSearch() } function showAccountRoomAtIndex(index) { const item = model.get(currentIndex === -1 ? 0 : currentIndex) const currentUserId = item.type === "Account" ? item.id : item.for_account showItemAtIndex(accountIndice[currentUserId] + 1 + index) } function cycleUnreadRooms(forward=true, highlights=false) { const prop = highlights ? "highlights": "unreads" const localProp = highlights ? "highlights": "local_unreads" const start = currentIndex === -1 ? 0: currentIndex let index = start while (true) { index += forward ? 1 : -1 if (index < 0) index = model.count - 1 if (index > model.count - 1) index = 0 if (index === start && highlights) return cycleUnreadRooms(forward, false) else if (index === start) return false const item = model.get(index) if (item.type === "Room" && (item[prop] || item[localProp])) { currentIndex = index return true } } } // Find latest highlight or unread. If oldest=true, find oldest instead. function latestUnreadRoom(oldest=false, highlights=false) { const prop = highlights ? "highlights": "unreads" const localProp = highlights ? "highlights": "local_unreads" // When highlights=true, we don't actually find the latest highlight, // but instead, the latest unread among all the highlighted rooms. let max = null let maxEvent = null for (let i = 0; i < model.count; i++) { const item = model.get(i) if ( item.type === "Room" && (item[prop] || item[localProp]) && (max === null || item.last_event_date < maxEvent === oldest) ) { max = i maxEvent = item.last_event_date } } if (max === null) return false // No unreads found currentIndex = max return true } function startCorrectItemSearch() { correctTimer.start() } function setCorrectCurrentItem() { if (! currentShouldBeRoom && ! currentShouldBeAccount) { currentIndex = -1 return null } for (let i = 0; i < model.count; i++) { const item = model.get(i) if (( currentShouldBeRoom && item.type === "Room" && item.id === wantedRoomId && item.for_account === wantedUserId ) || ( currentShouldBeAccount && item.type === "Account" && item.id === wantedUserId )) { currentIndex = i return true } } return false } highlightRangeMode: keepListCentered ? ListView.ApplyRange : ListView.NoHighlightRange model: ModelStore.get("all_rooms") delegate: DelegateChooser { role: "type" DelegateChoice { roleValue: "Account" AccountDelegate { width: roomList.width leftPadding: theme.spacing rightPadding: 0 // the right buttons have padding filterActive: Boolean(filter) enableKeybinds: Boolean( roomList.model.get(currentIndex) && ( roomList.model.get(currentIndex).for_account || roomList.model.get(currentIndex).id ) === model.id ) totalMessageIndicator.visible: false onLeftClicked: showItemAtIndex(model.index, true) onCollapsedChanged: if (wantedUserId === model.id) startCorrectItemSearch() onWentToAccountPage: roomList.currentIndex = model.index } } DelegateChoice { roleValue: "Room" RoomDelegate { width: roomList.width onLeftClicked: showItemAtIndex(model.index, true) } } } onFilterChanged: { py.callCoro("set_string_filter", ["all_rooms", filter], () => { if (filter) { currentIndex = 1 // highlight the first matching room return } const item = model.get(currentIndex) if ( ! filter && item && ( currentIndex === 1 || // required, related to the if above ( currentShouldBeAccount && wantedUserId !== item.id ) || ( currentShouldBeRoom && ( wantedUserId !== item.for_account || wantedRoomId !== item.id ) ) ) ) startCorrectItemSearch() }) } Connections { target: pageLoader onPreviousShown: (componentUrl, properties) => { if (setCorrectCurrentItem() === false) startCorrectItemSearch() } } Timer { // On startup, the account/room takes an unknown amount of time to // arrive in the model, try to find it until then. id: correctTimer interval: 200 running: currentIndex === -1 repeat: true triggeredOnStart: true onTriggered: setCorrectCurrentItem() onRunningChanged: if (running && currentIndex !== -1) currentIndex = -1 } Timer { id: showItemLimiter interval: 100 onTriggered: showItemAtIndex() } HShortcut { sequences: window.settings.Keys.Rooms.previous onActivated: { decrementCurrentIndex(); showItemLimiter.restart() } } HShortcut { sequences: window.settings.Keys.Rooms.next onActivated: { incrementCurrentIndex(); showItemLimiter.restart() } } HShortcut { sequences: window.settings.Keys.Rooms.previous_unread onActivated: { cycleUnreadRooms(false) && showItemLimiter.restart() } } HShortcut { sequences: window.settings.Keys.Rooms.next_unread onActivated: { cycleUnreadRooms(true) && showItemLimiter.restart() } } HShortcut { sequences: window.settings.Keys.Rooms.previous_highlight onActivated: cycleUnreadRooms(false, true) && showItemLimiter.restart() } HShortcut { sequences: window.settings.Keys.Rooms.next_highlight onActivated: cycleUnreadRooms(true, true) && showItemLimiter.restart() } HShortcut { sequences: window.settings.Keys.Rooms.latest_unread onActivated: latestUnreadRoom(false) && showItemLimiter.restart() } HShortcut { sequences: window.settings.Keys.Rooms.oldest_unread onActivated: latestUnreadRoom(true) && showItemLimiter.restart() } HShortcut { sequences: window.settings.Keys.Rooms.latest_highlight onActivated: latestUnreadRoom(false, true) && showItemLimiter.restart() } HShortcut { sequences: window.settings.Keys.Rooms.oldest_highlight onActivated: latestUnreadRoom(true, true) && showItemLimiter.restart() } Instantiator { model: Object.keys(window.settings.Keys.Accounts.AtIndex) delegate: Loader { sourceComponent: HShortcut { sequences: window.settings.Keys.Accounts.AtIndex[modelData] onActivated: goToAccountNumber(parseInt(modelData, 10) - 1) } } } Instantiator { model: Object.keys(window.settings.Keys.Rooms.AtIndex) delegate: Loader { sourceComponent: HShortcut { sequences: window.settings.Keys.Rooms.AtIndex[modelData] onActivated: showAccountRoomAtIndex(parseInt(modelData, 10) - 1) } } } Instantiator { model: Object.keys(window.settings.Keys.Rooms.Direct) delegate: Loader { sourceComponent: HShortcut { sequences: window.settings.Keys.Rooms.Direct[modelData] onActivated: showById(...modelData.split(/\s+/).reverse()) } } } Rectangle { anchors.fill: parent z: -100 color: theme.mainPane.listView.background } } mirage-0.7.2/src/gui/MainPane/TopBar.qml000066400000000000000000000122241407747233600177470ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." import "../Base" Rectangle { property RoomList roomList clip: true implicitHeight: theme.baseElementsHeight color: theme.mainPane.topBar.background HRowLayout { anchors.fill: parent HButton { backgroundColor: "transparent" icon.name: "settings" onClicked: settingsMenu.open() Layout.fillHeight: true HMenu { id: settingsMenu y: parent.height HMenuItem { icon.name: "add-account" text: qsTr("Add another account") onTriggered: { pageLoader.show("Pages/AddAccount/AddAccount.qml") roomList.startCorrectItemSearch() } } HMenuItem { icon.name: "more-settings" text: qsTr("Open configuration folder") onTriggered: py.callCoro("get_config_dir", [], Qt.openUrlExternally) } HMenuItem { icon.name: "theme" text: qsTr("Open theme folder") onTriggered: py.callCoro("get_theme_dir", [], Qt.openUrlExternally) } HMenuItem { icon.name: "documentation" text: qsTr("Online documentation") onTriggered: Qt.openUrlExternally( "https://github.com/mirukana/mirage/tree/master/docs" ) } HMenuItem { icon.name: "debug" text: qsTr("Developer console") onTriggered: mainUI.debugConsole.toggle() } } } HLabel { horizontalAlignment: HLabel.AlignHCenter verticalAlignment: HLabel.AlignVCenter color: theme.mainPane.topBar.nameVersionLabel text: qsTr("%1 %2") .arg(Qt.application.displayName).arg(Qt.application.version) Layout.fillWidth: true Layout.fillHeight: true } HButton { backgroundColor: "transparent" icon.name: mainUI.notificationLevel === UI.NotificationLevel.Enable ? "notifications-enable" : mainUI.notificationLevel === UI.NotificationLevel.Mute ? "notifications-mute" : "notifications-highlights-only" icon.color: mainUI.notificationLevel === UI.NotificationLevel.Enable ? theme.icons.colorize : mainUI.notificationLevel === UI.NotificationLevel.Mute ? theme.colors.negativeBackground : theme.colors.middleBackground onClicked: notificationsMenu.open() Layout.fillHeight: true HMenu { id: notificationsMenu y: parent.height HMenuItem { icon.name: "notifications-enable" text: qsTr("Enable notifications") checkable: true checked: mainUI.notificationLevel === UI.NotificationLevel.Enable onTriggered: mainUI.notificationLevel = UI.NotificationLevel.Enable } HMenuItem { icon.name: "notifications-highlights-only" icon.color: theme.colors.middleBackground text: qsTr("Highlights only (replies, keywords...)") checkable: true checked: mainUI.notificationLevel === UI.NotificationLevel.HighlightsOnly onTriggered: mainUI.notificationLevel = UI.NotificationLevel.HighlightsOnly } HMenuItem { icon.name: "notifications-mute" icon.color: theme.colors.negativeBackground text: qsTr("Mute all notifications") checkable: true checked: mainUI.notificationLevel === UI.NotificationLevel.Mute onTriggered: mainUI.notificationLevel = UI.NotificationLevel.Mute } } } HButton { visible: Layout.preferredWidth > 0 backgroundColor: "transparent" icon.name: "go-back-to-chat-from-main-pane" toolTip.text: qsTr("Go to chat") onClicked: mainPane.toggleFocus() Layout.preferredWidth: mainPane.collapse ? implicitWidth : 0 Layout.fillHeight: true Behavior on Layout.preferredWidth { HNumberAnimation {} } } } } mirage-0.7.2/src/gui/ModelStore.qml000066400000000000000000000027571407747233600171570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later pragma Singleton import QtQuick 2.12 import "PythonBridge" QtObject { property QtObject privates: QtObject { readonly property var store: ({}) readonly property PythonBridge py: PythonBridge {} readonly property Component model: Component { ListModel { property var modelId property var idToItems: ({}) // Used by HFilterModel signal fieldsChanged(int index, var changes) function findIndex(id, default_=null) { for (let i = 0; i < count; i++) if (get(i).id === id) return i return default_ } function find(id, default_=null) { return idToItems[id] || default_ } } } signal ensureModelExists(var modelId) onEnsureModelExists: py.callCoro("models.ensure_exists_from_qml", [modelId]) } function get(...modelId) { if (modelId.length === 1) modelId = modelId[0] if (! privates.store[modelId]) { // Using a signal somehow avoids a binding loop privates.ensureModelExists(modelId) privates.store[modelId] = privates.model.createObject(this, {modelId}) } return privates.store[modelId] } } mirage-0.7.2/src/gui/PageLoader.qml000066400000000000000000000076561407747233600171100ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Window 2.12 import QtGraphicalEffects 1.12 import "Base" import "MainPane" HLoader { id: pageLoader // List of previously loaded [componentUrl, {properties}] property var history: [] property int historyLength: 20 property int historyPosition: 0 readonly property alias appearAnimation: appearAnimation signal aboutToRecycle() signal recycled() signal previousShown(string componentUrl, var properties) function show(componentUrl, properties={}, alterHistory=true) { if (alterHistory) { // A new branch of history will be added. // The new branch replaces everything after the current point. while (historyPosition > 0) { history.shift() historyPosition-- } // Add entry to history history.unshift([componentUrl, properties]) if (history.length > historyLength) history.pop() } const recycle = window.uiState.page === componentUrl && componentUrl === "Pages/Chat/Chat.qml" && item if (recycle) { aboutToRecycle() for (const [prop, value] of Object.entries(properties)) item[prop] = value recycled() } else { pageLoader.setSource(componentUrl, properties) window.uiState.page = componentUrl } window.uiState.pageProperties = properties window.saveUIState() } function showRoom(userId, roomId) { show("Pages/Chat/Chat.qml", {userRoomId: [userId, roomId]}) } function showNthFromHistory(n, alterHistory=true) { const [componentUrl, properties] = history[n] show(componentUrl, properties, alterHistory) previousShown(componentUrl, properties) } function showPrevious(timesBack=1) { if (history.length < 2) return false showNthFromHistory(Math.min(timesBack, history.length - 1)) return true } function moveThroughHistory(relativeMovement=1) { if (history.length < 2) return false // Going beyond oldest entry in history if (historyPosition + relativeMovement >= history.length) { if (! window.settings.General.wrap_history) return false relativeMovement -= history.length // Going beyond newest entry in history } else if (historyPosition + relativeMovement < 0){ if (! window.settings.General.wrap_history) return false relativeMovement += history.length } historyPosition += relativeMovement showNthFromHistory(historyPosition, false) return true } function takeFocus() { pageLoader.item.forceActiveFocus() if (mainPane.collapse) mainPane.close() } clip: appearAnimation.running onLoaded: { takeFocus(); appearAnimation.restart() } onRecycled: { takeFocus(); appearAnimation.restart() } Component.onCompleted: { if (! py.startupAnyAccountsSaved) { pageLoader.show("Pages/AddAccount/AddAccount.qml") return } pageLoader.show(window.uiState.page, window.uiState.pageProperties) } HNumberAnimation { id: appearAnimation target: pageLoader.item property: "x" from: -pageLoader.width to: 0 easing.type: Easing.OutCirc factor: 2 } HShortcut { sequences: window.settings.Keys.last_page onActivated: showPrevious() } HShortcut { sequences: window.settings.Keys.earlier_page onActivated: moveThroughHistory(1) } HShortcut { sequences: window.settings.Keys.later_page onActivated: moveThroughHistory(-1) } } mirage-0.7.2/src/gui/Pages/000077500000000000000000000000001407747233600154135ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/AccountSettings/000077500000000000000000000000001407747233600205305ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/AccountSettings/AccountSettings.qml000066400000000000000000000020011407747233600243510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" HPage { id: page property string userId HTabbedBox { anchors.centerIn: parent width: Math.min(implicitWidth, page.availableWidth) height: Math.min(implicitHeight, page.availableHeight) showBackButton: mainUI.mainPane.normalOrForceCollapse backButton.icon.name: "go-back-to-main-pane" backButton.toolTip.text: qsTr("Back to main pane") backButton.onClicked: mainUI.mainPane.toggleFocus() tabBar: HTabBar { HTabButton { text: qsTr("General") } HTabButton { text: qsTr("Notifications") } HTabButton { text: qsTr("Security") } } General { userId: page.userId } Notifications { userId: page.userId } Security { userId: page.userId } } } mirage-0.7.2/src/gui/Pages/AccountSettings/DeviceDelegate.qml000066400000000000000000000124731407747233600241040ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" import "../../Base/HTile" HTile { id: deviceTile property HListView view property string userId property bool offline: false signal verified() signal blacklisted() signal renameRequest(string name) signal deleteRequest() backgroundColor: "transparent" compact: false leftPadding: theme.spacing * 2 rightPadding: 0 contentItem: ContentRow { tile: deviceTile spacing: 0 HCheckBox { id: checkBox activeFocusOnTab: false checked: view.checked[model.id] || false onClicked: view.toggleCheck(model.index) } HColumnLayout { Layout.leftMargin: theme.spacing HRowLayout { spacing: theme.spacing TitleLabel { text: model.display_name || qsTr("Unnamed") } TitleRightInfoLabel { tile: deviceTile text: utils.smartFormatDate(model.last_seen_date) } } SubtitleLabel { tile: deviceTile font.family: theme.fontFamily.mono text: model.last_seen_ip ? model.id + " " + model.last_seen_ip : model.id } } HButton { icon.name: "device-action-menu" toolTip.text: qsTr("Rename, verify or sign out") backgroundColor: "transparent" activeFocusOnTab: false onClicked: deviceTile.openMenu() Layout.fillHeight: true } } contextMenu: HMenu { id: actionMenu implicitWidth: Math.min(360 * theme.uiScale, window.width) onOpened: nameField.forceActiveFocus() HLabeledItem { width: parent.width label.topPadding: theme.spacing / 2 label.text: qsTr("Public display name:") label.horizontalAlignment: Qt.AlignHCenter HRowLayout { enabled: ! deviceTile.offline width: parent.width HTextField { id: nameField defaultText: model.display_name maximumLength: 255 horizontalAlignment: Qt.AlignHCenter onAccepted: deviceTile.renameRequest(text) Layout.fillWidth: true } HButton { icon.name: "apply" icon.color: theme.colors.positiveBackground onClicked: deviceTile.renameRequest(nameField.text) Layout.fillHeight: true } } } HMenuSeparator {} HLabel { id: noKeysLabel visible: model.type === "no_keys" width: parent.width height: visible ? implicitHeight : 0 // avoid empty space wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter textFormat: HLabel.RichText color: theme.colors.warningText text: qsTr( "This session doesn't support encryption or " + "failed to upload a verification key" ) } HMenuSeparator { visible: noKeysLabel.visible height: visible ? implicitHeight : 0 } HLabeledItem { width: parent.width label.text: qsTr("Actions:") label.horizontalAlignment: Qt.AlignHCenter AutoDirectionLayout { width: parent.width PositiveButton { enabled: model.type !== "no_keys" icon.name: "device-verify" text: model.type === "current" ? qsTr("Get verified") : model.type === "verified" ? qsTr("Reverify") : qsTr("Verify") onClicked: { actionMenu.focusOnClosed = null window.makePopup( "Popups/KeyVerificationPopup.qml", { focusOnClosed: nameField, deviceOwner: deviceTile.userId, deviceId: model.id, deviceName: model.display_name, ed25519Key: model.ed25519_key, deviceIsCurrent: model.type === "current", verifiedCallback: deviceTile.verified, blacklistedCallback: deviceTile.blacklisted, }, ) } } NegativeButton { text: qsTr("Sign out") enabled: ! deviceTile.offline icon.name: "device-delete" onClicked: deviceTile.deleteRequest() } } } } onLeftClicked: checkBox.clicked() DelegateTransitionFixer {} } mirage-0.7.2/src/gui/Pages/AccountSettings/DeviceSection.qml000066400000000000000000000044071407747233600237740ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HRowLayout { property HListView view readonly property int sectionCheckedCount: Object.values(deviceList.checked).filter( item => item.type === section ).length readonly property int sectionTotalCount: deviceList.sectionItemCounts[section] || 0 HCheckBox { id: checkBox padding: theme.spacing text: section === "current" ? qsTr("Current session") : section === "unset" ? qsTr("Unverified") : section === "no_keys" ? qsTr("Unverifiable") : section === "verified" ? qsTr("Verified") : section === "ignored" ? qsTr("Ignored") : qsTr("Blacklisted") tristate: true checkState: ! sectionCheckedCount ? Qt.Unchecked : sectionTotalCount === sectionCheckedCount ? Qt.Checked : Qt.PartiallyChecked nextCheckState: checkState === Qt.Checked ? Qt.Unchecked : Qt.Checked onClicked: { const indice = [] for (let i = 0; i < deviceList.count; i++) { if (deviceList.model.get(i).type === section) indice.push(i) } const checkedItems = Object.values(deviceList.checked) checkedItems.some(item => item.type === section) ? deviceList.uncheck(...indice) : deviceList.check(...indice) } Layout.fillWidth: true } HLabel { text: sectionCheckedCount ? qsTr("%1 / %2") .arg(sectionCheckedCount).arg(sectionTotalCount) : sectionTotalCount topPadding: checkBox.topPadding - theme.spacing * 0.75 rightPadding: theme.spacing * 1.5 verticalAlignment: Qt.AlignVCenter color: ["current", "verified"].includes(section) ? theme.colors.positiveText : ["unset", "ignored", "no_keys"].includes(section) ? theme.colors.warningText : theme.colors.errorText Layout.fillHeight: true } } mirage-0.7.2/src/gui/Pages/AccountSettings/General.qml000066400000000000000000000274331407747233600226310ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" import "../../Dialogs" HFlickableColumnPage { id: page property string userId readonly property QtObject account: ModelStore.get("accounts").find(userId) readonly property bool ready: account && account.profile_updated >= new Date(1) function takeFocus() { nameField.item.forceActiveFocus() } function applyChanges() { if (avatar.changed) { saveButton.avatarChangeRunning = true const path = Qt.resolvedUrl(avatar.sourceOverride).replace(/^file:/, "") py.callClientCoro(userId, "set_avatar_from_file", [path], () => { py.callClientCoro(userId, "update_own_profile", [], () => { saveButton.avatarChangeRunning = false }) }, (errType, [httpCode]) => { console.error("Avatar upload failed:", httpCode, errType) saveButton.avatarChangeRunning = false }) } if (nameField.item.changed) { saveButton.nameChangeRunning = true const name = nameField.item.text py.callClientCoro(userId, "set_displayname", [name] , () => { py.callClientCoro(userId, "update_own_profile", [], () => { saveButton.nameChangeRunning = false }) }) } if (aliasFieldItem.changed) { window.settings.Chat.Composer.Aliases[userId] = aliasFieldItem.text window.saveSettings() } if (ignoredUsersAreaItem.changed) { saveButton.ignoredUsersChangeRunning = true const users = ignoredUsers.userIds py.callClientCoro(userId, "set_ignored_users", users, () => { saveButton.ignoredUsersChangeRunning = false }) } } function cancel() { if ( ! nameField.item.changed && ! aliasFieldItem.changed && ! fileDialog.selectedFile && ! fileDialog.file ) { pageLoader.showPrevious() || mainUI.mainPane.toggleFocus() return } nameField.item.reset() aliasFieldItem.reset() fileDialog.selectedFile = "" fileDialog.file = "" } footer: AutoDirectionLayout { ApplyButton { id: saveButton property bool nameChangeRunning: false property bool avatarChangeRunning: false property bool ignoredUsersChangeRunning: false disableWhileLoading: false loading: nameChangeRunning || avatarChangeRunning || ignoredUsersChangeRunning enabled: avatar.changed || nameField.item.changed || (aliasFieldItem.changed && ! aliasFieldItem.error) || (ignoredUsersAreaItem.changed && ! ignoredUsersAreaItem.error) onClicked: applyChanges() } CancelButton { enabled: ! saveButton.loading onClicked: cancel() } } onKeyboardAccept: if (saveButton.enabled) saveButton.clicked() onKeyboardCancel: cancel() HUserAvatar { id: avatar property bool changed: Boolean(sourceOverride) clientUserId: page.userId userId: page.userId displayName: nameField.item.text mxc: account ? account.avatar_url : "" toolTipMxc: "" sourceOverride: fileDialog.selectedFile || fileDialog.file Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true // Layout.preferredWidth: 256 * theme.uiScale Layout.preferredHeight: width Rectangle { anchors.fill: parent z: 10 visible: opacity > 0 opacity: ! fileDialog.dialog.visible && ( (! avatar.mxc && ! avatar.changed) || avatar.hovered || ! ready || account.presence === "offline" ) ? 1 : 0 color: utils.hsluv( 0, 0, 0, (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7, ) Behavior on opacity { HNumberAnimation {} } Behavior on color { HColorAnimation {} } HoverHandler { id: overlayHover } MouseArea { anchors.fill: parent enabled: ready && account.presence !== "offline" acceptedButtons: Qt.NoButton cursorShape: overlayHover.hovered ? Qt.PointingHandCursor : Qt.ArrowCursor } HLoader { anchors.centerIn: parent width: avatar.width / 3 height: width source: "../../Base/HBusyIndicator.qml" active: ! ready opacity: active ? 1 : 0 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } } HColumnLayout { anchors.centerIn: parent spacing: currentSpacing width: parent.width opacity: ready && account.presence !== "offline" ? 1 : 0 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } HIcon { svgName: "upload-avatar" colorize: (! avatar.mxc && overlayHover.hovered) ? theme.colors.accentText : theme.icons.colorize dimension: avatar.width / 3 Layout.alignment: Qt.AlignCenter } Item { Layout.preferredHeight: theme.spacing } HLabel { text: avatar.mxc ? qsTr("Change profile picture") : qsTr("Upload profile picture") color: (! avatar.mxc && overlayHover.hovered) ? theme.colors.accentText : theme.colors.brightText Behavior on color { HColorAnimation {} } font.pixelSize: Math.max( theme.fontSize.big * avatar.width / 300, theme.fontSize.small, ) wrapMode: HLabel.WordWrap horizontalAlignment: Qt.AlignHCenter Layout.fillWidth: true } } } HFileDialogOpener { id: fileDialog enabled: ready fileType: HFileDialogOpener.FileType.Images dialog.title: qsTr("Select profile picture for %1") .arg(account ? account.display_name : "") } } HLabeledItem { label.text: qsTr("User ID:") Layout.fillWidth: true HRowLayout { width: parent.width HTextArea { id: idArea textFormat: HSelectableLabel.RichText wrapMode: HLabel.Wrap readOnly: true radius: 0 text: utils.coloredNameHtml("", userId, userId) Layout.fillWidth: true Layout.fillHeight: true } FieldCopyButton { textControl: idArea } } } HLabeledItem { id: nameField loading: ! ready label.text: qsTr("Display name:") Layout.fillWidth: true HTextField { width: parent.width enabled: ready && account.presence !== "offline" defaultText: ready ? account.display_name : "" maximumLength: 255 // TODO: Qt 5.14+: use a Binding enabled when text not empty color: utils.nameColor(text) } } HLabeledItem { id: aliasField readonly property var aliases: window.settings.Chat.Composer.Aliases readonly property string currentAlias: aliases[userId] || "" readonly property bool hasWhiteSpace: /\s/.test(item.text) readonly property string alreadyTakenBy: { if (! item.text) return "" for (const [id, idAlias] of Object.entries(aliases)) if (id !== userId && idAlias === item.text) return id return "" } label.text: qsTr("Composer alias:") errorLabel.text: hasWhiteSpace ? qsTr("Alias cannot include spaces") : alreadyTakenBy ? qsTr("Taken by %1").arg(alreadyTakenBy) : "" Layout.fillWidth: true HRowLayout { width: parent.width HTextField { id: aliasFieldItem error: aliasField.hasWhiteSpace || aliasField.alreadyTakenBy defaultText: aliasField.currentAlias placeholderText: qsTr("e.g. %1").arg(( nameField.item.text || (ready && account.display_name) || userId.substring(1) )[0]) Layout.fillWidth: true Layout.fillHeight: true } FieldHelpButton { helpText: qsTr( "From any chat, start a message with the specified " + "alias, followed by a space, to type and send as " + "this account.\n\n" + "The account must be a member of the room and have " + "permission to talk.\n\n"+ "To ignore the alias when typing, prepend it with a space." ) } } } HLabeledItem { id: ignoredUsers readonly property var userIds: ! ignoredUsersAreaItem.text.trim() ? [] : ignoredUsersAreaItem.text.trim().split(/\s+/) readonly property var invalidUserIds: { const result = [] for (const user of userIds) if (! /@.+:.+/.test(user)) result.push(user) return result } loading: ! ready label.text: qsTr("Ignored users:") errorLabel.text: invalidUserIds.length ? qsTr("Incomplete user ID: %1").arg(invalidUserIds.join(", ")) : "" Layout.fillWidth: true HRowLayout { width: parent.width HTextArea { id: ignoredUsersAreaItem error: ignoredUsers.invalidUserIds.length > 0 focusItemOnTab: ignoredUsersHelpButton placeholderText: qsTr("@user1:example.org @user2:ex.org") defaultText: ready ? JSON.parse(account.ignored_users).sort().join(" ") : "" Layout.fillWidth: true Layout.fillHeight: true } FieldHelpButton { id: ignoredUsersHelpButton helpText: qsTr( "List of user IDs, separated by a space, from which you " + "will not receive messages or room invites.\n\n" + "Their display name, avatar and online status will also " + "be hidden from room member lists.\n\n" + "When removing an user from the ignore list, restarting " + "%1 is needed to receive anything they might have sent " + "while being ignored." ).arg(Qt.application.displayName) } } } } mirage-0.7.2/src/gui/Pages/AccountSettings/Notifications.qml000066400000000000000000000074021407747233600240570ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" import "../../ShortcutBundles" HListView { id: root property string userId property bool enableFlickShortcuts: SwipeView ? SwipeView.isCurrentItem : true // {model.id: {notify, highlight, bubble, sound, urgency_hint}} property var pendingEdits: ({}) property string saveFutureId: "" function takeFocus() { // deviceList.headerItem.exportButton.forceActiveFocus() TODO } function save() { const args = [] for (const [modelId, kwargs] of Object.entries(pendingEdits)) { if (! model.find(modelId)) continue // pushrule was deleted const [kind, rule_id] = JSON.parse(modelId) args.push(Object.assign({}, {kind, rule_id}, kwargs)) } saveFutureId = py.callClientCoro( userId, "mass_tweak_pushrules_actions", args, () => { if (! root) return saveFutureId = "" pendingEdits = {} } ) } clip: true model: ModelStore.get(userId, "pushrules") implicitHeight: Math.min(window.height, contentHeight + bottomMargin) header: HColumnLayout { width: root.width HLoader { source: "../../Base/HBusyIndicator.qml" active: root.model.count === 0 opacity: active ? 1 : 0 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } Layout.alignment: Qt.AlignCenter Layout.topMargin: theme.spacing Layout.bottomMargin: Layout.topMargin } } section.property: "kind" section.delegate: HRowLayout { width: root.width HLabel { padding: theme.spacing font.pixelSize: theme.fontSize.big text: section === "override" ? qsTr("High priority general rules") : section === "content" ? qsTr("Message content rules") : section === "room" ? qsTr("Room rules") : section === "sender" ? qsTr("Sender rules") : qsTr("Low priority general rules") Layout.fillWidth: true } PositiveButton { readonly property var newRule: ({ id: '[section, ""]', kind: section, rule_id: "", order: 0, default: false, enabled: true, conditions: "[]", pattern: "", actions: "[]", notify: false, highlight: false, bubble: false, sound: false, urgency_hint: false, }) backgroundColor: "transparent" icon.name: "pushrule-add" iconItem.small: true Layout.fillHeight: true Layout.fillWidth: false onClicked: window.makePopup( "Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml", {userId: root.userId, rule: newRule, ruleExists: false}, ) } } delegate: PushRuleDelegate { page: root width: root.width } onPendingEditsChanged: utils.isEmptyObject(pendingEdits) ? autoSaveTimer.stop() : autoSaveTimer.restart() Component.onDestruction: ! utils.isEmptyObject(pendingEdits) && save() Timer { id: autoSaveTimer interval: 30000 onTriggered: root.save() } FlickShortcuts { flickable: root active: ! mainUI.debugConsole.visible && root.enableFlickShortcuts } } mirage-0.7.2/src/gui/Pages/AccountSettings/PushRuleButton.qml000066400000000000000000000023751407747233600242150ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../Base" HButton { property string toggles: "" property var nextValue: ! on property HButton requiresOn: null readonly property bool on: (requiresOn === null || requiresOn.on) && ( toggles && page.pendingEdits[model.id] && toggles in page.pendingEdits[model.id] ? Boolean(page.pendingEdits[model.id][toggles]) : toggles ? Boolean(model[toggles]) : true ) opacity: on ? 1 : theme.disabledElementsOpacity hoverEnabled: true backgroundColor: "transparent" onClicked: { if (requiresOn !== null && ! requiresOn.on) { requiresOn.clicked() if (! on) clicked() return } if (! toggles) return if (! (model.id in page.pendingEdits)) page.pendingEdits[model.id] = {} if ((! on) === Boolean(model[toggles])) delete page.pendingEdits[model.id][toggles] else page.pendingEdits[model.id][toggles] = nextValue if (! Object.keys(page.pendingEdits[model.id]).length) delete page.pendingEdits[model.id] page.pendingEditsChanged() } Behavior on opacity { HNumberAnimation {} } } mirage-0.7.2/src/gui/Pages/AccountSettings/PushRuleDelegate.qml000066400000000000000000000060351407747233600244510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/HTile" import "../../Base/Buttons" import "../../MainPane" import "../../Popups" HTile { id: root property Item page contentOpacity: model.enabled ? 1 : theme.disabledElementsOpacity hoverEnabled: false leftPadding: theme.spacing / 4 rightPadding: leftPadding contentItem: HColumnLayout { spacing: root.spacing / 2 TitleLabel { opacity: model.enabled ? 1 : theme.disabledElementsOpacity elide: Text.ElideNone wrapMode: HLabel.Wrap textFormat: HLabel.StyledText text: utils.formatPushRuleName(page.userId, model) Layout.fillWidth: true Layout.leftMargin: theme.spacing Layout.rightMargin: Layout.leftMargin } HRowLayout { PushRuleButton { id: notifyButton toggles: "notify" contentItem: MessageIndicator { indicatorTheme: theme.mainPane.listView.room.unreadIndicator unreads: 1 text: "+1" font.pixelSize: theme.fontSize.normal topPadding: leftPadding / 3 bottomPadding: topPadding } } PushRuleButton { requiresOn: notifyButton toggles: "highlight" contentItem: MessageIndicator { indicatorTheme: theme.mainPane.listView.room.unreadIndicator unreads: 1 highlights: 1 text: "+1" font.pixelSize: theme.fontSize.normal topPadding: leftPadding / 3 bottomPadding: topPadding } } PushRuleButton { requiresOn: notifyButton icon.name: "pushrule-action-bubble" toggles: "bubble" } PushRuleButton { requiresOn: notifyButton icon.name: "pushrule-action-sound" toggles: "sound" nextValue: on ? "" : model[toggles] ? model[toggles] : model.rule_id === ".m.rule.call" ? "ring" : "default" } PushRuleButton { requiresOn: notifyButton icon.name: "pushrule-action-urgency-hint" toggles: "urgency_hint" } HSpacer {} PushRuleButton { icon.name: "pushrule-edit" onClicked: root.clicked() } } } onClicked: window.makePopup( "Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml", {userId: page.userId, rule: model}, ) } mirage-0.7.2/src/gui/Pages/AccountSettings/Security.qml000066400000000000000000000234451407747233600230620ustar00rootroot00000000000000// SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" import "../../PythonBridge" import "../../ShortcutBundles" HColumnPage { id: page property string userId property bool enableFlickShortcuts: SwipeView ? SwipeView.isCurrentItem : true property string loadFutureId: "" readonly property QtObject account: ModelStore.get("accounts").find(userId) readonly property bool offline: ! account || account.presence === "offline" function takeFocus() { deviceList.headerItem.exportButton.forceActiveFocus() } function loadDevices() { loadFutureId = py.callClientCoro(userId, "devices_info", [], devs => { deviceList.uncheckAll() deviceList.model.clear() for (const device of devs) deviceList.model.append(device) loadFutureId = "" deviceList.sectionItemCounts = getSectionItemCounts() }) } function renameDevice(index, name) { const device = deviceList.model.get(index) device.display_name = name py.callClientCoro(userId, "rename_device", [device.id, name], ok => { if (! ok) deviceList.model.remove(index) // 404 happened }) } function deleteDevices(...indice) { if (indice.length === 1 && indice[0] === 0) { window.makePopup("Popups/SignOutPopup.qml", {userId: page.userId}) return } const deviceIds = [] let deleteOwnDevice = false for (const i of indice.sort()) { i === 0 ? deleteOwnDevice = true : deviceIds.push(deviceList.model.get(i).id) } window.makePopup( "Popups/DeleteDevicesPopup.qml", { userId: page.userId, deviceIds, deletedCallback: () => { deleteOwnDevice ? window.makePopup( "Popups/SignOutPopup.qml", { userId: page.userId }, ) : page.loadDevices() }, }, ) } function getSectionItemCounts() { const counts = {} for (let i = 0; i < deviceList.model.count; i++) { const section = deviceList.model.get(i).type section in counts ? counts[section] += 1 : counts[section] = 1 } return counts } function focusListController(top=true) { deviceList.currentIndex = top ? 0 : deviceList.count - 1 listController.forceActiveFocus() } contentHeight: Math.min( window.height, deviceList.contentHeight + deviceList.bottomMargin, ) Keys.forwardTo: [deviceList] HListView { id: deviceList // Don't bind directly to getSectionItemCounts(), laggy with big list property var sectionItemCounts: ({}) bottomMargin: theme.spacing clip: true keyNavigationEnabled: false model: ListModel {} header: HColumnLayout { readonly property alias exportButton: exportButton readonly property alias importButton: importButton readonly property alias signOutButton: signOutButton readonly property alias refreshButton: refreshButton spacing: theme.spacing x: spacing width: deviceList.width - x * 2 HLabel { text: qsTr("Decryption keys") font.pixelSize: theme.fontSize.big wrapMode: HLabel.Wrap topPadding: parent.spacing Layout.fillWidth: true } HLabel { text: qsTr( "The decryption keys for messages received in encrypted " + "rooms until present time can be exported " + "to a passphrase-protected file.

" + "You can then import this file on any Matrix account or " + "application, in order to decrypt these messages again." ) textFormat: Text.StyledText wrapMode: HLabel.Wrap Layout.fillWidth: true } AutoDirectionLayout { GroupButton { id: exportButton text: qsTr("Export") icon.name: "export-keys" onClicked: utils.makeObject( "Dialogs/ExportKeys.qml", page, { userId: page.userId }, obj => { loading = Qt.binding(() => obj.exporting) obj.dialog.open() } ) Keys.onBacktabPressed: page.focusListController(false) } GroupButton { id: importButton text: qsTr("Import") icon.name: "import-keys" onClicked: utils.makeObject( "Dialogs/ImportKeys.qml", page, { userId: page.userId }, obj => { obj.dialog.open() } ) Keys.onTabPressed: signOutButton.enabled ? refreshButton.forceActiveFocus() : page.focusListController() } } HLabel { text: qsTr("Sessions") font.pixelSize: theme.fontSize.big wrapMode: HLabel.Wrap topPadding: parent.spacing / 2 Layout.fillWidth: true } HLabel { text: qsTr( "New sessions are created the first time you sign in " + "from a different device or application." ) wrapMode: HLabel.Wrap Layout.fillWidth: true } AutoDirectionLayout { enabled: ! page.offline GroupButton { id: refreshButton text: qsTr("Refresh") loading: page.loadFutureId !== "" icon.name: "device-refresh-list" onClicked: page.loadDevices() } NegativeButton { id: signOutButton enabled: deviceList.model.count > 0 text: deviceList.selectedCount === 0 ? qsTr("Sign out others") : qsTr("Sign out checked") icon.name: "device-delete-checked" onClicked: deviceList.selectedCount ? page.deleteDevices(...deviceList.checkedIndice) : page.deleteDevices( ...utils.range(1, deviceList.count - 1), ) Keys.onTabPressed: page.focusListController() } } } section.property: "type" section.delegate: DeviceSection { width: deviceList.width view: deviceList } delegate: DeviceDelegate { width: deviceList.width view: deviceList userId: page.userId offline: page.offline onVerified: page.loadDevices() onBlacklisted: page.loadDevices() onRenameRequest: name => page.renameDevice(model.index, name) onDeleteRequest: page.deleteDevices(model.index) } Component.onCompleted: page.loadDevices() Layout.fillWidth: true Layout.fillHeight: true Keys.onEscapePressed: uncheckAll() Keys.onSpacePressed: if (currentItem) toggleCheck(currentIndex) Keys.onEnterPressed: if (currentItem) currentItem.openMenu(false) Keys.onReturnPressed: Keys.onEnterPressed(event) Keys.onMenuPressed: Keys.onEnterPressed(event) Keys.onUpPressed: page.focusListController(false) Keys.onDownPressed: page.focusListController() Item { id: listController Keys.onBacktabPressed: { if (parent.currentIndex === 0) { parent.currentIndex = -1 parent.headerItem.signOutButton.enabled ? parent.headerItem.signOutButton.forceActiveFocus() : parent.headerItem.importButton.forceActiveFocus() return } parent.decrementCurrentIndex() forceActiveFocus() } Keys.onTabPressed: { if (parent.currentIndex === parent.count - 1) { utils.flickToTop(deviceList) parent.currentIndex = -1 parent.headerItem.exportButton.forceActiveFocus() return } parent.incrementCurrentIndex() forceActiveFocus() } Keys.onUpPressed: ev => Keys.onBacktabPressed(ev) Keys.onDownPressed: ev => Keys.onTabPressed(ev) } HShortcut { sequences: window.settings.Keys.Security.refresh onActivated: deviceList.headerItem.refreshButton.clicked() } HShortcut { sequences: window.settings.Keys.Security.sign_out onActivated: deviceList.headerItem.signOutButton.clicked() } FlickShortcuts { flickable: deviceList active: ! mainUI.debugConsole.visible && page.enableFlickShortcuts } } } mirage-0.7.2/src/gui/Pages/AddAccount/000077500000000000000000000000001407747233600174205ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/AddAccount/AddAccount.qml000066400000000000000000000050171407747233600221430ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../../Base" SwipeView { id: swipeView clip: true interactive: serverBrowser.acceptedUrl onCurrentItemChanged: currentIndex === 0 ? serverBrowser.takeFocus() : signInLoader.takeFocus() Component.onCompleted: serverBrowser.takeFocus() HPage { id: serverPage ServerBrowser { id: serverBrowser anchors.centerIn: parent width: Math.min(implicitWidth, serverPage.availableWidth) height: Math.min(implicitHeight, serverPage.availableHeight) onAccepted: swipeView.currentIndex = 1 } } HPage { id: tabPage enabled: swipeView.currentItem === this HTabbedBox { anchors.centerIn: parent width: Math.min(implicitWidth, tabPage.availableWidth) height: Math.min(implicitHeight, tabPage.availableHeight) tabBar: HTabBar { shortcutsEnabled: visible && tabPage.enabled visible: signInLoader.sourceComponent !== signInLoader.signInSso HTabButton { text: qsTr("Sign in") } HTabButton { text: qsTr("Register") } HTabButton { text: qsTr("Reset") } } HLoader { id: signInLoader readonly property Component signInPassword: SignInPassword { serverUrl: serverBrowser.acceptedUrl displayUrl: serverBrowser.acceptedUserUrl onExitRequested: swipeView.currentIndex = 0 } readonly property Component signInSso: SignInSso { serverUrl: serverBrowser.acceptedUrl displayUrl: serverBrowser.acceptedUserUrl onExitRequested: swipeView.currentIndex = 0 } function takeFocus() { if (item) item.takeFocus() } sourceComponent: serverBrowser.loginFlows.includes("m.login.password") ? signInPassword : serverBrowser.loginFlows.includes("m.login.sso") && serverBrowser.loginFlows.includes("m.login.token") ? signInSso : null } Register {} Reset {} } } } mirage-0.7.2/src/gui/Pages/AddAccount/Register.qml000066400000000000000000000015631407747233600217240ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { function takeFocus() { registerButton.forceActiveFocus() } footer: AutoDirectionLayout { ApplyButton { id: registerButton text: qsTr("Register from Riot") icon.name: "register" onClicked: Qt.openUrlExternally("https://riot.im/app/#/register") Layout.fillWidth: true } } HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter text: qsTr( "Not implemented yet\n\n" + "You can create a new account from another client such as Riot." ) Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/AddAccount/Reset.qml000066400000000000000000000016171407747233600212220ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { function takeFocus() { resetButton.forceActiveFocus() } footer: AutoDirectionLayout { ApplyButton { id: resetButton text: qsTr("Reset password from Riot") icon.name: "reset-password" onClicked: Qt.openUrlExternally("https://riot.im/app/#/forgot_password") Layout.fillWidth: true } } HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter text: qsTr( "Not implemented yet\n\n" + "You can reset your password from another client such as Riot." ) Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/AddAccount/ServerBrowser.qml000066400000000000000000000231311407747233600227450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" import "../../PythonBridge" import "../../ShortcutBundles" HBox { id: box property bool knownHttps: window.getState(box, "knownHttps", false) property string acceptedUserUrl: "" property string acceptedUrl: "" property var loginFlows: [] property string saveName: "serverBrowser" property var saveProperties: ["acceptedUserUrl", "knownHttps"] property string loadingIconStep: "server-ping-bad" property string connectFutureId: "" property string fetchServersFutureId: "" readonly property bool canFocusMainPane: mainUI.mainPane.normalOrForceCollapse && mainUI.accountsPresent signal accepted() function takeFocus() { serverField.item.field.forceActiveFocus() } function fetchServers() { if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId) fetchServersFutureId = py.callCoro("fetch_homeservers", [], () => { fetchServersFutureId = "" }, (type, args, error, traceback) => { fetchServersFutureId = "" print( traceback) // TODO: display error graphically }) } function connect() { if (connectFutureId) py.cancelCoro(connectFutureId) connectTimeout.restart() const typedUrl = serverField.item.field.cleanText const args = [typedUrl] if (box.knownHttps) args[0] = args[0].replace(/^(https?:\/\/)?/, "https://") connectFutureId = py.callCoro("server_info", args, ([url, flows]) => { connectTimeout.stop() serverField.errorLabel.text = "" connectFutureId = "" if (! ( flows.includes("m.login.password") || ( flows.includes("m.login.sso") && flows.includes("m.login.token") ) )) { serverField.errorLabel.text = qsTr("No supported sign-in method for this homeserver.") return } acceptedUrl = url acceptedUserUrl = typedUrl loginFlows = flows accepted() }, (type, args, error, traceback, uuid) => { console.error(traceback) connectTimeout.stop() connectFutureId = "" let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args) type === "MatrixNotFound" ? text = qsTr("Invalid homeserver address") : type.startsWith("Matrix") ? text = qsTr("Connection failed: %1(%2)").arg(type).arg(args) : py.showError(type, traceback, uuid) serverField.errorLabel.text = text }) } padding: 0 implicitWidth: theme.controls.box.defaultWidth * 1.25 contentHeight: window.height header: HColumnLayout { HLabel { text: qsTr( "Choose a homeserver to create an account on, or the " + "homeserver where you have an account to sign in to:" ) wrapMode: HLabel.Wrap padding: theme.spacing Layout.fillWidth: true } HRowLayout { Repeater { model: [ qsTr("Ping"), qsTr("Name & location"), qsTr("Stability"), qsTr("Site"), ] HLabel { text: modelData elide: HLabel.ElideRight topPadding: theme.spacing / 2 bottomPadding: topPadding leftPadding: theme.spacing / (model.index === 0 ? 2 : 3) rightPadding: theme.spacing / (model.index === 3 ? 1.5 : 3) background: Rectangle { color: theme.controls.button.background } Layout.fillWidth: model.index === 1 } } } } footer: HLabeledItem { id: serverField readonly property bool knownServerChosen: serverList.model.find(item.cleanText) !== null label.text: qsTr("Homeserver address:") label.topPadding: theme.spacing / 2 label.bottomPadding: label.topPadding / 4 label.leftPadding: theme.spacing label.rightPadding: label.leftPadding errorLabel.leftPadding: label.leftPadding errorLabel.rightPadding: label.leftPadding errorLabel.bottomPadding: label.leftPadding HRowLayout { readonly property alias field: field readonly property alias apply: apply width: parent.width HButton { icon.name: "go-back-to-main-pane" icon.color: theme.colors.negativeBackground onClicked: mainUI.mainPane.toggleFocus() visible: Layout.preferredWidth > 0 Layout.fillHeight: true Layout.preferredWidth: box.canFocusMainPane ? implicitWidth : 0 Behavior on Layout.preferredWidth { HNumberAnimation {} } } HTextField { id: field readonly property string cleanText: text.toLowerCase().trim().replace(/\/+$/, "") inputMethodHints: Qt.ImhUrlCharactersOnly defaultText: window.getState(box, "acceptedUserUrl", "") placeholderText: "example.org" onTextEdited: { py.callCoro( "set_string_filter", ["filtered_homeservers", text], ) knownHttps = false serverList.currentIndex = -1 } Layout.fillWidth: true Layout.fillHeight: true Keys.onBacktabPressed: ev => Keys.onUpPressed(ev) Keys.onTabPressed: ev => Keys.onDownPressed(ev) Keys.onUpPressed: { serverList.decrementCurrentIndex() serverList.setFieldText(serverList.currentIndex) } Keys.onDownPressed: { serverList.incrementCurrentIndex() serverList.setFieldText(serverList.currentIndex) } } HButton { id: apply enabled: field.cleanText && ! field.error icon.name: "server-connect-to-address" icon.color: theme.colors.positiveBackground loading: box.connectFutureId !== "" disableWhileLoading: false onClicked: box.connect() Layout.fillHeight: true } } } onKeyboardAccept: if (serverField.item.apply.enabled) serverField.item.apply.clicked() onKeyboardCancel: if (box.canFocusMainPane) mainUI.mainPane.toggleFocus() onAccepted: window.saveState(this) Component.onDestruction: if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId) Timer { id: connectTimeout interval: 30 * 1000 onTriggered: { serverField.errorLabel.text = serverField.knownServerChosen ? qsTr("This homeserver seems unavailable. Verify your inter" + "net connection or try again later.") : qsTr("This homeserver seems unavailable. Verify the " + "entered address, your internet connection or try " + "again later.") } } Timer { interval: 1000 running: fetchServersFutureId === "" && ModelStore.get("homeservers").count === 0 repeat: true triggeredOnStart: true onTriggered: box.fetchServers() } Timer { interval: 200 running: true repeat: true onTriggered: box.loadingIconStep = "server-ping-" + ( box.loadingIconStep === "server-ping-bad" ? "medium" : box.loadingIconStep === "server-ping-medium" ? "good" : "bad" ) } FlickShortcuts { flickable: serverList active: ! mainUI.debugConsole.visible } HListView { id: serverList function setFieldText(fromItemIndex) { const url = model.get(fromItemIndex).id box.knownHttps = /^https:\/\//.test(url) serverField.item.field.text = url.replace(/^https:\/\//, "") } clip: true model: ModelStore.get("filtered_homeservers") delegate: ServerDelegate { width: serverList.width loadingIconStep: box.loadingIconStep onClicked: { serverList.setFieldText(model.index) serverField.item.apply.clicked() } } Layout.fillWidth: true Layout.fillHeight: true Rectangle { z: -10 anchors.fill: parent color: theme.colors.strongBackground } HLoader { id: busyIndicatorLoader anchors.centerIn: parent width: 96 * theme.uiScale height: width source: "../../Base/HBusyIndicator.qml" active: box.fetchServersFutureId && ! serverList.count opacity: active ? 1 : 0 Behavior on opacity { HNumberAnimation { factor: 2 } } } } } mirage-0.7.2/src/gui/Pages/AddAccount/ServerDelegate.qml000066400000000000000000000067321407747233600230440ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/HTile" HTile { id: root property string loadingIconStep backgroundColor: "transparent" contentOpacity: model.status === "Failed" ? 0.3 : 1 // XXX rightPadding: 0 compact: false contentItem: ContentRow { tile: root spacing: 0 HIcon { id: signalIcon svgName: model.status === "Failed" ? "server-ping-fail" : model.status === "Pinging" ? root.loadingIconStep : model.ping < 400 ? "server-ping-good" : model.ping < 800 ? "server-ping-medium" : "server-ping-bad" colorize: model.status === "Failed" ? theme.colors.negativeBackground : model.status === "Pinging" ? theme.colors.accentBackground : model.ping < 400 ? theme.colors.positiveBackground : model.ping < 800 ? theme.colors.middleBackground : theme.colors.negativeBackground Layout.fillHeight: true Layout.rightMargin: theme.spacing Behavior on colorize { HColorAnimation {} } HoverHandler { id: iconHover } HToolTip { visible: iconHover.hovered text: model.status === "Failed" ? qsTr("Connection failed") : model.status === "Pinging" ? qsTr("Contacting...") : qsTr("%1ms").arg(model.ping) } } HColumnLayout { Layout.rightMargin: theme.spacing TitleLabel { text: model.name } SubtitleLabel { tile: root text: model.country } } TitleRightInfoLabel { tile: root font.pixelSize: theme.fontSize.normal text: model.stability === -1 ? "" : qsTr("%1%").arg(Math.max(0, parseInt(model.stability, 10))) color: model.stability >= 95 ? theme.colors.positiveText : model.stability >= 85 ? theme.colors.warningText : theme.colors.errorText HoverHandler { id: rightInfoHover } HToolTip { readonly property var times: JSON.parse(model.downtimes_ms) readonly property real total: utils.sum(times) visible: model.stability !== -1 && rightInfoHover.hovered text: total === 0 ? qsTr("No downtimes in the last 30 days") : qsTr( "Last 30 days downtimes: %1, average: %2, " + "longest: %3, total: %4" ).arg(times.length) .arg(utils.formatRelativeTime(total / times.length)) .arg(utils.formatRelativeTime(Math.max.apply(Math,times))) .arg(utils.formatRelativeTime(total)) } } HButton { icon.name: "server-visit-website" backgroundColor: "transparent" onClicked: Qt.openUrlExternally(model.site_url) Layout.fillHeight: true } } Behavior on contentOpacity { HNumberAnimation {} } DelegateTransitionFixer {} } mirage-0.7.2/src/gui/Pages/AddAccount/SignInBase.qml000066400000000000000000000073131407747233600221210ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { id: page enum Security { Insecure, LocalHttp, Secure } property string serverUrl property string displayUrl: serverUrl property string loginFutureId: "" readonly property int security: serverUrl.startsWith("https://") ? SignInBase.Security.Secure : ["//localhost", "//127.0.0.1", "//:1"].includes( serverUrl.split(":")[1], ) ? SignInBase.Security.LocalHttp : SignInBase.Security.Insecure default property alias innerData: inner.data readonly property alias rememberAccount: rememberAccount readonly property alias errorMessage: errorMessage readonly property alias applyButton: applyButton signal exitRequested() function finishSignIn(receivedUserId) { errorMessage.text = "" page.loginFutureId = "" py.callCoro( rememberAccount.checked ? "saved_accounts.add": "saved_accounts.forget", [receivedUserId] ) pageLoader.show( "Pages/AccountSettings/AccountSettings.qml", {userId: receivedUserId}, ) } function cancel() { if (! page.loginFutureId) { page.exitRequested() return } py.cancelCoro(page.loginFutureId) page.loginFutureId = "" } flickable.topMargin: theme.spacing * 1.5 flickable.bottomMargin: flickable.topMargin footer: AutoDirectionLayout { ApplyButton { id: applyButton text: qsTr("Sign in") icon.name: "sign-in" loading: page.loginFutureId !== "" disableWhileLoading: false } CancelButton { onClicked: page.cancel() } } onKeyboardAccept: if (applyButton.enabled) applyButton.clicked() onKeyboardCancel: page.cancel() Component.onDestruction: if (loginFutureId) py.cancelCoro(loginFutureId) HButton { icon.name: "sign-in-" + ( page.security === SignInBase.Security.Insecure ? "insecure" : page.security === SignInBase.Security.LocalHttp ? "local-http" : "secure" ) icon.color: page.security === SignInBase.Security.Insecure ? theme.colors.negativeBackground : page.security === SignInBase.Security.LocalHttp ? theme.colors.middleBackground : theme.colors.positiveBackground text: page.security === SignInBase.Security.Insecure ? page.serverUrl : page.displayUrl.replace(/^(https?:\/\/)?(www\.)?/, "") onClicked: page.exitRequested() Layout.alignment: Qt.AlignCenter Layout.maximumWidth: parent.width } HColumnLayout { id: inner spacing: page.column.spacing } HCheckBox { id: rememberAccount checked: true text: qsTr("Remember my account") subtitle.text: qsTr( "An access token will be stored on this device to " + "automatically sign you in." ) Layout.fillWidth: true Layout.topMargin: theme.spacing / 2 } HLabel { id: errorMessage wrapMode: HLabel.Wrap horizontalAlignment: Text.AlignHCenter color: theme.colors.errorText visible: Layout.maximumHeight > 0 Layout.maximumHeight: text ? implicitHeight : 0 Behavior on Layout.maximumHeight { HNumberAnimation {} } Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/AddAccount/SignInPassword.qml000066400000000000000000000032501407747233600230450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" SignInBase { id: page function takeFocus() { idField.item.forceActiveFocus() } function signIn() { if (page.loginFutureId) page.loginFutureId = "" errorMessage.text = "" page.loginFutureId = py.callCoro( "password_auth", [idField.item.text.trim(), passField.item.text, page.serverUrl], page.finishSignIn, (type, args, error, traceback, uuid) => { page.loginFutureId = "" let txt = qsTr( "Invalid request, login type or unknown error: %1", ).arg(type) type === "MatrixForbidden" ? txt = qsTr("Invalid username or password") : type === "MatrixUserDeactivated" ? txt = qsTr("This account was deactivated") : py.showError(type, traceback, uuid) page.errorMessage.text = txt }, ) } applyButton.enabled: idField.item.text.trim() && passField.item.text applyButton.onClicked: page.signIn() HLabeledItem { id: idField label.text: qsTr("Username:") Layout.fillWidth: true HTextField { width: parent.width } } HLabeledItem { id: passField label.text: qsTr("Password:") Layout.fillWidth: true HTextField { width: parent.width echoMode: HTextField.Password } } } mirage-0.7.2/src/gui/Pages/AddAccount/SignInSso.qml000066400000000000000000000034041407747233600220100ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" SignInBase { id: page function takeFocus() { copyUrlButton.forceActiveFocus() } function startSignIn() { errorMessage.text = "" page.loginFutureId = py.callCoro("start_sso_auth",[serverUrl], url => { urlArea.text = url urlArea.cursorPosition = 0 Qt.openUrlExternally(url) page.loginFutureId = py.callCoro("continue_sso_auth",[],userId => { page.loginFutureId = "" page.finishSignIn(userId) }) }) } function cancel() { if (loginFutureId) { py.cancelCoro(page.loginFutureId) page.loginFutureId = "" } page.exitRequested() } implicitWidth: theme.controls.box.defaultWidth * 1.25 applyButton.text: qsTr("Waiting") applyButton.loading: true Component.onCompleted: page.startSignIn() HLabel { wrapMode: HLabel.Wrap text: qsTr( "Complete the single sign-on process in your web browser to " + "continue.\n\n" + "If no page appeared, you can also manually open this address:" ) Layout.fillWidth: true } HRowLayout { HTextArea { id: urlArea width: parent.width readOnly: true radius: 0 wrapMode: HTextArea.WrapAnywhere Layout.fillWidth: true Layout.fillHeight: true } FieldCopyButton { id: copyUrlButton textControl: urlArea } } } mirage-0.7.2/src/gui/Pages/AddChat/000077500000000000000000000000001407747233600167035ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/AddChat/AddChat.qml000066400000000000000000000017321407747233600207110ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HPage { id: page property string userId HTabbedBox { anchors.centerIn: parent width: Math.min(implicitWidth, page.availableWidth) height: Math.min(implicitHeight, page.availableHeight) showBackButton: mainUI.mainPane.normalOrForceCollapse backButton.icon.name: "go-back-to-main-pane" backButton.toolTip.text: qsTr("Back to main pane") backButton.onClicked: mainUI.mainPane.toggleFocus() tabBar: HTabBar { HTabButton { text: qsTr("Direct chat") } HTabButton { text: qsTr("Join group") } HTabButton { text: qsTr("Create group") } } DirectChat { userId: page.userId } JoinRoom { userId: page.userId } CreateRoom { userId: page.userId } } } mirage-0.7.2/src/gui/Pages/AddChat/CreateRoom.qml000066400000000000000000000074161407747233600214660ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { id: page property string userId readonly property QtObject account: ModelStore.get("accounts").find(userId) function takeFocus() { nameField.item.forceActiveFocus() } function create() { applyButton.loading = true errorMessage.text = "" const args = [ nameField.item.text, topicArea.item.text, publicCheckBox.checked, encryptCheckBox.checked, ! blockOtherServersCheckBox.checked, ] py.callClientCoro(userId, "new_group_chat", args, roomId => { applyButton.loading = false pageLoader.showRoom(userId, roomId) mainPane.roomList.startCorrectItemSearch() }, (type, args) => { applyButton.loading = false errorMessage.text = qsTr("Unknown error - %1: %2").arg(type).arg(args) }) } function cancel() { nameField.item.reset() topicArea.item.reset() publicCheckBox.reset() encryptCheckBox.reset() blockOtherServersCheckBox.reset() errorMessage.text = "" pageLoader.showPrevious() || mainUI.mainPane.toggleFocus() } enabled: account && account.presence !== "offline" footer: AutoDirectionLayout { ApplyButton { id: applyButton text: qsTr("Create") icon.name: "room-create" onClicked: create() } CancelButton { onClicked: cancel() } } onKeyboardAccept: if (applyButton.enabled) applyButton.clicked() onKeyboardCancel: cancel() HRoomAvatar { id: avatar clientUserId: page.userId roomId: "" displayName: nameField.item.text Layout.alignment: Qt.AlignCenter Layout.preferredWidth: 128 Layout.preferredHeight: Layout.preferredWidth CurrentUserAvatar { anchors.fill: parent z: 10 opacity: nameField.item.text ? 0 : 1 visible: opacity > 0 userId: page.userId account: page.account Behavior on opacity { HNumberAnimation {} } } } HLabeledItem { id: nameField label.text: qsTr("Name:") Layout.fillWidth: true HTextField { width: parent.width maximumLength: 255 } } HLabeledItem { id: topicArea label.text: qsTr("Topic:") Layout.fillWidth: true HTextArea { width: parent.width placeholderText: qsTr("This room is about...") focusItemOnTab: publicCheckBox } } HCheckBox { id: publicCheckBox text: qsTr("Make this room public") subtitle.text: qsTr("Anyone can join without being invited") Layout.fillWidth: true } EncryptCheckBox { id: encryptCheckBox Layout.fillWidth: true } HCheckBox { id: blockOtherServersCheckBox text: qsTr("Reject users from other matrix servers") subtitle.text: qsTr("Cannot be changed later!") subtitle.color: theme.colors.warningText Layout.fillWidth: true } HLabel { id: errorMessage wrapMode: HLabel.Wrap horizontalAlignment: Text.AlignHCenter color: theme.colors.errorText visible: Layout.maximumHeight > 0 Layout.maximumHeight: text ? implicitHeight : 0 Behavior on Layout.maximumHeight { HNumberAnimation {} } Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/AddChat/CurrentUserAvatar.qml000066400000000000000000000007431407747233600230420ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HUserAvatar { property QtObject account clientUserId: userId displayName: account ? account.display_name : "" mxc: account ? account.avatar_url : "" Layout.alignment: Qt.AlignCenter Layout.preferredWidth: 128 Layout.preferredHeight: Layout.preferredWidth } mirage-0.7.2/src/gui/Pages/AddChat/DirectChat.qml000066400000000000000000000061421407747233600214330ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { id: page property string userId readonly property QtObject account: ModelStore.get("accounts").find(userId) function takeFocus() { userField.item.forceActiveFocus() } function startChat() { applyButton.loading = true errorMessage.text = "" const args = [userField.item.text.trim(), encryptCheckBox.checked] py.callClientCoro(userId, "new_direct_chat", args, roomId => { applyButton.loading = false errorMessage.text = "" pageLoader.showRoom(userId, roomId) mainPane.roomList.startCorrectItemSearch() }, (type, args) => { applyButton.loading = false let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) if (type === "InvalidUserInContext") txt = qsTr("Can't start chatting with yourself") if (type === "InvalidUserId") txt = qsTr("Invalid user ID, expected format is " + "@username:homeserver") if (type === "MatrixNotFound") txt = qsTr("User not found, please verify the entered ID") if (type === "MatrixBadGateway") txt = qsTr( "Could not contact this user's server, " + "please verify the entered ID" ) errorMessage.text = txt }) } function cancel() { userField.item.reset() errorMessage.text = "" pageLoader.showPrevious() || mainUI.mainPane.toggleFocus() } enabled: account && account.presence !== "offline" footer: AutoDirectionLayout { ApplyButton { id: applyButton text: qsTr("Start chat") icon.name: "start-direct-chat" enabled: Boolean(userField.item.text.trim()) onClicked: startChat() } CancelButton { onClicked: page.cancel() } } onKeyboardAccept: if (applyButton.enabled) applyButton.clicked() onKeyboardCancel: cancel() CurrentUserAvatar { userId: page.userId account: page.account } HLabeledItem { id: userField label.text: qsTr("Peer user ID:") Layout.fillWidth: true HTextField { width: parent.width placeholderText: qsTr("@example:matrix.org") error: Boolean(errorMessage.text) } } EncryptCheckBox { id: encryptCheckBox Layout.fillWidth: true } HLabel { id: errorMessage wrapMode: HLabel.Wrap horizontalAlignment: Text.AlignHCenter color: theme.colors.errorText visible: Layout.maximumHeight > 0 Layout.maximumHeight: text ? implicitHeight : 0 Behavior on Layout.maximumHeight { HNumberAnimation {} } Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/AddChat/EncryptCheckBox.qml000066400000000000000000000007221407747233600224520ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../Base" HCheckBox { text: qsTr("Encrypt messages") subtitle.textFormat: Text.StyledText subtitle.text: qsTr("Only users you trust can decrypt the conversation") + `
` + qsTr("Cannot be disabled later!") + "" } mirage-0.7.2/src/gui/Pages/AddChat/JoinRoom.qml000066400000000000000000000052601407747233600211550ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "../../Base/Buttons" HFlickableColumnPage { id: page property string userId readonly property QtObject account: ModelStore.get("accounts").find(userId) function takeFocus() { roomField.item.forceActiveFocus() } function join() { joinButton.loading = true errorMessage.text = "" const args = [roomField.item.text.trim()] py.callClientCoro(userId, "room_join", args, roomId => { joinButton.loading = false errorMessage.text = "" pageLoader.showRoom(userId, roomId) mainPane.roomList.startCorrectItemSearch() }, (type, args) => { joinButton.loading = false let txt = qsTr("Unknown error - %1: %2").arg(type).arg(args) if (type === "ValueError") txt = qsTr("Unrecognized alias, room ID or URL") if (type === "MatrixNotFound") txt = qsTr("Room not found") if (type === "MatrixForbidden") txt = qsTr("You do not have permission to join this room") errorMessage.text = txt }) } function cancel() { roomField.item.reset() errorMessage.text = "" pageLoader.showPrevious() || mainUI.mainPane.toggleFocus() } enabled: account && account.presence !== "offline" footer: AutoDirectionLayout { ApplyButton { id: joinButton text: qsTr("Join") icon.name: "room-join" enabled: Boolean(roomField.item.text.trim()) onClicked: join() } CancelButton { onClicked: cancel() } } onKeyboardAccept: if (joinButton.enabled) joinButton.clicked() onKeyboardCancel: cancel() CurrentUserAvatar { userId: page.userId account: page.account } HLabeledItem { id: roomField label.text: qsTr("Alias, URL or room ID:") Layout.fillWidth: true HTextField { width: parent.width placeholderText: qsTr("#example:matrix.org") error: Boolean(errorMessage.text) } } HLabel { id: errorMessage wrapMode: HLabel.Wrap horizontalAlignment: Text.AlignHCenter color: theme.colors.errorText visible: Layout.maximumHeight > 0 Layout.maximumHeight: text ? implicitHeight : 0 Behavior on Layout.maximumHeight { HNumberAnimation {} } Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/Chat/000077500000000000000000000000001407747233600162725ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/AutoCompletion/000077500000000000000000000000001407747233600212345ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/AutoCompletion/CompletableUserDelegate.qml000066400000000000000000000027351407747233600264770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../Base" import "../../../Base/HTile" HTile { id: root property bool colorName: hovered backgroundColor: "transparent" contentItem: ContentRow { tile: root HUserAvatar { id: avatar clientUserId: chat.userId userId: model.id displayName: model.display_name mxc: model.avatar_url compact: root.compact radius: theme.chat.userAutoCompletion.avatarsRadius implicitHeight: compact ? theme.controls.avatar.compactSize : theme.controls.avatar.size / 1.5 } TitleLabel { textFormat: TitleLabel.StyledText text: (model.display_name || model.id) + ( model.display_name ? " ".repeat(2) + utils.htmlColorize( model.id, theme.chat.userAutoCompletion.userIds, ) : "" ) color: root.colorName ? utils.nameColor(model.display_name || model.id.substring(1)) : theme.chat.userAutoCompletion.displayNames Behavior on color { HColorAnimation {} } } } DelegateTransitionFixer {} } mirage-0.7.2/src/gui/Pages/Chat/AutoCompletion/UserAutoCompletion.qml000066400000000000000000000120521407747233600255500ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../.." import "../../../Base" import "../../../Base/HTile" HListView { id: root property HTextArea textArea property bool open: false property var originalWord: null property int replacementStart: -1 property int replacementEnd: -1 property bool autoOpenCompleted: false property var usersCompleted: ({}) // {userId: displayName} readonly property bool autoOpen: { if (autoOpenCompleted) return true const current = textArea.getWordBehindCursor() return current ? /^@.+/.test(current.word) : false } readonly property var wordToComplete: open ? originalWord || textArea.getWordBehindCursor() : null readonly property string modelFilter: autoOpen && wordToComplete ? wordToComplete.word.replace(/^@/, "") : open && wordToComplete ? wordToComplete.word : "" function replaceCompletionOrCurrentWord(withText) { Qt.inputMethod.reset() const current = textArea.getWordBehindCursor() if (! current) return replacementStart === -1 || replacementEnd === -1 ? textArea.remove(current.start, current.end + 1) : textArea.remove(replacementStart, replacementEnd) textArea.insertAtCursor(withText) } function previous() { if (open) { decrementCurrentIndex() return } open = true const args = [model.modelId, modelFilter] py.callCoro("set_string_filter", args, decrementCurrentIndex) } function next() { if (open) { incrementCurrentIndex() return } open = true const args = [model.modelId, modelFilter] py.callCoro("set_string_filter", args, incrementCurrentIndex) } function accept() { if (currentIndex !== -1) { const member = model.get(currentIndex) usersCompleted[member.id] = member.display_name.trim() usersCompletedChanged() } open = false } function cancel() { if (originalWord) replaceCompletionOrCurrentWord(originalWord.word) currentIndex = -1 open = false } visible: opacity > 0 opacity: open && count ? 1 : 0 bottomMargin: theme.spacing / 2 implicitHeight: open && count ? Math.min(window.height, contentHeight + topMargin + bottomMargin) : 0 model: ModelStore.get(chat.userId, chat.roomId, "autocompleted_members") delegate: CompletableUserDelegate { width: root.width colorName: hovered || root.currentIndex === model.index onClicked: { root.currentIndex = model.index root.accept() root.open = false } } onAutoOpenChanged: open = autoOpen onOpenChanged: if (! open) { originalWord = null replacementStart = -1 replacementEnd = -1 currentIndex = -1 autoOpenCompleted = false py.callCoro("set_string_filter", [model.modelId, ""]) } onModelFilterChanged: { if (! open) return py.callCoro("set_string_filter", [model.modelId, modelFilter]) } onCurrentIndexChanged: { if (currentIndex === -1) return if (! originalWord) originalWord = textArea.getWordBehindCursor() if (autoOpen) autoOpenCompleted = true const member = model.get(currentIndex) const replacement = member.display_name.trim() || member.id replaceCompletionOrCurrentWord(replacement) replacementStart = textArea.cursorPosition - replacement.length replacementEnd = textArea.cursorPosition } Behavior on opacity { HNumberAnimation {} } Behavior on implicitHeight { HNumberAnimation {} } Rectangle { anchors.fill: parent z: -1 color: theme.chat.userAutoCompletion.background } Connections { target: root.textArea onCursorPositionChanged: { if (! root.open) return const pos = root.textArea.cursorPosition const start = root.wordToComplete.start let end = root.wordToComplete.end + 1 if (root.currentIndex !== -1) { const member = root.model.get(root.currentIndex) const repl = member.display_name.trim() || member.id end = root.wordToComplete.start + repl.length } if (pos === root.textArea.length) return if (pos < start || pos > end) root.accept() } onTextChanged: { let changed = false for (const [id, name] of Object.entries(root.usersCompleted)) { if (! root.textArea.text.includes(name)) { delete root.usersCompleted[id] changed = true } } if (changed) root.usersCompletedChanged() } } } mirage-0.7.2/src/gui/Pages/Chat/Chat.qml000066400000000000000000000075571407747233600177020ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../.." import "../../Base" import "RoomPane" Item { id: chat // [userId, roomId] - Set this instead of changing the userId and roomId // properties one by one, else QML has time to be in an invalid state // between the two changes. property var userRoomId property string replyToEventId: "" property string replyToUserId: "" property string replyToDisplayName: "" property bool longLoading: false readonly property string userId: userRoomId[0] readonly property string roomId: userRoomId[1] readonly property QtObject userInfo: ModelStore.get("accounts").find(userRoomId[0]) readonly property QtObject roomInfo: ModelStore.get(userRoomId[0], "rooms").find(userRoomId[1]) readonly property bool ready: Boolean(userInfo && roomInfo) readonly property alias loader: loader readonly property alias roomPane: roomPaneLoader.item readonly property bool composerHasFocus: Boolean(loader.item && loader.item.composer.hasFocus) function clearReplyTo() { if (! replyToEventId) return replyToEventId = "" replyToUserId = "" replyToDisplayName = "" } onFocusChanged: if (focus && loader.item) loader.item.composer.takeFocus() onReadyChanged: longLoading = false HShortcut { sequences: window.settings.Keys.Chat.leave active: userInfo && userInfo.presence !== "offline" onActivated: window.makePopup( "Popups/LeaveRoomPopup.qml", { userId, roomId, roomName: roomInfo.display_name, inviterId: roomInfo.inviter_id, left: roomInfo.left, }, ) } Timer { interval: 300 running: ! ready onTriggered: longLoading = true } Connections { target: pageLoader onAboutToRecycle: chat.clearReplyTo() } HLoader { id: loader anchors.rightMargin: ! roomPane ? 0 : ready && ! ( roomPane.requireDefaultSize && roomPane.minimumSize > roomPane.maximumSize && ! roomPane.collapse ) ? roomPane.visibleSize : roomPane.calculatedSizeNoRequiredMinimum anchors.fill: parent visible: ! (ready && roomPane && roomPane.visibleSize >= chat.width) onLoaded: if (chat.focus) item.composer.takeFocus() source: ready ? "ChatPage.qml" : "" Behavior on anchors.rightMargin { HNumberAnimation {} } HLoader { anchors.centerIn: parent width: 96 * theme.uiScale height: width source: "../../Base/HBusyIndicator.qml" active: ready ? 0 : longLoading ? 1 : 0 opacity: active ? 1 : 0 Behavior on opacity { HNumberAnimation { factor: 2 } } } } HLoader { id: roomPaneLoader active: ready sourceComponent: RoomPane { id: roomPane readonly property alias appearAnimation: appearAnimation referenceSizeParent: chat maximumSize: chat.width - theme.minimumSupportedWidth * 1.5 HNumberAnimation { id: appearAnimation target: roomPane.contentTranslation property: "x" from: -chat.width + roomPane.width to: 0 easing.type: Easing.OutCirc factor: 2 running: true } Connections { target: pageLoader onRecycled: roomPane.appearAnimation.restart() } } } } mirage-0.7.2/src/gui/Pages/Chat/ChatPage.qml000066400000000000000000000062701407747233600204660ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "AutoCompletion" import "Composer" import "FileTransfer" import "Timeline" HColumnPage { id: chatPage property string loadMembersFutureId: "" property var lockedRoom: null // null or [userId, roomId] readonly property var userRoomId: chat.userRoomId readonly property alias roomHeader: roomHeader readonly property alias eventList: eventList readonly property alias typingMembers: typingMembers readonly property alias reply: reply readonly property alias transfers: transfers readonly property alias userCompletion: userCompletion readonly property alias composer: composer readonly property DropArea uploadDropArea: UploadDropArea { parent: window.mainUI anchors.fill: parent } function lockRoomPosition(lock) { if (lock && lockedRoom) py.callClientCoro( lockedRoom[0], "lock_room_position", [lockedRoom[1], false], ) lockedRoom = lock ? [chat.userId, chat.roomId] : null py.callClientCoro( chat.userId, "lock_room_position", [chat.roomId, lock], ) } padding: 0 column.spacing: 0 onUserRoomIdChanged: lockRoomPosition(true) Component.onDestruction: { lockRoomPosition(false) if (loadMembersFutureId) py.cancelCoro(loadMembersFutureId) } Timer { interval: 200 running: ! chat.roomInfo.inviter_id && ! chat.roomInfo.left onTriggered: loadMembersFutureId = py.callClientCoro( chat.userId, "load_all_room_members", [chat.roomId], () => { loadMembersFutureId = "" }, ) } RoomHeader { id: roomHeader Layout.fillWidth: true } EventList { id: eventList Layout.fillWidth: true Layout.fillHeight: true } TypingMembersBar { id: typingMembers typingMembers: JSON.parse(chat.roomInfo.typing_members) Layout.fillWidth: true } ReplyBar { id: reply replyToEventId: chat.replyToEventId replyToUserId: chat.replyToUserId replyToDisplayName: chat.replyToDisplayName onCancel: { chat.replyToEventId = "" chat.replyToUserId = "" chat.replyToDisplayName = "" } Layout.fillWidth: true } TransferList { id: transfers Layout.fillWidth: true Layout.minimumHeight: implicitHeight Layout.preferredHeight: implicitHeight * transferCount Layout.maximumHeight: chatPage.height / 6 Behavior on Layout.preferredHeight { HNumberAnimation {} } } UserAutoCompletion { id: userCompletion textArea: composer.messageArea clip: true Layout.fillWidth: true Layout.maximumHeight: chatPage.height / 4 } Composer { id: composer userCompletion: userCompletion eventList: eventList.eventList Layout.fillWidth: true Layout.maximumHeight: parent.height / 2 } } mirage-0.7.2/src/gui/Pages/Chat/Composer/000077500000000000000000000000001407747233600200615ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/Composer/Composer.qml000066400000000000000000000120211407747233600223570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../.." import "../../../Base" import "../../../Base/Buttons" import "../AutoCompletion" Rectangle { id: root property UserAutoCompletion userCompletion property alias eventList: messageArea.eventList readonly property bool widthStarved: width < 384 * theme.uiScale readonly property bool parted: chat.roomInfo.left readonly property string inviterId: chat.roomInfo.inviter_id readonly property string inviterColoredName: utils.coloredNameHtml(chat.roomInfo.inviter_name, inviterId) readonly property bool hasFocus: messageArea.activeFocus || joinButton.activeFocus || exitButton.activeFocus readonly property alias messageArea: messageArea function takeFocus() { joinButton.visible ? joinButton.forceActiveFocus() : exitButton.visible ? exitButton.forceActiveFocus() : messageArea.forceActiveFocus() } implicitHeight: Math.max(theme.baseElementsHeight, row.implicitHeight) color: theme.chat.composer.background HRowLayout { id: row anchors.fill: parent HUserAvatar { id: avatar readonly property QtObject writerInfo: ModelStore.get("accounts").find(clientUserId) clientUserId: messageArea.writerId userId: clientUserId mxc: writerInfo ? writerInfo.avatar_url : "" displayName: writerInfo ? writerInfo.display_name : "" radius: 0 } HScrollView { enabled: visible visible: ! root.inviterId && ! root.parted onVisibleChanged: if (root.hasFocus) root.takeFocus() Layout.fillHeight: true Layout.fillWidth: true MessageArea { id: messageArea autoCompletionOpen: userCompletion.open && userCompletion.count usersCompleted: userCompletion.usersCompleted onAutoCompletePrevious: userCompletion.previous() onAutoCompleteNext: userCompletion.next() onCancelAutoCompletion: userCompletion.cancel() onAcceptAutoCompletion: ! userCompletion.autoOpen || userCompletion.autoOpenCompleted ? userCompletion.accept() : null } } UploadButton { visible: ! root.inviterId && ! root.parted onVisibleChanged: if (root.hasFocus) root.takeFocus() Layout.fillHeight: true } HLabel { textFormat: Text.StyledText wrapMode: HLabel.Wrap visible: root.inviterId || root.parted verticalAlignment: HLabel.AlignVCenter text: root.parted && root.inviterId ? qsTr("Declined %1's invite").arg(root.inviterColoredName) : root.parted ? qsTr("No longer part of this room") : qsTr("Invited by %1").arg(root.inviterColoredName) leftPadding: theme.spacing rightPadding: leftPadding topPadding: theme.spacing / 2 bottomPadding: topPadding onVisibleChanged: if (root.hasFocus) root.takeFocus() Layout.fillWidth: true Layout.fillHeight: true } ApplyButton { id: joinButton icon.name: "invite-accept" text: widthStarved ? "" : qsTr("Join") visible: root.inviterId && ! root.parted onVisibleChanged: if (root.hasFocus) root.takeFocus() onClicked: { loading = true function callback() { joinButton.loading = false } py.callClientCoro(chat.userId, "join", [chat.roomId], callback) } Layout.fillWidth: false Layout.fillHeight: true Behavior on implicitWidth { HNumberAnimation {} } } CancelButton { id: exitButton icon.name: root.parted ? "room-forget" : "invite-decline" visible: root.inviterId || root.parted text: widthStarved ? "" : root.parted ? qsTr("Forget") : qsTr("Decline") onVisibleChanged: if (root.hasFocus) root.takeFocus() onClicked: { loading = true window.makePopup("Popups/LeaveRoomPopup.qml", { userId: chat.userId, roomId: chat.roomId, roomName: chat.roomInfo.display_name, inviterId: root.inviterId, left: root.parted, doneCallback: () => { exitButton.loading = false }, }) } Layout.fillWidth: false Layout.fillHeight: true Behavior on implicitWidth { HNumberAnimation {} } } } } mirage-0.7.2/src/gui/Pages/Chat/Composer/MessageArea.qml000066400000000000000000000162441407747233600227600ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Clipboard 0.1 import "../../.." import "../../../Base" HTextArea { id: area property HListView eventList property bool autoCompletionOpen: false property var usersCompleted: ({}) property string indent: " " property string userSetAsTyping: "" property bool textChangedSinceLostFocus: false readonly property var usableAliases: { const obj = {} const aliases = window.settings.Chat.Composer.Aliases // Get accounts that are members of this room with permission to talk for (const [id, alias] of Object.entries(aliases)) { const room = ModelStore.get(id, "rooms").find(chat.roomId) room && ! room.inviter_id && ! room.left && room.can_send_messages? obj[id] = alias.trim().split(/\s/)[0] : null } return obj } readonly property var candidateAliases: { if (! text) return [] const candidates = [] const words = text.split(" ") for (const [userId, alias] of Object.entries(usableAliases)) if ((words.length === 1 && alias.startsWith(words[0])) || (words.length > 1 && words[0] == alias)) candidates.push({id: userId, text: alias}) return candidates } readonly property var usingAlias: candidateAliases.length === 1 && text.includes(" ") ? candidateAliases[0] : null readonly property string writerId: usingAlias ? usingAlias.id : chat.userId readonly property string toSend: usingAlias ? text.replace(usingAlias.text + " ", "") : text readonly property int cursorY: text.substring(0, cursorPosition).split("\n").length - 1 readonly property int cursorX: cursorPosition - lines.slice(0, cursorY).join("").length - cursorY readonly property var lines: text.split("\n") readonly property string lineText: lines[cursorY] || "" readonly property string lineTextUntilCursor: lineText.substring(0, cursorX) // readonly property int deleteCharsOnBackspace: // lineTextUntilCursor.match(/^ +$/) ? // lineTextUntilCursor.match(/ {1,4}/g).slice(-1)[0].length : // 1 signal autoCompletePrevious() signal autoCompleteNext() signal acceptAutoCompletion() signal cancelAutoCompletion() function setTyping(typing) { if (! area.enabled) return if (typing && userSetAsTyping && userSetAsTyping !== writerId) py.callClientCoro( userSetAsTyping, "room_typing", [chat.roomId, false], ) const userId = typing ? writerId : userSetAsTyping userSetAsTyping = typing ? writerId : "" if (! userId) return // ! typing && ! userSetAsTyping py.callClientCoro(userId, "room_typing", [chat.roomId, typing]) } function addNewLine() { let indents = 0 const parts = lineText.split(indent) for (const [i, part] of parts.entries()) { if (i === parts.length - 1 || part) { break } indents += 1 } const add = indent.repeat(indents) area.insertAtCursor("\n" + add) } function sendText() { if (! toSend && ! chat.replyToEventId) return // Need to copy usersCompleted because the completion UI closing will // clear it before it reaches Python. const mentions = Object.assign({}, usersCompleted) const args = [chat.roomId, toSend, mentions, chat.replyToEventId] py.callClientCoro(writerId, "send_text", args) area.clear() clearReplyTo() } saveName: "composer" saveId: [chat.roomId, chat.userId] enabled: chat.roomInfo.can_send_messages disabledText: qsTr("You do not have permission to post in this room") placeholderText: qsTr("Type a message...") enableCustomImagePaste: true menuKeySpawnsMenu: ! (eventList && (eventList.currentItem || eventList.selectedCount)) backgroundColor: "transparent" focusedBorderColor: "transparent" tabStopDistance: 4 * 4 // 4 spaces focus: true onTextChanged: if (! text) setTyping(false) onToSendChanged: { textChangedSinceLostFocus = true if (toSend && (usingAlias || ! candidateAliases.length)) { setTyping(true) } } onEditingFinished: { // when focus is lost if (text && textChangedSinceLostFocus) { setTyping(false) textChangedSinceLostFocus = false } } onCustomImagePaste: window.makePopup( "Popups/ConfirmClipboardUploadPopup.qml", { userId: chat.userId, roomId: chat.roomId, roomName: chat.roomInfo.display_name, replyToEventId: chat.replyToEventId, }, popup => popup.replied.connect(chat.clearReplyTo), ) Keys.onEscapePressed: autoCompletionOpen ? cancelAutoCompletion() : clearReplyTo() Keys.onReturnPressed: ev => { if (autoCompletionOpen) acceptAutoCompletion() ev.accepted = true ev.modifiers & Qt.ShiftModifier || ev.modifiers & Qt.ControlModifier || ev.modifiers & Qt.AltModifier ? addNewLine() : sendText() } Keys.onEnterPressed: ev => Keys.returnPressed(ev) Keys.onMenuPressed: ev => { if (autoCompletionOpen) acceptAutoCompletion() if (eventList && eventList.currentItem) eventList.currentItem.openContextMenu() } Keys.onBacktabPressed: ev => { // if previous char isn't a space/tab/newline if (text.slice(cursorPosition - 1, cursorPosition).trim()) { ev.accepted = true autoCompletePrevious() } } Keys.onTabPressed: ev => { ev.accepted = true if (text.slice(cursorPosition - 1, cursorPosition).trim()) { autoCompleteNext() return } area.insertAtCursor(indent) } Keys.onUpPressed: ev => { ev.accepted = autoCompletionOpen if (autoCompletionOpen) autoCompletePrevious() } Keys.onDownPressed: ev => { ev.accepted = autoCompletionOpen if (autoCompletionOpen) autoCompleteNext() } Keys.onPressed: ev => { if (ev.text && autoCompletionOpen) acceptAutoCompletion() if (ev.matches(StandardKey.Copy) && ! area.selectedPlainText && eventList && (eventList.selectedCount || eventList.currentIndex !== -1)) { ev.accepted = true eventList.copySelectedDelegates() return } // FIXME: buggy // if (ev.modifiers === Qt.NoModifier && // ev.key === Qt.Key_Backspace && // ! area.selectedPlainText) // { // ev.accepted = true // area.remove( // cursorPosition - deleteCharsOnBackspace, // cursorPosition // ) // } } Connections { target: pageLoader onRecycled: { area.reset() area.loadState() } } } mirage-0.7.2/src/gui/Pages/Chat/Composer/UploadButton.qml000066400000000000000000000026141407747233600232170ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Clipboard 0.1 import CppUtils 0.1 import "../../../Base" import "../../../Dialogs" HButton { enabled: chat.roomInfo.can_send_messages icon.name: "upload-file" toolTip.text: chat.userInfo.max_upload_size ? qsTr("Send files (%1 max)").arg( CppUtils.formattedBytes(chat.userInfo.max_upload_size, 0), ) : qsTr("Send files") onClicked: sendFilePicker.dialog.open() HShortcut { sequences: window.settings.Keys.Chat.send_clipboard_path onActivated: window.makePopup( "Popups/ConfirmUploadPopup.qml", { userId: chat.userId, roomId: chat.roomId, roomName: chat.roomInfo.display_name, filePath: Clipboard.text.trim(), replyToEventId: chat.replyToEventId, }, popup => popup.replied.connect(chat.clearReplyTo), ) } SendFilePicker { id: sendFilePicker userId: chat.userId roomId: chat.roomId replyToEventId: chat.replyToEventId onReplied: chat.clearReplyTo() HShortcut { sequences: window.settings.Keys.Chat.send_file onActivated: sendFilePicker.dialog.open() } } } mirage-0.7.2/src/gui/Pages/Chat/FileTransfer/000077500000000000000000000000001407747233600206565ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/FileTransfer/Transfer.qml000066400000000000000000000146041407747233600231620ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import CppUtils 0.1 import "../../../Base" HColumnLayout { id: transfer property bool cancelPending: false property int msLeft: model.time_left property int transferred: model.transferred readonly property int speed: model.speed readonly property int totalSize: model.total_size readonly property string status: model.status readonly property bool paused: model.paused function cancel() { cancelPending = true // Python will delete this model item on cancel py.callClientCoro(chat.userId, "cancel_transfer", [model.id]) } function toggle_pause() { py.callClientCoro( chat.userId, "toggle_pause_transfer", [chat.roomId, model.id], ) } Behavior on height { HNumberAnimation {} } DelegateTransitionFixer {} HRowLayout { HIcon { svgName: model.is_upload ? "uploading" : "downloading" colorize: cancelPending || transfer.status === "Error" ? theme.colors.negativeBackground : transfer.paused ? theme.colors.middleBackground : theme.icons.colorize Layout.preferredWidth: theme.baseElementsHeight } HLabel { id: statusLabel property bool expand: status === "Error" readonly property string fileName: model.filepath.split("/").slice(-1)[0] readonly property string filePath: model.filepath.replace(/^file:\/\//, "") elide: expand ? Text.ElideNone : Text.ElideRight wrapMode: expand ? HLabel.Wrap : Text.NoWrap text: cancelPending ? qsTr("Cancelling...") : status === "Preparing" ? qsTr("Preparing file...") : status === "Transfering" ? fileName : status === "Caching" ? qsTr("Caching %1...").arg(fileName) : model.error === "MatrixForbidden" ? qsTr("Forbidden file type or quota exceeded: %1") .arg(fileName) : model.error === "MatrixTooLarge" ? qsTr("Too large for this server (%1 max): %2") .arg(CppUtils.formattedBytes(chat.userInfo.max_upload_size)) .arg(fileName) : model.error === "IsADirectoryError" ? qsTr("Can't upload folders, need a file: %1").arg(filePath) : model.error === "FileNotFoundError" ? qsTr("Non-existent file: %1").arg(filePath) : model.error === "PermissionError" ? qsTr("No permission to read this file: %1").arg(filePath) : qsTr("Unknown error for %1: %2 - %3") .arg(filePath).arg(model.error).arg(model.error_args) topPadding: theme.spacing / 2 bottomPadding: topPadding leftPadding: theme.spacing / 1.5 rightPadding: leftPadding Layout.fillWidth: true HoverHandler { id: statusLabelHover } HToolTip { text: parent.truncated ? parent.text : "" visible: text && statusLabelHover.hovered } } HSpacer {} Repeater { model: [ msLeft ? qsTr("-%1").arg(utils.formatDuration(msLeft)) : "", speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "", transferred && totalSize ? qsTr("%1/%2").arg(CppUtils.formattedBytes(transferred)) .arg(CppUtils.formattedBytes(totalSize)) : transferred || totalSize ? CppUtils.formattedBytes(transferred || totalSize) : "", ] HLabel { text: modelData visible: text && Layout.preferredWidth > 0 leftPadding: theme.spacing / 1.5 rightPadding: leftPadding Layout.preferredWidth: status === "Transfering" ? implicitWidth : 0 Behavior on Layout.preferredWidth { HNumberAnimation {} } } } HButton { visible: Layout.preferredWidth > 0 padded: false icon.name: transfer.paused ? "transfer-resume" : "transfer-pause" icon.color: transfer.paused ? theme.colors.positiveBackground : theme.colors.middleBackground toolTip.text: transfer.paused ? qsTr("Resume") : qsTr("Pause") onClicked: transfer.toggle_pause() // TODO: pausing downloads Layout.preferredWidth: status === "Transfering" && model.is_upload ? theme.baseElementsHeight : 0 Layout.fillHeight: true Behavior on Layout.preferredWidth { HNumberAnimation {} } } HButton { icon.name: "transfer-cancel" icon.color: theme.colors.negativeBackground onClicked: transfer.cancel() padded: false Layout.preferredWidth: theme.baseElementsHeight Layout.fillHeight: true } TapHandler { onTapped: { if (status === "Error") { transfer.cancel() } else { statusLabel.expand = ! statusLabel.expand } } } } HProgressBar { id: progressBar visible: Layout.maximumHeight !== 0 indeterminate: status !== "Transfering" || ! totalSize || ! transferred value: transferred to: totalSize // TODO: bake this in hprogressbar foregroundColor: cancelPending || status === "Error" ? theme.controls.progressBar.errorForeground : transfer.paused ? theme.controls.progressBar.pausedForeground : theme.controls.progressBar.foreground Layout.fillWidth: true Layout.maximumHeight: status === "Error" && indeterminate ? 0 : -1 Behavior on value { HNumberAnimation { duration: 1200 } } Behavior on Layout.maximumHeight { HNumberAnimation {} } } } mirage-0.7.2/src/gui/Pages/Chat/FileTransfer/TransferList.qml000066400000000000000000000014701407747233600240130ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../.." import "../../../Base" Rectangle { property int delegateHeight: 0 readonly property var firstDelegate: transferList.contentItem.visibleChildren[0] readonly property alias transferCount: transferList.count implicitWidth: 800 implicitHeight: firstDelegate ? firstDelegate.height : 0 color: theme.chat.fileTransfer.background opacity: implicitHeight ? 1 : 0 clip: true Behavior on implicitHeight { HNumberAnimation {} } HListView { id: transferList anchors.fill: parent model: ModelStore.get(chat.roomId, "transfers") delegate: Transfer { width: transferList.width } } } mirage-0.7.2/src/gui/Pages/Chat/InfoBar.qml000066400000000000000000000022061407747233600203250ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" Rectangle { default property alias rowLayoutData: rowLayout.data readonly property alias icon: icon readonly property alias label: label implicitHeight: label.text ? rowLayout.height : 0 opacity: implicitHeight ? 1 : 0 Behavior on implicitHeight { HNumberAnimation {} } HRowLayout { id: rowLayout width: parent.width spacing: theme.spacing HIcon { id: icon Layout.fillHeight: true Layout.leftMargin: rowLayout.spacing / 2 } HLabel { id: label elide: Text.ElideRight verticalAlignment: Text.AlignVCenter Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: rowLayout.spacing / 4 Layout.bottomMargin: rowLayout.spacing / 4 Layout.leftMargin: rowLayout.spacing / 2 Layout.rightMargin: rowLayout.spacing / 2 } } } mirage-0.7.2/src/gui/Pages/Chat/ReplyBar.qml000066400000000000000000000015001407747233600205210ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" InfoBar { property string replyToEventId: "" property string replyToUserId: "" property string replyToDisplayName: "" signal cancel() color: theme.chat.replyBar.background icon.svgName: "reply-to" label.textFormat: Text.StyledText label.text: replyToEventId ? utils.coloredNameHtml(replyToDisplayName, replyToUserId) : "" HButton { backgroundColor: "transparent" icon.name: "reply-cancel" icon.color: theme.colors.negativeBackground topPadding: 0 bottomPadding: 0 onClicked: cancel() Layout.fillHeight: true } } mirage-0.7.2/src/gui/Pages/Chat/RoomHeader.qml000066400000000000000000000160061407747233600210350ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" Rectangle { id: root readonly property bool center: goToMainPaneButton.show || window.settings.Chat.always_center_header readonly property HListView eventList: chatPage.eventList.eventList readonly property int selected: eventList.selectedCount === 1 && eventList.selectedText ? 0 : eventList.selectedCount implicitHeight: theme.baseElementsHeight color: theme.chat.roomHeader.background HRowLayout { id: row anchors.fill: parent RoomHeaderButton { id: goToMainPaneButton show: mainUI.mainPane.normalOrForceCollapse padded: false backgroundColor: "transparent" icon.name: "go-back-to-main-pane" toolTip.text: qsTr("Back to main pane") onClicked: mainUI.mainPane.toggleFocus() Layout.preferredWidth: show ? avatar.width : 0 } HSpacer { visible: root.center } HRoomAvatar { id: avatar clientUserId: chat.userId roomId: chat.roomId displayName: chat.roomInfo.display_name mxc: chat.roomInfo.avatar_url radius: 0 Layout.alignment: Qt.AlignTop } HLabel { id: mainLabel text: root.selected === 0 ? chat.roomInfo.display_name || qsTr("Empty room") : root.selected === 1 ? qsTr("%1 selected message").arg(root.selected) : qsTr("%1 selected messages").arg(root.selected) color: theme.chat.roomHeader.name elide: Text.ElideRight verticalAlignment: Text.AlignVCenter leftPadding: theme.spacing rightPadding: leftPadding // FIXME: these dirty manual calculations Layout.preferredWidth: Math.min( implicitWidth, row.width - goToMainPaneButton.width - avatar.width - encryptionStatusButton.width - copyButton.width - removeButton.width - deselectButton.width - goToRoomPaneButton.width ) Layout.fillWidth: ! topicLabel.text Layout.fillHeight: true HoverHandler { id: nameHover } } HLabel { id: topicLabel text: root.selected ? "" : chat.roomInfo.topic textFormat: Text.StyledText font.pixelSize: theme.fontSize.small color: theme.chat.roomHeader.topic elide: Text.ElideRight verticalAlignment: Text.AlignVCenter rightPadding: mainLabel.rightPadding Layout.preferredWidth: ! text ? 0 : Math.min( implicitWidth, row.width - goToMainPaneButton.width - avatar.width - mainLabel.width - encryptionStatusButton.width - copyButton.width - removeButton.width - deselectButton.width - goToRoomPaneButton.width ) Layout.fillWidth: text && ! root.center Layout.fillHeight: true HoverHandler { id: topicHover } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } HToolTip { readonly property string name: mainLabel.truncated ? (`${chat.roomInfo.display_name}`) : "" readonly property string topic: topicLabel.truncated ? chat.roomInfo.topic : "" visible: text && (nameHover.hovered || topicHover.hovered) label.textFormat: Text.StyledText text: name && topic ? (`${name}
${topic}`) : (name || topic) } RoomHeaderButton { id: encryptionStatusButton show: chat.roomInfo.encrypted && ! root.selected padded: false backgroundColor: "transparent" icon.name: chat.roomInfo.unverified_devices ? "device-unset" : "device-verified" icon.color: chat.roomInfo.unverified_devices ? theme.colors.middleBackground : theme.colors.positiveBackground toolTip.text: chat.roomInfo.unverified_devices ? qsTr("Some members in this encrypted room have " + "unverified devices") : qsTr("All members in this encrypted room are verified") onClicked: toolTip.instantToggle() Layout.preferredWidth: show ? avatar.width : 0 Layout.fillHeight: true } RoomHeaderButton { id: copyButton show: root.selected icon.name: "room-header-copy" toolTip.text: qsTr("Copy messages") toolTip.onClosed: toolTip.text = qsTr("Copy messages") onClicked: { root.eventList.copySelectedDelegates() toolTip.text = qsTr("Copied messages") toolTip.instantShow(2000) } } RoomHeaderButton { id: removeButton readonly property var events: root.eventList.redactableCheckedEvents show: root.selected enabled: events.length > 0 icon.name: "room-header-remove" toolTip.text: qsTr("Remove messages") onClicked: utils.makePopup( "Popups/RedactPopup.qml", window, { preferUserId: chat.userId, roomId: chat.roomId, eventSenderAndIds: events.map(ev => [ev.sender_id, ev.id]), onlyOwnMessageWarning: ! chat.roomInfo.can_redact_all && events.length < root.selected }, ) } RoomHeaderButton { id: deselectButton show: root.selected icon.name: "room-header-deselect" toolTip.text: qsTr("Deselect messages") onClicked: root.eventList.checked = [] } HSpacer { visible: root.center } RoomHeaderButton { id: goToRoomPaneButton show: chat.roomPane && chat.roomPane.normalOrForceCollapse padded: false backgroundColor: "transparent" icon.name: "go-to-room-pane" toolTip.text: qsTr("Go to room pane") onClicked: chat.roomPane.toggleFocus() Layout.preferredWidth: show ? avatar.width : 0 } } } mirage-0.7.2/src/gui/Pages/Chat/RoomHeaderButton.qml000066400000000000000000000006451407747233600222330ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HButton { property bool show: true visible: Layout.preferredWidth > 0 Layout.preferredWidth: show ? implicitWidth : 0 Layout.fillHeight: true Behavior on Layout.preferredWidth { HNumberAnimation {} } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/000077500000000000000000000000001407747233600200125ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/000077500000000000000000000000001407747233600220545ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/DeviceVerification.qml000066400000000000000000000073761407747233600263460ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../../Base" import "../../../../Base/Buttons" HFlickableColumnPage { id: page property string deviceOwner property string deviceOwnerDisplayName property string deviceId property string deviceName property string ed25519Key property HStackView stackView property Item previouslyFocused: null signal trustSet(bool trust) function close() { if (previouslyFocused) previouslyFocused.forceActiveFocus() stackView.pop() } footer: AutoDirectionLayout { PositiveButton { text: qsTr("They're the same") icon.name: "device-verified" onClicked: { loading = true py.callCoro( "verify_device", [deviceOwner, deviceId, ed25519Key.replace(/ /g, "")], () => { loading = false page.trustSet(true) page.close() } ) } } NegativeButton { text: qsTr("They differ") icon.name: "device-blacklisted" onClicked: { loading = true py.callCoro( "blacklist_device", [deviceOwner, deviceId, ed25519Key.replace(/ /g, "")], () => { loading = false page.trustSet(false) page.close() } ) } } CancelButton { id: cancelButton onClicked: page.close() } } onKeyboardCancel: page.close() HRowLayout { HButton { id: closeButton circle: true icon.name: "close-view" iconItem.small: true onClicked: page.close() Layout.rightMargin: theme.spacing } HLabel { text: qsTr("Verification") font.bold: true elide: HLabel.ElideRight horizontalAlignment: Qt.AlignHCenter Layout.fillWidth: true } Item { Layout.preferredWidth: closeButton.width } } HLabel { wrapMode: HLabel.Wrap textFormat: HLabel.StyledText text: qsTr( "Does %1 sees the same info in their session's account settings?" ).arg(utils.coloredNameHtml(deviceOwnerDisplayName, deviceOwner)) Layout.fillWidth: true } HTextArea { function formatInfo(info, value) { return ( `

` + info + `
` + value + `

` ) } readOnly: true wrapMode: HSelectableLabel.Wrap textFormat: HTextArea.RichText text: ( formatInfo(qsTr("Session name: "), page.deviceName) + formatInfo(qsTr("Session ID: "), page.deviceId) + formatInfo(qsTr("Session key: "), ""+page.ed25519Key+"") ) Component.onCompleted: { page.previouslyFocused = window.activeFocusItem forceActiveFocus() } Layout.fillWidth: true } HLabel { wrapMode: HLabel.Wrap text: qsTr( "If you already know this user, compare the info you see us" + "ing a trusted contact method, such as email or a phone call." ) Layout.fillWidth: true } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/MemberDelegate.qml000066400000000000000000000147531407747233600254430ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import Clipboard 0.1 import "../../../.." import "../../../../Base" import "../../../../Base/HTile" import "../../../../Popups" HTile { id: member property bool colorName: hovered property string getPresenceFutureId: "" backgroundColor: theme.chat.roomPane.listView.member.background contentOpacity: model.invited || model.ignored ? theme.chat.roomPane.listView.member.invitedOpacity : 1 contentItem: ContentRow { tile: member HUserAvatar { id: avatar clientUserId: chat.userId userId: model.id displayName: model.ignored ? "" : model.display_name mxc: model.ignored ? "" : model.avatar_url powerLevel: model.power_level invited: model.invited compact: member.compact presence: model.ignored ? "offline" : model.presence shiftMembershipIconPositionBy: roomPane.width >= width + 8 * 3 ? -8 : -4 } HColumnLayout { HRowLayout { spacing: theme.spacing TitleLabel { text: model.ignored ? model.id : (model.display_name || model.id) color: member.colorName ? utils.nameColor( model.ignored ? model.id : (model.display_name || model.id.substring(1)) ) : theme.chat.roomPane.listView.member.name Behavior on color { HColorAnimation {} } } TitleRightInfoLabel { id: lastActiveAt tile: member visible: ! model.ignored && presenceTimer.running hideUnderWidth: 130 } } SubtitleLabel { tile: member textFormat: SubtitleLabel.PlainText color: theme.chat.roomPane.listView.member.subtitle text: model.ignored ? qsTr("Ignored") : (model.status_msg.trim() || model.id) } HoverHandler { id: nameHover } HToolTip { visible: nameHover.hovered text: model.id + ( ! model.ignored && model.status_msg.trim() ? " - " + model.status_msg.trim() : "" ) } Timer { id: presenceTimer repeat: true running: ! model.ignored && ! model.currently_active && model.last_active_at > new Date(1) interval: new Date() - model.last_active_at < 60000 ? 10000 : 60000 triggeredOnStart: true onTriggered: lastActiveAt.text = Qt.binding(() => utils.formatRelativeTime(new Date() - model.last_active_at) ) } } } contextMenu: HMenu { HMenuItem { icon.name: "copy-user-id" text: qsTr("Copy user ID") onTriggered: Clipboard.text = model.id } HMenuItemPopupSpawner { icon.name: model.ignored ? "stop-ignore-user" : "ignore-user" icon.color: model.ignored ? theme.colors.positiveBackground : theme.colors.negativeBackground text: model.ignored ? qsTr("Stop ignoring") : qsTr("Ignore") popup: "Popups/IgnoreUserPopup.qml" properties: ({ userId: chat.userId, targetUserId: model.id, targetDisplayName: model.display_name, ignore: ! model.ignored }) } HMenuItemPopupSpawner { property bool permissionToKick: false icon.name: "room-kick" icon.color: theme.colors.negativeBackground text: model.invited ? qsTr("Disinvite") : qsTr("Kick") enabled: chat.userInfo.presence !== "offline" && permissionToKick popup: "Popups/RemoveMemberPopup.qml" properties: ({ userId: chat.userId, roomId: chat.roomId, targetUserId: model.id, targetDisplayName: model.display_name, operation: model.invited ? "disinvite" : "kick", }) Component.onCompleted: py.callClientCoro( chat.userId, "can_kick", [chat.roomId, model.id], can => { permissionToKick = can }, ) } HMenuItemPopupSpawner { property bool permissionToBan: false icon.name: "room-ban" icon.color: theme.colors.negativeBackground text: qsTr("Ban") enabled: chat.userInfo.presence !== "offline" && permissionToBan popup: "Popups/RemoveMemberPopup.qml" properties: ({ userId: chat.userId, roomId: chat.roomId, targetUserId: model.id, targetDisplayName: model.display_name, operation: "ban", }) Component.onCompleted: py.callClientCoro( chat.userId, "can_ban", [chat.roomId, model.id], can => { permissionToBan = can }, ) } } Component.onCompleted: if (model.presence === "offline" && model.last_active_at < new Date(1)) getPresenceFutureId = py.callClientCoro( chat.userId, "get_offline_presence", [model.id], () => { getPresenceFutureId = "" } ) Component.onDestruction: if (getPresenceFutureId) py.cancelCoro(getPresenceFutureId) Behavior on contentOpacity { HNumberAnimation {} } Behavior on spacing { HNumberAnimation {} } Binding on spacing { value: (roomPane.minimumSize - avatar.width) / 2 when: avatar && roomPane.width < avatar.width + theme.spacing * 2 } DelegateTransitionFixer {} } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/MemberDeviceDelegate.qml000066400000000000000000000031701407747233600265520ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../../Base" import "../../../../Base/Buttons" import "../../../../Base/HTile" HTile { id: deviceTile property string userId property string deviceOwner property string deviceOwnerDisplayName property HStackView stackView signal trustSet(bool trust) backgroundColor: "transparent" rightPadding: theme.spacing / 2 compact: false contentItem: ContentRow { tile: deviceTile spacing: 0 HColumnLayout { HRowLayout { spacing: theme.spacing TitleLabel { text: model.display_name || qsTr("Unnamed") } } SubtitleLabel { tile: deviceTile font.family: theme.fontFamily.mono text: model.id } } HIcon { svgName: "device-action-menu" Layout.fillHeight: true } } onClicked: { const item = stackView.push( "DeviceVerification.qml", { deviceOwner: deviceTile.deviceOwner, deviceOwnerDisplayName: deviceTile.deviceOwnerDisplayName, deviceId: model.id, deviceName: model.display_name, ed25519Key: model.ed25519_key, stackView: deviceTile.stackView }, ) item.trustSet.connect(deviceTile.trustSet) } DelegateTransitionFixer {} } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/MemberDeviceSection.qml000066400000000000000000000021321407747233600264410ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../../Base" HRowLayout { spacing: theme.spacing / 2 HIcon { svgName: "device-" + section colorize: section === "verified" ? theme.colors.positiveText : section === "blacklisted" ? theme.colors.errorText : theme.colors.warningText Layout.preferredHeight: dimension Layout.leftMargin: theme.spacing / 2 } HLabel { elide: HLabel.ElideRight verticalAlignment: Qt.AlignVCenter text: section === "unset" ? qsTr("Unverified sessions") : section === "verified" ? qsTr("Verified sessions") : section === "ignored" ? qsTr("Ignored sessions") : qsTr("Blacklisted sessions") Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: theme.spacing Layout.bottomMargin: theme.spacing Layout.rightMargin: theme.spacing / 2 } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/MemberProfile.qml000066400000000000000000000224071407747233600253240ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../.." import "../../../../Base" import "../../../../Base/Buttons" HListView { id: root property string userId property string roomId property int ownPowerLevel property int canSetPowerLevels property QtObject member // RoomMember model item property HStackView stackView property Item focusOnExit property bool powerLevelFieldFocused: false property string setPowerFutureId: "" property string getPresenceFutureId: "" function loadDevices() { py.callClientCoro(userId, "member_devices", [member.id], devices => { root.model.clear() for (const device of devices) root.model.append(device) }) } function exit() { stackView.pop() focusOnExit.forceActiveFocus() } clip: true bottomMargin: theme.spacing model: ListModel {} delegate: MemberDeviceDelegate { width: root.width userId: root.userId deviceOwner: member.id deviceOwnerDisplayName: member.display_name stackView: root.stackView onTrustSet: trust => root.loadDevices() } section.property: "type" section.delegate: MemberDeviceSection { width: root.width } header: HColumnLayout { x: theme.spacing width: root.width - x * 2 spacing: theme.spacing * 1.5 Component.onCompleted: powerLevel.enabled ? powerLevel.item.forceActiveFocus() : root.currentIndex = 0 HUserAvatar { clientUserId: chat.userId userId: member.id displayName: member.display_name mxc: member.avatar_url Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true Layout.maximumWidth: 256 * theme.uiScale Layout.preferredHeight: width Layout.topMargin: theme.spacing HButton { x: -theme.spacing * 0.75 y: x z: 999 circle: true icon.name: "close-view" iconItem.small: true onClicked: root.exit() } } HColumnLayout { // no spacing between these children HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter color: utils.nameColor(member.display_name || member.id) text: member.display_name.trim() || member.id Layout.fillWidth: true } HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter color: theme.colors.dimText text: member.id visible: member.display_name.trim() !== "" Layout.fillWidth: true } } HColumnLayout { HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter text: member.presence === "online" ? qsTr("Online") : member.presence === "unavailable" ? qsTr("Unavailable") : member.presence === "invisible" ? qsTr("Invisible") : qsTr("Offline / Unknown") color: member.presence === "online" ? theme.colors.positiveText : member.presence === "unavailable" ? theme.colors.warningText : theme.colors.halfDimText Layout.fillWidth: true } HLabel { wrapMode: HLabel.Wrap horizontalAlignment: Qt.AlignHCenter visible: ! member.currently_active && text !== "" color: theme.colors.dimText Timer { repeat: true triggeredOnStart: true running: ! member.currently_active && member.last_active_at > new Date(1) interval: new Date() - member.last_active_at < 60000 ? 1000 : 60000 onTriggered: parent.text = Qt.binding(() => qsTr("Last seen %1 ago").arg(utils.formatRelativeTime( new Date() - member.last_active_at, false, )) ) } Layout.fillWidth: true } } HLabel { wrapMode: HLabel.Wrap text: member.status_msg.trim() visible: text !== "" horizontalAlignment: lineCount > 1 ? Qt.AlignLeft : Qt.AlignHCenter color: theme.colors.halfDimText Layout.fillWidth: true } HLabeledItem { id: powerLevel elementsOpacity: item.field.opacity enabled: root.canSetPowerLevels && ( root.ownPowerLevel > member.power_level || (root.ownPowerLevel === 100 && member.id === userId) ) label.text: qsTr("Power level:") label.horizontalAlignment: Qt.AlignHCenter errorLabel.horizontalAlignment: Qt.AlignHCenter errorLabel.text: ! item.changed ? "" : item.fieldOverMaximum && root.userId === member.id ? qsTr("Can't set your own level higher") : item.fieldOverMaximum ? qsTr("Can't set level higher than your own") : item.uncappedLevel === root.ownPowerLevel ? qsTr("You won't be able to demote this user") : item.uncappedLevel < root.ownPowerLevel && root.userId === member.id ? qsTr("You won't be able to regain power") : "" errorLabel.color: item.uncappedLevel === root.ownPowerLevel || ( item.uncappedLevel < root.ownPowerLevel && root.userId === member.id ) ? theme.colors.warningText : theme.colors.errorText Layout.preferredWidth: parent.width PowerLevelControl { width: parent.width defaultLevel: member.power_level maximumLevel: root.ownPowerLevel rowSpacing: powerLevel.spacing onAccepted: applyButton.clicked() onFieldFocusedChanged: root.powerLevelFieldFocused = fieldFocused } } HRowLayout { visible: scale > 0 id: buttonsLayout scale: powerLevel.item.changed ? 1 : 0 Layout.preferredWidth: parent.width Layout.preferredHeight: implicitHeight * scale Layout.topMargin: -theme.spacing Behavior on scale { HNumberAnimation {} } HSpacer {} ApplyButton { id: applyButton enabled: ! powerLevel.item.fieldOverMaximum loading: setPowerFutureId !== "" text: "" onClicked: { setPowerFutureId = py.callClientCoro( userId, "room_set_member_power", [roomId, member.id, powerLevel.item.level], () => { setPowerFutureId = "" } ) } Layout.fillWidth: false Layout.alignment: Qt.AlignCenter } CancelButton { text: "" onClicked: { py.cancelCoro(setPowerFutureId) setPowerFutureId = "" powerLevel.item.reset() } Layout.fillWidth: false Layout.alignment: Qt.AlignCenter } HSpacer {} } Item { // This item is just to have some spacing at the bottom of header visible: root.count > 0 Layout.fillWidth: true } } Component.onCompleted: { loadDevices() if (member.presence === "offline" && member.last_active_at < new Date(1)) { getPresenceFutureId = py.callClientCoro( userId, "get_offline_presence", [member.id], () => { getPresenceFutureId = "" } ) } } Component.onDestruction: { if (setPowerFutureId) py.cancelCoro(setPowerFutureId) if (getPresenceFutureId) py.cancelCoro(getPresenceFutureId) } Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: if (! root.powerLevelFieldFocused && currentItem) { currentItem.leftClicked() currentItem.clicked() } Keys.onEscapePressed: root.exit() Connections { target: py.eventHandlers onDeviceUpdateSignal: forAccount => { if (forAccount === root.userId) root.loadDevices() } } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/MemberView/MemberView.qml000066400000000000000000000150521407747233600246340ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../.." import "../../../../Base" HColumnLayout { readonly property alias keybindFocusItem: filterField readonly property var modelSyncId: [chat.userRoomId[0], chat.userRoomId[1], "filtered_members"] readonly property alias viewDepth: stackView.depth readonly property alias filterField: filterField Connections { target: pageLoader onAboutToRecycle: { stackView.pop(stackView.initialItem) filterField.reset() } } HStackView { id: stackView background: Rectangle { color: theme.chat.roomPane.listView.background } initialItem: HListView { id: memberList clip: true delegate: MemberDelegate { id: member width: memberList.width colorName: hovered || memberList.currentIndex === model.index onLeftClicked: stackView.push( "MemberProfile.qml", { userId: chat.userId, roomId: chat.roomId, ownPowerLevel: Qt.binding(() => chat.roomInfo.own_power_level), canSetPowerLevels: Qt.binding(() => chat.userInfo.presence !== "offline" && chat.roomInfo.can_set_power_levels ), member: model, stackView: stackView, focusOnExit: filterField, }, ) } Keys.onTabPressed: memberList.incrementCurrentIndex() Keys.onBacktabPressed: memberList.decrementCurrentIndex() Keys.onEnterPressed: Keys.onReturnPressed(event) Keys.onReturnPressed: { currentItem.leftClicked() currentItem.clicked() } Keys.onMenuPressed: if (currentItem) currentItem.doRightClick(false) Timer { id: updateModelTimer interval: pageLoader.appearAnimation.duration running: true onTriggered: memberList.model = ModelStore.get(modelSyncId) } Connections { target: pageLoader onRecycled: { memberList.model = null updateModelTimer.restart() } } } Layout.fillWidth: true Layout.fillHeight: true } Rectangle { color: theme.chat.roomPane.bottomBar.background Layout.fillWidth: true Layout.preferredHeight: childrenRect.height HRowLayout { width: parent.width layoutDirection: expandButton.visible ? Qt.RightToLeft : Qt.LeftToRight HTextField { id: filterField backgroundColor: theme.chat.roomPane.bottomBar.filterMembers.background bordered: false opacity: width >= 16 * theme.uiScale ? 1 : 0 Layout.fillWidth: true // FIXME: fails to display sometimes for some reason if // declared normally Component.onCompleted: placeholderText = qsTr("Filter members") onTextChanged: { stackView.pop(stackView.initialItem) if (! stackView.currentItem.model) return py.callCoro("set_string_filter", [modelSyncId, text]) } onActiveFocusChanged: { if ( ! activeFocus && stackView.depth === 1 && stackView.currentItem.currentIndex === 0 ) { stackView.currentItem.currentIndex = -1 } } Keys.forwardTo: [stackView.currentItem] Keys.priority: Keys.AfterItem Keys.onEscapePressed: { if (stackView.depth === 1) stackView.currentItem.currentIndex = -1 roomPane.toggleFocus() if (window.settings.RoomList.escape_clears_filter) text = "" } Behavior on opacity { HNumberAnimation {} } } HColumnLayout { HButton { id: inviteButton icon.name: "room-send-invite" enabled: chat.userInfo.presence !== "offline" && chat.roomInfo.can_invite toolTip.text: enabled ? qsTr("Invite users to this room") : qsTr("No permission to invite users to this room") onClicked: window.makePopup( "Popups/InviteToRoomPopup.qml", { userId: chat.userId, roomId: chat.roomId, roomName: chat.roomInfo.display_name, invitingAllowed: Qt.binding(() => inviteButton.enabled), }, ) Layout.preferredHeight: filterField.implicitHeight HShortcut { sequences: window.settings.Keys.Chat.invite onActivated: if (inviteButton.enabled) inviteButton.clicked() } } HButton { id: expandButton icon.name: "room-pane-expand-search" backgroundColor: inviteButton.backgroundColor toolTip.text: qsTr("Expand search") visible: Layout.preferredHeight > 0 // Will trigger roomPane.requireDefaultSize onClicked: filterField.forceActiveFocus() Layout.preferredHeight: filterField.width < 32 * theme.uiScale ? filterField.implicitHeight : 0 Behavior on Layout.preferredHeight { HNumberAnimation {} } } } } } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/RoomPane.qml000066400000000000000000000071371407747233600222550ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../../Base" import "../../.." import "MemberView" MultiviewPane { id: roomPane readonly property QtObject accountModel: ModelStore.get("accounts").find(chat.roomInfo.for_account) // Using a property for this item because MultiviewPane sends its children // inside its SwipeView by default, while GTH must always cover the pane readonly property GlobalTapHandlers globalTapHandlers: GlobalTapHandlers { parent: roomPane.contentItem pageLoader: pageLoader } function toggleFocus() { if (roomPane.activeFocus) { if (roomPane.collapse) roomPane.close() pageLoader.takeFocus() return } roomPane.forceCollapse = false roomPane.open() swipeView.currentItem.keybindFocusItem.forceActiveFocus() } saveName: "roomPane" edge: Qt.RightEdge defaultSize: (buttonRepeater.count - (roomPane.collapse ? 0 : 1)) * buttonWidth buttonWidth: buttonRepeater.count >= 1 ? buttonRepeater.itemAt(1).implicitWidth : 0 requireDefaultSize: swipeView.currentIndex !== 0 || swipeView.currentItem.viewDepth > 1 || swipeView.currentItem.filterField.activeFocus buttonsBackgroundColor: theme.chat.roomPane.topBar.background background: Rectangle { color: theme.chat.roomPane.background } buttonRepeater.model: [ "back", "members", "files", "notifications", "history", "settings" ] buttonRepeater.delegate: HButton { visible: width > 0 width: modelData === "back" && ! roomPane.collapse ? 0 : implicitWidth height: theme.baseElementsHeight backgroundColor: "transparent" icon.name: modelData === "back" ? "go-back-to-chat-from-room-pane" : "room-view-" + modelData toolTip.text: modelData === "back" ? qsTr("Back to chat") : qsTr(modelData.charAt(0).toUpperCase() + modelData.slice(1)) autoExclusive: true checked: swipeView.currentIndex === 0 && index === 1 || swipeView.currentIndex === 1 && index === 5 enabled: ["back", "members", "settings"].includes(modelData) onClicked: modelData === "back" ? roomPane.toggleFocus() : (modelData === "members" && swipeView.currentIndex === 0) || (modelData === "settings" && swipeView.currentIndex === 1) ? roomPane.forceCollapse = true : modelData === "members" ? swipeView.currentIndex = 0 : swipeView.currentIndex = 1 Behavior on width { enabled: modelData === "back" HNumberAnimation {} } } Connections { target: swipeView onCurrentItemChanged: roomPane.swipeView.currentItem.keybindFocusItem.forceActiveFocus() } Connections { target: pageLoader onAboutToRecycle: roomPane.swipeView.currentIndex = 0 } MemberView { HDrawerSwipeHandler { drawer: roomPane onCloseRequest: roomPane.toggleFocus() } } SettingsView { enabled: accountModel.presence !== "offline" } HShortcut { sequences: window.settings.Keys.Chat.focus_room_pane onActivated: roomPane.toggleFocus() } HShortcut { sequences: window.settings.Keys.Chat.hide_room_pane onActivated: roomPane.forceCollapse = ! roomPane.forceCollapse } } mirage-0.7.2/src/gui/Pages/Chat/RoomPane/SettingsView.qml000066400000000000000000000135441407747233600231670ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../../../Base" import "../../../Base/Buttons" HFlickableColumnPage { id: settingsView property string saveFutureId: "" readonly property bool anyChange: nameField.item.changed || topicArea.item.area.changed || encryptCheckBox.changed || requireInviteCheckbox.changed || forbidGuestsCheckBox.changed readonly property Item keybindFocusItem: nameField.item.enabled ? nameField.item : copyIdButton function save() { if (saveFutureId) py.cancelCoro(saveFutureId) const args = [ chat.roomId, nameField.item.changed ? nameField.item.text : undefined, topicArea.item.area.changed ? topicArea.item.area.text : undefined, encryptCheckBox.changed ? true : undefined, requireInviteCheckbox.changed ? requireInviteCheckbox.checked : undefined, forbidGuestsCheckBox.changed ? forbidGuestsCheckBox.checked : undefined, ] function onDone() { saveFutureId = "" } saveFutureId = py.callClientCoro( chat.userId, "room_set", args, onDone, onDone, ) } function cancel() { if (saveFutureId) { py.cancelCoro(saveFutureId) saveFutureId = "" } nameField.item.reset() topicArea.item.area.reset() encryptCheckBox.reset() requireInviteCheckbox.reset() forbidGuestsCheckBox.reset() } enableFlickShortcuts: ! chat.composerHasFocus background: Rectangle { color: theme.chat.roomPane.roomSettings.background } footer: AutoDirectionLayout { ApplyButton { id: applyButton enabled: anyChange loading: saveFutureId !== "" disableWhileLoading: false onClicked: save() } CancelButton { enabled: anyChange || saveFutureId !== "" onClicked: cancel() } } onKeyboardAccept: if (applyButton.enabled) applyButton.clicked() onKeyboardCancel: cancel() Connections { target: pageLoader onAboutToRecycle: cancel() } HRoomAvatar { id: avatar clientUserId: chat.userId roomId: chat.roomId displayName: nameField.item.text || chat.roomInfo.display_name mxc: chat.roomInfo.avatar_url // enabled: chat.roomInfo.can_set_avatar # put this in "change avatar" Layout.fillWidth: true Layout.preferredHeight: width Layout.maximumWidth: 256 * theme.uiScale Layout.alignment: Qt.AlignCenter } HLabeledItem { label.text: qsTr("Room ID:") Layout.fillWidth: true HRowLayout { width: parent.width HTextArea { id: idAreaItem wrapMode: HLabel.WrapAnywhere readOnly: true radius: 0 text: chat.roomId Layout.fillWidth: true Layout.fillHeight: true } FieldCopyButton { id: copyIdButton textControl: idAreaItem } } } HLabeledItem { id: nameField label.text: qsTr("Name:") Layout.fillWidth: true HTextField { width: parent.width maximumLength: 255 defaultText: chat.roomInfo.given_name enabled: chat.roomInfo.can_set_name } } HLabeledItem { id: topicArea elementsOpacity: topicAreaIn.opacity label.text: qsTr("Topic:") Layout.fillWidth: true HScrollView { readonly property alias area: topicAreaIn clip: true width: parent.width height: Math.min(topicAreaIn.implicitHeight, settingsView.height / 2) HTextArea { id: topicAreaIn placeholderText: qsTr("This room is about...") defaultText: chat.roomInfo.plain_topic enabled: chat.roomInfo.can_set_topic focusItemOnTab: encryptCheckBox.checked ? requireInviteCheckbox : encryptCheckBox } } } HCheckBox { id: encryptCheckBox text: qsTr("Encrypt messages") subtitle.text: qsTr("Only users you trust can decrypt the conversation") + `
` + ( chat.roomInfo.encrypted ? qsTr("Cannot be disabled") : qsTr("Cannot be disabled later!") ) + "" subtitle.textFormat: Text.StyledText defaultChecked: chat.roomInfo.encrypted enabled: chat.roomInfo.can_set_encryption && ! chat.roomInfo.encrypted Layout.fillWidth: true } HCheckBox { id: requireInviteCheckbox text: qsTr("Require being invited") subtitle.text: qsTr("Users need an invite from a member to join") defaultChecked: chat.roomInfo.invite_required enabled: chat.roomInfo.can_set_join_rules Layout.fillWidth: true } HCheckBox { id: forbidGuestsCheckBox text: qsTr("Forbid guests") subtitle.text: qsTr("Users without accounts can't get invited or join") defaultChecked: ! chat.roomInfo.guests_allowed enabled: chat.roomInfo.can_set_guest_access Layout.fillWidth: true } // HCheckBox { TODO // text: qsTr("Make this room visible in the public room directory") // checked: chat.roomInfo.published_in_directory // Layout.fillWidth: true // } HSpacer {} } mirage-0.7.2/src/gui/Pages/Chat/Timeline/000077500000000000000000000000001407747233600200405ustar00rootroot00000000000000mirage-0.7.2/src/gui/Pages/Chat/Timeline/DayBreak.qml000066400000000000000000000005441407747233600222400ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../../Base" HNoticePage { text: model.date.toLocaleDateString() color: theme.chat.daybreak.text backgroundColor: theme.chat.daybreak.background radius: theme.chat.daybreak.radius } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventAudio.qml000066400000000000000000000005561407747233600226240ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtAV 1.7 import "../../.." import "../../../Base" import "../../../Base/MediaPlayer" AudioPlayer { readonly property bool hovered: hover.hovered HoverHandler { id: hover } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventContent.qml000066400000000000000000000307611407747233600231760ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../Base" import "../../.." HRowLayout { id: eventContent readonly property var mentions: JSON.parse(model.mentions) readonly property string mentionsCSS: { const lines = [] for (const [name, link] of mentions) { if (! link.match(/^https?:\/\/matrix.to\/#\/@.+/)) continue lines.push( `.mention[data-mention='${utils.escapeHtml(name)}'] ` + `{ color: ${utils.nameColor(name)} }` ) } return "" } readonly property string senderText: hideNameLine ? "" : ( `<${compact ? "span" : "div"} class='sender'>` + utils.coloredNameHtml(model.sender_name, model.sender_id) + (compact ? ": " : "") + (compact ? "" : "") ) property string contentText: utils.processedEventText(model) readonly property string timeText: utils.formatTime(model.date, false) readonly property string stateText: `` + ` ⧗` : // U+29D7 model.read_by_count ? `color="${theme.chat.message.readCounter}"> ⦿ ` + model.read_by_count : // U+29BF ">" ) + "" readonly property bool pureMedia: ! contentText && linksRepeater.count readonly property bool hoveredSelectable: contentHover.hovered readonly property string hoveredLink: linksRepeater.lastHovered && linksRepeater.lastHovered.hovered ? linksRepeater.lastHovered.mediaUrl : contentLabel.hoveredLink readonly property alias contentLabel: contentLabel readonly property int xOffset: onRight ? Math.min( contentColumn.width - contentLabel.paintedWidth - contentLabel.leftPadding - contentLabel.rightPadding, contentColumn.width - linksRepeater.widestChild - ( pureMedia ? 0 : contentLabel.leftPadding + contentLabel.rightPadding ), ) : 0 readonly property int maxMessageWidth: contentText.includes("
") || contentText.includes("") ?
        -1 :
        window.settings.Chat.max_messages_line_length < 0 ?
        -1 :
        Math.ceil(
            mainUI.fontMetrics.averageCharacterWidth *
            window.settings.Chat.max_messages_line_length
        )

    readonly property alias selectedText: contentLabel.selectedPlainText

    spacing: theme.chat.message.horizontalSpacing
    layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight

    Item {
        id: avatarWrapper
        visible: ! onRight
        opacity: combine ? 0 : 1

        Layout.alignment: Qt.AlignTop
        Layout.preferredHeight: combine ? 1 : Layout.preferredWidth
        Layout.preferredWidth:
            compact ?
            theme.chat.message.collapsedAvatarSize :
            theme.chat.message.avatarSize

        HUserAvatar {
            id: avatar
            clientUserId: chat.userId
            userId: model.sender_id
            displayName: model.sender_name
            mxc: model.sender_avatar
            width: parent.width
            height: combine ? 1 : parent.Layout.preferredWidth
            radius: theme.chat.message.avatarRadius

        }
    }

    HColumnLayout {
        id: contentColumn

        Layout.fillWidth: true
        Layout.alignment: Qt.AlignVCenter

        HSelectableLabel {
            id: contentLabel
            visible: ! pureMedia
            enableLinkActivation: ! eventList.selectedCount

            selectByMouse:
                eventList.selectedCount <= 1 &&
                eventDelegate.checked &&
                textSelectionBlocker.point.scenePosition === Qt.point(0, 0)

            topPadding: theme.chat.message.verticalSpacing
            bottomPadding: topPadding
            leftPadding: eventContent.spacing
            rightPadding: leftPadding

            color: model.event_type === "RoomMessageNotice" ?
                   theme.chat.message.noticeBody :
                   theme.chat.message.body

            font.italic: model.event_type === "RoomMessageEmote"
            wrapMode: TextEdit.Wrap
            textFormat: Text.RichText
            text:
                // CSS
                theme.chat.message.styleInclude + mentionsCSS +

                // Sender name & message body
                (
                    compact && contentText.match(/^\s*<(p|h[1-6])>/) ?
                    contentText.replace(
                        /(^\s*<(p|h[1-6])>)/, "$1" + senderText,
                    ) :
                    senderText + contentText
                ) +

                // Time
                // For some reason, if there's only one space,
                // times will be on their own lines most of the time.
                "  " +
                `` +
                timeText +
                "" +

                stateText

            transform: Translate { x: xOffset }

            Layout.maximumWidth: eventContent.maxMessageWidth
            Layout.fillWidth: true

            onSelectedTextChanged: if (selectedPlainText) {
                eventList.delegateWithSelectedText = model.id
                eventList.selectedText             = selectedPlainText
            } else if (eventList.delegateWithSelectedText === model.id) {
                eventList.delegateWithSelectedText = ""
                eventList.selectedText             = ""
            }

            Connections {
                target: eventList
                onCheckedChanged: contentLabel.deselect()
                onDelegateWithSelectedTextChanged: {
                    if (eventList.delegateWithSelectedText !== model.id)
                        contentLabel.deselect()
                }
            }

            HoverHandler { id: contentHover }

            PointHandler {
                id: mousePointHandler

                property bool checkedNow: false

                acceptedButtons: Qt.LeftButton
                acceptedModifiers: Qt.NoModifier
                acceptedPointerTypes:
                    PointerDevice.GenericPointer | PointerDevice.Eraser

                onActiveChanged: {
                    if (active &&
                            ! eventDelegate.checked &&
                            (! parent.hoveredLink ||
                            ! parent.enableLinkActivation)) {

                        eventList.check(model.index)
                        checkedNow = true
                    }

                    if (! active && eventDelegate.checked) {
                        checkedNow ?
                        checkedNow = false :
                        eventList.uncheck(model.index)
                    }
                }
            }

            PointHandler {
                id: mouseShiftPointHandler
                acceptedButtons: Qt.LeftButton
                acceptedModifiers: Qt.ShiftModifier
                acceptedPointerTypes:
                    PointerDevice.GenericPointer | PointerDevice.Eraser

                onActiveChanged: {
                    if (active &&
                            ! eventDelegate.checked &&
                            (! parent.hoveredLink ||
                            ! parent.enableLinkActivation)) {

                        eventList.checkFromLastToHere(model.index)
                    }
                }
            }

            TapHandler {
                id: touchTapHandler
                acceptedButtons: Qt.LeftButton
                acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
                onTapped:
                    if (! parent.hoveredLink || ! parent.enableLinkActivation)
                        eventDelegate.toggleChecked()
            }

            TapHandler {
                id: textSelectionBlocker
                acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
            }

            HToolTip {
                readonly property bool keyboardShow:
                    eventList.showFocusedSeenTooltips &&
                    eventList.currentIndex === model.index &&
                    model.read_by_count > 0

                instant: keyboardShow
                visible:
                    eventContent.hoveredLink === "#state-text" || keyboardShow

                label.textFormat: HLabel.StyledText
                text: {
                    if (! visible) return ""

                    if (model.is_local_echo) return qsTr("Sending message...")

                    const members =
                        ModelStore.get(chat.userId, chat.roomId, "members")

                    const readBy = Object.entries(
                        JSON.parse(model.last_read_by)
                    ).sort((a, b) => a[1] - b[1])  // sort by values (dates)

                    const lines  = []

                    for (const [userId, epoch] of readBy) {
                        const member = members.find(userId)

                        const by  = utils.coloredNameHtml(
                            member ? member.display_name: userId, userId,
                        )
                        const at = utils.formatRelativeTime(
                            new Date(epoch) - model.date,
                        )
                        lines.push(qsTr("Seen by %1 %2 after").arg(by).arg(at))
                    }

                    return lines.join("
") } } Rectangle { id: contentBackground width: Math.max( parent.paintedWidth + parent.leftPadding + parent.rightPadding, linksRepeater.summedWidth + (pureMedia ? 0 : parent.leftPadding + parent.rightPadding), ) height: contentColumn.height radius: theme.chat.message.radius z: -100 color: eventDelegate.checked && ! contentLabel.selectedPlainText && ! mousePointHandler.active && ! mouseShiftPointHandler.active ? theme.chat.message.checkedBackground : isOwn? theme.chat.message.ownBackground : theme.chat.message.background Behavior on color { HColorAnimation {} } Rectangle { visible: model.event_type === "RoomMessageNotice" // y: parent.height / 2 - height / 2 width: theme.chat.message.noticeLineWidth height: parent.height radius: parent.radius color: utils.nameColor( model.sender_name || model.sender_id.substring(1), ) } } } HRepeater { id: linksRepeater property EventMediaLoader lastHovered: null model: { const links = JSON.parse(eventDelegate.currentModel.links) if (eventDelegate.currentModel.media_url) links.push(eventDelegate.currentModel.media_url) return links } EventMediaLoader { singleMediaInfo: eventDelegate.currentModel mediaUrl: modelData showSender: pureMedia ? senderText : "" showDate: pureMedia ? timeText : "" showLocalEcho: pureMedia && ( singleMediaInfo.is_local_echo || singleMediaInfo.read_by_count ) ? stateText : "" transform: Translate { x: xOffset } onHoveredChanged: if (hovered) linksRepeater.lastHovered = this Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding Layout.leftMargin: pureMedia ? 0 : eventContent.spacing Layout.rightMargin: pureMedia ? 0 : eventContent.spacing Layout.preferredWidth: item ? item.width : -1 Layout.preferredHeight: item ? item.height : -1 } } } HSpacer {} } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventContextMenu.qml000066400000000000000000000114711407747233600240320ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import "../../.." import "../../../Base" import "../../../PythonBridge" HMenu { id: menu property HListView eventList property int eventIndex: -1 property Item eventDelegate: null // TODO: Qt 5.13: just use itemAtIndex() property string hoveredLink: "" readonly property QtObject event: eventList.model.get(eventIndex) || null readonly property bool isEncryptedMedia: event && Object.keys(JSON.parse(event.media_crypt_dict)).length > 0 readonly property var mediaType: // Utils.Media. or null event && event.media_http_url ? eventList.getMediaType(event) : hoveredLink ? utils.getLinkType(hoveredLink) : null function spawn(eventIndex, eventDelegate, hoveredLink="") { menu.eventIndex = eventIndex menu.eventDelegate = eventDelegate menu.hoveredLink = hoveredLink menu.popup() } onClosed: { hoveredLink = "" eventIndex = -1 } HMenuItem { icon.name: "toggle-select-message" text: event && event.id in eventList.checked ? qsTr("Deselect") : qsTr("Select") onTriggered: eventList.toggleCheck(eventIndex) } HMenuItem { visible: eventList.selectedCount >= 2 icon.name: "deselect-all-messages" text: qsTr("Deselect all") onTriggered: eventList.checked = {} } HMenuItem { visible: eventIndex > 0 icon.name: "select-until-here" text: qsTr("Select until here") onTriggered: eventList.checkFromLastToHere(eventIndex) } HMenuItem { icon.name: "open-externally" text: qsTr("Open externally") visible: Boolean(event && event.media_url) onTriggered: eventList.openMediaExternally(event) } HMenuItem { icon.name: "copy-local-path" text: qsTr("Copy local path") visible: Boolean(event && event.media_local_path) onTriggered: Clipboard.text = event.media_local_path.replace(/^file:\/\//, "") } HMenuItem { id: copyMedia icon.name: "copy-link" visible: menu.mediaType !== null && ! menu.isEncryptedMedia text: ! visible ? "" : menu.mediaType === Utils.Media.File ? qsTr("Copy file address") : menu.mediaType === Utils.Media.Image ? qsTr("Copy image address") : menu.mediaType === Utils.Media.Video ? qsTr("Copy video address") : menu.mediaType === Utils.Media.Audio ? qsTr("Copy audio address") : qsTr("Copy link address") onTriggered: Clipboard.text = event.media_http_url || menu.hoveredLink } HMenuItem { icon.name: "copy-text" text: eventList.selectedCount ? qsTr("Copy selection") : event && event.media_url ? qsTr("Copy filename") : qsTr("Copy text") onTriggered: { if (! eventList.selectedCount){ Clipboard.text = JSON.parse(event.source).body || utils.stripHtmlTags(utils.processedEventText(event)) return } eventList.copySelectedDelegates() } } HMenuItem { icon.name: "reply-to" text: qsTr("Reply") onTriggered: { chat.replyToEventId = event.id chat.replyToUserId = event.sender_id chat.replyToDisplayName = event.sender_name } } HMenuItemPopupSpawner { readonly property var events: eventList.selectedCount ? eventList.redactableCheckedEvents : event && eventList.canRedact(event) ? [event] : [] icon.name: "remove-message" text: qsTr("Remove") enabled: properties.eventSenderAndIds.length popup: "Popups/RedactPopup.qml" properties: ({ preferUserId: chat.userId, roomId: chat.roomId, eventSenderAndIds: events.map(ev => [ev.sender_id, ev.id]), onlyOwnMessageWarning: ! chat.roomInfo.can_redact_all && events.length < eventList.selectedCount }) } HMenuItem { icon.name: "debug" text: qsTr("Debug") onTriggered: mainUI.debugConsole.toggle(eventDelegate, ".j t.dict()") } HMenuItemPopupSpawner { icon.name: "clear-messages" text: qsTr("Clear messages") popup: "Popups/ClearMessagesPopup.qml" properties: ({ userId: chat.userId, roomId: chat.roomId, preClearCallback: eventList.uncheckAll, }) } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventDelegate.qml000066400000000000000000000102511407747233600232660ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import "../../.." import "../../../Base" HColumnLayout { id: eventDelegate property string fetchProfilesFutureId: "" // Remember timeline goes from newest message at index 0 to oldest readonly property var previousModel: eventList.model.get(model.index + 1) readonly property var nextModel: eventList.model.get(model.index - 1) readonly property QtObject currentModel: model readonly property bool isFocused: model.index === eventList.currentIndex readonly property bool compact: window.settings.General.compact readonly property bool checked: model.id in eventList.checked readonly property bool isOwn: chat.userId === model.sender_id readonly property bool isRedacted: model.event_type === "RedactedEvent" readonly property bool onRight: ! eventList.ownEventsOnLeft && isOwn readonly property bool combine: eventList.canCombine(previousModel, model) readonly property bool talkBreak: eventList.canTalkBreak(previousModel, model) readonly property bool dayBreak: eventList.canDayBreak(previousModel, model) readonly property bool hideNameLine: model.event_type === "RoomMessageEmote" || ! ( model.event_type.startsWith("RoomMessage") || model.event_type.startsWith("RoomEncrypted") ) || onRight || combine readonly property int cursorShape: eventContent.hoveredLink ? Qt.PointingHandCursor : eventContent.hoveredSelectable ? Qt.IBeamCursor : Qt.ArrowCursor readonly property int separationSpacing: theme.spacing * ( dayBreak ? 4 : talkBreak ? 6 : combine && compact ? 0.25 : combine ? 0.5 : compact ? 1 : 2 ) readonly property alias eventContent: eventContent function dict() { let event = eventList.model.get(model.index) event = JSON.parse(JSON.stringify(event)) event.source = JSON.parse(event.source) return event } function openContextMenu() { eventList.contextMenu.spawn( model.index, eventDelegate, eventContent.hoveredLink, ) } function toggleChecked() { eventList.toggleCheck(model.index) } width: eventList.width - eventList.leftMargin - eventList.rightMargin // Needed because of eventList's MouseArea which steals the // HSelectableLabel's MouseArea hover events onCursorShapeChanged: eventList.cursorShape = cursorShape Component.onCompleted: if (model.fetch_profile) fetchProfilesFutureId = py.callClientCoro( chat.userId, "get_event_profiles", [chat.roomId, model.id], // The if avoids segfault if eventDelegate is already destroyed () => { if (eventDelegate) fetchProfilesFutureId = "" } ) Component.onDestruction: if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId) ListView.onRemove: eventList.uncheck(model.id) DelegateTransitionFixer {} Item { Layout.fillWidth: true visible: model.event_type !== "RoomCreateEvent" Layout.preferredHeight: separationSpacing } DayBreak { visible: dayBreak Layout.fillWidth: true Layout.minimumWidth: parent.width Layout.bottomMargin: separationSpacing } EventContent { id: eventContent Layout.fillWidth: true } TapHandler { acceptedButtons: Qt.LeftButton acceptedModifiers: Qt.NoModifier onTapped: toggleChecked() } TapHandler { acceptedButtons: Qt.LeftButton acceptedModifiers: Qt.ShiftModifier onTapped: eventList.checkFromLastToHere(model.index) } TapHandler { acceptedButtons: Qt.RightButton acceptedPointerTypes: PointerDevice.GenericPointer | PointerDevice.Pen onTapped: openContextMenu() } TapHandler { acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen onLongPressed: openContextMenu() } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventFile.qml000066400000000000000000000031771407747233600224440ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import CppUtils 0.1 import "../../.." import "../../../Base" import "../../../Base/HTile" HTile { id: file property EventMediaLoader loader width: Math.min( eventDelegate.width, eventContent.maxMessageWidth, Math.max( window.settings.Chat.Files.min_file_width * window.settings.General.zoom, implicitWidth, ), ) height: Math.max(theme.chat.message.avatarSize, implicitHeight) contentItem: ContentRow { tile: file HIcon { svgName: "download" } HColumnLayout { TitleLabel { elide: Text.ElideMiddle text: loader.singleMediaInfo.media_title || qsTr("Untitled file") } SubtitleLabel { tile: file text: CppUtils.formattedBytes( loader.singleMediaInfo.media_size, ) } } } onMiddleClicked: leftClicked() onRightClicked: eventDelegate.openContextMenu() onLeftClicked: eventList.selectedCount ? eventDelegate.toggleChecked() : loader.isMedia ? eventList.openMediaExternally(singleMediaInfo) : Qt.openUrlExternally(loader.mediaUrl) Binding on backgroundColor { value: theme.chat.message.checkedBackground when: eventDelegate.checked } Behavior on backgroundColor { HColorAnimation {} } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventImage.qml000066400000000000000000000112261407747233600226010ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../.." import "../../../Base" HMxcImage { id: image property EventMediaLoader loader readonly property real zoom: window.settings.General.zoom readonly property real maxHeight: eventList.height * window.settings.Chat.Files.max_thumbnail_height_ratio * zoom readonly property size fitSize: utils.fitSize( // Minimum display size window.settings.Chat.Files.min_thumbnail_size[0] * zoom, window.settings.Chat.Files.min_thumbnail_size[1] * zoom, // Real size ( loader.singleMediaInfo.thumbnail_width || loader.singleMediaInfo.media_width || implicitWidth || 800 ) * theme.uiScale, ( loader.singleMediaInfo.thumbnail_height || loader.singleMediaInfo.media_height || implicitHeight || 600 ) * theme.uiScale, // Maximum display size Math.min( Math.max( maxHeight, window.settings.Chat.Files.min_thumbnail_size[0] * zoom, ), pureMedia ? Infinity : eventContent.maxMessageWidth, eventDelegate.width - eventContent.spacing - avatarWrapper.width - eventContent.spacing * 2, // padding ), Math.max( maxHeight, window.settings.Chat.Files.min_thumbnail_size[1] * zoom, ), ) readonly property bool hovered: hover.hovered function openInternally() { eventList.openImageViewer( singleMediaInfo, loader.mediaUrl.startsWith("mxc://") ? "" : loader.mediaUrl, ) } function openExternally() { loader.isMedia ? eventList.openMediaExternally(singleMediaInfo) : Qt.openUrlExternally(loader.mediaUrl) } width: fitSize.width height: fitSize.height horizontalAlignment: Image.AlignLeft clientUserId: chat.userId title: thumbnail ? loader.thumbnailTitle : loader.title animated: eventList.isAnimated(loader.singleMediaInfo, loader.mediaUrl) forcePause: Object.keys(window.visiblePopups).length > 0 thumbnail: ! animated && loader.thumbnailMxc mxc: thumbnail ? (loader.thumbnailMxc || loader.mediaUrl) : (loader.mediaUrl || loader.thumbnailMxc) cryptDict: JSON.parse( thumbnail && loader.thumbnailMxc ? loader.singleMediaInfo.thumbnail_crypt_dict : loader.singleMediaInfo.media_crypt_dict ) onCachedPathChanged: eventList.thumbnailCachedPaths[loader.singleMediaInfo.id] = cachedPath TapHandler { acceptedButtons: Qt.LeftButton acceptedModifiers: Qt.NoModifier gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: { if (eventList.selectedCount) { eventDelegate.toggleChecked() return } window.settings.Chat.Files.click_opens_externally ? image.openExternally() : image.openInternally() } } TapHandler { acceptedButtons: Qt.MiddleButton acceptedModifiers: Qt.NoModifier gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: window.settings.Chat.Files.click_opens_externally ? image.openInternally() : image.openExternally() } TapHandler { acceptedModifiers: Qt.ShiftModifier gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: eventList.checkFromLastToHere(singleMediaInfo.index) } HoverHandler { id: hover } EventImageTextBubble { anchors.left: parent.left anchors.top: parent.top text: loader.showSender textFormat: Text.StyledText opacity: hover.hovered || eventDelegate.isFocused ? 0 : 1 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } } EventImageTextBubble { anchors.right: parent.right anchors.bottom: parent.bottom text: loader.showDate + loader.showLocalEcho textFormat: Text.RichText opacity: hover.hovered || eventDelegate.isFocused ? 0 : 1 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } } Rectangle { anchors.fill: parent visible: opacity > 0 color: theme.chat.message.checkedBackground opacity: eventDelegate.checked ? theme.chat.message.thumbnailCheckedOverlayOpacity : 0 Behavior on opacity { HNumberAnimation {} } } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventImageTextBubble.qml000066400000000000000000000011321407747233600245550ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../../Base" HLabel { id: bubble anchors.margins: theme.spacing / 4 topPadding: theme.spacing / 2 bottomPadding: topPadding leftPadding: theme.spacing / 1.5 rightPadding: leftPadding font.pixelSize: theme.fontSize.small background: Rectangle { color: Qt.hsla(0, 0, 0, 0.7) radius: theme.radius } Binding on visible { value: false when: ! Boolean(bubble.text) } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventList.qml000066400000000000000000000513141407747233600224740ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Window 2.12 import Clipboard 0.1 import "../../.." import "../../../Base" import "../../../PythonBridge" import "../../../ShortcutBundles" Rectangle { readonly property var modelSyncId: [chat.userRoomId[0], chat.userRoomId[1], "events"] readonly property alias eventList: eventList readonly property alias contextMenu: contextMenu color: theme.chat.eventList.background HShortcut { sequences: window.settings.Keys.Messages.unfocus_or_deselect onActivated: { eventList.selectedCount ? eventList.checked = {} : eventList.currentIndex = -1 } } HShortcut { sequences: window.settings.Keys.Messages.previous onActivated: eventList.focusPreviousMessage() } HShortcut { sequences: window.settings.Keys.Messages.next onActivated: eventList.focusNextMessage() } HShortcut { active: eventList.currentItem sequences: window.settings.Keys.Messages.select onActivated: eventList.toggleCheck(eventList.currentIndex) } HShortcut { active: eventList.currentItem sequences: window.settings.Keys.Messages.select_until_here onActivated: eventList.checkFromLastToHere(eventList.currentIndex) } HShortcut { sequences: window.settings.Keys.Messages.seen_tooltips onActivated: eventList.showFocusedSeenTooltips = ! eventList.showFocusedSeenTooltips } HShortcut { readonly property var events: eventList.selectedCount ? eventList.redactableCheckedEvents : eventList.currentItem && eventList.canRedact(eventList.currentItem.currentModel) ? [eventList.currentItem.currentModel] : eventList.currentItem ? [] : null function findLastRemovableDelegate() { for (let i = 0; i < eventList.model.count && i <= 1000; i++) { const event = eventList.model.get(i) if (eventList.canRedact(event) && mainUI.accountIds.includes(event.sender_id)) return [event] } return [] } enabled: (events && events.length > 0) || events === null sequences: window.settings.Keys.Messages.remove onActivated: window.makePopup( "Popups/RedactPopup.qml", { preferUserId: chat.userId, roomId: chat.roomId, eventSenderAndIds: (events || findLastRemovableDelegate()).map( ev => [ev.sender_id, ev.id], ), isLast: ! events, onlyOwnMessageWarning: ! chat.roomInfo.can_redact_all && events && events.length < eventList.selectedCount } ) } HShortcut { sequences: window.settings.Keys.Messages.reply onActivated: { let event = eventList.model.get(0) if (eventList.currentIndex !== -1) { event = eventList.model.get(eventList.currentIndex) } else if (eventList.selectedCount) { event = eventList.getSortedChecked.slice(-1)[0] } else { // Find most recent event that wasn't sent by us for (let i = 0; i < eventList.model.count && i <= 1000; i++) { const potentialEvent = eventList.model.get(i) if (potentialEvent.sender_id !== chat.userId) { event = potentialEvent break } } } if (! event) return if (event.id === chat.replyToEventId) { chat.clearReplyTo() return } chat.replyToEventId = event.id chat.replyToUserId = event.sender_id chat.replyToDisplayName = event.sender_name } } HShortcut { sequences: window.settings.Keys.Messages.open_links_files onActivated: { const indice = eventList.getFocusedOrSelectedOrLastMediaEvents(true) for (const i of Array.from(indice).sort().reverse()) { const event = eventList.model.get(i) if (event.media_url || event.thumbnail_url) { eventList.getMediaType(event) === Utils.Media.Image ? eventList.openImageViewer(event) : eventList.openMediaExternally(event) continue } for (const url of JSON.parse(event.links)) { utils.getLinkType(url) === Utils.Media.Image ? eventList.openImageViewer(event, url) : Qt.openUrlExternally(url) } } } } HShortcut { sequences: window.settings.Keys.Messages.open_links_files_externally onActivated: { const indice = eventList.getFocusedOrSelectedOrLastMediaEvents(true) for (const i of Array.from(indice).sort().reverse()) { const event = eventList.model.get(i) if (event.media_url) { eventList.openMediaExternally(event) continue } for (const url of JSON.parse(event.links)) Qt.openUrlExternally(url) } } } HShortcut { sequences: window.settings.Keys.Messages.copy_files_path onActivated: { const paths = [] const indice = eventList.getFocusedOrSelectedOrLastMediaEvents(false) for (const i of Array.from(indice).sort().reverse()) { const event = eventList.model.get(i) if (event.media_local_path) paths.push( event.media_local_path.replace(/^file:\/\//, ""), ) } if (paths.length > 0) Clipboard.text = paths.join("\n") } } HShortcut { active: eventList.currentItem sequences: window.settings.Keys.Messages.debug onActivated: mainUI.debugConsole.toggle( eventList.currentItem, ".j t.dict()", ) } HShortcut { sequences: window.settings.Keys.Messages.clear_all onActivated: window.makePopup( "Popups/ClearMessagesPopup.qml", { userId: window.uiState.pageProperties.userRoomId[0], roomId: window.uiState.pageProperties.userRoomId[1], preClearCallback: eventList.uncheckAll, } ) } FlickShortcuts { active: chat.composerHasFocus flickable: eventList } HListView { id: eventList property string updateMarkerFutureId: "" property string loadPastEventsFutureId: "" property bool moreToLoad: true property bool ownEventsOnLeft: window.settings.Chat.own_messages_on_left_above < 0 ? false : width > window.settings.Chat.own_messages_on_left_above * theme.uiScale property string delegateWithSelectedText: "" property string selectedText: "" property bool showFocusedSeenTooltips: false property alias cursorShape: cursorShapeArea.cursorShape readonly property bool shouldLoadPastEvents: ! chat.roomInfo.inviter_id && ! chat.roomInfo.left && moreToLoad && visibleArea.yPosition < 0.1 readonly property var thumbnailCachedPaths: ({}) // {event.id: path} readonly property var redactableCheckedEvents: getSortedChecked().filter(ev => eventList.canRedact(ev)) readonly property alias contextMenu: contextMenu function focusCenterMessage() { const previous = highlightRangeMode highlightRangeMode = HListView.NoHighlightRange currentIndex = indexAt(0, contentY + height / 2) highlightRangeMode = previous } function focusPreviousMessage() { currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ? focusCenterMessage() : incrementCurrentIndex() } function focusNextMessage() { currentIndex === -1 && visibleEnd.y < contentHeight - height / 4 ? focusCenterMessage() : eventList.currentIndex === 0 ? eventList.currentIndex = -1 : decrementCurrentIndex() } function copySelectedDelegates() { if (eventList.selectedText) { Clipboard.text = eventList.selectedText return } if (! eventList.selectedCount && eventList.currentIndex !== -1) { const model = eventList.model.get(eventList.currentIndex) const source = JSON.parse(model.source) Clipboard.text = model.media_http_url && utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ? model.media_http_url : "body" in source ? source.body : utils.stripHtmlTags(utils.processedEventText(model)) return } const contents = [] for (const model of eventList.getSortedChecked()) { const source = JSON.parse(model.source) contents.push( model.media_http_url && utils.isEmptyObject(JSON.parse(model.media_crypt_dict)) ? model.media_http_url : "body" in source ? source.body : utils.stripHtmlTags(utils.processedEventText(model)) ) } Clipboard.text = contents.join("\n\n") } function canRedact(eventModel) { return eventModel.event_type !== "RedactedEvent" && (chat.roomInfo.can_redact_all || mainUI.accountIds.includes(eventModel.sender_id)) } function canCombine(item, itemAfter) { if (! item || ! itemAfter) return false return Boolean( ! canTalkBreak(item, itemAfter) && ! canDayBreak(item, itemAfter) && item.sender_id === itemAfter.sender_id && utils.minutesBetween(item.date, itemAfter.date) <= 5 ) } function canTalkBreak(item, itemAfter) { if (! item || ! itemAfter) return false return Boolean( ! canDayBreak(item, itemAfter) && utils.minutesBetween(item.date, itemAfter.date) >= 20 ) } function canDayBreak(item, itemAfter) { if (itemAfter && itemAfter.event_type === "RoomCreateEvent") return true if (! item || ! itemAfter || ! item.date || ! itemAfter.date) return false return item.date.getDate() !== itemAfter.date.getDate() } function loadPastEvents() { loadPastEventsFutureId = py.callClientCoro( chat.userId, "load_past_events", [chat.roomId], more => { moreToLoad = more loadPastEventsFutureId = "" } ) } function getFocusedOrSelectedOrLastMediaEvents(acceptLinks=false) { if (eventList.selectedCount) return eventList.checkedIndice if (eventList.currentIndex !== -1) return [eventList.currentIndex] // Find most recent event that's a media or contains links for (let i = 0; i < eventList.model.count && i <= 1000; i++) { const ev = eventList.model.get(i) const links = JSON.parse(ev.links) if (ev.media_url || (acceptLinks && links.length)) return [i] } } function getMediaType(event) { if (event.event_type === "RoomAvatarEvent") return Utils.Media.Image const mainType = event.media_mime.split("/")[0].toLowerCase() const fileEvents = ["RoomMessageFile", "RoomEncryptedFile"] return ( mainType === "image" ? Utils.Media.Image : mainType === "video" ? Utils.Media.Video : mainType === "audio" ? Utils.Media.Audio : fileEvents.includes(event.event_type) ? Utils.Media.File : null ) } function isAnimated(event, forLink="") { const link = forLink || event.media_url return ( event.media_mime === "image/gif" || utils.urlExtension(link).toLowerCase() === "gif" ) } function getThumbnailTitle(event) { return event.media_title.replace( /\.[^\.]+$/, event.thumbnail_mime === "image/jpeg" ? ".jpg" : event.thumbnail_mime === "image/png" ? ".png" : event.thumbnail_mime === "image/gif" ? ".gif" : event.thumbnail_mime === "image/tiff" ? ".tiff" : event.thumbnail_mime === "image/svg+xml" ? ".svg" : event.thumbnail_mime === "image/webp" ? ".webp" : event.thumbnail_mime === "image/bmp" ? ".bmp" : ".thumbnail" ) || utils.urlFileName(event.media_url) } function openImageViewer(event, forLink="", callback=null) { // if forLink is empty, this must be a media event const title = event.media_title || utils.urlFileName(forLink || event.media_url) // The thumbnail/cached path will be the full GIF const fullMxc = forLink || (isAnimated(event, forLink) ? "" : event.media_url) window.makePopup( "Popups/ImageViewerPopup/ImageViewerPopup.qml", { clientUserId: chat.userId, thumbnailTitle: getThumbnailTitle(event), thumbnailMxc: event.thumbnail_url, thumbnailPath: eventList.thumbnailCachedPaths[event.id], thumbnailCryptDict: JSON.parse(event.thumbnail_crypt_dict), fullTitle: title, fullMxc: fullMxc, fullCryptDict: JSON.parse(event.media_crypt_dict), fullFileSize: event.media_size, overallSize: Qt.size( event.media_width || event.thumbnail_width || implicitWidth || // XXX 800, event.media_height || event.thumbnail_height || implicitHeight || // XXX 600, ) }, obj => { obj.openExternallyRequested.connect(() => { forLink ? Qt.openUrlExternally(forLink) : eventList.openMediaExternally(event) }) if (callback) callback(obj) }, ) } function getLocalOrDownloadMedia(event, callback) { if (event.media_local_path) { callback(event.media_local_path) return } const args = [ chat.userId, event.media_url, event.media_title, chat.roomId, event.media_size, JSON.parse(event.media_crypt_dict), ] py.callCoro("media_cache.get_media", args, callback) } function openMediaExternally(event) { eventList.getLocalOrDownloadMedia(event, Qt.openUrlExternally) } anchors.fill: parent enabled: ! window.anyPopup clip: true keyNavigationWraps: false leftMargin: theme.spacing rightMargin: theme.spacing topMargin: theme.spacing bottomMargin: theme.spacing verticalLayoutDirection: ListView.BottomToTop delegate: EventDelegate {} highlight: Rectangle { color: theme.chat.message.focusedHighlight opacity: theme.chat.message.focusedHighlightOpacity } // Since the list is BottomToTop, this is actually a header footer: Item { width: eventList.width height: (button.height + theme.spacing * 2) * opacity opacity: eventList.loadPastEventsFutureId ? 1 : 0 visible: opacity > 0 Behavior on opacity { HNumberAnimation {} } HButton { readonly property bool offline: chat.userInfo.presence === "offline" id: button width: Math.min( parent.width,implicitWidth + leftPadding + rightPadding, ) anchors.centerIn: parent loading: parent.visible && ! offline icon.name: offline ? "feature-unavailable-offline" : "" icon.color: offline ? theme.colors.negativeBackground : theme.icons.colorize text: offline ? qsTr("Cannot load history offline") : qsTr("Loading previous messages...") enableRadius: true iconItem.small: true } } Connections { target: pageLoader onRecycled: { eventList.model = null eventList.cacheBuffer = 0 updateModelTimer.restart() } } Timer { id: updateModelTimer interval: pageLoader.appearAnimation.duration / 2 running: true onTriggered: { eventList.model = ModelStore.get(modelSyncId) increaseBufferTimer.restart() } } Timer { id: increaseBufferTimer interval: 1000 running: true // Keep x scroll pages cached, to limit the amount of images having // to be reloaded from network. We delay increasing this to reduce // the lag when switching rooms and loading all the delegates. onTriggered: eventList.cacheBuffer = Screen.desktopAvailableHeight * 2 } Timer { interval: 200 running: eventList.shouldLoadPastEvents && ! eventList.loadPastEventsFutureId triggeredOnStart: true onTriggered: eventList.loadPastEvents() } Component.onDestruction: { if (loadPastEventsFutureId) py.cancelCoro(loadPastEventsFutureId) } MouseArea { id: cursorShapeArea anchors.fill: parent acceptedButtons: Qt.NoButton } EventContextMenu { id: contextMenu eventList: eventList } Connections { target: pageLoader onRecycled: eventList.moreToLoad = true } } Timer { interval: Math.max(100, window.settings.Chat.mark_read_delay * 1000) running: ! eventList.updateMarkerFutureId && ( chat.roomInfo.unreads || chat.roomInfo.highlights || chat.roomInfo.local_unreads ) && Qt.application.state === Qt.ApplicationActive && eventList.visibleEnd.y > eventList.contentHeight - 100 onTriggered: { for (let i = 0; i < eventList.model.count; i++) { const item = eventList.model.get(i) if (item.sender !== chat.userId) { eventList.updateMarkerFutureId = py.callCoro( "update_room_read_marker", [chat.roomId, item.event_id], () => { eventList.updateMarkerFutureId = "" }, () => { eventList.updateMarkerFutureId = "" }, ) return } } } } HNoticePage { text: qsTr("No messages to show yet") visible: eventList.model.count < 1 anchors.fill: parent } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventMediaLoader.qml000066400000000000000000000024701407747233600237260ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../.." import "../../../Base" HLoader { id: loader property QtObject singleMediaInfo property string mediaUrl property string showSender: "" property string showDate: "" property string showLocalEcho: "" readonly property string title: singleMediaInfo.media_title || utils.urlFileName(mediaUrl) readonly property string thumbnailTitle: eventList.getThumbnailTitle(singleMediaInfo) readonly property bool isMedia: eventList.getMediaType(singleMediaInfo) !== null readonly property int type: isMedia ? eventList.getMediaType(singleMediaInfo) : utils.getLinkType(mediaUrl) readonly property string cachedLocalPath: "" readonly property string thumbnailMxc: singleMediaInfo.thumbnail_url readonly property bool hovered: item ? item.hovered : false visible: Boolean(item) x: eventContent.spacing onTypeChanged: { if (type === Utils.Media.Image) { var file = "EventImage.qml" } else if (type !== Utils.Media.Page) { var file = "EventFile.qml" } else { return } loader.setSource(file, {loader}) } } mirage-0.7.2/src/gui/Pages/Chat/Timeline/EventVideo.qml000066400000000000000000000005561407747233600226310ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtAV 1.7 import "../../.." import "../../../Base" import "../../../Base/MediaPlayer" VideoPlayer { readonly property bool hovered: hover.hovered HoverHandler { id: hover } } mirage-0.7.2/src/gui/Pages/Chat/TypingMembersBar.qml000066400000000000000000000011411407747233600222140ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" InfoBar { property var typingMembers: [] color: theme.chat.typingMembers.background icon.svgName: "typing" // TODO: animate label.textFormat: Text.StyledText label.text: { const tm = typingMembers if (tm.length === 0) return "" if (tm.length === 1) return qsTr("%1 is typing...").arg(tm[0]) return qsTr("%1 are typing...").arg(utils.commaAndJoin(tm)) } } mirage-0.7.2/src/gui/Pages/Chat/UploadDropArea.qml000066400000000000000000000051401407747233600216470ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "AutoCompletion" import "Composer" import "FileTransfer" import "Timeline" DropArea { property var dragEvent: null property int insertedTextStart: -1 property int insertedTextEnd: -1 function eventFiles() { if (! dragEvent) return [] return dragEvent.urls.filter(uri => uri.match(/^file:\/\//)) } function reset() { if (popup.opened) popup.close() dragEvent = null insertedTextStart = -1 insertedTextEnd = -1 } onPositionChanged: drag => dragEvent = drag onEntered: drag => { print(JSON.stringify( drag, null, 4)) dragEvent = drag if (eventFiles().length && ! popup.opened) popup.open() if (! drag.hasText || eventFiles().length) return insertedTextStart = composer.messageArea.cursorPosition composer.messageArea.insertAtCursor(drag.text.replace(/\n+$/, "")) insertedTextEnd = composer.messageArea.cursorPosition } onExited: { if (insertedTextStart !== -1 && insertedTextEnd !== -1) composer.messageArea.remove(insertedTextStart, insertedTextEnd) reset() } onDropped: drag => { dragEvent = drag for (const path of eventFiles()) { window.makePopup( "Popups/ConfirmUploadPopup.qml", { userId: chat.userId, roomId: chat.roomId, roomName: chat.roomInfo.display_name, filePath: path.replace(/^file:\/\//, ""), replyToEventId: chat.replyToEventId, }, popup => popup.replied.connect(chat.clearReplyTo), ) drag.accepted = true } reset() } HPopup { id: popup background: null Column { spacing: theme.spacing HIcon { anchors.horizontalCenter: parent.horizontalCenter svgName: "drop-file-upload" dimension: Math.min( 56 * theme.uiScale, Math.min(window.width, window.height) / 2, ) } HLabel { wrapMode: HLabel.Wrap width: Math.min(implicitWidth, popup.maximumPreferredWidth) font.pixelSize: theme.fontSize.big text: qsTr("Drop files to send") } } } } mirage-0.7.2/src/gui/Pages/Default.qml000066400000000000000000000003041407747233600175070ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import "../Base" HNoticePage { text: qsTr("No chat selected") } mirage-0.7.2/src/gui/Popups/000077500000000000000000000000001407747233600156425ustar00rootroot00000000000000mirage-0.7.2/src/gui/Popups/ClearMessagesPopup.qml000066400000000000000000000021111407747233600221120ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string userId: "" property string roomId: "" property var preClearCallback: null page.footer: AutoDirectionLayout { ApplyButton { id: clearButton text: qsTr("Clear") icon.name: "clear-messages" onClicked: { if (preClearCallback) preClearCallback() py.callClientCoro(userId, "clear_events", [roomId]) popup.close() } } CancelButton { onClicked: popup.close() } } SummaryLabel { text: qsTr("Clear this room's messages?") } DetailsLabel { text: qsTr( "The messages will only be removed on your side. " + "They will be available again after you restart the application." ) } onOpened: clearButton.forceActiveFocus() } mirage-0.7.2/src/gui/Popups/ConfirmClipboardUploadPopup.qml000066400000000000000000000044341407747233600237700ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" // // Make sure to initialize the image provider by // importing this first: import Clipboard 0.1 HColumnPopup { id: popup property string userId property string roomId property string roomName property string replyToEventId: "" signal replied() contentWidthLimit: theme.controls.popup.defaultWidth * 1.25 page.footer: AutoDirectionLayout { ApplyButton { id: uploadButton text: qsTr("Send") icon.name: "confirm-uploading-file" onClicked: { const args = [ popup.roomId, Clipboard.image, popup.replyToEventId || undefined, ] py.callClientCoro(popup.userId, "send_clipboard_image", args) if (popup.replyToEventId) popup.replied() popup.close() } } CancelButton { id: cancelButton onClicked: popup.close() } } onOpened: uploadButton.forceActiveFocus() SummaryLabel { text: qsTr("Send copied image to %1?") .arg(utils.htmlColorize(roomName, theme.colors.accentText)) textFormat: Text.StyledText } HImage { id: image property int updateCounter: 0 source: "image://clipboard/" + updateCounter sourceSize.width: popup.contentWidthLimit onUpdateCounterChanged: { source = "Need an invalid value to update properly, don't know why" source = "image://clipboard/" + updateCounter } Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: status === Image.Ready ? width / (implicitWidth / implicitHeight) : 96 * theme.uiScale // for spinner Behavior on Layout.preferredHeight { HNumberAnimation {} } Connections { target: Clipboard onContentChanged: Clipboard.hasImage ? image.updateCounter += 1 : popup.close() } } } mirage-0.7.2/src/gui/Popups/ConfirmUploadPopup.qml000066400000000000000000000040611407747233600221440ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HColumnPopup { id: popup property string userId property string roomId property string roomName property string filePath property string replyToEventId: "" readonly property string fileName: filePath.split("/").slice(-1)[0] signal replied() contentWidthLimit: theme.controls.popup.defaultWidth * 1.25 page.footer: AutoDirectionLayout { ApplyButton { id: uploadButton text: qsTr("Send") icon.name: "confirm-uploading-file" onClicked: { const args = [ popup.roomId, popup.filePath, popup.replyToEventId || undefined, ] py.callClientCoro(popup.userId, "send_file", args) if (popup.replyToEventId) popup.replied() popup.close() } } CancelButton { id: cancelButton onClicked: popup.close() } } onOpened: uploadButton.forceActiveFocus() SummaryLabel { text: qsTr("Send %1 to %2?") .arg(utils.htmlColorize(fileName, theme.colors.accentText)) .arg(utils.htmlColorize(roomName, theme.colors.accentText)) textFormat: Text.StyledText } HImage { source: popup.filePath.startsWith("file://") ? popup.filePath : "file://" + popup.filePath visible: status !== Image.Error sourceSize.width: popup.contentWidthLimit Layout.fillWidth: true Layout.fillHeight: true Layout.preferredHeight: status === Image.Ready ? width / (implicitWidth / implicitHeight) : 96 * theme.uiScale // for spinner Behavior on Layout.preferredHeight { HNumberAnimation {} } } } mirage-0.7.2/src/gui/Popups/DeleteDevicesPopup.qml000066400000000000000000000025471407747233600221160ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" import "../Base/Buttons" PasswordPopup { id: popup property string userId property var deviceIds // array property var deletedCallback: null property string deleteFutureId: "" function verifyPassword(pass, callback) { deleteFutureId = py.callClientCoro( userId, "delete_devices_with_password", [deviceIds, pass], () => { deleteFutureId = "" callback(true) }, (type, args) => { callback( type === "MatrixUnauthorized" ? false : qsTr("Unknown error: %1 - %2").arg(type).arg(args) ) }, ) } summary.text: qsTr("Enter your account's password to continue:") validateButton.text: deviceIds.length > 1 ? qsTr("Sign out %1 devices").arg(deviceIds.length) : qsTr("Sign out %1 device").arg(deviceIds.length) validateButton.icon.name: "sign-out" onClosed: { if (deleteFutureId) py.cancelCoro(deleteFutureId) if (deleteFutureId || acceptedPassword && deletedCallback) deletedCallback() } } mirage-0.7.2/src/gui/Popups/DetailsLabel.qml000066400000000000000000000004351407747233600207040ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" HLabel { wrapMode: HLabel.Wrap visible: Boolean(text) Layout.fillWidth: true } mirage-0.7.2/src/gui/Popups/HColumnPopup.qml000066400000000000000000000015631407747233600207530ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" HPopup { id: popup default property alias pageData: page.columnData property int contentWidthLimit: theme.controls.popup.defaultWidth readonly property alias page: page signal keyboardAccept() HColumnPage { id: page implicitWidth: Math.min( popup.maximumPreferredWidth, popup.contentWidthLimit, ) implicitHeight: Math.min( popup.maximumPreferredHeight, implicitHeaderHeight + implicitFooterHeight + topPadding + bottomPadding + implicitContentHeight, ) useVariableSpacing: false Keys.onReturnPressed: popup.keyboardAccept() Keys.onEnterPressed: popup.keyboardAccept() } } mirage-0.7.2/src/gui/Popups/HFlickableColumnPopup.qml000066400000000000000000000014561407747233600225510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" HPopup { id: popup default property alias pageData: page.columnData readonly property alias page: page signal keyboardAccept() HFlickableColumnPage { id: page implicitWidth: Math.min( popup.maximumPreferredWidth, theme.controls.popup.defaultWidth, ) implicitHeight: Math.min( popup.maximumPreferredHeight, implicitHeaderHeight + implicitFooterHeight + contentHeight, ) flickShortcuts.disableIfAnyPopupOrMenu: false Keys.onReturnPressed: popup.keyboardAccept() Keys.onEnterPressed: popup.keyboardAccept() } } mirage-0.7.2/src/gui/Popups/HPopupShortcut.qml000066400000000000000000000010141407747233600213200ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import "../Base" HShortcut { enabled: active onSequencesChanged: check() onSequenceChanged: check() function check() { if (sequences.includes("Escape") || sequence === "Escape") console.warn( qsTr("%1: assigning Escape to a popup action causes conflicts") .arg(sequence || JSON.stringify(sequences)) ) } } mirage-0.7.2/src/gui/Popups/IgnoreUserPopup.qml000066400000000000000000000035761407747233600214760ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: root property string userId property string targetUserId property string targetDisplayName property bool ignore function apply() { py.callClientCoro(userId, "ignore_user", [targetUserId, ignore]) root.close() } page.footer: AutoDirectionLayout { ApplyButton { id: ignoreButton icon.name: root.ignore ? "ignore-user" : "stop-ignore-user" text: root.ignore ? qsTr("Ignore") : qsTr("Stop ignoring") onClicked: root.apply() } CancelButton { onClicked: root.close() } } onOpened: ignoreButton.forceActiveFocus() SummaryLabel { readonly property string userText: utils.coloredNameHtml(root.targetDisplayName, root.targetUserId) textFormat: Text.StyledText text: root.ignore ? qsTr("Ignore %1?").arg(userText) : qsTr("Stop ignoring %1?").arg(userText) } DetailsLabel { text: root.ignore ? qsTr( "You will no longer see their messages and invites.\n\n" + "Their name, avatar and online status will also be hidden " + "in room member lists." ) : qsTr( "You will receive their messages and room invites again.\n\n" + "Their names, avatar and online status will also become " + "visible in room member lists.\n\n" + "After restarting %1, any message or room invite they had " + "sent while being ignored will become visible." ).arg(Qt.application.displayName) } } mirage-0.7.2/src/gui/Popups/ImageViewerPopup/000077500000000000000000000000001407747233600210725ustar00rootroot00000000000000mirage-0.7.2/src/gui/Popups/ImageViewerPopup/ImageViewerPopup.qml000066400000000000000000000111631407747233600250370ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Window 2.12 import "../../Base" HPopup { id: popup property string clientUserId property string thumbnailTitle property string thumbnailMxc property string thumbnailPath: "" property var thumbnailCryptDict property string fullTitle property string fullMxc property var fullCryptDict property int fullFileSize property size overallSize property bool alternateScaling: false property bool activedFullScreen: false property bool imagesPaused: false property real imagesRotation: 0 property real animatedRotationTarget: 0 property real imagesSpeed: 1 property var availableSpeeds: [16, 8, 2, 1.75, 1.5, 1.25, 1, 0.75, 0.5] readonly property alias info: info readonly property alias canvas: canvas readonly property alias buttons: buttons readonly property alias autoHideTimer: autoHideTimer readonly property bool isAnimated: canvas.thumbnail.animated || canvas.full.animated readonly property bool imageLargerThanWindow: overallSize.width > window.width || overallSize.height > window.height readonly property bool imageEqualToWindow: overallSize.width == window.width && overallSize.height == window.height readonly property int paintedWidth: canvas.full.status === Image.Ready ? canvas.full.animatedPaintedWidth || canvas.full.paintedWidth : canvas.thumbnail.animatedPaintedWidth || canvas.thumbnail.paintedWidth readonly property int paintedHeight: canvas.full.status === Image.Ready ? canvas.full.animatedPaintedHeight || canvas.full.paintedHeight : canvas.thumbnail.animatedPaintedHeight || canvas.thumbnail.paintedHeight readonly property bool canAutoHide: paintedHeight * canvas.thumbnail.scale > height - info.implicitHeight - buttons.implicitHeight && ! infoHover.hovered && ! buttonsHover.hovered readonly property bool autoHide: canAutoHide && ! autoHideTimer.running signal openExternallyRequested() function showFullScreen() { if (activedFullScreen) return window.showFullScreen() popup.activedFullScreen = true if (! imageLargerThanWindow) popup.alternateScaling = true } function exitFullScreen() { if (! activedFullScreen) return window.showNormal() popup.activedFullScreen = false if (! imageLargerThanWindow) popup.alternateScaling = false } function toggleFullScreen() { const isFull = window.visibility === Window.FullScreen return isFull ? exitFullScreen() : showFullScreen() } margins: 0 background: null onAboutToHide: exitFullScreen() HNumberAnimation { target: popup property: "imagesRotation" from: popup.imagesRotation to: popup.animatedRotationTarget easing.type: Easing.OutCirc onToChanged: restart() } Item { implicitWidth: window.width implicitHeight: window.height ViewerCanvas { id: canvas anchors.fill: parent viewer: popup } HoverHandler { readonly property point position: point.position enabled: popup.canAutoHide onPositionChanged: if (Math.abs(point.velocity.x + point.velocity.y) >= 0.05) autoHideTimer.restart() } Timer { id: autoHideTimer interval: window.settings.Chat.Files.autohide_image_controls_after } ViewerInfo { id: info viewer: popup width: parent.width y: (parent.width < buttons.width * 4 || layout.vertical) && popup.autoHide ? -height : parent.width < buttons.width * 4 || layout.vertical ? 0 : parent.height - (popup.autoHide ? 0 : height) maxTitleWidth: y <= 0 ? -1 : buttons.x - buttons.width / 2 Behavior on y { HNumberAnimation {} } HoverHandler { id: infoHover } } ViewerButtons { id: buttons anchors.horizontalCenter: parent.horizontalCenter width: Math.min(calculatedWidth, parent.width) y: parent.height - (popup.autoHide ? 0 : height) viewer: popup Behavior on y { HNumberAnimation {} } HoverHandler { id: buttonsHover } } } } mirage-0.7.2/src/gui/Popups/ImageViewerPopup/ViewerButtons.qml000066400000000000000000000114301407747233600244240ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Window 2.12 import ".." import "../../Base" HFlow { id: root property HPopup viewer property color backgroundsColor: viewer.info.y >= viewer.height - viewer.info.height ? "transparent" : theme.controls.button.background readonly property real calculatedWidth: utils.sumChildrenImplicitWidths(visibleChildren) HButton { id: pause backgroundColor: root.backgroundsColor icon.name: viewer.imagesPaused ? "image-play" : "image-pause" toolTip.text: viewer.imagesPaused ? qsTr("Play") : qsTr("Pause") onClicked: viewer.imagesPaused = ! viewer.imagesPaused visible: viewer.isAnimated HPopupShortcut { sequences: window.settings.Keys.ImageViewer.pause onActivated: pause.clicked() } } HButton { backgroundColor: root.backgroundsColor text: qsTr("%1x").arg(utils.round(viewer.imagesSpeed)) label.font.pixelSize: theme.fontSize.big height: pause.height topPadding: 0 bottomPadding: 0 toolTip.text: qsTr("Change speed") onClicked: speedMenu.popup() visible: viewer.isAnimated HPopupShortcut { sequences: window.settings.Keys.ImageViewer.slow_down onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.min( viewer.availableSpeeds.indexOf(viewer.imagesSpeed) + 1, viewer.availableSpeeds.length - 1, )] } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.speed_up onActivated: viewer.imagesSpeed = viewer.availableSpeeds[Math.max( viewer.availableSpeeds.indexOf(viewer.imagesSpeed) - 1, 0, )] } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.reset_speed onActivated: viewer.imagesSpeed = 1 } } HButton { id: rotateLeft backgroundColor: root.backgroundsColor icon.name: "image-rotate-left" toolTip.text: qsTr("Rotate left") autoRepeat: true autoRepeatDelay: 20 autoRepeatInterval: 300 onPressed: viewer.animatedRotationTarget -= 45 HPopupShortcut { sequences: window.settings.Keys.ImageViewer.rotate_left onActivated: viewer.animatedRotationTarget -= 45 } } HButton { id: rotateRight backgroundColor: root.backgroundsColor icon.name: "image-rotate-right" toolTip.text: qsTr("Rotate right") autoRepeat: true autoRepeatDelay: 20 autoRepeatInterval: 300 onPressed: viewer.animatedRotationTarget += 45 HPopupShortcut { sequences: window.settings.Keys.ImageViewer.rotate_right onActivated: viewer.animatedRotationTarget += 45 } } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.reset_rotation onActivated: viewer.animatedRotationTarget = 0 } HButton { id: expand backgroundColor: root.backgroundsColor icon.name: "image-alt-scale-mode" toolTip.text: viewer.imageLargerThanWindow ? qsTr("Expand to original size") : qsTr("Expand to screen") checked: viewer.alternateScaling onClicked: viewer.alternateScaling = ! viewer.alternateScaling HPopupShortcut { sequences: window.settings.Keys.ImageViewer.expand onActivated: expand.clicked() } } HButton { id: fullScreen backgroundColor: root.backgroundsColor icon.name: "image-fullscreen" toolTip.text: qsTr("Fullscreen") checked: window.visibility === Window.FullScreen onClicked: viewer.toggleFullScreen() visible: Qt.application.supportsMultipleWindows HPopupShortcut { sequences: window.settings.Keys.ImageViewer.fullscreen onActivated: fullScreen.clicked() } } HButton { id: close // always visible backgroundColor: root.backgroundsColor icon.name: "image-close" toolTip.text: qsTr("Close") onClicked: viewer.close() HPopupShortcut { sequences: window.settings.Keys.ImageViewer.close onActivated: close.clicked() } } HMenu { id: speedMenu Repeater { model: viewer.availableSpeeds HMenuItem { text: qsTr("%1x").arg(modelData) onClicked: viewer.imagesSpeed = modelData label.horizontalAlignment: HLabel.AlignHCenter } } } } mirage-0.7.2/src/gui/Popups/ImageViewerPopup/ViewerCanvas.qml000066400000000000000000000132701407747233600242050ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import ".." import "../.." import "../../Base" HFlickable { id: flickable property HPopup viewer readonly property alias thumbnail: thumbnail readonly property alias full: full contentWidth: Math.max(window.width, viewer.paintedWidth * thumbnail.scale) contentHeight: Math.max(window.height, viewer.paintedHeight * thumbnail.scale) ScrollBar.vertical: null TapHandler { acceptedButtons: Qt.LeftButton | Qt.RightButton onTapped: viewer.close() gesturePolicy: TapHandler.ReleaseWithinBounds } TapHandler { acceptedButtons: Qt.MiddleButton gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: viewer.openExternallyRequested() } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: { if (wheel.modifiers !== Qt.ControlModifier) { wheel.accepted = false return } wheel.accepted = true const add = wheel.angleDelta.y / 120 / 5 thumbnail.scale = Math.max( 0.1, Math.min(10, thumbnail.scale + add), ) } } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.pan_left onActivated: utils.flickPages(flickable, -0.2, true, 5) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.pan_right onActivated: utils.flickPages(flickable, 0.2, true, 5) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.pan_up onActivated: utils.flickPages(flickable, -0.2, false, 5) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.pan_down onActivated: utils.flickPages(flickable, 0.2, false, 5) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.zoom_out onActivated: thumbnail.scale = Math.max(0.1, thumbnail.scale - 0.2) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.zoom_in onActivated: thumbnail.scale = Math.min(10, thumbnail.scale + 0.2) } HPopupShortcut { sequences: window.settings.Keys.ImageViewer.reset_zoom onActivated: resetScaleAnimation.start() } HMxcImage { id: thumbnail anchors.centerIn: parent width: viewer.alternateScaling && viewer.imageLargerThanWindow ? viewer.overallSize.width : viewer.alternateScaling ? window.width : Math.min(window.width, viewer.overallSize.width) height: viewer.alternateScaling && viewer.imageLargerThanWindow ? viewer.overallSize.height : viewer.alternateScaling ? window.height : Math.min(window.height, viewer.overallSize.height) clientUserId: viewer.clientUserId showPauseButton: false showProgressBar: false pause: viewer.imagesPaused speed: viewer.imagesSpeed rotation: viewer.imagesRotation fillMode: HMxcImage.PreserveAspectFit title: viewer.thumbnailTitle mxc: viewer.thumbnailMxc cachedPath: viewer.thumbnailPath cryptDict: viewer.thumbnailCryptDict // Use only cachedPath if set, don't waste time refetching thumb canUpdate: ! cachedPath Behavior on width { HNumberAnimation { overshoot: viewer.alternateScaling? 2 : 3 } } Behavior on height { HNumberAnimation { overshoot: viewer.alternateScaling? 2 : 3 } } Binding on showProgressBar { value: false when: ! thumbnail.show } HNumberAnimation { id: resetScaleAnimation target: thumbnail property: "scale" from: thumbnail.scale to: 1 overshoot: 2 } Timer { // Timer to not disappear before full image is done rendering interval: 1000 running: full.status === HMxcImage.Ready onTriggered: thumbnail.show = false } HMxcImage { id: full anchors.fill: parent clientUserId: viewer.clientUserId thumbnail: false showPauseButton: false pause: viewer.imagesPaused speed: viewer.imagesSpeed rotation: viewer.imagesRotation fillMode: parent.fillMode title: viewer.fullTitle mxc: viewer.fullMxc cryptDict: viewer.fullCryptDict // Image never loads at 0 opacity or with visible: false opacity: status === HMxcImage.Ready ? 1 : 0.01 Behavior on opacity { HNumberAnimation {} } } Item { anchors.centerIn: parent width: viewer.paintedWidth height: viewer.paintedHeight TapHandler { acceptedPointerTypes: PointerDevice.GenericPointer gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: { thumbnail.scale === 1 ? viewer.alternateScaling = ! viewer.alternateScaling : resetScaleAnimation.start() } onDoubleTapped: viewer.toggleFullScreen() } TapHandler { acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen gesturePolicy: TapHandler.ReleaseWithinBounds onTapped: viewer.autoHideTimer.restart() } } } } mirage-0.7.2/src/gui/Popups/ImageViewerPopup/ViewerInfo.qml000066400000000000000000000062171407747233600236700ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import CppUtils 0.1 import "../../Base" Rectangle { property HPopup viewer property int maxTitleWidth: -1 readonly property alias layout: layout readonly property alias title: title readonly property alias dimensions: dimensions readonly property alias fileSize: fileSize implicitHeight: Math.max(theme.baseElementsHeight, childrenRect.height) color: utils.hsluv(0, 0, 0, 0.8) Behavior on implicitHeight { HNumberAnimation {} } AutoDirectionLayout { id: layout width: parent.width - theme.spacing * 2 anchors.horizontalCenter: parent.horizontalCenter columnSpacing: theme.spacing HLabel { id: title text: viewer.fullTitle elide: HLabel.ElideMiddle topPadding: layout.vertical ? theme.spacing / 2 : 0 verticalAlignment: HLabel.AlignVCenter horizontalAlignment: layout.vertical ? HLabel.AlignHCenter : HLabel.AlignLeft Layout.fillWidth: maxTitleWidth < 0 Layout.fillHeight: true Layout.maximumWidth: maxTitleWidth } HSpacer { visible: ! title.Layout.fillWidth } HLabel { id: dimensions text: qsTr("%1 x %2") .arg( viewer.canvas.full.animatedImplicitWidth || viewer.canvas.full.implicitWidth || viewer.overallSize.width ).arg( viewer.canvas.full.animatedImplicitHeight || viewer.canvas.full.implicitHeight || viewer.overallSize.height ) elide: HLabel.ElideRight topPadding: theme.spacing / 2 bottomPadding: topPadding horizontalAlignment: HLabel.AlignHCenter verticalAlignment: HLabel.AlignVCenter Layout.fillWidth: layout.vertical Layout.fillHeight: true } HLabel { id: fileSize visible: viewer.fullFileSize !== 0 text: CppUtils.formattedBytes(viewer.fullFileSize) elide: HLabel.ElideRight horizontalAlignment: HLabel.AlignHCenter verticalAlignment: HLabel.AlignVCenter Layout.fillWidth: layout.vertical Layout.fillHeight: true } HLoader { source: "../../Base/HBusyIndicator.qml" visible: Layout.preferredWidth > 0 active: viewer.canvas.full.showProgressBar Layout.topMargin: theme.spacing / 2 Layout.bottomMargin: Layout.topMargin Layout.alignment: Qt.AlignCenter Layout.preferredWidth: active ? height : 0 Layout.preferredHeight: theme.baseElementsHeight - theme.spacing Behavior on Layout.preferredWidth { HNumberAnimation {} } } Item { visible: layout.vertical height: theme.spacing / 2 } } } mirage-0.7.2/src/gui/Popups/InvalidAccessTokenPopup.qml000066400000000000000000000025231407747233600231140ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string userId function addAccount() { window.mainUI.pageLoader.show("Pages/AddAccount/AddAccount.qml") } page.footer: AutoDirectionLayout { ApplyButton { id: signBackButton text: qsTr("Sign back in") icon.name: "sign-back-in" onClicked: { addAccount() popup.close() } } CancelButton { text: qsTr("Close") onClicked: popup.close() } } onClosed: if ( window.uiState.pageProperties.userId === userId || (window.uiState.pageProperties.userRoomId || [])[0] === userId ) addAccount() SummaryLabel { text: qsTr("Signed out from %1").arg(coloredNameHtml("", userId)) textFormat: SummaryLabel.StyledText } DetailsLabel { text: qsTr( "You have been disconnected from another session, " + "by the server for security reasons, or the access token in " + "your configuration file is invalid." ) } onOpened: signBackButton.forceActiveFocus() } mirage-0.7.2/src/gui/Popups/InviteToRoomPopup.qml000066400000000000000000000104661407747233600220060ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HColumnPopup { id: popup property string userId property string roomId property string roomName property bool invitingAllowed: true property string inviteFutureId: "" property var successfulInvites: [] property var failedInvites: [] function invite() { inviteButton.loading = true const inviteesLeft = inviteArea.text.trim().split(/\s+/).filter( user => ! successfulInvites.includes(user) ) inviteFutureId = py.callClientCoro( userId, "room_mass_invite", [roomId, ...inviteesLeft], ([successes, errors]) => { inviteFutureId = "" if (errors.length < 1) { popup.close() return } successfulInvites = successes failedInvites = errors inviteButton.loading = false } ) } page.footer: AutoDirectionLayout { ApplyButton { id: inviteButton text: qsTr("Invite") icon.name: "room-send-invite" enabled: invitingAllowed && Boolean(inviteArea.text.trim()) onClicked: invite() } CancelButton { id: cancelButton onClicked: popup.close() } } onOpened: inviteArea.forceActiveFocus() onClosed: if (inviteFutureId) py.cancelCoro(inviteFutureId) onInvitingAllowedChanged: if (! invitingAllowed && inviteFutureId) py.cancelCoro(inviteFutureId) SummaryLabel { text: qsTr("Invite users to %1").arg( utils.htmlColorize(roomName, theme.colors.accentText), ) textFormat: Text.StyledText } HScrollView { clip: true Layout.fillWidth: true Layout.fillHeight: true HTextArea { id: inviteArea focusItemOnTab: inviteButton.enabled ? inviteButton : cancelButton placeholderText: qsTr("User IDs (e.g. @bob:matrix.org @alice:localhost)") } } HLabel { id: errorMessage readonly property string allErrors: { // TODO: handle these: real user not found const lines = [] for (const [user, error] of failedInvites) { const type = py.getattr( py.getattr(error, "__class__"), "__name__", ) lines.push( type === "InvalidUserId" ? qsTr("%1 is not a valid user ID, expected format is " + "@username:homeserver").arg(user) : type === "UserFromOtherServerDisallowed" ? qsTr("This room rejects users from other matrix " + "servers, can't invite %1").arg(user) : type === "MatrixNotFound" ? qsTr("%1 not found, please verify the entered ID") .arg(user) : type === "MatrixBadGateway" ? qsTr("Could not contact %1's server, " + "please verify the entered ID").arg(user) : type === "MatrixUnsupportedRoomVersion" ? qsTr("%1's server does not support this room's version") .arg(user) : type === "MatrixForbidden" ? qsTr("%1 is banned from this room") .arg(user) : qsTr("Unknown error while inviting %1: %2 - %3") .arg(user).arg(type).arg(py.getattr(error, "args")) ) } return lines.join("\n\n") } visible: Layout.maximumHeight > 0 wrapMode: HLabel.Wrap color: theme.colors.errorText text: invitingAllowed ? allErrors : qsTr("You do not have permission to invite users to this room") Layout.maximumHeight: text ? implicitHeight : 0 Layout.fillWidth: true Behavior on Layout.maximumHeight { HNumberAnimation {} } } } mirage-0.7.2/src/gui/Popups/KeyVerificationPopup.qml000066400000000000000000000066101407747233600224770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string deviceOwner property string deviceId property string deviceName property string ed25519Key property bool deviceIsCurrent: false property var verifiedCallback: null property var blacklistedCallback: null page.footer: AutoDirectionLayout { PositiveButton { visible: ! deviceIsCurrent text: qsTr("They match") icon.name: "device-verified" onClicked: { loading = true py.callCoro( "verify_device", [deviceOwner, deviceId, ed25519Key.replace(/ /g, "")], () => { if (verifiedCallback) verifiedCallback() popup.close() } ) } } NegativeButton { visible: ! popup.deviceIsCurrent text: qsTr("They differ") icon.name: "device-blacklisted" onClicked: { loading = true py.callCoro( "blacklist_device", [deviceOwner, deviceId, ed25519Key.replace(/ /g, "")], () => { if (blacklistedCallback) blacklistedCallback() popup.close() } ) } } CancelButton { id: cancelButton onClicked: popup.close() Binding on text { value: qsTr("Exit") when: popup.deviceIsCurrent } } } onOpened: infoArea.forceActiveFocus() SummaryLabel { text: deviceIsCurrent ? qsTr("Your session's info:") : qsTr("Do these info match on your other session?") } HTextArea { id: infoArea function formatInfo(info, value) { return ( `

` + info + `` + " " + value + `

` ) } readOnly: true wrapMode: HSelectableLabel.Wrap textFormat: HTextArea.RichText text: ( formatInfo(qsTr("Session name:"), popup.deviceName) + formatInfo(qsTr("Session ID:"), popup.deviceId) + formatInfo(qsTr("Session key:"), ""+ popup.ed25519Key+"") ) Layout.fillWidth: true } DetailsLabel { text: deviceIsCurrent ? qsTr( "To be verified by one of your other session, compare these " + "info with the ones shown on that session.\n\n" + "To be verified by another user, send them these info. " + "If you already know them, use a trusted contact method, " + "such as email or a phone call." ) : qsTr( "Compare with the info in your other session's account " + "settings. " + "If they differ, your account's security may be compromised." ) } } mirage-0.7.2/src/gui/Popups/LeaveRoomPopup.qml000066400000000000000000000070201407747233600212710ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string userId: "" property string roomId: "" property string roomName: "" property string inviterId: "" property bool left: false property var doneCallback: null function ignoreInviter() { if (! inviterId) return py.callClientCoro(userId, "ignore_user", [inviterId, true]) } function leave() { leaveButton.loading = true py.callClientCoro(userId, "room_leave", [roomId], doneCallback) if (ignoreInviterCheck.checked) popup.ignoreInviter() popup.close() } function forget() { leaveButton.loading = true py.callClientCoro(userId, "room_forget", [roomId], () => { if (window.uiState.page === "Pages/Chat/Chat.qml" && window.uiState.pageProperties.userRoomId[0] === userId && window.uiState.pageProperties.userRoomId[1] === roomId) { window.mainUI.pageLoader.showPrevious() || window.mainUI.pageLoader.show("Pages/Default.qml") } Qt.callLater(popup.destroy) }) if (ignoreInviterCheck.checked) popup.ignoreInviter() } page.footer: AutoDirectionLayout { ApplyButton { id: leaveButton icon.name: popup.left ? "room-forget" : "room-leave" text: popup.left ? qsTr("Forget") : popup.inviterId ? qsTr("Decline") : qsTr("Leave") onClicked: forgetCheck.checked || popup.left ? popup.forget() : popup.leave() } CancelButton { onClicked: popup.close() } } onOpened: leaveButton.forceActiveFocus() onClosed: if (doneCallback) doneCallback() SummaryLabel { readonly property string roomText: utils.htmlColorize(popup.roomName, theme.colors.accentText) textFormat: Text.StyledText text: popup.left ? qsTr("Forget the history for %1?").arg(roomText) : popup.inviterId ? qsTr("Decline invite to %1?").arg(roomText) : qsTr("Leave %1?").arg(roomText) } DetailsLabel { text: popup.left ? forgetCheck.subtitle.text : qsTr( "If this room is private, you will not be able to rejoin it " + "without a new invite." ) } HCheckBox { id: ignoreInviterCheck visible: Boolean(popup.inviterId) mainText.textFormat: HLabel.StyledText // We purposely display inviter's user ID instead of display name here. // Someone could take the name of one of our contact, which would // not be disambiguated and lead to confusion. text: qsTr("Ignore sender %1").arg( utils.coloredNameHtml("", popup.inviterId), ) subtitle.text: qsTr("Automatically hide their invites and messages") Layout.fillWidth: true } HCheckBox { id: forgetCheck visible: ! popup.left text: qsTr("Forget this room's history") subtitle.text: qsTr( "Access to previously received messages will be lost.\n" + "If all members forget a room, servers will erase it." ) Layout.fillWidth: true } } mirage-0.7.2/src/gui/Popups/PasswordPopup.qml000066400000000000000000000065101407747233600212050ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property bool validateWhileTyping: false property string acceptedPassword: "" property var passwordValid: null property bool okClicked: false readonly property alias summary: summary readonly property alias details: details readonly property alias validateButton: validateButton signal cancelled() function verifyPassword(pass, callback) { // Can be reimplemented when using this component. // Pass to the callback true on success, false on invalid password, // or a custom error message string. callback(true) } function validate() { const password = passwordField.text okClicked = true validateButton.loading = true errorMessage.text = "" verifyPassword(password, result => { if (result === true) { passwordValid = true popup.acceptedPassword = password popup.close() } else if (result === false) { passwordValid = false } else { errorMessage.text = result } validateButton.loading = false }) } page.footer: AutoDirectionLayout { ApplyButton { id: validateButton text: qsTr("Confirm") enabled: Boolean(passwordField.text) onClicked: validate() } CancelButton { onClicked: { popup.close() cancelled() } } } onAboutToShow: { okClicked = false acceptedPassword = "" passwordValid = null errorMessage.text = "" } onOpened: passwordField.forceActiveFocus() onKeyboardAccept: if (validateButton.enabled) validateButton.clicked() SummaryLabel { id: summary } DetailsLabel { id: details } HRowLayout { spacing: theme.spacing HTextField { id: passwordField echoMode: HTextField.Password focus: true error: passwordValid === false onTextChanged: passwordValid = validateWhileTyping ? verifyPassword(text) : null Layout.fillWidth: true } HIcon { visible: Layout.preferredWidth > 0 svgName: passwordValid ? "ok" : "cancel" colorize: passwordValid ? theme.colors.positiveBackground : theme.colors.negativeBackground Layout.preferredWidth: passwordValid === null || (validateWhileTyping && ! okClicked && ! passwordValid) ? 0 : implicitWidth Behavior on Layout.preferredWidth { HNumberAnimation {} } } } HLabel { id: errorMessage wrapMode: HLabel.Wrap color: theme.colors.errorText visible: Layout.maximumHeight > 0 Layout.maximumHeight: text ? implicitHeight : 0 Behavior on Layout.maximumHeight { HNumberAnimation {} } Layout.fillWidth: true } } mirage-0.7.2/src/gui/Popups/Pre070SettingsDetectedPopup.qml000066400000000000000000000031661407747233600235470ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: root property string path readonly property string docs: "https://github.com/mirukana/mirage/tree/master/docs" page.footer: AutoDirectionLayout { CancelButton { id: cancelButton text: qsTr("Close") onClicked: root.close() } } onOpened: cancelButton.forceActiveFocus() SummaryLabel { leftPadding: theme.spacing / 2 rightPadding: leftPadding textFormat: SummaryLabel.StyledText text: qsTr("Old configuration file %1 detected").arg( utils.htmlColorize("settings.json", theme.colors.accentText), ) } DetailsLabel { leftPadding: theme.spacing / 2 rightPadding: leftPadding textFormat: DetailsLabel.StyledText text: qsTr( "The configuration format has changed and settings.json " + "is no longer supported. " + `Visit the new config documentation for ` + "more info.

" + "This warning will stop appearing if the file " + `${path.replace(/^file:\/\//, "")} is ` + "renamed, deleted or moved away." ) MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor } } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/000077500000000000000000000000001407747233600221565ustar00rootroot00000000000000mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/ContentRule.qml000066400000000000000000000010601407747233600251300ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HColumnLayout { readonly property alias idField: idField HLabeledItem { label.text: qsTr("Word or glob pattern to match:") Layout.fillWidth: true HTextField { id: idField width: parent.width defaultText: rule.kind === "content" ? rule.pattern : "" } } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/CustomLabel.qml000066400000000000000000000005561407747233600251110ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" HLabel { opacity: enabled ? 1 : theme.disabledElementsOpacity wrapMode: HLabel.Wrap Layout.fillWidth: true Behavior on opacity { HNumberAnimation {} } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/GeneralRule.qml000066400000000000000000000112671407747233600251050ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HColumnLayout { id: root readonly property alias idField: idField readonly property var matrixConditions: { const results = [] for (let i = 0; i < conditionRepeater.count; i++) results.push(conditionRepeater.itemAt(i).control.matrixObject) return results } spacing: theme.spacing / 2 HLabeledItem { label.text: rule.default ? qsTr("Rule ID:") : qsTr("Rule name:") Layout.fillWidth: true HTextField { id: idField width: parent.width defaultText: rule.rule_id } } HRowLayout { Layout.topMargin: theme.spacing / 2 CustomLabel { text: qsTr("Conditions for messages to trigger this rule:") } PositiveButton { icon.name: "pushrule-condition-add" iconItem.small: true Layout.fillHeight: true Layout.fillWidth: false onClicked: addConditionMenu.open() HMenu { id: addConditionMenu x: -width + parent.width y: parent.height HMenuItem { text: qsTr("Room has a certain number of members") onTriggered: conditionRepeater.model.append({ kind: "room_member_count", is: "2", }) } HMenuItem { text: qsTr("Message property matches value") onTriggered: conditionRepeater.model.append({ kind: "event_match", key: "content.body", pattern: "", }) } HMenuItem { text: qsTr("Message contains my display name") onTriggered: conditionRepeater.model.append({ kind: "contains_display_name", }) } HMenuItem { text: qsTr( "Sender has permission to trigger special notification" ) onTriggered: conditionRepeater.model.append({ kind: "sender_notification_permission", key: "room", }) } HMenuItem { text: qsTr("Custom JSON condition") onTriggered: conditionRepeater.model.append({ condition: ({kind: "example"}), }) } } } } CustomLabel { text: qsTr("No conditions added, all messages will match") color: theme.colors.dimText visible: Layout.preferredHeight > 0 Layout.preferredHeight: conditionRepeater.count ? 0 : implicitHeight Behavior on Layout.preferredHeight { HNumberAnimation {} } } Repeater { id: conditionRepeater model: ListModel {} Component.onCompleted: { // Dummy item to setup all the possible roles for this model model.append({ kind: "", key: "", pattern: "", condition: {}, is: "" }) for (const c of JSON.parse(rule.conditions)) model.append(c) model.remove(0) } HRowLayout { readonly property Item control: loader.item spacing: theme.spacing HLoader { id: loader readonly property var condition: model readonly property string filename: model.kind === "event_match" ? "PushEventMatch" : model.kind === "contains_display_name" ? "PushContainsDisplayName" : model.kind === "room_member_count" ? "PushRoomMemberCount" : model.kind === "sender_notification_permission" ? "PushSenderNotificationPermission" : "PushUnknownCondition" asynchronous: false source: "PushConditions/" + filename + ".qml" Layout.fillWidth: true } NegativeButton { icon.name: "pushrule-condition-remove" iconItem.small: true Layout.fillHeight: true Layout.fillWidth: false onClicked: conditionRepeater.model.remove(model.index) } } } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/000077500000000000000000000000001407747233600251275ustar00rootroot00000000000000mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/CustomFlow.qml000066400000000000000000000005021407747233600277410ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../../../Base" HFlow { spacing: theme.spacing / 2 // transitions break CustomLabel opacity for some reason populate: null add: null move: null } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/PushContainsDisplayName.qml000066400000000000000000000004701407747233600324100ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import ".." import "../../../Base" CustomLabel { readonly property var matrixObject: ({kind: model.kind}) text: qsTr("Message contains my display name") } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/PushEventMatch.qml000066400000000000000000000031451407747233600305430ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." import "../../../Base" CustomFlow { readonly property var matrixObject: ({ kind: model.kind, key: keyCombo.editText, pattern: patternField.text, }) CustomLabel { text: qsTr("Message") verticalAlignment: CustomLabel.AlignVCenter height: keyCombo.height } HComboBox { id: keyCombo width: Math.min(implicitWidth, parent.width) editText: condition.key editable: true currentIndex: model.indexOf(condition.key) model: [...new Set([ "content.body", "content.msgtype", "room_id", "sender", "state_key", "type", condition.key, ])].sort() } CustomLabel { text: keyCombo.editText === "content.body" ? qsTr("has") : qsTr("is") verticalAlignment: CustomLabel.AlignVCenter height: keyCombo.height } HTextField { id: patternField defaultText: condition.pattern width: Math.min(implicitWidth, parent.width) placeholderText: ({ "content.body": qsTr("text..."), "content.msgtype": qsTr("e.g. m.image"), "room_id": qsTr("!room:example.org"), "sender": qsTr("@user:example.org"), "state_key": qsTr("@user:example.org"), "type": qsTr("e.g. m.room.message"), }[keyCombo.editText] || qsTr("value")) } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/PushRoomMemberCount.qml000066400000000000000000000024311407747233600315570ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." import "../../../Base" CustomFlow { readonly property var matrixObject: ({ kind: model.kind, is: operatorCombo.operators[operatorCombo.currentIndex] .replace("==", "") + countSpin.value, }) CustomLabel { text: qsTr("Room has") verticalAlignment: CustomLabel.AlignVCenter height: operatorCombo.height } HComboBox { readonly property var operators: ["==", ">=", "<=", ">", "<"] id: operatorCombo width: Math.min(implicitWidth, parent.width) currentIndex: operators.indexOf(/[=<>]+/.exec(condition.is + "==")[0]) model: [ qsTr("exactly"), qsTr("at least"), qsTr("at most"), qsTr("more than"), qsTr("less than"), ] } HSpinBox { id: countSpin width: Math.min(implicitWidth, parent.width) defaultValue: parseInt(condition.is.replace(/[=<>]/, ""), 10) } CustomLabel { text: qsTr("members") verticalAlignment: CustomLabel.AlignVCenter height: operatorCombo.height } } PushSenderNotificationPermission.qml000066400000000000000000000016071407747233600342670ustar00rootroot00000000000000mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." import "../../../Base" CustomFlow { readonly property var matrixObject: ({ kind: model.kind, key: keyCombo.editText, }) CustomLabel { text: qsTr("Sender has permission to send") verticalAlignment: CustomLabel.AlignVCenter height: keyCombo.height } HComboBox { id: keyCombo width: Math.min(implicitWidth, parent.width) editable: true editText: condition.key currentIndex: model.indexOf(condition.key) model: [...new Set(["room", condition.key])].sort() } CustomLabel { text: qsTr("notifications") verticalAlignment: CustomLabel.AlignVCenter height: keyCombo.height } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushConditions/PushUnknownCondition.qml000066400000000000000000000015601407747233600320120ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import ".." import "../../../Base" AutoDirectionLayout { readonly property var matrixObject: { try { JSON.parse(jsonField.text) } catch (e) { // TODO return condition.condition } } rowSpacing: theme.spacing / 2 columnSpacing: rowSpacing CustomLabel { text: qsTr("Custom JSON:") verticalAlignment: CustomLabel.AlignVCenter Layout.fillWidth: false Layout.fillHeight: true } HTextField { // TODO: validate the JSON id: jsonField font.family: theme.fontFamily.mono defaultText: JSON.stringify(condition.condition) Layout.fillWidth: true } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/PushRuleSettingsPopup.qml000066400000000000000000000243671407747233600272210ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import ".." import "../.." import "../../Base" import "../../Base/Buttons" HFlickableColumnPopup { id: root property string userId // A rule item from ModelStore.get(userId, "pushrules") property var rule property bool ruleExists: true property string saveFutureId: "" property string removeFutureId: "" readonly property bool generalChecked: overrideRadio.checked || underrideRadio.checked readonly property string checkedKind: overrideRadio.checked ? "override" : contentRadio.checked ? "content" : roomRadio.checked ? "room" : senderRadio.checked ? "sender" : "underride" function save() { const details = swipeView.currentItem const isBefore = positionCombo.currentIndex === 0 const position = positionCombo.visible && ! positionCombo.isCurrent ? positionCombo.model[positionCombo.currentIndex].rule_id : undefined const actions = [] const sfx = soundCheck.checked ? soundCombo.editText : false notifyCheck.checked && actions.push("notify") actions.push({set_tweak: "highlight", value: highlightCheck.checked}) actions.push({set_tweak: "bubble", value: bubbleCheck.checked}) actions.push({set_tweak: "sound", value: sfx}) actions.push({set_tweak: "urgency_hint", value: urgencyCheck.checked}) const args = [ checkedKind, details.idField.text, rule.kind, rule.rule_id, isBefore && position ? position : undefined, ! isBefore && position ? position : undefined, ! disableCheck.checked, generalChecked ? details.matrixConditions : undefined, contentRadio.checked ? details.idField.text : undefined, actions, ] saveFutureId = py.callClientCoro(userId, "edit_pushrule", args, root.close) } function remove() { const args = [rule.kind, rule.rule_id] removeFutureId = py.callClientCoro(userId, "remove_pushrule", args, root.close) } page.implicitWidth: Math.min(maximumPreferredWidth, 550 * theme.uiScale) page.footer: AutoDirectionLayout { ApplyButton { id: applyButton text: qsTr("Save changes") enabled: swipeView.currentItem.idField.text.trim() !== "" loading: saveFutureId !== "" onClicked: root.save() } CancelButton { text: qsTr("Cancel changes") onClicked: root.close() } NegativeButton { icon.name: "pushrule-remove" text: qsTr("Remove rule") enabled: ! root.rule.default loading: removeFutureId !== "" onClicked: root.remove() } } onKeyboardAccept: applyButton.clicked() CustomLabel { visible: root.rule.default text: qsTr("Some settings cannot be changed for default server rules") color: theme.colors.warningText } HColumnLayout { enabled: ! root.rule.default spacing: theme.spacing / 2 CustomLabel { text: qsTr("Rule type:") } HRadioButton { id: overrideRadio text: "High priority general rule" subtitle.text: qsTr( "Control notifications for messages matching certain " + "conditions" ) defaultChecked: root.rule.kind === "override" Layout.fillWidth: true } HRadioButton { id: contentRadio text: "Message content rule" subtitle.text: qsTr( "Control notifications for text messages containing a " + "certain word" ) defaultChecked: root.rule.kind === "content" Layout.fillWidth: true } HRadioButton { id: roomRadio text: "Room rule" subtitle.text: qsTr( "Control notifications for all messages received in a " + "certain room" ) defaultChecked: root.rule.kind === "room" Layout.fillWidth: true } HRadioButton { id: senderRadio text: "Sender rule" subtitle.text: qsTr( "Control notifications for all messages sent by a " + "certain user" ) defaultChecked: root.rule.kind === "sender" Layout.fillWidth: true } HRadioButton { id: underrideRadio text: "Low priority general rule" subtitle.text: qsTr( "A general rule tested only after every other rule types" ) defaultChecked: root.rule.kind === "underride" Layout.fillWidth: true } } SwipeView { id: swipeView enabled: ! root.rule.default clip: true interactive: false currentIndex: overrideRadio.checked ? 0 : contentRadio.checked ? 1 : roomRadio.checked ? 2 : senderRadio.checked ? 3 : 4 Layout.fillWidth: true Behavior on implicitHeight { HNumberAnimation {} } GeneralRule { enabled: SwipeView.isCurrentItem } ContentRule { enabled: SwipeView.isCurrentItem } RoomRule { enabled: SwipeView.isCurrentItem } SenderRule { enabled: SwipeView.isCurrentItem } GeneralRule { enabled: SwipeView.isCurrentItem } } HColumnLayout { spacing: theme.spacing / 2 CustomLabel { text: qsTr("Actions for messages that trigger this rule:") bottomPadding: theme.spacing / 2 } HCheckBox { id: notifyCheck text: qsTr("Mark as unread") defaultChecked: root.rule.notify Layout.fillWidth: true } HCheckBox { id: highlightCheck text: qsTr("Highlight, mark as important") enabled: notifyCheck.checked defaultChecked: root.rule.highlight Layout.fillWidth: true } HCheckBox { id: bubbleCheck text: qsTr("Show notification bubble") enabled: notifyCheck.checked defaultChecked: root.rule.bubble Layout.fillWidth: true } HFlow { enabled: notifyCheck.checked spacing: theme.spacing / 2 Layout.fillWidth: true HCheckBox { id: soundCheck text: qsTr("Play sound: ") defaultChecked: root.rule.sound height: soundCombo.height } HComboBox { id: soundCombo readonly property string current: root.rule.sound || "default" model: [...new Set(["default", "ring", current])] editText: current currentIndex: model.indexOf(current) editable: true } } HCheckBox { id: urgencyCheck text: Qt.platform === "windows" ? qsTr("Make taskbar application icon flash") : Qt.platform === "osx" ? qsTr("Make dock application icon flash") : qsTr("Highlight the application window") enabled: notifyCheck.checked defaultChecked: root.rule.urgency_hint Layout.fillWidth: true } } HLabeledItem { visible: ! rule.default && positionCombo.model.length > 1 label.text: qsTr("Rule position:") Layout.fillWidth: true HComboBox { id: positionCombo property int currentPosition: 0 readonly property string name: ! model.length ? "" : utils.stripHtmlTags( utils.formatPushRuleName(root.userId, model[currentIndex]) ) readonly property bool isCurrent: root.ruleExists && model.length && currentIndex === currentPosition && root.rule.kind === root.checkedKind width: parent.width currentIndex: currentPosition displayText: ! model.length ? "" : isCurrent ? qsTr("Current") : currentIndex === 0 ? qsTr('Before "%1"').arg(name) : qsTr('After "%1"').arg(name) model: { currentPosition = 0 const choices = [] const rules = ModelStore.get(userId, "pushrules") for (let i = 0; i < rules.count; i++) { const item = rules.get(i) const isCurrent = item.kind === root.checkedKind && item.rule_id === root.rule.rule_id if (isCurrent && choices.length) currentPosition = choices.length - 1 if (item.kind === root.checkedKind && ! item.default) { if (! choices.length) choices.push(item) if (! isCurrent) choices.push(item) } } return choices } delegate: HMenuItem { readonly property string name: utils.formatPushRuleName(root.userId, modelData) label.textFormat: HLabel.StyledText text: root.ruleExists && model.index === positionCombo.currentPosition && root.rule.kind === root.checkedKind ? qsTr("Current") : model.index === 0 ? qsTr('Before "%1"').arg(name) : qsTr('After "%1"').arg(name) onTriggered: positionCombo.currentIndex = model.index } } } HCheckBox { id: disableCheck text: qsTr("Disable this rule") defaultChecked: ! root.rule.enabled Layout.fillWidth: true } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/RoomRule.qml000066400000000000000000000011551407747233600244370ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HColumnLayout { readonly property alias idField: idField HLabeledItem { label.text: qsTr("Room ID:") Layout.fillWidth: true HTextField { id: idField width: parent.width defaultText: rule.kind === "room" ? rule.rule_id : "" placeholderText: qsTr("!room:example.org") maximumLength: 255 } } } mirage-0.7.2/src/gui/Popups/PushRuleSettingsPopup/SenderRule.qml000066400000000000000000000011601407747233600247370ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../Base" import "../../Base/Buttons" HColumnLayout { readonly property alias idField: idField HLabeledItem { label.text: qsTr("User ID:") Layout.fillWidth: true HTextField { id: idField width: parent.width defaultText: rule.kind === "sender" ? rule.rule_id : "" placeholderText: qsTr("@alice:example.org") maximumLength: 255 } } } mirage-0.7.2/src/gui/Popups/RedactPopup.qml000066400000000000000000000041641407747233600206100ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string preferUserId: "" property string roomId: "" property var eventSenderAndIds: [] // [[senderId, event.id], ...] property bool onlyOwnMessageWarning: false property bool isLast: false function remove() { const idsForSender = {} // {senderId: [event.id, ...]} for (const [senderId, eventClientId] of eventSenderAndIds) { if (! idsForSender[senderId]) idsForSender[senderId] = [] idsForSender[senderId].push(eventClientId) } for (const [senderId, eventClientIds] of Object.entries(idsForSender)) py.callClientCoro( mainUI.accountIds.includes(senderId) ? senderId : preferUserId, "room_mass_redact", [roomId, reasonField.item.text, ...eventClientIds] ) popup.close() } page.footer: AutoDirectionLayout { ApplyButton { text: qsTr("Remove") icon.name: "remove-message" onClicked: remove() } CancelButton { onClicked: popup.close() } } onOpened: reasonField.item.forceActiveFocus() onKeyboardAccept: popup.remove() SummaryLabel { text: isLast ? qsTr("Remove your last message?") : eventSenderAndIds.length > 1 ? qsTr("Remove %1 messages?").arg(eventSenderAndIds.length) : qsTr("Remove this message?") } DetailsLabel { color: theme.colors.warningText text: onlyOwnMessageWarning ? qsTr("Only your messages will be removed due this " + "room's permissions") : "" } HLabeledItem { id: reasonField label.text: qsTr("Optional reason:") Layout.fillWidth: true HTextField { width: parent.width } } } mirage-0.7.2/src/gui/Popups/RemoveMemberPopup.qml000066400000000000000000000034571407747233600217770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string userId property string roomId property string targetUserId property string targetDisplayName property string operation // "disinvite", "kick" or "ban" readonly property string coloredTarget: utils.coloredNameHtml(targetDisplayName, targetUserId) function remove() { py.callClientCoro( userId, operation === "ban" ? "room_ban" : "room_kick", [roomId, targetUserId, reasonField.item.text || null], ) popup.close() } page.footer: AutoDirectionLayout { ApplyButton { text: operation === "disinvite" ? qsTr("Disinvite") : operation === "kick" ? qsTr("Kick") : qsTr("Ban") icon.name: operation === "ban" ? "room-ban" : "room-kick" onClicked: remove() } CancelButton { onClicked: popup.close() } } onOpened: reasonField.item.forceActiveFocus() onKeyboardAccept: popup.remove() SummaryLabel { textFormat: Text.StyledText text: operation === "disinvite" ? qsTr("Disinvite %1 from the room?").arg(coloredTarget) : operation === "kick" ? qsTr("Kick %1 out of the room?").arg(coloredTarget) : qsTr("Ban %1 from the room?").arg(coloredTarget) } HLabeledItem { id: reasonField label.text: qsTr("Optional reason:") Layout.fillWidth: true HTextField { width: parent.width } } } mirage-0.7.2/src/gui/Popups/SignOutPopup.qml000066400000000000000000000042731407747233600207770ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import ".." import "../Base" import "../Base/Buttons" HFlickableColumnPopup { id: popup property string userId: "" page.footer: AutoDirectionLayout { PositiveButton { id: exportButton text: qsTr("Export keys") icon.name: "export-keys" onClicked: utils.makeObject( "Dialogs/ExportKeys.qml", window.mainUI, { userId }, obj => { loading = Qt.binding(() => obj.exporting) obj.done.connect(signOutButton.clicked) obj.dialog.open() } ) } MiddleButton { id: signOutButton text: qsTr("Sign out now") icon.name: "sign-out" onClicked: { const showAdd = ModelStore.get("accounts").count < 2 || window.uiState.pageProperties.userId === userId || (window.uiState.pageProperties.userRoomId || [])[0] === userId if (showAdd) { const page = "Pages/AddAccount/AddAccount.qml" window.mainUI.pageLoader.show(page) } py.callCoro("logout_client", [userId]) popup.close() } } CancelButton { onClicked: popup.close() } } onOpened: exportButton.forceActiveFocus() SummaryLabel { text: qsTr("Backup your decryption keys before signing out?") } DetailsLabel { text: qsTr( "Signing out will delete your device's information and the keys " + "required to decrypt messages in encrypted rooms.\n\n" + "You can export your keys to a passphrase-protected file " + "before signing out.\n\n" + "This will allow you to restore access to your messages when " + "you sign in again, by importing this file in your account " + "settings." ) } } mirage-0.7.2/src/gui/Popups/SummaryLabel.qml000066400000000000000000000004611407747233600207530ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../Base" HLabel { wrapMode: HLabel.Wrap font.bold: true visible: Boolean(text) Layout.fillWidth: true } mirage-0.7.2/src/gui/Popups/UnexpectedErrorPopup.qml000066400000000000000000000056211407747233600225230ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import "../Base" import "../Base/Buttons" import "../PythonBridge" as PythonBridge HColumnPopup { id: root property var errors: [] // [{type, message, traceback}] contentWidthLimit: Math.min(window.width / 1.5, 864 * theme.uiScale) page.footer: AutoDirectionLayout { PositiveButton { id: reportButton text: qsTr("Report") icon.name: "report-error" onClicked: Qt.openUrlExternally( "https://github.com/mirukana/mirage/blob/master/docs/" + "CONTRIBUTING.md#issues" ) } CancelButton { text: qsTr("Ignore") onClicked: root.close() } } onErrorsChanged: if (errors.length) open() onOpened: reportButton.forceActiveFocus() onClosed: { errors = [] errorsChanged() } Behavior on implicitHeight { HNumberAnimation {} } SummaryLabel { readonly property string types: { const colored = [] const color = theme.colors.accentText for (const error of root.errors) { const coloredType = utils.htmlColorize(error.type, color) if (! colored.includes(coloredType)) colored.push(coloredType) } return colored.join(", ") } textFormat: Text.StyledText text: root.errors.length > 1 ? qsTr("Unexpected errors occured: %1").arg(types) : qsTr("Unexpected error occured: %1").arg(types) } HScrollView { clip: true Layout.fillWidth: true Layout.fillHeight: true HTextArea { id: detailsArea readOnly: true font.family: theme.fontFamily.mono focusItemOnTab: hideCheckBox text: { const parts = [] for (const error of root.errors) { parts.push(error.type + ": " + (error.message || "...")) parts.push(error.traceback || qsTr("Traceback missing")) parts.push("─".repeat(30)) } return parts.slice(0, -1).join("\n\n") // Leave out last ──── } } } HCheckBox { id: hideCheckBox text: root.errors.length > 1 ? qsTr("Hide these types of error until restart") : qsTr("Hide this type of error until restart") onCheckedChanged: { for (const error of errors) checked ? PythonBridge.Globals.hideErrorTypes.add(error.type) : PythonBridge.Globals.hideErrorTypes.delete(error.type) } Layout.fillWidth: true } } mirage-0.7.2/src/gui/PythonBridge/000077500000000000000000000000001407747233600167525ustar00rootroot00000000000000mirage-0.7.2/src/gui/PythonBridge/EventHandlers.qml000066400000000000000000000103701407747233600222300ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../.." import ".." QtObject { signal deviceUpdateSignal(string forAccount) function onNotificationRequested( id, critical, bubble, sound, urgencyHint, title, body, image, ) { const level = window.mainUI.notificationLevel if (level === UI.NotificationLevel.Mute) return if (level === UI.NotificationLevel.HighlightsOnly && ! critical) return if (window.notifiedIds.has(id)) return window.notifiedIds.add(id) window.notifiedIdsChanged() if (Qt.application.state === Qt.ApplicationActive) return if (bubble) py.callCoro("desktop_notify", [title, body, image]) if (sound) py.callCoro("sound_notify") if (urgencyHint) { const msec = critical ? window.settings.Notifications.highlight_flash_time * 1000 : window.settings.Notifications.flash_time * 1000 // -1 ? 0 for no time out : msec if (msec !== 0) window.alert(msec === -1 ? 0 : msec) } } function onCoroutineDone(uuid, result, error, traceback) { if (! Globals.pendingCoroutines[uuid]) return const onSuccess = Globals.pendingCoroutines[uuid].onSuccess const onError = Globals.pendingCoroutines[uuid].onError delete Globals.pendingCoroutines[uuid] Globals.pendingCoroutinesChanged() if (error) { const type = py.getattr(py.getattr(error, "__class__"), "__name__") const args = py.getattr(error, "args") if (type === "CancelledError") return onError ? onError(type, args, error, traceback, uuid) : py.showError(type, traceback, "", uuid) return } if (onSuccess) onSuccess(result) } function onLoopException(message, error, traceback) { // No need to log these here, the asyncio exception handler does it const type = py.getattr(py.getattr(error, "__class__"), "__name__") py.showError(type, traceback, "", message) } function onPre070SettingsDetected(path) { window.makePopup("Popups/Pre070SettingsDetectedPopup.qml", {path}) } function onUserFileChanged(type, newData) { if (type === "Theme") { window.theme = Qt.createQmlObject(newData, window, "theme") utils.theme = window.theme return } type === "Settings" ? window.settings = newData : type === "NewTheme" ? window.themeRules = newData : type === "UIState" ? window.uiState = newData : type === "History" ? window.history = newData : null } function onModelItemSet(syncId, indexThen, indexNow, changedFields) { const model = ModelStore.get(syncId) if (indexThen === undefined) { // print("insert", syncId, indexThen, indexNow, // JSON.stringify(changedFields)) model.insert(indexNow, changedFields) model.idToItems[changedFields.id] = model.get(indexNow) model.idToItemsChanged() } else { // print("set", syncId, indexThen, indexNow, // JSON.stringify(changedFields)) model.set(indexThen, changedFields) if (indexThen !== indexNow) model.move(indexThen, indexNow, 1) model.fieldsChanged(indexNow, changedFields) } } function onModelItemDeleted(syncId, index, count=1, ids=[]) { // print("delete", syncId, index, count, ids) const model = ModelStore.get(syncId) model.remove(index, count) for (let i = 0; i < ids.length; i++) { delete model.idToItems[ids[i]] } if (ids.length) model.idToItemsChanged() } function onModelCleared(syncId) { // print("clear", syncId) const model = ModelStore.get(syncId) model.clear() model.idToItems = {} } function onDevicesUpdated(forAccount) { deviceUpdateSignal(forAccount) } function onInvalidAccessToken(userId) { window.makePopup("Popups/InvalidAccessTokenPopup.qml", {userId}) } } mirage-0.7.2/src/gui/PythonBridge/Globals.qml000066400000000000000000000005331407747233600210510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later pragma Singleton import QtQuick 2.12 QtObject { readonly property var pendingCoroutines: ({}) readonly property var hideErrorTypes: new Set([ "gaierror", "SSLError", "MatrixInvalidAccessToken", ]) } mirage-0.7.2/src/gui/PythonBridge/PythonBridge.qml000066400000000000000000000040751407747233600220710ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import io.thp.pyotherside 1.5 import CppUtils 0.1 import "." Python { id: py readonly property var pendingCoroutines: Globals.pendingCoroutines function setattr(obj, attr, value, callback=null) { py.call(py.getattr(obj, "__setattr__"), [attr, value], callback) } function callCoro(name, args=[], onSuccess=null, onError=null) { const uuid = name + "." + CppUtils.uuid() Globals.pendingCoroutines[uuid] = {onSuccess, onError} Globals.pendingCoroutinesChanged() call("BRIDGE.call_backend_coro", [name, uuid, args]) return uuid } function callClientCoro( accountId, name, args=[], onSuccess=null, onError=null ) { const uuid = accountId + "." + name + "." + CppUtils.uuid() Globals.pendingCoroutines[uuid] = {onSuccess, onError} Globals.pendingCoroutinesChanged() // Ensure the client exists or wait for it to exist callCoro("get_client", [accountId, [name, args]], () => { // Now that we're sure it won't error, run that client's function call("BRIDGE.call_client_coro", [accountId, name, uuid, args]) }) return uuid } function cancelCoro(uuid) { delete Globals.pendingCoroutines[uuid] call("BRIDGE.cancel_coro", [uuid]) } function saveConfig(backend_attribute, data) { if (! py.ready) { return } // config not done loading yet callCoro(backend_attribute + ".set_data", [data]) } function showError(type, traceback, sourceIndication="", message="") { console.error(`python: ${sourceIndication}\n${traceback}`) if (Globals.hideErrorTypes.has(type)) { console.info("Not showing popup for ignored error type " + type) return } const popup = window.mainUI.unexpectedErrorPopup popup.errors.unshift({type, message, traceback}) popup.errorsChanged() } } mirage-0.7.2/src/gui/PythonBridge/PythonRootBridge.qml000066400000000000000000000027141407747233600227330ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import CppUtils 0.1 import "." PythonBridge { property bool ready: false property bool startupAnyAccountsSaved: false readonly property EventHandlers eventHandlers: EventHandlers {} Component.onCompleted: { for (var func in eventHandlers) { if (! eventHandlers.hasOwnProperty(func)) continue if (! func.startsWith("on")) continue setHandler(func.replace(/^on/, ""), eventHandlers[func]) } addImportPath("src") addImportPath("qrc:/src") importNames("backend.qml_bridge", ["BRIDGE"], () => { callCoro("get_settings", [], ([settings, state, hist, theme, themeRules]) => { CppUtils.setProxy(settings.General.proxy || "") window.settings = settings window.uiState = state window.history = hist window.theme = Qt.createQmlObject(theme, window, "theme") utils.theme = window.theme window.themeRules = themeRules callCoro("saved_accounts.any_saved", [], any => { if (any) { callCoro("load_saved_accounts", []) } startupAnyAccountsSaved = any ready = true }) }) }) } } mirage-0.7.2/src/gui/PythonBridge/qmldir000066400000000000000000000000421407747233600201610ustar00rootroot00000000000000singleton Globals 0.1 Globals.qml mirage-0.7.2/src/gui/ShortcutBundles/000077500000000000000000000000001407747233600175045ustar00rootroot00000000000000mirage-0.7.2/src/gui/ShortcutBundles/FlickShortcuts.qml000066400000000000000000000031541407747233600231710ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" HQtObject { id: root property Item flickable: parent property bool active: true property bool disableIfAnyPopupOrMenu: true HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.up onActivated: utils.flickPages(flickable, -1 / 10) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.down onActivated: utils.flickPages(flickable, 1 / 10) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.page_up onActivated: utils.flickPages(flickable, -1) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.page_down onActivated: utils.flickPages(flickable, 1) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.top onActivated: utils.flickToTop(flickable) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.Scrolling.bottom onActivated: utils.flickToBottom(flickable) } } mirage-0.7.2/src/gui/ShortcutBundles/TabShortcuts.qml000066400000000000000000000016311407747233600226450ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import "../Base" HQtObject { id: root property Item container: parent property bool active: container.count > 1 property bool disableIfAnyPopupOrMenu: true HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.previous_tab onActivated: container.setCurrentIndex( utils.numberWrapAt(container.currentIndex - 1, container.count), ) } HShortcut { active: root.active disableIfAnyPopupOrMenu: root.disableIfAnyPopupOrMenu sequences: window.settings.Keys.next_tab onActivated: container.setCurrentIndex( utils.numberWrapAt(container.currentIndex + 1, container.count), ) } } mirage-0.7.2/src/gui/TrayIcon.qml000066400000000000000000000031351407747233600166210ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick.Controls 2.12 import Qt.labs.platform 1.1 import Qt.labs.folderlistmodel 2.12 SystemTrayIcon { property ApplicationWindow window property alias settingsFolder: showUpWatcher.folder property string iconPack: theme ? theme.icons.preferredPack : "thin" property FolderListModel showUpWatcher: FolderListModel { id: showUpWatcher showDirs: false showHidden: true nameFilters: [".show"] onCountChanged: { if (count) { window.restoreFromTray() py.importModule("os", () => { py.call("os.remove", [get(0, "filePath")]) }) } } } visible: true tooltip: Qt.application.displayName icon.source: `../icons/${iconPack}/tray-icon.png` onActivated: { if (reason === SystemTrayIcon.MiddleClick) Qt.quit() else if (reason !== SystemTrayIcon.Context) window.visible ? window.hide() : window.restoreFromTray() } menu: Menu { MenuItem { text: window.visible ? "Minimize to tray" : qsTr("Open %1").arg(Qt.application.displayName) onTriggered: window.visible ? window.hide() : window.restoreFromTray() } MenuItem { text: qsTr("Quit %1").arg(Qt.application.displayName) onTriggered: Qt.quit() } } } mirage-0.7.2/src/gui/UI.qml000066400000000000000000000120501407747233600154020ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Window 2.12 import QtGraphicalEffects 1.12 import "." import "Base" import "MainPane" import "Popups" Item { id: mainUI enum NotificationLevel { Mute, HighlightsOnly, Enable } property int notificationLevel: settings.Notifications.start_level === "highlights_only" ? UI.NotificationLevel.HighlightsOnly : settings.Notifications.start_level === "mute" ? UI.NotificationLevel.Mute : UI.NotificationLevel.Enable property bool accountsPresent: ModelStore.get("accounts").count > 0 || py.startupAnyAccountsSaved readonly property var accountIds: { const ids = [] const model = ModelStore.get("accounts") for (let i = 0; i < model.count; i++) ids.push(model.get(i).id) return ids } readonly property alias debugConsole: debugConsole readonly property alias mainPane: mainPane readonly property alias pageLoader: pageLoader readonly property alias unexpectedErrorPopup: unexpectedErrorPopup readonly property alias fontMetrics: fontMetrics readonly property alias idleManager: idleManager focus: true Component.onCompleted: window.mainUI = mainUI HShortcut { sequences: window.settings.Keys.python_debugger onActivated: py.call("BRIDGE.pdb") } HShortcut { sequences: window.settings.Keys.python_remote_debugger onActivated: py.call("BRIDGE.pdb", [[], true]) } HShortcut { sequences: window.settings.Keys.zoom_in onActivated: { window.settings.General.zoom += 0.1 window.saveSettings() } } HShortcut { sequences: window.settings.Keys.zoom_out onActivated: { window.settings.General.zoom = Math.max(0.1, window.settings.General.zoom - 0.1) window.saveSettings() } } HShortcut { sequences: window.settings.Keys.reset_zoom onActivated: { window.settings.General.zoom = 1 window.saveSettings() } } HShortcut { sequences: window.settings.Keys.compact onActivated: { window.settings.General.compact = ! window.settings.General.compact window.saveSettings() } } HShortcut { sequences: window.settings.Keys.quit onActivated: Qt.quit() } HShortcut { sequences: window.settings.Keys.notifications_highlights_only onActivated: mainUI.notificationLevel = mainUI.notificationLevel === UI.NotificationLevel.HighlightsOnly ? UI.NotificationLevel.Enable : UI.NotificationLevel.HighlightsOnly } HShortcut { sequences: window.settings.Keys.notifications_mute onActivated: mainUI.notificationLevel = mainUI.notificationLevel === UI.NotificationLevel.Mute ? UI.NotificationLevel.Enable : UI.NotificationLevel.Mute } FontMetrics { id: fontMetrics font.family: theme.fontFamily.sans font.pixelSize: theme.fontSize.normal font.pointSize: -1 } IdleManager { id: idleManager } DebugConsole { id: debugConsole target: mainUI visible: false } LinearGradient { id: mainUIGradient visible: ! image.visible anchors.fill: parent start: theme.ui.gradientStart end: theme.ui.gradientEnd gradient: Gradient { GradientStop { position: 0.0; color: theme.ui.gradientStartColor } GradientStop { position: 1.0; color: theme.ui.gradientEndColor } } } HImage { id: image visible: Boolean(Qt.resolvedUrl(source)) fillMode: Image.PreserveAspectCrop animatedFillMode: AnimatedImage.PreserveAspectCrop source: theme.ui.image sourceSize.width: Screen.width sourceSize.height: Screen.height anchors.fill: parent asynchronous: false } MainPane { id: mainPane maximumSize: parent.width - theme.minimumSupportedWidth * 1.5 // Drawers (side panes) are actually Popups, which are considered to be // different "layer". Input handlers will only get events occuring // in the layer they were declared in. GlobalTapHandlers { pageLoader: pageLoader } } PageLoader { id: pageLoader anchors.fill: parent anchors.leftMargin: mainPane.requireDefaultSize && mainPane.minimumSize > mainPane.maximumSize ? mainPane.calculatedSizeNoRequiredMinimum : mainPane.visibleSize visible: mainPane.visibleSize < mainUI.width GlobalTapHandlers { pageLoader: parent } } UnexpectedErrorPopup { id: unexpectedErrorPopup } } mirage-0.7.2/src/gui/Utils.qml000066400000000000000000000447731407747233600162060ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import CppUtils 0.1 QtObject { enum Media { Page, File, Image, Video, Audio } property QtObject theme: null property bool keyboardFlicking: false readonly property var imageExtensions: [ "bmp", "gif", "jpg", "jpeg", "png", "pbm", "pgm", "ppm", "xbm", "xpm", "tiff", "webp", "svg", ] readonly property var videoExtensions: [ "3gp", "avi", "flv", "m4p", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogv", "qt", "vob", "webm", "wmv", "yuv", ] readonly property var audioExtensions: [ "pcm", "wav", "raw", "aiff", "flac", "m4a", "tta", "aac", "mp3", "ogg", "oga", "opus", ] function makeObject(urlComponent, parent=null, properties={}, callback=null) { let comp = urlComponent if (! Qt.isQtObject(urlComponent)) { // It's an url or path string to a component comp = Qt.createComponent(urlComponent, Component.Asynchronous) } let ready = false comp.statusChanged.connect(status => { if ([Component.Null, Component.Error].includes(status)) { console.error("Failed creating component: ", comp.errorString()) } else if (! ready && status === Component.Ready) { const incu = comp.incubateObject( parent, properties, Qt.Asynchronous, ) if (incu.status === Component.Ready) { if (callback) callback(incu.object) ready = true return } incu.onStatusChanged = (istatus) => { if (incu.status === Component.Error) { console.error("Failed incubating object: ", incu.errorString()) } else if (istatus === Component.Ready && callback && ! ready) { if (callback) callback(incu.object) ready = true } } } }) if (comp.status === Component.Ready) comp.statusChanged(comp.status) } function makePopup(urlComponent, parent, properties={}, callback=null, autoDestruct=true) { makeObject(urlComponent, parent, properties, (popup) => { popup.open() if (autoDestruct) popup.closed.connect(() => { popup.destroy() }) if (callback) callback(popup) }) } function sum(array) { if (array.length < 1) return 0 return array.reduce((a, b) => (isNaN(a) ? 0 : a) + (isNaN(b) ? 0 : b)) } function range(startOrEnd, end=null, ) { // range(3) → [0, 1, 2, 3] // range(3, 6) → [3, 4, 5, 6] // range(3, -1) → [3, 2, 1, 0, -1] const numbers = [] let realStart = end ? startOrEnd : 0 let realEnd = end ? end : startOrEnd if (realEnd < realStart) for (let i = realStart; i >= realEnd; i--) numbers.push(i) else for (let i = realStart; i <= realEnd; i++) numbers.push(i) return numbers } function chunk(array, chunkSize) { const chunks = [] for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)) } return chunks } function isEmptyObject(obj) { return Object.entries(obj).length === 0 && obj.constructor === Object } function objectUpdate(current, update) { return Object.assign({}, current, update) } function objectUpdateRecursive(current, update) { for (const key of Object.keys(update)) { if ((key in current) && typeof(current[key]) === "object" && typeof(update[key]) === "object") { objectUpdateRecursive(current[key], update[key]) } else { current[key] = update[key] } } } function numberWrapAt(num, max) { return num < 0 ? max + (num % max) : (num % max) } function hsluv(hue, saturation, lightness, alpha=1.0) { return CppUtils.hsluv(hue, saturation, lightness, alpha) } function hueFrom(string) { // Calculate and return a unique hue between 0 and 360 for the string let hue = 0 for (let i = 0; i < string.length; i++) { hue += string.charCodeAt(i) * 99 } return hue % 360 } function nameColor(name, dim=false) { return hsluv( hueFrom(name), dim ? theme.controls.displayName.dimSaturation : theme.controls.displayName.saturation, dim ? theme.controls.displayName.dimLightness : theme.controls.displayName.lightness, ) } function coloredNameHtml(name, userId, displayText=null, dim=false) { // substring: remove leading @ return ``+ escapeHtml(displayText || name || userId) + "" } function escapeHtml(text) { // Replace special HTML characters by encoded alternatives return text.replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") } function stripHtmlTags(text) { // XXX: Potentially unsafe! return text.replace(/<\/?[^>]+(>|$)/g, "") } function plain2Html(text) { // Escape html, convert `\n` into `
` tags and `\t` into four spaces return escapeHtml(text).replace(/\n/g, "
") .replace(/\t/g, " " * 4) } function htmlColorize(text, color) { return `${text}` } function processedEventText(ev) { const type = ev.event_type const unknownMsg = type === "RoomMessageUnknown" const sender = coloredNameHtml(ev.sender_name, ev.sender_id) if (type === "RoomMessageEmote") return ev.content.match(/^\s*<(p|h[1-6])>/) ? ev.content.replace(/(^\s*<(p|h[1-6])>)/, `$1${sender} `) : sender + " " + ev.content if (type.startsWith("RoomMessage") && ! unknownMsg) return ev.content if (type.startsWith("RoomEncrypted")) return ev.content if (type === "RedactedEvent") { // FIXME: this can generate an "argument missing" warning because // QML first gets notified of the event type change, *then* of the // content change. let content = qsTr(ev.content).arg(sender) if (ev.content.includes("%2")) content = content.arg(coloredNameHtml( ev.redacter_name, ev.redacter_id, "", true, )) return qsTr( `` + content + "" ) } if (ev.content.includes("%2")) { const target = coloredNameHtml(ev.target_name, ev.target_id) return qsTr(ev.content).arg(sender).arg(target) } return qsTr(ev.content).arg(sender) } function filterMatches(filter, text) { if (! filter) return true const filter_lower = filter.toLowerCase() if (filter_lower === filter) { // Consider case only if filter isn't all lowercase (smart case) filter = filter_lower text = text.toLowerCase() } for (const word of filter.split(" ")) { if (word && ! text.includes(word)) { return false } } return true } function filterMatchesAny(filter, ...texts) { for (const text of texts) { if (filterMatches(filter, text)) return true } return false } function fitSize(minWidth, minHeight, width, height, maxWidth, maxHeight) { if (width >= height) { const new_width = Math.min(Math.max(width, minWidth), maxWidth) return Qt.size(new_width, height / (width / new_width)) } const new_height = Math.min(Math.max(height, minHeight), maxHeight) return Qt.size(width / (height / new_height), new_height) } function minutesBetween(date1, date2) { return ((date2 - date1) / 1000) / 60 } function dateIsDay(date, dayDate) { return date.getDate() === dayDate.getDate() && date.getMonth() === dayDate.getMonth() && date.getFullYear() === dayDate.getFullYear() } function dateIsToday(date) { return dateIsDay(date, new Date()) } function dateIsYesterday(date) { const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) return dateIsDay(date, yesterday) } function formatTime(time, seconds=true) { return Qt.formatTime( time, Qt.locale().timeFormat( seconds ? Locale.LongFormat : Locale.NarrowFormat ).replace(/\./g, ":").replace(/ t$/, "") // en_DK.UTF-8 locale wrongfully gives "." separators; // also remove the timezone at the end ) } function smartFormatDate(date) { return ( date < new Date(1) ? "" : // e.g. "03:24" dateIsToday(date) ? formatTime(date, false) : // e.g. "5 Dec" date.getFullYear() === new Date().getFullYear() ? Qt.formatDate(date, "d MMM") : // e.g. "Jan 2020" Qt.formatDate(date, "MMM yyyy") ) } function formatRelativeTime(milliseconds, shortForm=true) { const seconds = Math.floor(milliseconds / 1000) if (shortForm) { return ( seconds < 60 ? qsTr("%1s").arg(seconds) : seconds < 60 * 60 ? qsTr("%1mn").arg(Math.floor(seconds / 60)) : seconds < 60 * 60 * 24 ? qsTr("%1h").arg(Math.floor(seconds / 60 / 60)) : seconds < 60 * 60 * 24 * 30 ? qsTr("%1d").arg(Math.floor(seconds / 60 / 60 / 24)) : seconds < 60 * 60 * 24 * 30 * 12 ? qsTr("%1mo").arg(Math.floor(seconds / 60 / 60 / 24 / 30)) : qsTr("%1y").arg(Math.floor(seconds / 60 / 60 / 24 / 30 / 12)) ) } return ( seconds < 60 ? qsTr("%1 seconds").arg(seconds) : seconds < 60 * 60 ? qsTr("%1 minutes").arg(Math.floor(seconds / 60)) : seconds < 60 * 60 * 24 ? qsTr("%1 hours").arg(Math.floor(seconds / 60 / 60)) : seconds < 60 * 60 * 24 * 30 ? qsTr("%1 days").arg(Math.floor(seconds / 60 / 60 / 24)) : seconds < 60 * 60 * 24 * 30 * 12 ? qsTr("%1 months").arg(Math.floor(seconds / 60 / 60 / 24 / 30)) : qsTr("%1 years").arg(Math.floor(seconds / 60 / 60 / 24 / 30 / 12)) ) } function formatDuration(milliseconds) { const totalSeconds = milliseconds / 1000 const hours = Math.floor(totalSeconds / 3600) let minutes = Math.floor((totalSeconds % 3600) / 60) let seconds = Math.floor(totalSeconds % 60) if (seconds < 10) seconds = `0${seconds}` if (hours < 1) return `${minutes}:${seconds}` if (minutes < 10) minutes = `0${minutes}` return `${hours}:${minutes}:${seconds}` } function round(floatNumber, decimalDigits=2) { return parseFloat(floatNumber.toFixed(decimalDigits)) } function commaAndJoin(array) { if (array.length === 0) return "" if (array.length === 1) return array[0] return qsTr("%1 and %2") .arg(array.slice(0, -1).join(qsTr(", "))) .arg(array.slice(-1)[0]) } function flickPages(flickable, pages, horizontal=false, multiplier=8) { // Adapt velocity and deceleration for the number of pages to flick. // If this is a repeated flicking, flick faster than a single flick. if (! flickable.interactive) return keyboardFlicking = true const futureVelocity = (horizontal ? -flickable.width : -flickable.height) * pages const currentVelocity = horizontal ? -flickable.horizontalVelocity : -flickable.verticalVelocity const goFaster = (futureVelocity < 0 && currentVelocity < futureVelocity / 2) || (futureVelocity > 0 && currentVelocity > futureVelocity / 2) const magicNumber = 2.5 const normalDecel = flickable.flickDeceleration const normalMaxSpeed = flickable.maximumFlickVelocity const fastMultiply = pages && multiplier / (1 - Math.log10(Math.abs(pages))) flickable.maximumFlickVelocity = 5000 flickable.flickDeceleration = Math.max( goFaster ? normalDecel : -Infinity, Math.abs(normalDecel * magicNumber * pages), ) const flick = futureVelocity * magicNumber * (goFaster ? fastMultiply : 1) horizontal ? flickable.flick(flick, 0) : flickable.flick(0, flick) flickable.maximumFlickVelocity = normalMaxSpeed flickable.flickDeceleration = normalDecel keyboardFlicking = false } function flickToTop(flickable) { if (! flickable.interactive) return if (flickable.visibleArea.yPosition < 0) return flickable.contentY = flickable.originY flickable.flick(0, 1000) // Force the delegates to load and bounce } function flickToBottom(flickable) { if (! flickable.interactive) return if (flickable.visibleArea.yPosition < 0) return flickable.contentY = flickable.originY + flickable.contentHeight - flickable.height flickable.flick(0, -1000) } function urlFileName(url) { return url.toString().split("/").slice(-1)[0].split("?")[0] } function urlExtension(url) { return urlFileName(url).split(".").slice(-1)[0] } function getLinkType(url) { const ext = urlExtension(url).toLowerCase() return ( imageExtensions.includes(ext) ? Utils.Media.Image : videoExtensions.includes(ext) ? Utils.Media.Video : audioExtensions.includes(ext) ? Utils.Media.Audio : Utils.Media.Page ) } function sumChildrenImplicitWidths(visibleChildren, spacing=0) { let sum = 0 for (let i = 0; i < visibleChildren.length; i++) { const item = visibleChildren[i] if (item) sum += (item.width > 0 ? item.implicitWidth : 0) + spacing } return sum } function getWordAtPosition(text, position) { // getWordAtPosition("foo bar", 1) → {word: "foo", start: 0, end: 2} let seen = -1 for (var word of text.split(/(\s+)/)) { var start = seen + 1 seen += word.length if (seen >= position) return {word, start, end: seen} } return {word, start, end: seen} } function getClassPathRegex(obj) { const regexParts = [] let parent = obj while (parent) { if (! parent.ntheme || ! parent.ntheme.classes.length) { parent = parent.parent continue } const names = [] const end = regexParts.length ? "\\.)?" : ")" for (let i = 0; i < parent.ntheme.classes.length; i++) names.push(parent.ntheme.classes[i].name) regexParts.push("(" + names.join("|") + end) parent = parent.parent } return new RegExp("^" + regexParts.reverse().join("") + "$") } function formatPushRuleName(userId, rule) { // rule: item from ModelStore.get(, "pushrules") const roomColor = theme.colors.accentText const room = ModelStore.get(userId, "rooms").find(rule.rule_id) const text = rule.rule_id === ".m.rule.master" ? qsTr("Any message") : rule.rule_id === ".m.rule.suppress_notices" ? qsTr("Messages sent by bots") : rule.rule_id === ".m.rule.invite_for_me" ? qsTr("Received room invites") : rule.rule_id === ".m.rule.member_event" ? qsTr("Membership, name & avatar changes") : rule.rule_id === ".m.rule.contains_display_name" ? qsTr("Messages containing my display name") : rule.rule_id === ".m.rule.tombstone" ? qsTr("Room migration alerts") : rule.rule_id === ".m.rule.reaction" ? qsTr("Emoji reactions") : rule.rule_id === ".m.rule.roomnotif" ? qsTr("Messages containing %1").arg( htmlColorize("@room", roomColor), ) : rule.rule_id === ".m.rule.contains_user_name" ? qsTr("Contains %1").arg(coloredNameHtml( "", userId, userId.split(":")[0].substring(1), )): rule.rule_id === ".m.rule.call" ? qsTr("Incoming audio calls") : rule.rule_id === ".m.rule.encrypted_room_one_to_one" ? qsTr("Encrypted 1-to-1 messages") : rule.rule_id === ".m.rule.room_one_to_one" ? qsTr("Unencrypted 1-to-1 messages") : rule.rule_id === ".m.rule.message" ? qsTr("Unencrypted group messages") : rule.rule_id === ".m.rule.encrypted" ? qsTr("Encrypted group messages") : rule.rule_id === ".im.vector.jitsi" ? qsTr("Incoming Jitsi calls") : rule.kind === "content" ? qsTr('Contains "%1"').arg(rule.pattern) : rule.kind === "sender" ? coloredNameHtml("", rule.rule_id) : room && room.display_name && rule.kind !== "room" ? qsTr("Messages in room %1").arg( htmlColorize(escapeHtml(room.display_name), roomColor) ) : room && room.display_name ? escapeHtml(room.display_name) : escapeHtml(rule.rule_id) return rule.enabled ? text : qsTr("%1 (disabled rule)").arg(text) } } mirage-0.7.2/src/gui/Window.qml000066400000000000000000000073651407747233600163510ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Controls 2.12 import "." import "Base" import "PythonBridge" ApplicationWindow { id: window // FIXME: Qt 5.13.1 bug, this randomly stops updating after the cursor // leaves the window until it's clicked again. readonly property bool hidden: Qt.application.state === Qt.ApplicationSuspended || Qt.application.state === Qt.ApplicationHidden || window.visibility === window.Minimized || window.visibility === window.Hidden property var notifiedIds: new Set() property var mainUI: null property var settings: ({}) property var uiState: ({}) property var history: ({}) property var theme: null property var themeRules: null property string settingsFolder readonly property var visibleMenus: ({}) readonly property var visiblePopups: ({}) readonly property bool anyMenu: Object.keys(visibleMenus).length > 0 readonly property bool anyPopup: Object.keys(visiblePopups).length > 0 readonly property bool anyPopupOrMenu: anyMenu || anyPopup function saveSettings() { settingsChanged() py.saveConfig("settings", settings) } function saveUIState() { uiStateChanged() py.saveConfig("ui_state", uiState) } function saveHistory() { historyChanged() py.saveConfig("history", history) } function saveState(obj) { // obj should have these properties: saveName (str), // saveId (str, default "ALL"), and saveProperties (array of str) if (! obj.saveName || ! obj.saveProperties || obj.saveProperties.length < 1) return const propertyValues = {} for (const prop of obj.saveProperties) { propertyValues[prop] = obj[prop] } utils.objectUpdateRecursive(uiState, { [obj.saveName]: { [obj.saveId || "ALL"]: propertyValues }, }) saveUIState() } function getState(obj, property, defaultValue=undefined) { try { const props = uiState[obj.saveName][obj.saveId || "ALL"] return property in props ? props[property] : defaultValue } catch(err) { return defaultValue } } function makePopup( urlComponent, properties={}, callback=null, autoDestruct=true, ) { utils.makePopup( urlComponent, window, properties, callback, autoDestruct, ) } function restoreFromTray() { window.show() window.raise() window.requestActivate() } flags: Qt.WA_TranslucentBackground minimumWidth: theme ? theme.minimumSupportedWidth : 240 minimumHeight: theme ? theme.minimumSupportedHeight : 120 width: Math.min(screen.width, 1152) height: Math.min(screen.height, 768) visible: ArgumentParser.ready && ! ArgumentParser.startInTray color: "transparent" onClosing: if (py.ready && settings.General.close_to_tray) { close.accepted = false hide() } PythonRootBridge { id: py } Utils { id: utils } HLoader { anchors.fill: parent source: py.ready ? "" : "LoadingScreen.qml" } HLoader { // true makes the initially loaded chat page invisible for some reason asynchronous: false anchors.fill: parent focus: true scale: py.ready ? 1 : 0.5 source: ArgumentParser.ready && py.ready ? (ArgumentParser.loadQml || "UI.qml") : "" Behavior on scale { HNumberAnimation { overshoot: 3; factor: 1.2 } } } TrayIcon { window: window settingsFolder: window.settingsFolder } } mirage-0.7.2/src/gui/qmldir000066400000000000000000000001301407747233600155610ustar00rootroot00000000000000singleton ModelStore 0.1 ModelStore.qml singleton ArgumentParser 0.1 ArgumentParser.qml mirage-0.7.2/src/icons/000077500000000000000000000000001407747233600147035ustar00rootroot00000000000000mirage-0.7.2/src/icons/thin/000077500000000000000000000000001407747233600156455ustar00rootroot00000000000000mirage-0.7.2/src/icons/thin/account-settings.svg000066400000000000000000000026541407747233600216670ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/add-account.svg000066400000000000000000000021161407747233600205500ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/add-chat.svg000066400000000000000000000002261407747233600200330ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/apply.svg000066400000000000000000000002561407747233600175160ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/bookmark-add.svg000066400000000000000000000011441407747233600207210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/bookmark-remove.svg000066400000000000000000000011241407747233600214640ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/broken-image.svg000066400000000000000000000015371407747233600207340ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/cancel.svg000066400000000000000000000003661407747233600176200ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/check-mark-partial.svg000066400000000000000000000002701407747233600220240ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/check-mark.svg000066400000000000000000000003061407747233600203720ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/clear-messages.svg000066400000000000000000000012401407747233600212560ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/close-view.svg000066400000000000000000000006221407747233600204430ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/combo-box-close.svg000066400000000000000000000002601407747233600213540ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/combo-box-open.svg000066400000000000000000000002571407747233600212160ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/confirm-uploading-file.svg000066400000000000000000000003061407747233600227170ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/copy-link.svg000066400000000000000000000015041407747233600202730ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/copy-local-path.svg000066400000000000000000000017651407747233600213730ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/copy-room-id.svg000066400000000000000000000003201407747233600206770ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/copy-text.svg000066400000000000000000000005121407747233600203200ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/copy-user-id.svg000066400000000000000000000013171407747233600207100ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/cut-text.svg000066400000000000000000000020321407747233600201400ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/debug.svg000066400000000000000000000031131407747233600174520ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/deselect-all-messages.svg000066400000000000000000000004651407747233600225360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/developer-console.svg000066400000000000000000000033401407747233600220130ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-action-menu.svg000066400000000000000000000013301407747233600220370ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-blacklisted.svg000066400000000000000000000010011407747233600220740ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-current.svg000066400000000000000000000005651407747233600213130ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-delete-checked.svg000066400000000000000000000012421407747233600224500ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-delete.svg000066400000000000000000000012421407747233600210640ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-ignored.svg000066400000000000000000000005651407747233600212600ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-refresh-list.svg000066400000000000000000000005731407747233600222370ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-rename.svg000066400000000000000000000015331407747233600210740ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-unset.svg000066400000000000000000000006721407747233600207660ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-verified.svg000066400000000000000000000007041407747233600214210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/device-verify.svg000066400000000000000000000006621407747233600211330ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/documentation.svg000066400000000000000000000004301407747233600212340ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/download.svg000066400000000000000000000004721407747233600202000ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/downloading.svg000066400000000000000000000002371407747233600206750ustar00rootroot00000000000000mirage-0.7.2/src/icons/thin/drop-file-upload.svg000066400000000000000000000003031407747233600215250ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/email.svg000066400000000000000000000004241407747233600174550ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/every-room.svg000066400000000000000000000005131407747233600204710ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/expand.svg000066400000000000000000000002311407747233600176410ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/export-keys.svg000066400000000000000000000004651407747233600206650ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/feature-unavailable-offline.svg000066400000000000000000000002471407747233600237250ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/field-help.svg000066400000000000000000000007631407747233600204050ustar00rootroot00000000000000mirage-0.7.2/src/icons/thin/go-back-to-chat-from-main-pane.svg000066400000000000000000000003651407747233600240360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/go-back-to-chat-from-room-pane.svg000066400000000000000000000003671407747233600240700ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/go-back-to-main-pane.svg000066400000000000000000000003671407747233600221620ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/go-to-room-pane.svg000066400000000000000000000003651407747233600213120ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/ignore-user.svg000066400000000000000000000012751407747233600206320ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-alt-scale-mode.svg000066400000000000000000000007631407747233600222430ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-close.svg000066400000000000000000000005471407747233600205610ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-fullscreen.svg000066400000000000000000000004271407747233600216130ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-pause.svg000066400000000000000000000004621407747233600205650ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-play.svg000066400000000000000000000001741407747233600204150ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-rotate-left.svg000066400000000000000000000025221407747233600216750ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-rotate-right.svg000066400000000000000000000024561407747233600220660ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/image-speed.svg000066400000000000000000000012121407747233600205420ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/import-keys.svg000066400000000000000000000004671407747233600206600ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/invite-accept.svg000066400000000000000000000002561407747233600211240ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/invite-decline.svg000066400000000000000000000003661407747233600212720ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/invite-received.svg000066400000000000000000000006071407747233600214530ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/join.svg000066400000000000000000000005211407747233600173230ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/menu-add-chat.svg000066400000000000000000000005531407747233600210000ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/menu-item-check-mark.svg000066400000000000000000000003061407747233600222700ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/more-settings.svg000066400000000000000000000036561407747233600212000ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/notifications-enable.svg000066400000000000000000000012101407747233600224550ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/notifications-highlights-only.svg000066400000000000000000000023701407747233600243500ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/notifications-mute.svg000066400000000000000000000014431407747233600222110ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/ok.svg000066400000000000000000000002561407747233600170020ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/open-externally.svg000066400000000000000000000004201407747233600215100ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/paste-text.svg000066400000000000000000000007011407747233600204620ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/phone.svg000066400000000000000000000006711407747233600175030ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-fullscreen-exit.svg000066400000000000000000000002701407747233600227700ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-fullscreen.svg000066400000000000000000000002701407747233600220210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-loop.svg000066400000000000000000000011041407747233600206250ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-pause.svg000066400000000000000000000003671407747233600210030ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-play.svg000066400000000000000000000001741407747233600206270ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-restart.svg000066400000000000000000000006061407747233600213460ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-speed.svg000066400000000000000000000012121407747233600207540ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-track-audio.svg000066400000000000000000000004721407747233600220660ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-track-subtitle.svg000066400000000000000000000003001407747233600226060ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-track-video.svg000066400000000000000000000004641407747233600220740ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-volume-high.svg000066400000000000000000000017151407747233600221100ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-volume-low.svg000066400000000000000000000012541407747233600217700ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/player-volume-mute.svg000066400000000000000000000012241407747233600221360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/presence-busy.svg000066400000000000000000000011441407747233600211520ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/presence-invisible.svg000066400000000000000000000015621407747233600221600ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/presence-offline.svg000066400000000000000000000013761407747233600216210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/presence-online.svg000066400000000000000000000012161407747233600214540ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/previously-set-status.svg000066400000000000000000000005601407747233600227220ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-action-add.svg000066400000000000000000000002261407747233600222360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-action-bubble.svg000066400000000000000000000002701407747233600227400ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-action-sound.svg000066400000000000000000000023361407747233600226420ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-action-urgency-hint.svg000066400000000000000000000032471407747233600241300ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-add.svg000066400000000000000000000002261407747233600207630ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-condition-add.svg000066400000000000000000000002261407747233600227470ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-condition-remove.svg000066400000000000000000000003661407747233600235210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-edit.svg000066400000000000000000000013701407747233600211610ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/pushrule-remove.svg000066400000000000000000000006341407747233600215330ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/redo.svg000066400000000000000000000003641407747233600173220ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reduced-menu.svg000066400000000000000000000002301407747233600207360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reduced-room-buttons.svg000066400000000000000000000004471407747233600224540ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/register.svg000066400000000000000000000011521407747233600202110ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reload-config-files.svg000066400000000000000000000005731407747233600222040ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/remove-message.svg000066400000000000000000000007051407747233600213070ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reply-cancel.svg000066400000000000000000000003661407747233600207510ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reply-to.svg000066400000000000000000000004051407747233600201400ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/report-error.svg000066400000000000000000000005121407747233600210260ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/reset-password.svg000066400000000000000000000021071407747233600213500ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/retry.svg000066400000000000000000000006061407747233600175350ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-ban.svg000066400000000000000000000006151407747233600201020ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-create.svg000066400000000000000000000014261407747233600206060ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-forget.svg000066400000000000000000000006341407747233600206310ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-header-copy.svg000066400000000000000000000005121407747233600215360ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-header-deselect.svg000066400000000000000000000003661407747233600223630ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-header-remove.svg000066400000000000000000000007051407747233600220650ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-join.svg000066400000000000000000000003231407747233600202750ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-kick.svg000066400000000000000000000003401407747233600202560ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-leave.svg000066400000000000000000000003401407747233600204310ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-menu-notifications.svg000066400000000000000000000012101407747233600231450ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-pane-expand-search.svg000066400000000000000000000006571407747233600230130ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-pin.svg000066400000000000000000000007251407747233600201320ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-send-invite.svg000066400000000000000000000010401407747233600215600ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-unpin.svg000066400000000000000000000007251407747233600204750ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-view-files.svg000066400000000000000000000004301407747233600214070ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-view-history.svg000066400000000000000000000005601407747233600220120ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-view-members.svg000066400000000000000000000012161407747233600217420ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-view-notifications.svg000066400000000000000000000012101407747233600231530ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/room-view-settings.svg000066400000000000000000000003371407747233600221530ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/search.svg000066400000000000000000000006711407747233600176370ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/select-all-text.svg000066400000000000000000000004451407747233600214000ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/select-until-here.svg000066400000000000000000000005071407747233600217210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-connect-to-address.svg000066400000000000000000000003651407747233600233720ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-ping-bad.svg000066400000000000000000000010561407747233600213550ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-ping-fail.svg000066400000000000000000000003661407747233600215450ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-ping-good.svg000066400000000000000000000007341407747233600215610ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-ping-medium.svg000066400000000000000000000010341407747233600221030ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/server-visit-website.svg000066400000000000000000000004731407747233600224740ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/set-status.svg000066400000000000000000000010541407747233600205020ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/settings.svg000066400000000000000000000026541407747233600202350ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-back-in.svg000066400000000000000000000010071407747233600206260ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-in-insecure.svg000066400000000000000000000006241407747233600215470ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-in-local-http.svg000066400000000000000000000006241407747233600220010ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-in-secure.svg000066400000000000000000000003551407747233600212210ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-in.svg000066400000000000000000000010071407747233600177300ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/sign-out.svg000066400000000000000000000012421407747233600201320ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/start-direct-chat.svg000066400000000000000000000004321407747233600217070ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/status.svg000066400000000000000000000002001407747233600177010ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/stop-ignore-user.svg000066400000000000000000000010561407747233600216120ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/submenu-arrow.svg000066400000000000000000000003631407747233600211760ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/theme.svg000066400000000000000000000006111407747233600174660ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/toggle-select-message.svg000066400000000000000000000004351407747233600225500ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/transfer-cancel.svg000066400000000000000000000003661407747233600214420ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/transfer-pause.svg000066400000000000000000000003671407747233600213330ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/transfer-resume.svg000066400000000000000000000001741407747233600215120ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/tray-icon.png000066400000000000000000000037601407747233600202660ustar00rootroot00000000000000PNG  IHDR\rfsBIT|d pHYsyyCaMtEXtSoftwarewww.inkscape.org<mIDATxݿo]w{c凄ԁ 3#M m 5S" )qmg/DK"g8 ,dcal)I%!RC}^?ܳ(7ecw$uY~>~? $&@b $&@b $&@b $&@b $&"Z={x69MBrhB0{o]gxn f@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@b $&@bКk },:VK5.0 1HL 1HL 1HL 1HL 1бR˽Zj}@.3;w/R; $&@b $&@b $&@b $&@b $&@b RZ"=R˕.Zg߾^ZwyY[v |&7>僈Z9|wkOQk3S:}{OFfUp'?-c8iL' xL9N~pW8,ud2N~x*]L8E)F>"iOÍw^?0Uzn쇽GZ+\j{]?DxΣF?}Lo=ʂ=x~G)߸N/Gi=y8ѕ7{/iw}*+ڃ-?>@wG) t gg\s3x0'?M0NOK\s&'p2ќ4Os '?q<'Of/'?e "cJ ӿG.F=Cy:}&e (NZF?;}h݊SY)[凣|-?8aM'?̮ J0O'?ohpC7Н!]N~PX Y  pp%[8O9h헂e?:e|xZ2>k`%Ƶx~!Z6v? 0t%"_7 `.@KLSZt?5 N&4n'Zf@hIp  ` %@6&sL @.$@&7>,JD\)yT9&>Q&M-|-xSJ2[}MJax(qFx!";p7k_Gė~E8g`l}$^o+õ/|Dwi0]6R8">$08&AwA2 ! Ip6`GhIp:@sL M2 f#4$8 l'$XUK`{WbDm;yIENDB`mirage-0.7.2/src/icons/thin/typing.svg000066400000000000000000000015331407747233600177020ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/undo.svg000066400000000000000000000003621407747233600173340ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/unknown-devices-inspect.svg000066400000000000000000000006711407747233600231540ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/unknown-devices-warning.svg000066400000000000000000000005111407747233600231450ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/upload-avatar.svg000066400000000000000000000010141407747233600211220ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/upload-file.svg000066400000000000000000000014771407747233600206000ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/uploading.svg000066400000000000000000000002411407747233600203450ustar00rootroot00000000000000mirage-0.7.2/src/icons/thin/user-invited.svg000066400000000000000000000006071407747233600210070ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/user-power-100.svg000066400000000000000000000002511407747233600207720ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/user-power-50.svg000066400000000000000000000003401407747233600207150ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/user-power-default.svg000066400000000000000000000012161407747233600221200ustar00rootroot00000000000000 mirage-0.7.2/src/icons/thin/username.svg000066400000000000000000000012161407747233600202050ustar00rootroot00000000000000 mirage-0.7.2/src/images/000077500000000000000000000000001407747233600150355ustar00rootroot00000000000000mirage-0.7.2/src/main.cpp000066400000000000000000000200141407747233600152150ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later // This file creates the application, registers custom objects for QML // and launches Window.qml (the root component). #include // must be first include to avoid clipboard.h errors #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #endif #include "utils.h" #include "clipboard.h" #include "clipboard_image_provider.h" QLockFile *lockFile = nullptr; void loggingHandler( QtMsgType type, const QMessageLogContext &context, const QString &msg ) { // Override default QML logger to provide colorful logging with times Q_UNUSED(context) // Hide dumb warnings about thing we can't fix without breaking // compatibilty with Qt < 5.14/5.15 if (msg.contains("QML Binding: Not restoring previous value because")) return; if (msg.contains("QML Connections: Implicitly defined onFoo properties")) return; // Hide layout-related spam introduced in Qt 5.14 if (msg.contains("Qt Quick Layouts: Detected recursive rearrange.")) return; const char* level = type == QtDebugMsg ? "~" : type == QtInfoMsg ? "i" : type == QtWarningMsg ? "!" : type == QtCriticalMsg ? "X" : type == QtFatalMsg ? "F" : "?"; QString boldColor = "", color = "", clearFormatting = ""; #ifdef Q_OS_UNIX // Don't output escape codes if stderr is piped or redirected to a file if (isatty(fileno(stderr))) { const QString ansiColor = type == QtInfoMsg ? "2" : // green type == QtWarningMsg ? "3" : // yellow type == QtCriticalMsg ? "1" : // red type == QtFatalMsg ? "5" : // purple "4"; // blue boldColor = "\e[1;3" + ansiColor + "m"; color = "\e[3" + ansiColor + "m"; clearFormatting = "\e[0m"; } #endif fprintf( stderr, "%s%s%s %s%s |%s %s\n", boldColor.toUtf8().constData(), level, clearFormatting.toUtf8().constData(), color.toUtf8().constData(), QDateTime::currentDateTime().toString("hh:mm:ss").toUtf8().constData(), clearFormatting.toUtf8().constData(), msg.toUtf8().constData() ); } void onExitSignal(int signum) { QApplication::exit(128 + signum); } bool setLockFile(QString configPath) { QDir settingsFolder(configPath); if (! settingsFolder.mkpath(".")) { qFatal("Could not create config directory"); exit(EXIT_FAILURE); } lockFile = new QLockFile(settingsFolder.absoluteFilePath(".lock")); lockFile->tryLock(0); switch (lockFile->error()) { case QLockFile::NoError: return true; case QLockFile::LockFailedError: { qWarning("Opening already running instance"); QFile showFile(settingsFolder.absoluteFilePath(".show")); showFile.open(QIODevice::WriteOnly); showFile.close(); return false; } default: qFatal("Cannot create lock file: no permission or unknown error"); exit(EXIT_FAILURE); } } int main(int argc, char *argv[]) { qInstallMessageHandler(loggingHandler); // Define some basic info about the app before creating the QApplication QApplication::setOrganizationName("mirage"); QApplication::setApplicationName("mirage"); QApplication::setApplicationDisplayName("Mirage"); QApplication::setApplicationVersion("0.7.2"); QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QString customConfigDir(qEnvironmentVariable("MIRAGE_CONFIG_DIR")); QString settingsFolder( customConfigDir.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + QApplication::applicationName() : customConfigDir ); if (! setLockFile(settingsFolder)) return EXIT_SUCCESS; QApplication app(argc, argv); // Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals signal(SIGINT, onExitSignal); #ifdef Q_OS_UNIX signal(SIGHUP, onExitSignal); #endif // Force the default universal QML style, notably prevents // KDE from hijacking base controls and messing up everything QQuickStyle::setStyle("Fusion"); QQuickStyle::setFallbackStyle("Default"); // Register default theme fonts. Take the files from the // Qt resource system if possible (resources stored in the app executable), // else the local file system. // The dev qmake flag disables the resource system for faster builds. QFileInfo qrcPath(":src/gui/Window.qml"); QString src = qrcPath.exists() ? ":/src" : "src"; QList fontFamilies; fontFamilies << "roboto" << "hack"; QList fontVariants; fontVariants << "regular" << "italic" << "bold" << "bold-italic"; foreach (QString family, fontFamilies) { foreach (QString var, fontVariants) { QFontDatabase::addApplicationFont( src + "/fonts/" + family + "/" + var + ".ttf" ); } } // Create the QML engine and get the root context. // We will add it some properties that will be available globally in QML. QQmlEngine engine; QQmlContext *objectContext = new QQmlContext(engine.rootContext()); // To able to use Qt.quit() from QML side QObject::connect( &engine, &QQmlEngine::quit, &app, &QApplication::quit, Qt::QueuedConnection ); // Set the debugMode properties depending of if we're running in debug mode // or not (`qmake CONFIG+=dev ...`, default in autoreload.py) #ifdef QT_DEBUG objectContext->setContextProperty("debugMode", true); #else objectContext->setContextProperty("debugMode", false); #endif // Register our custom non-visual QObject singletons, // that will be importable anywhere in QML. Example: // import Clipboard 0.1 // ... // Component.onCompleted: print(Clipboard.text) qmlRegisterSingletonType( "Clipboard", 0, 1, "Clipboard", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { Q_UNUSED(scriptEngine) Clipboard *clipboard = new Clipboard(); // Register out custom image providers. // QML will be able to request an image from them by setting an // `Image`'s `source` to `image:///` engine->addImageProvider( "clipboard", new ClipboardImageProvider(clipboard) ); return clipboard; } ); qmlRegisterSingletonType( "CppUtils", 0, 1, "CppUtils", [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * { Q_UNUSED(engine) Q_UNUSED(scriptEngine) return new Utils(); } ); // Create the QML root component by loading its file from the Qt Resource // System or local file system if not possible. QQmlComponent component( &engine, qrcPath.exists() ? "qrc:/src/gui/Window.qml" : "src/gui/Window.qml" ); if (component.isError()) { for (QQmlError e : component.errors()) { qCritical( "%s:%d:%d: %s", e.url().toString().toStdString().c_str(), e.line(), e.column(), e.description().toStdString().c_str() ); } qFatal("One or more errors have occurred, exiting"); app.exit(EXIT_FAILURE); } component.create(objectContext)->setProperty("settingsFolder", settingsFolder); // Finally, execute the app. Return its exit code after clearing the lock. int exit_code = app.exec(); delete lockFile; return exit_code; } mirage-0.7.2/src/sounds/000077500000000000000000000000001407747233600151035ustar00rootroot00000000000000mirage-0.7.2/src/sounds/default.wav000066400000000000000000004042041407747233600172520ustar00rootroot00000000000000RIFF|WAVEfmt DdataXO4P7QFSA\U]Qfcfdktlshj`_WNQAF<=871}/~,r!z&imc^W PKB<01! {{rrgdWUGD;9=?KYfzyrkmx ! 76 HKU)[.`>jHrWziuzthXB' Y6 {kbX^Xc Y]PE6s;  $1:IWi}'Qy)=Vd (Kt "$)"('118H@aFSV_`aca]ZL<#|_Cf/vSC xsB` L;,lT2' yZ,rsD>ycm9r}5w>Kq[wID"m|zEe :aSJI t`h4un)8 Dbga&Rj$7YT+,Q1-?D_P*:3@1}'r `ya' 8mb YZyylNd#\ye<\*C72 )`4$>Nz65Vk2nw|wv^m[YSPtYy?yak  '^\>>U)2Id  L  Y P  ^x  b =KO=;p [  k &. #5iaGTk0-/P=Q|9*oXA6)\?F>F&^4SW'b vJQ x,IrVt zAJZ:  }  = ` > E  92J % ]Vg;q$;%gQ  7 o j; j Y $ rg]c 'v? yU.6we^> !r M8sJ"_pD4+;]o@Q7qhZhpT1N o(Z"IBu>cPPf-> V\!T%4x/ {  mN n P~ ?$  *1Xw|Q122O{X9i5|h< } eQ &F4! A F r +D ? %XTb1/Ab{3_mlG/tk@xvC0`q]KX+FiF*+\/_bFDL Nn@1w.PvrD?H*svm'2*FiT=% O J   ] Ku'   0 p + U  P " _t 3|IcKx<   _C 8 < @ V 1 = ""/ym)'[llV1A/:aQq|`w1| qW; \<:,1~|z/IW,DtH'e`/UvzF mV<tO+w89-Q?-y W` df   z 8n G3 Fp J # A v v V ` Jg q%%3-w9,RPk, r >;   6K,Iaq + ! 2/s D:/5U`7Qs~jtr]AC9xlBzZ@'c*k"Dy/:f~ n&d c>IAobmHIH't sb{@}2`Nx! K    E u  N S0 @v < Q  i 1 ,@  > l&rR zMTq^$DDcTB _(  G i A ,H ,  L ) F11P1%&$xol5N! Rh@ AGFIX}2v +`p`Z(>f h7EM r0.w>   6  u  IG ow`C'ucsI3 aU/Dd  >N  *  Y&   * r H m z  9 9` # O1;_~@v|}YY}\qQlp~-%5)NO:}YEon`dA,p%!L`k:)FYERJ2A.EQCx  c z l   - uC C P  ] Z x :  ,&5}+$!6"M<##I#"-! KwbZ+c  S p$ H  k  g  N    K    x  u   ``nA RD Rk#X!'kp2I6\^d?AQbm~i3F1a*=;4-^JAP4g{{O+ 5~8A9uM:\$M ' V x sl E ?|&,;Hj-w1 mHmJ>a.  @  fJ A3 r'   2i   K0 q C   $p   C }) 0  sFA6pk&eh}pk_]Zs-8=TIh&[`6\E&bXZO5E"lcZ2+D".jL},ux qn[;,qElN#RE mFrz0E + 0   @   ?Lw \ .Eb   ? <X 8<M p    ) S X 8 1 BjV:) ) A TbsO_EC1*a [70.* }GUf7-@G_66~Y?{E|B""#ytdf-Y]}}s5Z&p \Tp . !  b SB 9z5 5 { ~  3i  KS  , h  . T tj *  Z M y 1    g @kiz   !i< +>-U)f z-"4Iz=|++StqwpZTw,}5ap )   C L      ,1k]K u " 6   a  ? ]  J +W    NC96 P hO ? '2O t@}E(2>]PMtC {&uzN(&M'<9DNqxR?\PmV<#cpnWxgSc/nYua^%*Q;d6   $ s8'   p d   q%  & ]  K @ f    X N    9K k L O  6 e^0Sy*E Q Xu  ).R{qfgsjA[4qL3jf-;kO3./ZN~/-Hvbr$~R-&v{]I)RQ]"?>sx)SeJ4AXA^@C Y`8!"I  t  .+ \ Y J  w  _ ? 7 6a  U  F Vok~U Vm  C  2 ma (0r^Q  }9    1 ] m I  H` |voY$H/rD<~h;RQoi6i?5IyPn  4  / ^ @  KjxgbeQ>`xQYjr;XAeicSE%Z?T.)'XX,l6V(Rz:$N"/wT%|*` ]nZe,rg YWf=6:`k n * C  4X /  D hb Y *+ C  4 K   $ N c t v [ o i [ j | !VY49F}kGELi<A qhpENDVG_g/5 sE"yieSY5rrE;Gk9L 5NU'8Bp?IN/fRO8=$FJVd U1\f-g $    @ }h= ]p  q   $ V ] t=    V   J?iB  e$ //@,%J#{tL?W15&Y+LL}KHT6rbCLHZWVY4mOI0WQ"0U8 FuTG KjO"SmW!3]J8"n V [    } 'Qcb9gfsQ,]^3Q8 w A e ]mI#iJuW'F[ T i=yVf>'=Com.M4l9i : (8PJoX5%,'VBgb9DDzu$up"sFuI8NCPj ' < ^ " \  w  /   iR F 5P > Ry> DC6RS6Qtq"    i ? T.\z$W0!ps%nWQ!9p(s4lD)#+X?']|J:nR^?S&29B>18o4-;Bd|T.HjX9(?c mYNC`B ~V>ME"Sd,+?])isL  D8 [ }0  ?  q 4%l{eE0jG*K,Hz0  CbNG&D  , ) n{ % ~ ( d : g@:IQ|#hbF1}aI^q^K[nP:2+Mr+z5 L`>Ci/o ?~[.{0R>s}>TG/ilUvO\/&ZD4b2 h\ - 0K z  k \9 *z   H  Ll;>n1   [ ,Cctv];J5U.[k[ @ d\ $ ` 4'I7ej%VyqhnOh8Z -3f&bH-Pu&xhIA/XV~(/NG#L2fmxc.os3)4}F> O ~? t  :     1 a V   Fp  ON Z \ 30 OM F N  M" L n  7  T= 9EjN<#2<{lwb A G[&GcTM2/e&Is!~[O]YOhA<-}y49(d<Cn}hp$){InI>aTz?Td@+CoRa0cyoH2I- hg e l R  i, / j 3  1 z x Z N ~"   a } l   ,  h9 J I@ov; * e R  e IM Z  e  ,  'm  , * kJfze vOPV50mFwF~>6tUokUMhasKC&2+DBg2D"N+T"ZrM8xn{9{nLxpCY`[i$$56Isl_&0I,|97cC.6Ar     -  S _  2   :{ } EN<nC vQ &  4) O  F *<) & b u " w { g ik  _ cd  , #  czUM4>v1HH%/WZVz|+#va=b9e5K+"K>bE/z5Dt qp`)Yt::|a0/U?*~^!ObDK^{ j e    ' ;  T r    6i  _  y O l n h -S <   H I '  m l  { Q   T ] T~>YuzZ|Q4WRG(e". iF/'J@x0 Y{joN6t<`4mxL%sv*oiG}NIA?AnM zPkxE \     1 q / h :   n 5   k    1 u   ~ V' i  r  P H 4S    s  c   c /k f5*aP180d#x0}w$2jw3v/f`fvRQ/VAetJ2yHR%q1^{^D*sPSKtb=*7 (uT&*dxy) ^ J * e r  A " N  [  m  ) `n  a  $ N   +i n0   9 N vA   _ :r 1 v   :e }~DlUUPz#%HuJR;,iFY#W=} 4zO17\Isri ct:gI*AU=Q%.\/:Z#wlTA)[   P7 #{   a $  3 / } M  XJ h % r    T 1   p f(  w U w  6 ]   gq   K6  [   ~P Ld:Mu,JgPdg\(SJJ! ;~kvn(nqQR!PUgAtzc]Q/&J4Y@s B5*jE42Wte,R9%gy5;\?0Ex< Dt-   B 1   | 2 h X & y   <  \   N = o  J  YO  k) uRmsW&KGM<}> Y"cHV~n<3 i:KW#s;jBH{`lCG 5Xxn )1JdA|Pq[>gKmE_Xx@#tL~"5Sf#M07 bq 8i   q% n  C   G #    L h e e  b  q ? f u  < U { 2 J_Ey.DY-d9@[p\{#/0&\$K,n`D9|P 0~kBpn3 (?r!'Y4os; pR$V4=vZR3uW)' ?gM5kF6GBl5c   B &h * 4 G a   u  ?  @ T f  ` o P  k & 1 g  [ W h GD{FK*E@CZ,#MS bs@D!RPlDMbB] l8z(1-g>.m YQ]W/9QM)\v7I^fo/+tGvAOE z  i  < h  8k  "   4  |   c   ; # ? " S ^ o / b X R * ~ I T  #   ? (   " WTys%K>Xr ]PK!Yf`~ CHvjw>O8[c CACal{ t 5 b]8\GX' i2 vm sDJH"(ybxEXw!n A^6LdhBW*GPPbi$eQ*zo _5' .mf<*ho). w=n(1T_<:9O* 9 z) ~  b  ;9 P $   [. } J s ) D R ( K R G V a  s  T  ^ A  D  D % D _  5g~6Y)5F:+(y$-}(lub^,oW ORTjqF3KfEZEJK"mGb`ZSex% y,$vgd-%0w) T _   $  1( O  a  .G |   T " ( : %  W 6 o q 2 ;    \ <  a 4pj-6A=x&233 5j7(.\ /"8d kidcbi]S\JZVYnbsnoB82wTAMQc1r .Bm!kA/,%d[F(ji6 `p4!Mu_ ktuKsp4 1 h e   [J w   : T \q WM <   kM J I!NIlo#H<g1<,BnJsS(#G)o3{_<QDwxgSqXg*3@[uh9/ve@ "O=`ih$kj[Qdg}^Md D:Ov \h;_#j%BJO@(:Xt_x8Yuy  l m_bkwn:wdKonR{| >?2d4&d^ R;:Re&Q=v377}[6EJ"9HXOZGB6FOero[-O[3^2e1h$`#m'+E"S"K  0mO'jJ<Y ,Ki|/:D[-syYcAJOgD#S_""Y =`&5)g`Ry"D]|c`d42}(h(z+f{e SuW{J m6_]$l{-q]@  ?x! l ,;ULpY F@o20Zi:(^vL(%zR<7hOn}Yj{2\~-Fg: o: V H O, {IkG8Z-Jc^nvW4O^p<Q_G%HYEki WHgu8]a3dhAM+&C}}`CE?f|,Nq{"`V?"`po: _7z+sd*-C/`i`4vx7b!br}YL1 EO }qZc, 9~ K%?duTADpv    W z+ PHg q>(wp|I5+I:mv_?'I9#o:N1*@];;uoEs8o88iN9/2 A.UD}ILB@[}]TNl}XxvJRT *`GWV}@nm>%][^ fsTPL  8 = 8 Y ` 9 m:k  ) K V -   A jw f a T X k  : M 3 ( 0y &D_O6$<,ke? ({x'o {*_^NhiEg{gE\1TP$jvi$~OgU$Yx W:N~9z rh zlSRObdZBV okO(meJt/R_9Em S nx h 2 w   O  ~  <~ gO   d 8 H  r9|XH! ?ax> tC|m2jTgs~EtG_CXOeFV?8a :=nu*}) iMx7071tMy@K?y2%&3>Nw$*kF{no2]yPJf<#O[nrSGm: Ci i O  1 ~I o "   R  s A  $X@aOh;UTW8g" s1>o {:[ k:C CgGMW78b>4=?d!"Tv p.X NV .s7HOU,eBkW-p&->g* tOg):JDF:W@d?f.x##,C Z$)*M   2'U1j/9GT*{KgG4R#TEYe1~wWge ivv]t[Di-r[ -i'`<:(~#B4V{z^?,,qCz[Q>}YDzyU5-eM0u$ fZ]Fd!t<[~u k.LP.)kwqnh6iKjwp n:8q0YPq[F{M0klJj0IB]yV>Q2/%s)~5+9[M/w1ix 4sal#:q) Y3MuNoOm);.>ZJ;'(X:gN%;fkUp)Ta @l #Y<OO^ZkypU;2&\!#6i?I2?*y\oSN9%9R6eR-QX%:6pVZ2"if.K Yi#9xQ7t#.l^-'gB3! jAMn;HgT\k#cY)BW'"(OsceYNtIy4wH 17]Xk Dc       } Ti *L %= &( P- " $  5 d   $ 8< ] p G k F  cpB) EDX(T:BNA-\"m!R W_83a0h!1L\Ri-}T;AV*9vgu~%]{LhD/p`0"t!! +pJt^j."Q3JW"T?}(  X V  " K f d Lz -N 9     H * # ( F z! , 9 T Qk   q SJ g y gF EcxKzAW/TP$ lWs"msi`w0ji/Z TEJq]FjC(SJ de Yia4l#MHM=$ P&w8 |             ! a f   S    C q   w R   z  -O0],D{DLrBt2~A~\RW*W+o-[CD P+R &&.3gOWB>b?p?9Y:irg\5$c.tmh}^9o_O T~:^cVe0^A#Y)|{c/Hi  ? g /n Ev a` eL q1 i" e c1 bQ q   - ^ j   M x    q X,  X UD  [ow'{xJK;N~!Z|h\bSa]FjllYaI%2RBSe4l1ku-XDCJl 3l:*h_&l*PmJLsG =U@V*V &sz$gkQ![r4k#-RkA1&/-0/>0;=6/04F[k W +    A ; ! W | i-F9>m_FoX wu.}Co1t~;1+pME(LS:i`|RH}uK|WB!$666&`*-KsmGTNe;NmuBRAqec}Y]e*',,(9]B>R(^[L4! 3?N\rp<dgX\@sGM*#=qBbUmDG,]8>W[nmzrrIq^> Ip O WLmm<&{VG+Kpft&xmF|0zikT)cz? MhXiXJC1_7C+HSB;+[y>]hrxmZ\6O.}i1]SZgwX\hg\g5(B8@/l'wAk%`&An]+b6k1%CWaa +rIEw*D), %/Ru+Ls_""U] L#qR*!Kf"FovR-r$'jG)B2|/p:c+%1603 cK)1;reqXM+<m77DJXr#k~*JO `caY@#*<T:-jQ40027]ACZpoCXV v6jj5/ KG 8c!W~5S>kFJMqrCf"@}yL13vQ{@Va.N!{ErbH8.<LKGGHQtbktrZ<+w ;rh\z/zrL@0`hZmsnE~$"%7Qdxa&7R(cLcdl`A WtD'mB6Rc<43V{2%ma7T#qMmhW?W3 .CVsj2u Cg @ .8^bl(}]{]N2z2v>rV >$-5KZs9{1?Ikx9^X&phA}wz= eZ;[/2Pqo*bEOp4%\{52|F. u.mVXj(9>Qk!U7x%=L9&o2qI]&c+rM9I(`<!A7M`kUz, uZ6&\ *ABBs=DS]%j(MY^@W#mH&/-QMQO+y SaQ<bA6HC6olH"|5M5'/xG:$DHf+_|pcq'U7zoz1X#dR}|rpXf0(!^MdA,2%}gR~ j; BhU#[^/jBC'y+5.n+wr5>3KQNKUSAV@rIqrbTgr T[z@G|iI26[%:WTB%d."UBg#$)7KqvvvZ,]|_bqNm!r?=#HhX5&lg;?UCQ9BSjNod=tXr,<$* c0AWF/1W`Ho+(*hoFD%,z5&#}d nB@AkJSG!|:~3AjRZ#=!rj =meo eA?x Ki+ 55' cYA-|56U }y oU >>KIm 6]+0"j<R_*eF*i@VOh z&AgF< 6:5o#< *S$d^%?|XPWr{Hcojn|dp !{*IccdY`ejt27USiD8*w NMxA=o}bJK#bc.ux*n\ &#*[-,8=M`q,Nt}|`e3c 3l7w\[S>ruq }amuuc1bB$":PcT8nq}H(ESctJA!lM/ Y=,` R$Qw`YC,#B-]%FSs5`qWN-Qtr<X#('Si%7ABt?*:>4D\/nwK! ,U(q1& g'H,lCJ~Qn>wJ) 7g-|md>s:f0+IKIG$gLN,R{mJ7i5.wXri ]!+ =f>]YLpE7u)pk (8Om_4 h  w/&BSlH#  *@McPq\tNr5^UA:0- nSz@8X7!hU1/:RhiQ+<q/mP!DpN64i?O]5pbYf5o|2[T#6EPZX] C#85HQfttXBC].|f:lA7P^qxVJ .^R"}Kr6rQ~scc8J5vc5s>m=}bC* 6Z0ZN*v;zfBe1#x=jeTyI=556173BY>VY6ou| ((u7n)o.s%(,-, 0Z~-_!83'KHHu]Y^G :'IXllMwhpc$gr|~5 %yH_S:f$l|d>L&n4~m`WH3&0 (7~VoxnaY?I+u{R5 .KXer\HZ!L GFO]yLCs4\ !4D3ISEwAw2}+g&]%D7BD7n<|;<-# 2Op!Nj>\z%- *<TUlqK N  ztxp*m*c"L5jSK yU;)'*d.X944<79IY{}A[H-MF==4HmGia\hVuNz=x4z!y| < l%$"Q AS?c >Jw#Jf-F^n4Piz{vqxnFm vkP3~Q(1 :"</2&!$ ~fxTZ;9&{KvQB-%$uP# 8QguzizMv%lhWUFHANNkq?q.5C%EQBFERb~1Y7wPSYQ>07b&C[}+Ga|  a 6  .RanbJgB[5?8<2 >*6?*1,'<?H7)vT?pI !l2 {|mj^UG* U?vWC0)&+5?GN"L6FU=a,e)h\)^3aNrh>T f^"V2AG0d" (:"PG^]wvx{wf\+KWJJZ|Qp3A\k qJ9*&)-60:$UWmlti\S60 lM  ldI>K!|ozusU#m+}|yeKfO<16$ &D j $ +-AVNnY`RM58%t,9=he~!~G^fT?A<ENSa]cV7HP;m!x .wH_^Yw9}9$i%K ],~pkY;x2 jFVymd\QE+ *@QQP?@)2) :F$]:pFYsxk?&+ \=Y<gw&KV+n 0*F>/?Q/k4Zz,z8k<`.br{cG8%-/CS{aTj!shs_ct^kZ^dVaKs4reF%_(!86U10- *<[rhY94*'(-'/%!+.0'6AP\ej rv=AL9!_5Pk6VlqdV]U^[UJ 2/Zse!T.dBg>DKLjv|[D#xeUQUYX_LC"{nU>E\wmpVk;W A91AQ_p*_L4/5 f $w@efI>(-';Dl\Jm,z%!:St7kv`RP3P@^SjTzY`gys\05{TnmZggsp`L:kuqw{*}.y@bDM\,gzvgHm>_$G)4+ $6+4"&:6;86FmPJt*wV9k>+Ocru~s:ij\G4! 4IPYT_dy{znxz%=Mr(*0(K!`N,Gpz?_^5oM(tkcYJ6{eM7%  ,AcYEj|ut}`e\QU6R,] b$C^sM{ja\S\SXPF> 2#(2)K$U5d@eYiulhsz+HQv/Q]wpr ^'P:2M"V\bcfqo`F$&7QYt}o^L5' xze\^0L@,veLB/61COq_VkBr#r rmijkrpXx4vxtyruwvlwb|b`]aXXS K-QRIgZXon~}|ul6`Q[fUqUaf~}@[ $-<8K?fJw]k y+iB]ONWI_Bf9i8t$z$5FO\^^`NQ:3o\ D5oL8pX/k]Q8*~];fI0 ~xonnwy;Whx?] !3/IMbu"H~"*@H_,iBvHxfuqk_KB1+ %- BFY_p"eQx"fL1 uT7{miced`~eX^L`'_]]]WVWL(IA<5e,2 pYG=(% &ERdr w(}3HzNXtbt`pmrku})P>/mTq Gk 6Kr3E_n&:Vj{yiYD"_yGg0N9 oM-nJ SbFnO6 ^6./7CI^h||}emd1pVx +$[wK87=A@$7h%cM3GmciTZaFrA890*oSA:<_;=6*c=!{h Zl*|D]}+7=>Sc"Mv%:Pmvw)ZR' $P92^0u4v'sd hsL^tj^> Z- nni{!+/$ u_7$ ,`]x[4oGveUS;JrN@@*wZNFA\peuxdM&eT>9: F,SOdnv}mYE+6J/e7If0BhV0U\ZZdqPJKQ,PjG/.nC[WWl"tir[)uhb_rvzx@ncA<%'&$0)Pblf^OMxIpZjpmtjf =#54~$/b:5YrgMF)H' $ <D=7,3=`}qJD8Ce:j *-6/(*@Rl 9SwS%AS^fzoXw2#K`||p{_h`6H:'hMxovm ,tA*RSTTNaR*R ZjziBgG(Usd K%FOA~HMIL'?>7F3S"I+M&D0D;CDHQN[^hiu2MS~ &Szh=; ]t,Gaxm\B"lO$tG3&$#yeRG>vF]ELY7_'pyX"gD`9%;f{w` A0/L$o''=B_q $&:;HVXj\w^URFMQhHy~+7HTpheSB63]{(IzYr`UL>3% qNt5P .oN+ kO#uO yi,KADP \_ceeui~bVY]bi+hCgU\sQ{C- 04KUm!%@N-eSo}~uvc\ V&SKYp^dhbZ:MX8s. :sv[V><8g"$(&$nK5#y^9( s6\=,DQYa\/_`hsiD0" qQ<;)V{!$%/-CG[ejm_+S8>F2^(w$.2I"JOYwOV?C55997<X<3&Bj[D (Y&Ik~bWA:9;@A7-vrimeb\SPEnAK@3;=BCS\ozsMqC-MolS& o#k9SQF^2} &D`{jVH;9=Vc$3@Xr !;G]~if{U5$ /S~`A#NmfJ&99=7394DJWbipmlUk3]XE@24/608$ |m\RPE-GE2Q#[[fcvyF "6wD\LMS.Yis:_mQ5E_7NNPJ<>7@HYan q#n9kKaTX`XfSn]~co~ "FXqktJ)}f M?5Z^8,A`~S8nI)vbSRMXT^SP@8s*M3.4Pj'-5x=WB'U ]zo]H'24F k7t $FPin\{FDAIS ]dkfg U S ??@,J4_B|JHSFSK%T)U(]$`$m'l5zDya~vvnT?g*SM5gvF&9Sbt|sW~:}mpakdrvp]9yk\[RFG2B7CKB^N~JQ?7~{2}@lYYZ?a_]aayjguRUT`ik q(d?TUBf!.j2Obn|&0G K%]H`\b}eXaU[_arpyjZ=/Y:CU rnI-   &/BLMSFb>=/!&*v;nCWHF>9zjX`m h L1% 9DXkvxt[o>v0"%0*1J,h, Fk)8>?PG]Xeln!{(|<9FBJMXhw #!(8JUkr[5zuhc]O1E30=81+-+<2x3_!7qaGI90&|^RE< =-3L5p)r_bXc[ aVH;%Mh,Jm  L`}wslk%m>tZvn*GUmwpN5  "*v*O) -(4={JfbQtOIRROR<7k iT\MXVW_NQ9r-AxgD8 #6_{/KsdYu5uykndqurYR=<72g:6BC3FXGF@?7>5D=U/V6oJrHE="C_CZ}sC&kDET&q}\3  '7>GA=){XK( }hiNZ=/:)7FWo{\-CKgw.cBH^.g qrrrssZRDC6 1#D\r 7c8Uiy~ ~+a/OB+Uf!4(Q9j-|7%4`xK ,>}V@i lR=8%KYhhrv~{q{{}xs[D&~rpshkWnIe;]/N#>$  %pAZl;" 3E}dTt82sFTK/P RN\_{iM:%>e9T|s^^ Q/L\Lu@D43*#" &0>KcmsfaT\\>gas~ p"c4HM.h~TM#nI)LeoNC,!!(@C\et}daG<! fC# viH3 $GhpS/(BZh}s[G.)z=VN5j'U}jQ8 &Lz:e}wtqid`Z`]gqw7RglQ!8Q|{Q( mC9YuwO;#.> dqyfJ$97MXnqS~,~|sdXB24!Qu"+Z;HCN Xgu_I+"Ff{`G .Y-VkN5 .3VVmoosdcZUXZdn},9Sc~gE-_V<poG9'Yoykd S7O@8X2c|wW,  ~cJ4%  ,>Siw'd6Qa8fxxrqy]Q=0@YslB?P 3Vq~l_QIC::403)0*41AFTfo&~;GRY\`bca^UN:,! .EN\~kOu+ e1TC3Vmk~ynXG( $.6kGCP"Za]^RN>A4D:NHRF G,(#$77KYsrcP>(-Oeyc0SYLIGJF;EL;c-f lmmu k\WVg{18@"?*<2*! &7HThwpO{"dI(qj bd"l4j?qHcSTP;RPKTWcauO9*% ?l&4:;9-% !()<*F'I$C!=304IX9Jo|+sJjnj{a]L;{sWN:554HOeou{uqWnFi1brv~eIl1["PKMO^`tnpC~!rvpmrytq}w{wgT>3"!"&/ERisg?) 8Uo#,+IS7E H?!6!&/#G"Z-}>Up$H}&}5^HNM5^$gy vOFh6h.b.o#w zf~H;jzc`A01iB2!!yaNFCJPX[WM 9#*7^7Ljh~K09W:Oi.vC]r$:!V9WLiTgW|PL@GEYi+.1+% -;1Z/sF?F6!?]{tHz~~nJ6hgQD9/,4CW\k-z^~4ujVO:5n)D $ vPuA:2/+*)%% u\!H")13A=OM i"l.EAI9/,P}x!rQjpf^VL3DfB`vyjsT^;U EK<{DG=31 |S4 l?'[++"v7mJscL6 pWBrN ?**$($.'   8]kZNK6VUYhnoyNuQx0MKfv F&Q $Uj+Z3vKp+7E]keP9$z`K8%uQ . Y1_G i+]l9M1QYi@D)|i gOYCNELRWaaikrr]0JcEBNUo'xTe$O-Mh,c7m =mP&$-,7BA^\uzv|pol(hAsFqSN{SySmK\RNB4F+6-r@\*c q+GgS^,;V2ym__kNFT)JTWfs *W*Q5)oGZsOv$V 'V "T,?b/:A84'$~`H(}g`A.yh_JsNR>LK`ul9~xE&|>M(mJx+{ ~dL=,'.LOlx2KYt;m :~_uwqy y@p TwzfS4J\4.  QZ"GC2n*$$#&("(A+M+d/X1Y7C;6>#HJ N]RcRVD<(v`Q 5"15QW}kYw3 h;_Q o6V yP.n[C/}l\>'!B^tqeP.Me88&!KDu$4#BRQ}QSI AM6r1+0 34;fDLOY;Ypaps8OHcn}z,rB[XNq9&sT7p;Gm/}IntK>(b5] 5 ~byPR6-!~hWNtDeES?>;+4/)+6Jh|iQ@27!f+a ;z6Ni-^8!gLmE~ P%{27I/Sgt HpQ Ap AkvbM3)b8n=f#CwyBVG,{TV U(xC$fO)oR0|hKD27o/g6[-K06'#"%6QJ"iCj(W"Y4!IZ^{q{$RF6NiCt>w>z!4MBRk_cfln$rEyvu{3KxefoT2}|tqz~^>yCo%D2DEqE43p5 sO/f6 R+N&^8 qdT9'fBfaG8_1Cs0Z&Rr<*gY!K%f+)8F2ln+LPzgx(Nt[7y8Vm+EZifahSLE66- ',3?EzRTKN76U  Ik@h0iHV2 oEyWV2 iXD};r)^SF:5/f444;BI]fhD$!Fm 7$XK{s/ Q)CoEt(Vr,N| < l3cGn9KeKr/t'QtmbN8+vX~/ [q)jG?`-aq4N kW;9xL#~dI3V!t^T};u+`YMJE\U9Vr}.~JjdVPJFTcX{!'7ZL`l H rAnBxGo 6E^r4r(9^5^vys}%vJppbb*UaRC:*;DURX]du|sUw0i]PME_C/<8. \/f4X,ehH1# t,Ex gUFq4= Y)vnclUMV-JOSXl\y/j9Ym=8)+&>'j"#$+)@QY{/V%sAX{ 3#_`9;i[ -NB}Zn6~ N4`uq^[%FM?,  '!|6b7EA*C MCSHXE#H44'FxEk8|T&\&XpZL;F5!# j<#~fdL;9 +_(70DZrT9&Is5ZFYDl -Fh@b*W B(i[']7Wq&VHAincSD =+3H)i xT< CMuZ{@K! kN2q/h|QK6$ x> }Y5jVD-'fF '=XumWN5))My(S$F!QEzl%CJfk"2=`| >y1[KpXFt>n3uLq]jpex]SG9&d>~`$~{rnZ+HGU;g8; Zt+]G1dCzkwLNB)%dP+{F`kTLLI"QSKNI?D%=NHkQi.M {:UiLm&P){d00cJh(S}A<_An~yvtwt!u%p+g,_3J:D@(KR\[bSYV>$F*2!_$'FpxGFcdPA b1hW4o'N2qeXCE5&!t"c,E@+V q@g>i*C"b3{^z$ J~=Tz 9s)QvM!2JDzRTc_@ffide]LG39(O"j~ '24G=H:0$ }c8 `&IujL:cG!T4 qVN8g-@wP\-;zT*zplVP 67)Sy .$W/qBKXagu=Qx9"XMjx:g+Y;l>UuA^#4@Vg}ybJ+[0~nJo77 hT2"jO& wrj\\2NB3$oG\M*rb A,  )-AKymt{nd`UM2AU=q.5*6>Kf,wJd|-@3h`R{8Lo +=`.s]";Tu:K]~mqmavXy@~<)) !i;dv5c L/P/|hP:#|kZK6mB uT<zaK2dA2 #6nD^_Fl:/##;k%+ *)*(9B;TVoa-G`*y:]y 7l>T{,Nn#?o=`| /IgslZQB9+"[2\3hd@7nD fQ.ucRD4"](|_}Ai0\J9'\5.Ri~o_I?0)91c'@;XT h,b=jUlbmvv#$G9qTet'Q!GxHn!9Oi 1S}.KdxscE. jEjS6Q `?"|O1m]C7|pdM9feNEm5u+ h?i?f.~i[EX3& skTF'ZA}]B!wcO8h]KGBELWg|th^ST;QeZ^fgg%_EXYNyG@AGQbq6X%N 64K|lJl,?%W:pF\Y^RJ4/! '05@RRea|hgbR>1 bCpErl\TLeB)@-,g 8scOr8]#I-~b@'~jZB:%oN ;(!!04LWqy{nmc `_2\Laj`dmits:sQ{ixw|{6Z}""W/CRg9}i!8av!-8BR^q!!,%"!)3.E6R9a8r=r.}.xwslhhejjbh>g `QM|1?+iW3eE~iaREBy-f0D,mN=$}yjdTE9'%9Tl   )@au,15;4934 7GAuH`h4d#Ll"0./6(597:=J;QDjFoPW_grqyjW<hP!twga^\4PSGJAD;9b5O)'+m^A, }lXvL}Cw4DBMN[Xcawb~dUXCC72-,!b6nO,zpjy^kZWPOP@I=J"DG<7-"t `I3) yy{{~nujnfhecf^e ^c;aWbrhgmmmqek_e-\Dchfhtr| }.=cv *Vx #2?IO)TN-R%F-J(A.H(B3L0H=RFNLQ[N^Fk<j2opjugvlvpspgoOj5eb\ZJLa:<:1(.!(y"V;" lT:(y|swzzz-EZw$CS|pztvt|},TlEX  !/,;0K1Q8b5c9o@q@qQxPmdr^hrhm^yX|PID;93,' ueM@ xQ1oW3yfrXjDb8V&M@4#x}zyqgwatR}E:/)$!)%2-0/3K*_-y#' &$ '&-F)_0....169:D_R]q~3Bdu#4.E7VOdZocyvvzhfQN> < .('12;:61s*V?$   rN3}XsDne bSO:1s XD+ sfL}Cn"gZ_W_^fcpjzz~{~ (?[w:U|5 Qu%:G_w#=[s"+>H)]BpSdw)8MXlsrh[HC3*aK% khSQHqEV@68, sU4 f}cT@A4%xoUDt+pf`c[hapq|~|qwq}+yR^~~{zyw9R{;[z&AXu;Ti $ 5A5UEdXvcrqt~{y{(~1LwZpke|TK8, y]B ts_]rV]OFR+GE;/ZH#l\SS:*+ p]Q@.uzope`UJA3,1?&\"f&-%0.,, %>%Z&~(3DTp.Ll ,?2`Eds (;K[t (9;CGG)I7LGFXJjFyB<4.o_@* hR{<g%^LC7) eT2uZT;0y`S>1"W A$z]XGC@?@,D/D;BCBK5[1g'x%'<A5IMWiUfh|.I_ }-A\q &7O` t/{>Yp '<\n  +*<785g'V$A) `Q9!lUC0!{aQ9'sW5 m]RjHG>*6 2)+&%'x%]&U(A)0),41BCX]sw,BZt(0GSi!~:Te8Rr )FHg,nG~\~.=Zo )5=FSuVn`OaId(a[TI>/&y lL<% |kR='rcH6 iE'siYoMSF>8> -:):1ZDII6R$T^ c kv}+Ohs0|/u>]8QF:@+JEKILJED}8d1\ ?8 }{wc`JV)J2*te4+siXR~EbBF=.893<8ADKtXb`Nm=v0& "1AWg{ !$.:GXTtp,&1AMYPp`gqt~7F`r*;N`m|p`Q7,    qSA){s`YlHX>C-,!t^K-o"gKD-%a G .  *%B=Yy]emayKQ6  -7JRed}zsztmotp}y=Tj(@e##;CUe {+?Pfu  *:PX{  &.8HO`gry|~i~P;! }uvgdSqM_?;510  xjO8!pYN60ndPF3+j]N @4' !"./?BV[nt|ztvt{} *:bn #>X&p'EE_^|{2;P]j%}9E^c|).4EEXal{}tcO};}*ttonofi]ZyMbGO570# odG7r]F7qg_PKp?a5H1:)-&*%037C@NJXSh_|u"@Tr,@.Z7oR]s #,=-DHXW]pq}x39IOVW]aar|erdpTLx1/v y lif\YWI~Pl>d9J0H(&jrbiIS;8#$ wh}VzC{3qwts}y/:LYn*,%O0P:tIqYf{*8RY4v2IKY`gkqpvvx$z9D|Vv\pgfiXkNi;h.c`e`hglmmfcYPiFU:G0/(% tkkaVTOG:<11 ~ofXND4'27WTwv 4)H2\DkIZ`n} (8F!L\+a.t6}DIQX[]a[aY]TUQ'I/H:<G5H*UOROJIGAE@D~?q@e7U4H'4' uphPXG@32~pVV=?., #-6@L`n&<"P1c9~JYjy *8"B5JFRYXiYv[^]cblkzuz{ tpl[[DC,1-(:A>Q>O=?22b']$E A*weXA~2qdVB3 tkgbc_[YzQfUUPJS:W7](c)lv}  %3=NXpx  ,!7$]+b26CJThn12CGSci %=ET_elxvvaI: {^O:(p\uHs8f#c[TMD60raSCC:;=;?5?2A9GHTvXvbniljmwftcbUZNQNRQZW ^!a9^Fd[[nex\cabljwt2Ok%8CJWZeo})+<>EJIPQUW_ahpr|}p^T:1 jzVxAr*qopmid^VNBZ=P115!$ +"ucPF75(0&.+*.&0%1(40:}>LwOdsbyynxokmgmmptvw|){>~Pcw(MZx!4CV_px  "#2-A<GNMXWWaXf\keoso|og~^q@y2tx x||~z}smpYkAe,a\\RWKKE<:g,S+7! jdYMMGEGJGPNNVPS[U`hi|}z{}"4Rcz #<Ui & :BX"a*l,z1~:6CBCPJVXYa^ecddjargv~o{yjr[zPs8q+xltppwswqunrk_iJ^4`$V UQKIAB=8y5d/N,6&!$  }ylfc_bdlqv||~ ,HXt&AKjw%/ @CQ"S$X$_-c*f0r3q6z;|@yC{KyHpQsMcPaRZQJVMX6[<`(b%fgjejbj`hbegnbceY^Ib<Y,YRKFC59++)"r#aO=.  *5DQ\p{#1=QXo| &,1":(@5@2L?I>OEMJPJKKVMNLUI\LVAdF^Af9_:f-^&a^_\b`aeeehbda|^b]UUKX7N2O"MA L:A61/"%xhZL=9$% "$29IQal{'9DVaiz #!.''8/-*=.469*851/51*0.3!2%6:7 C:FAGAJBFC?@}9t=`7U=B83<$7;23+(#|ocNH3- }|}yvqqdl _da._5bAcMcWecgof~jhhkjqox}1<Tar  ""$28,<6O4V8b4j;u6w8<7@9>37+)~seZL<3um]}TtIm8a2ZNH:2!nkTN@7.) %,>HSdi} '2E,O1e;rEIUYgmz (, 8<G/E6RKKTReMwP~JOKKMMKMED:3*   %!.&2~,b3^(K,<. xdWQ>u7l*ZOC 81!vl`WH>-! &6@X]ry !.B(O3]?tF{W[pr#$055<BGHMR[[`]qdzbf_bZ\WWQUEN7>($   s m W L:* thsPgL^2K1<0{jaXLI<:+* !9CSg p ,);9KGW)T<gIeXvj|s )="K)`3j3wF@WSa`hehbe[_QVK I D76+   +(6+5+*$q g VF9( wuki`UVHF;;)+" }qaRB2" ,4GSdo % : H#]5e@|Pbn +2L'^)g4<=OF[Q^VZVROFE+70:@'=(MHS SX\acjjhl`cS~SoD`CU6D/4,)p_qSkHW6I/@!&"|q_R@3!#1;JVfu %3*A6PE\Qn`vm!/BP _w{*)01515/0))(-78@>AD>EBAI=C<}3x4e!\#LA0( rdTtFg8T&K> 2(! ykaNG9+# %#67CN[cw|-:N2W9jJxT`nv$8GVer     ))77CFIQLMLFG;B0:n'o*`TP ?:*"ym\L>)to ^VKD:2.%" l_LA0( "!*+38AFPW`jq| )2,B-M?]CkSWelv59U]q} $!'+-358:<=8r9r.g0Z"V#GB6 /#olXwTo>e:Y%UJE=93,)"liP P9='!,$$ *)20:;EJUZdpw".5!B(H:QD\Na\neupw)9BV]ow|njbUTB<1$qh[mQqH]@_2O2KC7 5)$! pn^WPC D 6(:./23:+;-F&C'R M `^pr!)&.-56>DIU,^8bEtOq]dn{}!'68CKVZpjqi\XED/+}nkzZnSZH[=D/F*71,$ zv ihY Y LF@;!1)7.'94:)F+G-R"X+`i#w|"!'-.9<?KG,X4VCcNg^ph|w :3EJQ Xdfu|  yoeXQD:/" xtiqffZWTJF@B1/./  {~oqhc]XLMBA$>+97=99M*(xtgcWQKt@k?[/P/E9.% ntce_ZX U MQAD$>'71<=5B8T-~(mjUTA;/& yyme`PRFCE> C A?A:!=&6-885@7I8W;[>jCsK|OT]]helqs~%%6:HS\jq22BIM_[lmx   }rgYRB:-!z qjgYZLG<2'x|cfQMD54&'~r r f k_aVSIC>2 5*+. %1$'*/()>-5)P-K*a+c.u4|3@>LOWYaanjy {"-3ADRXdsz *8<IQTbbmtx~}qi_UE<,!{whi\WSHI69~!s%j a ZOM@;5.('}vylref`ZXS(S%O3R6L;VLNHX_Q_XoS|WTWY]blqy %,>AVTjjx )*=<QN]^filqv{tyceVP}E}=t)p+s_mT[LDA25%&xzig\XKO=E6=-5%+"" #"" $}%|#&~&~,y-y7t:sDtJlSxXl]}hpixv|z&2>DU\dnr|! 0/=;HFNNRTU\Ud^ckbsdsepdlic{koanfXc\cH[Dc<U4\/R&SLM EGA?::1.( wsdcWQKEA><:867/7*2,1288A@DEHGLLVZej{~%,=<OQ^foz  !%'&2-6596<6;6<6?:?B=E9|Eq6jAa5W>R6C=A7+8/;3701+')"$"!}ok_\RNI>?2-*! $%.1:=FHNUX`go} *:>TTgnp #%%+&-,.339;;>>=@<?<A>?C<AAv8pBi7[?[6G=F51=,595321*.%& up\Z J E=7/)'"    !-.@>OPYddxu "*:@UWkn{     |wgaUND>-)}veeOR;B,; 2'  *(58>KN]^qm  $5=E TXkt|   vs_]KH8/$ uq^_LI<8-( "      ! !#&&,0"< <H"NV"_j$q"!!,*B @TXeo x    !~p#lZ$WK!=$<%*(# xoh[WOEA;32**$"  '%,.38>BKPWafqz+0?DSValo}%%34A?JKTVa_nov{{|tlc_OO{9<s&w&kib[YNLFA=51)%yxgiZZMQBJ>B@;>:;898;::@=HzBOtL}WrW}bq^ork|{w{{x}{}} %.7BDRU\hkt~ &*3<?JMRYVd]lgts}~}pma\TLF<{4/t{!t m oac\RND?83+'&~ }uno`cU[JSCMAIBFFFGIHLJNOPXV_~aiylwyu~ !42DFQZ`fotw }('4/C;NHWT_Zcaekisqwxzxyvxu}v|vpvlycv]zRrMuDn>g7j0Y'_"RPN CG;:3,'! ~}rvhpcj_c]][YXXX\[]|fv_wltgrlvqppuvt}u{ttrtpus{~%'08?DRQ_ai nuv&19@MI^Zahjqxwx{ksba]RPGA43!  { vnf\VJF?46$)tylpjbg^a[_W[{[rXq\cYl^YYcbT`V`OnKfLtBtDyACBAGDPFTJUOVX[_hlt| $.4=FLSYadnq{ '37FKY_grw|tufi[WNJ?=// |oi]TMB96&$  trjeb]ZYOVKNKKKLsNlMcO_STOSYIRF]CX MI!U$W2`9dBrJqWWggpzy 01@HQ[akny~~qoc\TIE92+ uxfd[MP>{=r2o'f$c]XVPOJHF?F8J4O7P?OKzMySjPpWaXd\]eYfVpSsQ|PIQCSCNJNMSQXVZ\cajmr+|2{<CMQcanqv!%+99ELP]`int||wygmX]GL:8.' ~ytmfbXWHI8:}(x)mje`[ZRSRKLHJEGHHKNLUQXU]x[wdpdoikontfyldkegebfcfigppvx~} %-5;GNTaanqz{##-26=@GOQ]]ihto{ty~|slf]VMH~=9}3y$|'rwmleb[XNLF?@24(( xulmbe\`YZWTUOUMYN[T][a`dh~hn}p{s~zz}|~" 01;AGQV[chns w z"1,>;GJOTS`Wh`mmpxru{~u|jq}be}[TzTGvDz<p.q2ii"a_Z YPQHFA86*({}xtyovpqnplmmknqmwt{| !)(92@CIPVW bcj#n"o-t,w8y6~A~@KLVY^henopvryw|{xztnodf~Z\QQ{I~E{@x8v5r)o/lfe[[UOKG>A35*&|||~y|zz}~ .(68;CHGWL`Xdgk"p(t/w7{:yBEwJP}Q[Xg_pmuwyy{}y{ru|jngf`\VUJI@|;7x0t+v'm!mkcc `VWMHE;81-%# }}|x|y{|{} $$1.:;?GJKSQX [^cf!j"n2p1q=w@sHwMxUuQx_uVvfs^ulrkssuwl{t}kekX`T~TMMCC=63,'"{szkrhgg]`SUJG=:/0$# |{zywyzz} #+,46?@INRY^biinqsvu ~ x{ +,7:@IKPSU[\_}exg|nqptujvfzb|X|Y~L}P|D|E~;x90x+'yu}rqnfia`^ZYTNKE?76+)# ~ywwrzoyrwuvyu{x}| "%12=@DNPV\aekpr}z ,*66>?DCIHPyN~VtVs[k]hc^abdOgRbKi@cDg3c4i-a"l bja g\bZWVTNSLKJCC?86/+' yr{ hufmgfeddddcffjfohvrwz &'06=CLNY]_jitw{  !''1.687<:;?=|?vFqBmJcJbKUMVJKMGJCI:L;C,M-C#IFEH AE=A7:/4.0,'*!% ~|xrpliihehbgcdhgilinqpyy} +&5;BKOP^Xfemqyy "##%%,'3-646885<4y;u8v7g<p6Y>d5P?O4L;>7B3364.*0$-+**$+'  {}xsvjufpilnkqmtqtxy %&4/C<PIY Vcbmlvu } &&&,*1256<8><<A9D<C@@A~Bw?x?l=r=b:h=\7[;T6P9F3D5=17-41(&++"""  }z~r{rothqlmompun{ryz} %#32@@IMTS[] ^ keso{v} #%$&%(&)*(~,)v,,n+w-h*o/c)g-[+_+U*W(N(N(D'B(:&4&0&'""#     %&,/67>BHJRU[^`ee hl ipp pu puspsponnikhdg!`^!WUN#KE#A">8%71#1&"( $%   "$,-54<<CDNHYO\Ya]fdghh j jl ikmhkfgdb \_!U XR&OP(H G(C#;)<%3)1(((+(')* % ,#-$*'&)$%$""   ((048BAHMPR[Sd]fegjmnprqwpvq tq qppnk kfg`]Z!SS"J$L$C#E);%;*2)/*+, *#2)3 + /0..,-,-+*(+$&!!  !*-08>@GLQT_Yidkomttu{w~{~ ~ {~rznqmfm]eXXRKJC=</0(     ||{uv~}{} #%+44A@I NQ X[^ffmnsv v*"y0)}4,5/959;:>~>=zB>v@x>t=m@o<dCe9`FY9\DO=N=L@@6E<3397(0/0,!.(, $(!  ~{z{~ #%,19:ECPL[S"d&]'i1e,o:n7uBs@}JwJR|RZ[]e^j}dkzjmzo{nyvworuqrmsjqetal^q[mSkTpIeLo@`@i8a2\/`$P$XIKF @=<17&* zx{{{zxyyzz *(45=@EKQTZ"Zb,a,i5c6pBkArLtOtUx_{`wh~lznzt~szx{{{zxu{kyjljdd`YZOPKCE:;1}3x#~(iudd c WYQNHC:6.' ||}zx|{}#**-54A=JFPQUX\&^$_0f-b7k9fAlBkNlJnXiWo\jan_jimahqjciuegevaq^t]x[vSxW|IuOxCvDu?t:s8o3q/n+m#j#idd[ ] RVIMAE9:1/("  &!+,.54:=>EFIL NRSW"V[*Y'^.Z2^5^8]B[>`KUEbOSR^NRZZQOXVZPXM\M]D\G`Aa>c=`4e9^0d/\+_%[&]Y\U[QWLMEB@977*3%%$  "%',-257:=@?F B JFK LK(R#L0R,P3R7R4OAU;KFTBJEMKHHJOCOIN;UEO:T>R8T7S2U3Q*Z+M'_J%\KRKM D M<K8D878+3#,   #"*+.61=9=C? G FJFRE&UO)P&V-P1W0S<U5UBP?V@MHUBMIPFNMJEKTDDETAI?T9M:R3M5Q0M+M-K$I"K!EID BA> 9?/9&5-!   "$(--66 9 C9MAL L&L&R-P2W0Q=]8S@_DXA\L]J[J]T[L\XZT\WW]ZVVbQVT_OZL[N^FYG]F\:Y@_5X8[.W0T'V(O#NLDI> @9<09(/#%  !")+12;4F; JGKSN!XU&Y&\.],_:`6a>fAbCiHcIgLiPcPhSdYcTc`aW_`^_\aX^VcU^MbQ_I`H`Cb@Y9d;U0c3U)Z*V Q"RNFL > C>6;01,'#!  #*-279@?FG LNOVV"Z ],\(b0`1d9a9h?a@kCbGiGdMfHcRdN`SeT]U`UYZZUW^TUR_MTM^GRHY>U?S8T4T3N(V+J#R GID?A867,2')!! $ ,*037:? ?EHETFX%O!V+Z*V/_2\5]8`;Y@a?[H`A[N_E]O\L\OZOXQWTQORYNPLXJUISAVEP<R;Q8M4P/J.M(I$I"GDBC :<640/%+"  "&)-2188:C;K@M!MI(U$K+Y(O1Z.Q5Y7U:S?W?QBXENFXHOHRJQJJLSJCOQL>OLN>PBL;T<F7R3H6H'L2@"J%? H:E8>843.-(%#   #!),,070@3B 8E=F DG'FM,D#Q0E*L1K6F2L;G;G8IEC7FIC>@FE@8DEC2G>C1I3D0J+E-E&F+D@'E;C<=< 99623+/&' " #$%(+,2/83<7= : ;A8G6H: F>"@#B";&A'7+=)6-8+535*285)/81-+2*3(-$5- 5030.2* /( ,')"''   ($&2&7-63:5 :< 9?<=A>C;E#9E&; @&=);%=*8+;'3/5'1/0*1,*-*+*/",(,.$*,&.!- %%+)$   %$-(1.63<6C<H@ IC L FMILPJSMNR"H$R%G%L+K)E+L2?+F6>/@5:2876/1911*5&7)47'74927/ 22-1+/(,'(%#%"     )%30797D;IEHOOQVUYZ[[^\ `^b\e ]b\]YYVRUMN!KH"F!B!= >"28#*/##!%"$$!    %&2/9;>BGEMPNYT`Xe [g `g c heigghdgbdc]dW`TUQOILAF:=25,('      %.+.:3?=CEJJN QV U \ X b [e [g]c_bbaa`^`[\WWSRNIN@I9@632(, $      $%-+478==FAJHMOOS VV YW\X ] X X\U\ UYVV UQSJPEKBCA=;7 31* * #        "%)-.449<=?BDDKDNILOIPJLOIPHNIHKCIAC>=:842/,-#*%           !%$$/'30373=4=:= ?> A?D <C =? ? ; > 7 > 3 = 2 82 03 (1 # )"         ! $)&*- +0 11 56 5:5=6:< 8?:(9<(:&8&=+1(?,.,:*011+00*.+1)-%4** 7#+ 3-+.-'2 3,"   $!''--- 41 596<;!<>%=">&?)>)>->->0>2>1>8;4@;7:@93@>;3>8A3<3A/?1;*C*>'?"@#<@<<;7;1 8 .4,0)*'%   !'%(*,1.61<2>8=>; @ ??C=E;F:E#:"A)<'<-:.:/4273521825,7.:*5&?&7>%9><>>< =<< :;6<58508*7'1&*%$!     $)'-.03679=9D<FBC GCG FEGFEEEB E"A%C#<+A%8.=-7)734*24.//0'1+3 2%3114/2 3 - 5)1())*$   !%#++.0568=:AAB E GG IKHNKL M"K'O&I-O'I1K-J3D0K7A2G;>3C9<8>8=949891<.:.9*9"8(76!36/ 7-3 +,,!.%   $+% ,0, 90896>="< C&?$D+C*A.H/B.D4E1D6D7H7>=H8?AD9;AB;7?><6A::4E580D09,@,;%;*=5$:3411, 0 '.!( !   !%) &1&5)5.4!4!6#7(:'5+?-7.83A008D259;8:;9:6>:=2>4B5;-@3;,@*=-<'@'9'@!8!<#58 605.--&&&   $+  +!,(,+-- 1"/%0'1,0-3,342/233631290628.<25+A/4+C)6-?#8+?!7&? 4!>3=18504*0+*&'"#    $%"' $('+(-)/+ /)#.$*$,')*,&(-,*(,(.(,'-#0(/2+.3$.10//..+, + (+$+!*%    %('"' % (&+$,!+#'"%#'%$("&%#,"$- ',+,,,-,.*/ + +/ (+)('(!& $      "!!##%'%)'(',& - '**)(*(&(#( "%""$#!#!#$%&'%($ *$*$& '%#$ !"   !!&#(((.+-1.41 3 42609/7/7/"30#."3%*%2'(&.+&&(,&'!+$(-#)0!)/*-(+% (&&'%$""   !"'#,%,*,. ./1 1023120/$3+"1#.#-),$/*()--%.&,(2.'12!/5-5-410 0 0/,0'-('("$    !& +#-%, + ' 0& 0)-+-*-'/$&.%'( &##&($'%)&#)+$/#+'&'%% ' %$$$&  "## ## &!%$!'"%##% ' %$ & '."(')%,$+(&, " .#'( (& !   %%$&(((*,+--,/0, 3,0 ..-,,',%,"+ )""$#%"'!%## # % $!"#    !")+!(*'+*), (- & + '(+%-#*$!&& #$"! !!"  ##& $!     mirage-0.7.2/src/themes/000077500000000000000000000000001407747233600150555ustar00rootroot00000000000000mirage-0.7.2/src/themes/Glass.qpl000066400000000000000000000423751407747233600166570ustar00rootroot00000000000000// vim: syntax=qml // Base variables real uiScale: window.settings.General.zoom int minimumSupportedWidth: 240 * uiScale int minimumSupportedHeight: 120 * uiScale int contentIsWideAbove: 472 * uiScale int baseElementsHeight: 36 * uiScale int spacing: 12 * uiScale int radius: 4 * uiScale int animationDuration: 100 real loadingElementsOpacity: 0.8 real disabledElementsOpacity: 0.3 fontSize: int smaller: 13 * uiScale int small: 13 * uiScale int normal: 16 * uiScale int big: 20 * uiScale int bigger: 32 * uiScale int biggest: 48 * uiScale fontFamily: string sans: "Roboto" string mono: "Hack" colors: int hue: 240 real intensity: 1.0 real coloredTextIntensity: intensity * 71 real dimColoredTextIntensity: intensity * 60 int saturation: 60 int bgSaturation: saturation / 1.5 int coloredTextSaturation: saturation + 20 int dimColoredTextSaturation: saturation real opacity: 0.7 color weakBackground: hsluv(hue, bgSaturation, intensity * 2.5, opacity) color mediumBackground: hsluv(hue, bgSaturation, intensity * 7, opacity) color strongBackground: hsluv(hue, bgSaturation * 2, intensity, opacity) color accentBackground: hsluv(hue, saturation, intensity * 42, 1) color accentElement: hsluv(hue, saturation * 1.5, intensity * 52, 1) color strongAccentElement: hsluv(hue, saturation * 1.5, intensity * 72, 1) color positiveBackground: hsluv(155, saturation * 1.5, intensity * 65, 1) color middleBackground: hsluv(60, saturation * 1.5, intensity * 65, 1) color negativeBackground: hsluv(0, saturation * 1.5, intensity * 54, 1) color alertBackground: negativeBackground color brightText: hsluv(0, 0, intensity * 100) color text: hsluv(0, 0, intensity * 85) color halfDimText: hsluv(0, 0, intensity * 72) color dimText: hsluv(0, 0, intensity * 60) color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity) color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity) // Example of an animation, set running: true to enable NumberAnimation on hue running: false from: 0 to: 360 duration: 10000 loops: Animation.Infinite icons: string preferredPack: "thin" // "transparent" to disable colorizing color colorize: hsluv(0, 0, colors.intensity * 90) color disabledColorize: "white" int smallDimension: 16 * uiScale int dimension: 22 * uiScale // Generic UI controls controls: scrollBar: int width: theme.spacing color track: colors.strongBackground color slider: colors.accentElement color hoveredSlider: colors.accentElement color pressedSlider: colors.strongAccentElement int sliderPadding: 2 int sliderRadius: theme.radius box: int defaultWidth: minimumSupportedWidth color background: colors.mediumBackground int radius: theme.radius popup: int defaultWidth: minimumSupportedWidth * 1.75 color background: colors.mediumBackground color windowOverlay: hsluv(0, 0, 0, 0.7) header: color background: colors.mediumBackground button: color background: colors.strongBackground color text: colors.text color focusedBorder: colors.accentElement int focusedBorderWidth: 2 color hoveredOverlay: hsluv(0, 0, 50, 0.2) color pressedOverlay: hsluv(0, 0, 50, 0.5) color checkedOverlay: colors.accentBackground tab: color text: controls.button.text color background: controls.button.background color alternateBackground: hsluv( colors.hue, colors.bgSaturation * 1.25, colors.intensity * 4, Math.max(0.6, colors.opacity) ) color bottomLine: background color focusedBorder: colors.accentElement int focusedBorderWidth: 1 color hoveredOverlay: controls.button.hoveredOverlay color pressedOverlay: controls.button.pressedOverlay color checkedOverlay: controls.button.checkedOverlay menu: color background: hsluv( colors.hue, colors.bgSaturation * 2, colors.intensity, Math.max(0.9, colors.opacity), ) color border: "black" real borderWidth: 2 menuItem: color background: "transparent" color text: controls.button.text color hoveredOverlay: controls.button.hoveredOverlay color pressedOverlay: controls.button.hoveredOverlay color checkedOverlay: controls.button.hoveredOverlay checkBox: color checkIconColorize: colors.accentElement color boxBackground: controls.button.background int boxSize: 24 * uiScale color boxBorder: "black" color boxHoveredBorder: colors.accentElement color boxPressedBorder: colors.strongAccentElement color text: controls.button.text color subtitle: colors.dimText listView: color highlight: hsluv( colors.hue, colors.bgSaturation * 2, 0, colors.opacity / 2, ) color highlightBorder: colors.strongAccentElement int highlightBorderThickness: 1 textField: color background: colors.strongBackground color focusedBackground: background int borderWidth: 1 color border: "transparent" color focusedBorder: colors.accentElement color errorBorder: colors.negativeBackground color text: colors.text color focusedText: colors.text color placeholderText: colors.dimText textArea: color background: colors.strongBackground int borderWidth: 1 color border: "transparent" color focusedBorder: colors.accentElement color errorBorder: colors.negativeBackground color text: colors.text color placeholderText: controls.textField.placeholderText toolTip: color background: colors.strongBackground color text: colors.text color border: "black" int borderWidth: 2 progressBar: int height: Math.max(2, spacing / 2) color background: colors.strongBackground color foreground: colors.accentElement color pausedForeground: colors.middleBackground color errorForeground: colors.negativeBackground circleProgressBar: int thickness: Math.max(2, spacing / 2) color background: colors.strongBackground color foreground: colors.accentElement color errorForeground: colors.negativeBackground color text: colors.text real indeterminateSpan: 0.5 // 0-1 slider: int radius: 2 int height: controls.progressBar.height color background: controls.progressBar.background color foreground: controls.progressBar.foreground handle: int size: 20 color inside: hsluv(0, 0, 90) color pressedInside: "white" color border: "black" color pressedBorder: colors.strongAccentElement avatar: int size: baseElementsHeight int compactSize: baseElementsHeight / 2 int radius: theme.radius hoveredImage: int size: 192 color background: hsluv(0, 0, 0, 0.4) background: int saturation: colors.saturation int lightness: Math.min(50, colors.intensity * 23) real opacity: 1.0 letter: int saturation: colors.saturation + 20 int lightness: colors.intensity * 60 real opacity: 1.0 displayName: int saturation: colors.coloredTextSaturation int lightness: colors.coloredTextIntensity int dimSaturation: colors.dimColoredTextSaturation int dimLightness: colors.dimColoredTextIntensity presence: color online: colors.positiveBackground color unavailable: colors.middleBackground color offline: hsluv(0, 0, 60, 1) color border: "black" int borderWidth: 2 * uiScale real opacity: 1.0 real radius: 6.0 * uiScale // Specific interface parts ui: // The background image can be an URL or local file path // (in the form file://, e.g. file:///home/user/images/foo.png). // If not specified, the gradient will be shown instead. url image: "" point gradientStart: Qt.point(0, 0) point gradientEnd: Qt.point(window.width, window.height) color gradientStartColor: hsluv(0, 0, 0, 0.5) color gradientEndColor: hsluv(0, 0, 0, 0.5) mainPane: color background: "transparent" topBar: color background: colors.strongBackground color nameVersionLabel: colors.text accountBar: color background: colors.mediumBackground account: real collapsedOpacity: 0.3 color background: "transparent" int avatarRadius: controls.avatar.radius color selectedBackground: colors.accentBackground real selectedBackgroundOpacity: 0.3 color selectedBorder: colors.strongAccentElement int selectedBorderSize: 1 unreadIndicator: color background: colors.strongBackground color text: colors.accentText bool bold: false color border: Qt.darker(text, 2) int borderWidth: 1 int radius: theme.radius / 2 color highlightBackground: colors.strongBackground color highlightText: colors.errorText bool highlightBold: false color highlightBorder: Qt.darker(highlightText, 2) int highlightBorderWidth: 1 int highlightRadius: theme.radius / 2 listView: color background: colors.mediumBackground real offlineOpacity: 0.5 account: real collapsedOpacity: 0.3 color background: "transparent" color name: colors.text int avatarRadius: controls.avatar.radius int collapsedAvatarRadius: controls.avatar.size / 2 room: real leftRoomOpacity: 0.65 color background: "transparent" color name: colors.text color unreadName: colors.brightText color lastEventDate: colors.halfDimText color subtitle: colors.dimText color subtitleQuote: chat.message.quote int avatarRadius: controls.avatar.radius int collapsedAvatarRadius: controls.avatar.radius unreadIndicator: color background: colors.strongBackground color text: colors.accentText bool bold: false color border: Qt.darker(text, 2) int borderWidth: 1 int radius: theme.radius / 2 color highlightBackground: colors.strongBackground color highlightText: colors.errorText bool highlightBold: false color highlightBorder: Qt.darker(highlightText, 2) int highlightBorderWidth: 1 int highlightRadius: theme.radius / 2 bottomBar: color background: "transparent" color settingsButtonBackground: colors.strongBackground color filterFieldBackground: colors.strongBackground chat: roomHeader: color background: colors.strongBackground color name: colors.text color topic: colors.dimText roomPane: color background: "transparent" topBar: color background: colors.strongBackground listView: color background: colors.mediumBackground member: real invitedOpacity: 0.5 color background: "transparent" color name: colors.text color subtitle: colors.dimText color adminIcon: hsluv(60, colors.saturation * 2.25, 60) color moderatorIcon: adminIcon color invitedIcon: hsluv(0, colors.saturation * 2.25, 60) roomSettings: color background: colors.mediumBackground bottomBar: color background: colors.strongBackground inviteButton: color background: "transparent" filterMembers: color background: "transparent" eventList: color background: "transparent" message: int avatarSize: 56 * uiScale int collapsedAvatarSize: 32 * uiScale int avatarRadius: controls.avatar.radius int radius: theme.radius int horizontalSpacing: theme.spacing / 1.25 int verticalSpacing: theme.spacing / 1.75 color focusedHighlight: colors.accentBackground real focusedHighlightOpacity: 0.4 color background: colors.weakBackground color ownBackground: colors.mediumBackground color checkedBackground: colors.accentBackground color body: colors.text color date: colors.dimText color localEcho: colors.dimText color readCounter: colors.accentText color redactedBody: colors.dimText color noticeBody: colors.halfDimText int noticeLineWidth: 1 * uiScale color quote: hsluv( 135, colors.coloredTextSaturation, colors.coloredTextIntensity, ) color link: colors.link color code: colors.code string styleSheet: "* { white-space: pre-wrap }" + "a { color: " + link + " }" + "p { margin-top: 0 }" + "code { font-family: " + fontFamily.mono + "; " + "color: " + code + " }" + "h1, h2, h3 { font-weight: normal }" + "h1 { font-size: " + fontSize.biggest + "px }" + "h2 { font-size: " + fontSize.bigger + "px }" + "h3 { font-size: " + fontSize.big + "px }" + "h4 { font-size: " + fontSize.normal + "px }" + "h5 { font-size: " + fontSize.small + "px }" + "h6 { font-size: " + fontSize.smaller + "px }" + "table { margin-top: " + theme.spacing + "px; " + " margin-bottom: " + theme.spacing + "px }" + "td { padding-left: " + theme.spacing / 2 + "px; " + " padding-right: " + theme.spacing / 2 + "px; " + " padding-top: " + theme.spacing / 4 + "px; " + " padding-bottom: " + theme.spacing / 4 + "px } " + "li { margin-top: " + theme.spacing + "px; " + " margin-bottom: " + theme.spacing + "px } " + ".sender { margin-bottom: " + spacing / 2 + " }" + ".quote { color: " + quote + " }" + ".mention { text-decoration: none; }" + ".room-id-mention, .room-alias-mention { font-weight: bold; }" string styleInclude: '\n' real thumbnailCheckedOverlayOpacity: 0.4 daybreak: color background: colors.mediumBackground color text: colors.text int radius: theme.radius inviteBanner: color background: colors.mediumBackground leftBanner: color background: colors.mediumBackground unknownDevices: color background: colors.mediumBackground typingMembers: color background: hsluv( colors.hue, colors.saturation, colors.intensity * 9, 0.52 ) replyBar: color background: chat.typingMembers.background fileTransfer: color background: chat.typingMembers.background userAutoCompletion: color background: chat.typingMembers.background int avatarsRadius: controls.avatar.radius color displayNames: colors.text color userIds: colors.dimText composer: color background: colors.strongBackground uploadButton: color background: "transparent" mediaPlayer: hoverPreview: int maxHeight: 192 progress: int height: 8 color background: hsluv(0, 0, 0, 0.5) controls: int iconSize: icons.dimension int volumeSliderWidth: 100 int speedSliderWidth: 100 color background: hsluv( colors.hue, colors.saturation * 1.25, colors.intensity * 2, 0.85, ) mirage-0.7.2/src/themes/Midnight.qpl000066400000000000000000000427751407747233600173550ustar00rootroot00000000000000// vim: syntax=qml // Base variables real uiScale: window.settings.General.zoom int minimumSupportedWidth: 240 * uiScale int minimumSupportedHeight: 120 * uiScale int contentIsWideAbove: 472 * uiScale int baseElementsHeight: 36 * uiScale int spacing: 12 * uiScale int radius: 4 * uiScale int animationDuration: 100 real loadingElementsOpacity: 0.8 real disabledElementsOpacity: 0.3 fontSize: int smaller: 13 * uiScale int small: 13 * uiScale int normal: 16 * uiScale int big: 20 * uiScale int bigger: 32 * uiScale int biggest: 48 * uiScale fontFamily: string sans: "Roboto" string mono: "Hack" colors: int hue: 240 real intensity: 1.0 real coloredTextIntensity: intensity * 71 real dimColoredTextIntensity: intensity * 60 int saturation: 60 int bgSaturation: saturation int coloredTextSaturation: saturation + 20 int dimColoredTextSaturation: saturation real opacity: 0.7 color weakBackground: hsluv(hue, bgSaturation, intensity * 2.5, opacity) color mediumBackground: hsluv(hue, bgSaturation, intensity * 7, opacity) color strongBackground: hsluv(hue, bgSaturation * 2, intensity, opacity) color accentBackground: hsluv(hue, saturation, intensity * 40, 1) color accentElement: hsluv(hue, saturation * 1.5, intensity * 52, 1) color strongAccentElement: hsluv(hue, saturation * 1.5, intensity * 72, 1) color positiveBackground: hsluv(155, saturation * 1.5, intensity * 65, 1) color middleBackground: hsluv(60, saturation * 1.5, intensity * 65, 1) color negativeBackground: hsluv(0, saturation * 1.5, intensity * 54, 1) color alertBackground: negativeBackground color brightText: hsluv(0, 0, intensity * 100) color text: hsluv(0, 0, intensity * 85) color halfDimText: hsluv(0, 0, intensity * 72) color dimText: hsluv(0, 0, intensity * 60) color positiveText: hsluv(155, coloredTextSaturation, coloredTextIntensity) color warningText: hsluv(60, coloredTextSaturation, coloredTextIntensity) color errorText: hsluv(0, coloredTextSaturation, coloredTextIntensity) color accentText: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color link: hsluv(hue, coloredTextSaturation, coloredTextIntensity) color code: hsluv(hue + 10, coloredTextSaturation, coloredTextIntensity) // Example of an animation, set running: true to enable NumberAnimation on hue running: false from: 0 to: 360 duration: 10000 loops: Animation.Infinite icons: string preferredPack: "thin" // "transparent" to disable colorizing color colorize: hsluv(0, 0, colors.intensity * 90) color disabledColorize: "white" int smallDimension: 16 * uiScale int dimension: 22 * uiScale // Generic UI controls controls: scrollBar: int width: theme.spacing color track: colors.strongBackground color slider: colors.accentElement color hoveredSlider: colors.accentElement color pressedSlider: colors.strongAccentElement int sliderPadding: 2 int sliderRadius: theme.radius box: int defaultWidth: minimumSupportedWidth color background: colors.mediumBackground int radius: theme.radius popup: int defaultWidth: minimumSupportedWidth * 1.75 color background: colors.mediumBackground color windowOverlay: hsluv(0, 0, 0, 0.7) header: color background: colors.strongBackground button: color background: colors.strongBackground color text: colors.text color focusedBorder: colors.accentElement int focusedBorderWidth: 2 color hoveredOverlay: hsluv(0, 0, 50, 0.2) color pressedOverlay: hsluv(0, 0, 50, 0.5) color checkedOverlay: colors.accentBackground tab: color text: controls.button.text color background: controls.button.background color alternateBackground: hsluv( colors.hue, colors.bgSaturation * 1.25, colors.intensity * 4, Math.max(0.6, colors.opacity) ) color bottomLine: background color focusedBorder: colors.accentElement int focusedBorderWidth: 1 color hoveredOverlay: controls.button.hoveredOverlay color pressedOverlay: controls.button.pressedOverlay color checkedOverlay: controls.button.checkedOverlay menu: color background: hsluv( colors.hue, colors.bgSaturation * 2, colors.intensity, Math.max(0.9, colors.opacity), ) color border: "black" real borderWidth: 2 menuItem: color background: "transparent" color text: controls.button.text color hoveredOverlay: controls.button.hoveredOverlay color pressedOverlay: controls.button.hoveredOverlay color checkedOverlay: controls.button.hoveredOverlay checkBox: color checkIconColorize: colors.accentElement color boxBackground: controls.button.background int boxSize: 24 * uiScale color boxBorder: "black" color boxHoveredBorder: colors.accentElement color boxPressedBorder: colors.strongAccentElement color text: controls.button.text color subtitle: colors.dimText listView: color highlight: hsluv( colors.hue, colors.bgSaturation * 2, colors.intensity * 1, colors.opacity / 1.5, ) color highlightBorder: colors.strongAccentElement int highlightBorderThickness: 1 textField: color background: colors.strongBackground color focusedBackground: background int borderWidth: 1 color border: "transparent" color focusedBorder: colors.accentElement color errorBorder: colors.negativeBackground color text: colors.text color focusedText: colors.text color placeholderText: colors.dimText textArea: color background: colors.strongBackground int borderWidth: 1 color border: "transparent" color focusedBorder: colors.accentElement color errorBorder: colors.negativeBackground color text: colors.text color placeholderText: controls.textField.placeholderText toolTip: color background: colors.strongBackground color text: colors.text color border: "black" int borderWidth: 2 progressBar: int height: Math.max(2, spacing / 2) color background: colors.strongBackground color foreground: colors.accentElement color pausedForeground: colors.middleBackground color errorForeground: colors.negativeBackground circleProgressBar: int thickness: Math.max(2, spacing / 2) color background: colors.strongBackground color foreground: colors.accentElement color errorForeground: colors.negativeBackground color text: colors.text real indeterminateSpan: 0.5 // 0-1 slider: int radius: 2 int height: controls.progressBar.height color background: controls.progressBar.background color foreground: controls.progressBar.foreground handle: int size: 20 color inside: hsluv(0, 0, 90) color pressedInside: "white" color border: "black" color pressedBorder: colors.strongAccentElement avatar: int size: baseElementsHeight int compactSize: baseElementsHeight / 2 int radius: theme.radius hoveredImage: int size: 192 color background: hsluv(0, 0, 0, 0.4) background: int saturation: colors.saturation int lightness: Math.min(50, colors.intensity * 25) real opacity: 1.0 letter: int saturation: colors.saturation + 20 int lightness: colors.intensity * 60 real opacity: 1.0 displayName: int saturation: colors.coloredTextSaturation int lightness: colors.coloredTextIntensity int dimSaturation: colors.dimColoredTextSaturation int dimLightness: colors.dimColoredTextIntensity presence: color online: colors.positiveBackground color unavailable: colors.middleBackground color offline: hsluv(0, 0, 60, 1) color border: "black" int borderWidth: 2 * uiScale real opacity: 1.0 real radius: 6.0 * uiScale // Specific interface parts ui: // The background image can be an URL or local file path // (in the form file://, e.g. file:///home/user/images/foo.png). // If not specified, the gradient will be shown instead. url image: "../../images/midnight.jpg" point gradientStart: Qt.point(0, 0) point gradientEnd: Qt.point(window.width, window.height) color gradientStartColor: hsluv(colors.hue, 100, colors.intensity * 8) color gradientEndColor: hsluv(colors.hue + 50, 30, colors.intensity * 22) // To have a solid color instead, // set gradientStartColor and gradientEndColor to the same value, e.g.: // color gradientStartColor: hsluv(0, 0, 0, 0.5) // color gradientEndColor: hsluv(0, 0, 0, 0.5) mainPane: color background: "transparent" topBar: color background: colors.strongBackground color nameVersionLabel: colors.text accountBar: color background: colors.mediumBackground account: color selectedBackground: colors.accentBackground real selectedBackgroundOpacity: 0.3 color selectedBorder: colors.strongAccentElement int selectedBorderSize: 1 unreadIndicator: color background: colors.strongBackground color text: colors.accentText bool bold: false color border: Qt.darker(text, 2) int borderWidth: 1 int radius: theme.radius / 2 color highlightBackground: colors.strongBackground color highlightText: colors.errorText bool highlightBold: false color highlightBorder: Qt.darker(highlightText, 2) int highlightBorderWidth: 1 int highlightRadius: theme.radius / 2 listView: color background: colors.mediumBackground real offlineOpacity: 0.5 account: real collapsedOpacity: 0.3 color background: "transparent" color name: colors.text int avatarRadius: controls.avatar.radius int collapsedAvatarRadius: controls.avatar.size / 2 room: real leftRoomOpacity: 0.65 color background: "transparent" color name: colors.text color unreadName: colors.brightText color lastEventDate: colors.halfDimText color subtitle: colors.dimText color subtitleQuote: chat.message.quote int avatarRadius: controls.avatar.radius int collapsedAvatarRadius: controls.avatar.radius unreadIndicator: color background: colors.strongBackground color text: colors.accentText bool bold: false color border: Qt.darker(text, 2) int borderWidth: 1 int radius: theme.radius / 2 color highlightBackground: colors.strongBackground color highlightText: colors.errorText bool highlightBold: false color highlightBorder: Qt.darker(highlightText, 2) int highlightBorderWidth: 1 int highlightRadius: theme.radius / 2 bottomBar: color background: "transparent" color settingsButtonBackground: colors.strongBackground color filterFieldBackground: colors.strongBackground chat: roomHeader: color background: controls.header.background color name: colors.text color topic: colors.dimText roomPane: color background: "transparent" topBar: color background: colors.strongBackground listView: color background: colors.mediumBackground member: real invitedOpacity: 0.5 color background: "transparent" color name: colors.text color subtitle: colors.dimText color adminIcon: hsluv(60, colors.saturation * 2.25, 60) color moderatorIcon: adminIcon color invitedIcon: hsluv(0, colors.saturation * 2.25, 60) roomSettings: color background: colors.mediumBackground bottomBar: color background: colors.strongBackground inviteButton: color background: "transparent" filterMembers: color background: "transparent" eventList: color background: "transparent" message: int avatarSize: 56 * uiScale int collapsedAvatarSize: 32 * uiScale int avatarRadius: controls.avatar.radius int radius: theme.radius int horizontalSpacing: theme.spacing / 1.25 int verticalSpacing: theme.spacing / 1.75 color focusedHighlight: colors.accentBackground real focusedHighlightOpacity: 0.4 color background: colors.weakBackground color ownBackground: colors.mediumBackground color checkedBackground: colors.accentBackground color body: colors.text color date: colors.dimText color localEcho: colors.dimText color readCounter: colors.accentText color redactedBody: colors.dimText color noticeBody: colors.halfDimText int noticeLineWidth: 1 * uiScale color quote: hsluv( 135, colors.coloredTextSaturation, colors.coloredTextIntensity, ) color link: colors.link color code: colors.code string styleSheet: "* { white-space: pre-wrap }" + "a { color: " + link + " }" + "p { margin-top: 0 }" + "code { font-family: " + fontFamily.mono + "; " + "color: " + code + " }" + "h1, h2, h3 { font-weight: normal }" + "h1 { font-size: " + fontSize.biggest + "px }" + "h2 { font-size: " + fontSize.bigger + "px }" + "h3 { font-size: " + fontSize.big + "px }" + "h4 { font-size: " + fontSize.normal + "px }" + "h5 { font-size: " + fontSize.small + "px }" + "h6 { font-size: " + fontSize.smaller + "px }" + "table { margin-top: " + theme.spacing + "px; " + " margin-bottom: " + theme.spacing + "px }" + "td { padding-left: " + theme.spacing / 2 + "px; " + " padding-right: " + theme.spacing / 2 + "px; " + " padding-top: " + theme.spacing / 4 + "px; " + " padding-bottom: " + theme.spacing / 4 + "px } " + "li { margin-top: " + theme.spacing / 2 + "px; " + " margin-bottom: " + theme.spacing / 2 + "px; }" + ".sender { margin-bottom: " + spacing / 2 + " }" + ".quote { color: " + quote + " }" + ".mention { text-decoration: none; }" + ".room-id-mention, .room-alias-mention { font-weight: bold; }" string styleInclude: '\n' real thumbnailCheckedOverlayOpacity: 0.4 daybreak: color background: colors.mediumBackground color text: colors.text int radius: theme.radius inviteBanner: color background: colors.mediumBackground leftBanner: color background: colors.mediumBackground unknownDevices: color background: colors.mediumBackground typingMembers: color background: hsluv( colors.hue, colors.saturation, colors.intensity * 9, 0.52 ) replyBar: color background: chat.typingMembers.background fileTransfer: color background: chat.typingMembers.background userAutoCompletion: color background: chat.typingMembers.background int avatarsRadius: controls.avatar.radius color displayNames: colors.text color userIds: colors.dimText composer: color background: colors.strongBackground uploadButton: color background: "transparent" mediaPlayer: hoverPreview: int maxHeight: 192 progress: int height: 8 color background: hsluv(0, 0, 0, 0.5) controls: int iconSize: icons.dimension int volumeSliderWidth: 100 int speedSliderWidth: 100 color background: hsluv( colors.hue, colors.saturation * 1.25, colors.intensity * 2, 0.85, ) mirage-0.7.2/src/utils.h000066400000000000000000000060771407747233600151130ustar00rootroot00000000000000// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later // The Utils class exposes various useful functions for QML that aren't // provided by the `Qt` object. #ifndef UTILS_H #define UTILS_H #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #ifndef NO_X11 #define USE_LINUX_AUTOAWAY #include #endif #endif #include "../submodules/hsluv-c/src/hsluv.h" class Utils : public QObject { Q_OBJECT public: Utils() {}; public slots: QString formattedBytes(qint64 bytes, int precision = 2) { return this->appLocale.formattedDataSize( bytes, precision, QLocale::DataSizeTraditionalFormat ); } QString uuid() const { return QUuid::createUuid().toString(QUuid::WithoutBraces); } QColor hsluv(qreal hue, qreal sat, qreal luv, qreal alpha = 1.0) const { double red, green, blue; hsluv2rgb( hue, qMax(0.0, qMin(100.0, sat)), qMax(0.0, qMin(100.0, luv)), &red, &green, &blue ); return QColor::fromRgbF( qMax(0.0, qMin(1.0, red)), qMax(0.0, qMin(1.0, green)), qMax(0.0, qMin(1.0, blue)), qMax(0.0, qMin(1.0, alpha)) ); } int idleMilliseconds() const { #ifdef Q_OS_DARWIN return -1; #elif defined(USE_LINUX_AUTOAWAY) if (! this->waylandDisplay.isEmpty()) return -1; Display *display = XOpenDisplay(NULL); if (! display) return -1; int supportedVersion = 0, error = 0; if (! XScreenSaverQueryExtension(display, &supportedVersion, &error)) return -1; XScreenSaverInfo *info = XScreenSaverAllocInfo(); XScreenSaverQueryInfo(display, DefaultRootWindow(display), info); XFree(info); const int idle = info->idle; XCloseDisplay(display); return idle; #elif defined(Q_OS_WINDOWS) return -1; #else return -1; #endif } void setProxy(QUrl url) const { const QUrl envProxy = QUrl(qEnvironmentVariable("http_proxy")); if (! envProxy.isEmpty()) url = envProxy; if (url.isEmpty()) return; const QString scheme = url.scheme(); if (scheme != "socks5" && scheme != "http") { qCritical() << "Unsupported proxy type on the Qt side:" << scheme; return; } QNetworkProxy proxy; proxy.setType( scheme == "socks5" ? QNetworkProxy::Socks5Proxy : QNetworkProxy::HttpProxy ); proxy.setHostName(url.host()); proxy.setPort(url.port() == -1 ? 0 : url.port()); proxy.setUser(url.userName()); proxy.setPassword(url.password()); QNetworkProxy::setApplicationProxy(proxy); } private: QLocale appLocale; QString waylandDisplay = qEnvironmentVariable("WAYLAND_DISPLAY"); }; #endif mirage-0.7.2/submodules/000077500000000000000000000000001407747233600151635ustar00rootroot00000000000000mirage-0.7.2/submodules/RadialBarDemo/000077500000000000000000000000001407747233600176115ustar00rootroot00000000000000mirage-0.7.2/submodules/SortFilterProxyModel/000077500000000000000000000000001407747233600213035ustar00rootroot00000000000000mirage-0.7.2/submodules/gel/000077500000000000000000000000001407747233600157325ustar00rootroot00000000000000mirage-0.7.2/submodules/hsluv-c/000077500000000000000000000000001407747233600165445ustar00rootroot00000000000000mirage-0.7.2/submodules/qsyncable/000077500000000000000000000000001407747233600171445ustar00rootroot00000000000000