pax_global_header00006660000000000000000000000064133714724120014516gustar00rootroot0000000000000052 comment=7559cfc0291871fa05acadb4ff3f19a4a89c997c goodvibes-v0.4.2/000077500000000000000000000000001337147241200136705ustar00rootroot00000000000000goodvibes-v0.4.2/.gitignore000066400000000000000000000000601337147241200156540ustar00rootroot00000000000000*~ /build /docs/goodvibes.readthedocs.io/_build goodvibes-v0.4.2/.gitlab-ci.yml000066400000000000000000000017721337147241200163330ustar00rootroot00000000000000debian-gcc: image: registry.gitlab.com/goodvibes/goodvibes/debian:buster stage: build script: - bash -x ./.gitlab-ci/build.sh debian-clang: image: registry.gitlab.com/goodvibes/goodvibes/debian:buster stage: build only: [ tags ] variables: CC: clang script: - bash -x ./.gitlab-ci/build.sh fedora-gcc: image: registry.gitlab.com/goodvibes/goodvibes/fedora:28 stage: build only: [ tags ] script: - bash -x ./.gitlab-ci/build.sh fedora-clang: image: registry.gitlab.com/goodvibes/goodvibes/fedora:28 stage: build only: [ tags ] variables: CC: clang script: - bash -x ./.gitlab-ci/build.sh debian-gcc-no-ui: image: registry.gitlab.com/goodvibes/goodvibes/debian:buster stage: build only: [ tags ] script: - bash -x ./.gitlab-ci/build.sh -Dui-enabled=false debian-gcc-no-feat-hotkeys: image: registry.gitlab.com/goodvibes/goodvibes/debian:buster stage: build only: [ tags ] script: - bash -x ./.gitlab-ci/build.sh -Dfeat-hotkeys=false goodvibes-v0.4.2/.gitlab-ci/000077500000000000000000000000001337147241200156015ustar00rootroot00000000000000goodvibes-v0.4.2/.gitlab-ci/Dockerfile.debian000066400000000000000000000010301337147241200210060ustar00rootroot00000000000000FROM debian:buster RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive \ apt-get install --yes --no-install-recommends \ clang \ gcc \ gettext \ libglib2.0-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ libgtk-3-dev \ libkeybinder-3.0-dev \ libsoup2.4-dev \ locales \ meson \ pkg-config \ && rm -rf /var/lib/apt/lists/* RUN useradd \ --create-home \ --shell /bin/bash \ --uid 4321 \ builder USER builder WORKDIR /home/builder ENV LANG C.UTF-8 goodvibes-v0.4.2/.gitlab-ci/Dockerfile.fedora000066400000000000000000000006671337147241200210430ustar00rootroot00000000000000FROM fedora:28 RUN dnf -y install \ clang \ findutils \ gcc \ gettext-devel \ glib2-devel \ gstreamer1-devel \ gstreamer1-plugins-base-devel \ gtk3-devel \ keybinder3-devel \ libsoup-devel \ meson \ pkgconfig \ tar \ xz \ && dnf clean all RUN useradd \ --create-home \ --shell /bin/bash \ --uid 4321 \ builder USER builder WORKDIR /home/builder ENV LANG C.UTF-8 goodvibes-v0.4.2/.gitlab-ci/build.sh000077500000000000000000000015001337147241200172330ustar00rootroot00000000000000#!/bin/bash set -e set -u CPPFLAGS="\ -std=gnu99 \ -Wall \ -Wextra \ -Wshadow \ -Werror" # As long as we use GtkStatusIcon, we need no-deprecated-declarations CPPFLAGS="$CPPFLAGS -Wno-deprecated-declarations" # For some casts to GFunc, we get a warning. Typically: # # error: cast between incompatible function types from # ‘void (*)(void *)’ to ‘void (*)(void *, void *)’ # g_list_foreach(core_objects, (GFunc) g_object_unref, NULL); # ^ # gcc prefers no-cast-function-type # clang prefers no-bad-function-cast # It's unclear if an agreement will be reached. if [ "${CC:-}" == "clang" ]; then CPPFLAGS="$CPPFLAGS -Wno-bad-function-cast" else CPPFLAGS="$CPPFLAGS -Wno-cast-function-type" fi export CPPFLAGS meson build $@ ninja -C build ninja -C build test goodvibes-v0.4.2/.gitlab-ci/docker-build-image.sh000077500000000000000000000007711337147241200215710ustar00rootroot00000000000000#!/bin/bash set -e set -u fail() { echo >&2 "$@"; exit 1; } [ $# -eq 1 ] || fail "Usage: $(basename $0) " DOCKERFILE=$1 [ -e $DOCKERFILE ] || fail "File '$DOCKERFILE' does not exist" FROM=$(grep '^FROM' $DOCKERFILE | sed 's/^FROM *//') [ "$FROM" ] || fail "Failed to parse dockerfile" TAG=registry.gitlab.com/goodvibes/goodvibes/$FROM docker build \ --tag $TAG \ --file $DOCKERFILE \ . cat << EOF -------- Now you might just want to push the image: docker push $TAG EOF goodvibes-v0.4.2/.gitlab-ci/docker-run-image.sh000077500000000000000000000005761337147241200213010ustar00rootroot00000000000000#!/bin/bash # This script is for developers, not CI. set -e set -u fail() { echo >&2 "$@"; exit 1; } [ $# -eq 1 ] || fail "Usage: $(basename $0) " IMAGE=$1 docker run \ --rm \ --tty \ --interactive \ --hostname gvbuilder \ --user root:root \ --volume $(pwd):/home/builder/goodvibes \ --workdir /home/builder/goodvibes \ $IMAGE \ bash goodvibes-v0.4.2/COPYING000066400000000000000000001045131337147241200147270ustar00rootroot00000000000000 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 . goodvibes-v0.4.2/HACKING.md000066400000000000000000000255771337147241200152760ustar00rootroot00000000000000HACKING ======= Here's the place to get started when hacking Goodvibes. Table of Contents ----------------- 1. [Download](#download) 2. [Compilation](#compilation) 3. [Program Invocation](#program-invocation) 4. [Additional Tools](#additional-tools) 5. [Code Overview](#code-overview) 6. [How To Code That](#how-to-code-that) 7. [Coding Style](#coding-style) 8. [Contribution](#contribution) Download -------- To get the latest source, clone the repository from GitLab. git clone https://gitlab.com/goodvibes/goodvibes.git Additional repositories are available: # Debian packaging files git clone https://gitlab.com/goodvibes/goodvibes-debian.git Compilation ----------- Goodvibes is built using [Meson][] and [Ninja][]. The build commands are: meson build cd build ninja Goodvibes build is quite modular. Features that require an external library can be disabled at build time. To see all the options available, enter the build directory and run: meson configure For example, to disable the hotkeys feature: meson configure -Dfeat-hotkeys=false To disable compilation of the user interface (also disable every ui-related features): meson configure -Dui=false Additionally, it's possible to run a few tests: meson test [meson]: http://mesonbuild.com [ninja]: https://ninja-build.org/ Program Invocation ------------------ #### From the source tree (no installation) If you want to run Goodvibes from the source tree, without installing it, use the launcher script. ./goodvibes-launcher.sh The script assumes that the build directory is named `build`. Why do you need a script? Some libraries expect shared resources to be installed in some standard locations. To be more accurate: - GLib expects schemas to be installed at `/usr/share/glib-2.0/schemas/` or similar. - GTK+ expects icons to be installed at `/usr/share/icons` or similar. It's possible to customize this behavior with environment variables though, and that's what the launcher script does. Have a look at it for details. Also, don't expect any kind of desktop integration to work when you run from the source tree. Ie, the desktop won't find any desktop file installed in the standard location (`/usr/share/applications`), and various things might not work: notifications, icon display here or there, etc. #### From a non-standard directory If for some reason you install Goodvibes in a non-standard directory (`/opt` or `/home/user` or whatever), you will be hit by the same problems mentioned above: the shared resources won't be found at run-time. So please refer to the launcher script to see which environment variables you need to set before running Goodvibes. #### Command-line options To get a brief overview of the command-line options available, use `--help`. The option you will use the most is `-l` to change the log level. Here is a typical line: goodvibes -l dbg Colors are enabled by default, but you can disable it with `-c` if it hurts your eyes. Colors are automatically disabled when logs are redirected to a file. Logs are all sent to `stderr`, whatever the log level. Internally, we use the GLib to ouput log messages. For more details, refer to the page [GLib Message Output and Debugging Functions][]. Some of the libraries we use provide additional command-line options. To see them all, invoke with `--help-all`. For more details, refer to: - [Running GLib Applications][] - [Running GStreamer Applications][] - [Running GTK+ Applications][] Hardcore GTK+ debugging can be done with the [GtkInspector][]: goodvibes --gtk-debug=interactive [glib message output and debugging functions]: https://developer.gnome.org/glib/stable/glib-Message-Logging.html [running glib applications]: https://developer.gnome.org/glib/stable/glib-running.html [running gstreamer applications]: https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/gst-running.html [running gtk+ applications]: https://developer.gnome.org/gtk3/stable/gtk-running.html [gtkinspector]: https://wiki.gnome.org/Projects/GTK+/Inspector #### Change language For testing, it can be convenient to change the language. For that, use the `LANGUAGE` environment variable. LANGUAGE=fr goodvibes Note that gettext will look for the message files in the path given to `bindtextdomain(3)`, so make sure that this path contains the message files. This is logged when Goodvibes starts. Additional Tools ---------------- [GSettings][] is part of GLib, and is the component in charge of handling the application settings. It comes with a command-line tool named - as you can guess - `gsettings`. This is useful to witness Goodvibes reading and writing its configuration, and also to get or set some values. gsettings monitor io.gitlab.Goodvibes.Core gsettings list-recursively io.gitlab.Goodvibes.Core [DConf][] is the backend for GSettings. It's possible to play directly with the `dconf` command, therefore by-passing completely GSettings. dconf watch /io/gitlab/Goodvibes/ dconf reset -f /io/gitlab/Goodvibes/ [gsettings]: https://developer.gnome.org/gio/stable/GSettings.html [dconf]: https://wiki.gnome.org/Projects/dconf Code Overview ------------- Goodvibes is written in C/GObject. The code is heavily object-oriented, and if you're not familiar with GObject, you will have to accept that some magic is going on, here and there. Good luck with that ;) The code is neatly split into different parts: - `framework`: the basic stuff we need. - `core`: the core of Goodvibes, basically enough to have the software up and running, without the ui. - `ui`: the GTK+ user interface. - `feat`: features that can be enable/disabled at compile-time. I guess it's quite similar to the plugins you often find in music players on GNU/Linux. Except that I didn't dare to call it plugin, for plugins are usually something discovered and loaded at run-time, not at compile-time. - `libcaphe`: a library to handle system sleep inhibition. Actually GTK+ can already do that through GtkApplication, so one day this will go away. How To Code That ---------------- #### Adding a new setting Settings are mapped directly to GObject properties. You must ensure that: - The object must be a global instance, part of the `core`, `ui` or `feat` system. - The object must implement the `GvConfigurable` interface. You must bind the property to a setting using `g_settings_bind()`. This **must NOT be done** at construct-time, but later on in the `configure()` function (implemented from the GvConfigurable interface). You must understand that the startup procedure is made in two steps: 1. Every objects are *created*. No errors are expected at this stage. 2. Every objects are *configured*. Serious things start to happen, there might be errors, and it's important to be ready to report it to the user. That's why the configure step is run *after* everything is created. #### Reporting an error to the user Here's how to report an error to the user from an object: - The object must have been registered using the function `gv_framework_register_object()`. - The object must implement the `GvErrorable` interface. Then, it's just a matter of invoking the function `gv_errorable_emit_error()`. A few things to notice: - You shouldn't call this function too early (aka during construction), as the system is not ready yet. - The user message **must be** translatable as it's intended for *users*. - It's up to you to drop an additional log (`WARNING()` should be used). - The log message **must NOT be** translatable as it's intended for *developers*. Coding Style ------------ Coding style matters to me. I keep the codebase as clean and neat as possible. I'm a bit of a maniac about that, I tell you. #### Indentation The code is currently indented using [Artistic Style][]. There's a script to automatically indent the whole thing: ./scripts/code/indent.sh all You can (and should) also indent your staged changes before commiting: ./scripts/code/indent.sh staged [artistic style]: http://astyle.sourceforge.net/ #### Comments For comments, always use C style (aka `/* ... */`). #### Codetags For codetags, always use C++ style (aka `//`). Here are the codetags currently in use: - `WISHED` Things I wish, but will probably never do. - `TODO` Things that should be done pretty soon. - `FIXME` Things obviously broken. Always try to make it a one-liner if possible, or at least describe the problem in one line, then add more details on the following lines. Stick to these conventions, and then getting a list of things is easy: # TODO list ack 'TODO' # List everything ack '// ' Here are some links that discuss codetags: - - #### GObject C Files Layout If you find yourself writing a new file, therefore creating a new object, fear not! There's a script that generates all the boilerplate. ./scripts/code/gv-object-make.sh Have a look at the structure of the C file created, and please notice a few things. Functions are ordered from low-level to high-level, to avoid the need of declaring functions before defining it. So basically, you read the file from bottom to top. Functions are grouped in sections, titled by a comment such as: /* * Section name */ Most of the time, these sections are always the same, because implementing a GObject always boils down to the same thing, more or less: GObject inherited methods, property accessors, signal handlers, public methods, private methods, helpers. Sometimes we implement an interface, and that's all there is to it. So I try to show this consistent layout and this simplicity in the C file, and to have it always the same way, instead of throwing code in a complete mess. If you stick to these conventions, then it's easy to find your way in the code: whatever file you're looking at, they all look the same. Contribution ------------ Contributing is better done through GitLab, please follow the usual workflow: - Have your account setup - Fork the project - Create a branch for your work - Work - Create a merge request If you hack around for your own use-case, and think that other users could benefit from your hacks, feel free to share it and discuss it on GitLab. If we decide that your hacks should be integrated in Goodvibes proper, then you will have to make sure it looks good, with meaningful commits that fit into the existing design, proper commit messages, respect for the coding style, and so on. Please note that my first goal for Goodvibes is to ensure a long-term maintenance. There are more than enough software with a short lifetime around, and I don't want to add another to the list. It means I want the code to be clean, readable, maintainable. Clean design and zero hacks are my goals. Dirty hacks are OK to solve an outstanding problem quickly, or to demonstrate a possible solution. But they are not OK for long-term maintenance, and therefore won't make it upstream. goodvibes-v0.4.2/MAINTAINING.md000066400000000000000000000121451337147241200157530ustar00rootroot00000000000000MAINTAINING =========== Updating translations --------------------- Before every release, it's time to update everything regarding translations, so that translators have a chance to do their homework ! First, ensure that the file `po/POTFILES` is still up to date. This is still a manual process, but it wouldn't be that hard to write a little script. Then, and before modifying the po files on our side, synchronize with Weblate and lock it. We can do it either via the web interface, either via the client. # Lock Weblate translation wlc lock # Make Weblate push its changes to the git repo wlc push # Pull changes from the git repo to your local copy git pull Then, update the *translation template*, aka. `po/goodvibes.pot`, along with the *message catalogs*, aka. the po files. # Update the pot file and the po files [ -d build ] || meson build ninja -C build goodvibes-pot ninja -C build goodvibes-update-po # Commit changes git add po/*.{po,pot} git commit -m"i18n: Update goodvibes.pot and po files" # Push changes to upstream repository git push At last, we can unlock Weblate and update it. # Tell Weblate to pull changes (not needed if Weblate follows your repo automatically) wlc pull # Unlock translations wlc unlock For more details, refer to the weblate workflow as described at: . Releasing --------- #### Translation bits Be sure to update everything related to **translation** (see above). Only then you can go on with: - Update the translator list in the *About* dialog. - Update the translator list in the documentation. - Write down changes in the `NEWS` file. In bash, it translates to something like that: ./scripts/translators.sh code vi src/ui/gv-about-dialog.c git commit -am"ui: Update translation credits" ./scripts/translators.sh doc vi docs/goodvibes.readthedocs.io/credits.rst git commit -am"doc: Update translation credits" vi NEWS git commit -am"news: Update translations" Then: - Ensure `NEWS` is up-to-date (check Git history and GitLab milestones). - Bump the version in `meson.build`. - Git commit, git tag, git push. In bash, here you go: vi NEWS v=0.3.5 sed -i -E "s/version: '([0-9.]*)'/version: '$v'/" meson.build git add NEWS meson.build git commit -m "Bump version to ${v:?}" git tag "v${v:?}" git push && git push --tags Done. Packaging --------- After releasing, update the Debian packaging files in the `goodvibes-debian` repository. Basically, just bump the changelog, there's nothing else to do. DEBFULLNAME=$(git config user.name) \ DEBEMAIL=$(git config user.email) \ dch --distribution $(dpkg-parsechangelog --show-field Distribution) \ --newversion ${v:?}-0ebo1 Git commit, git push. Done. git add debian/changelog git commit -m "Version ${v:?}" git push Then, just fire the script `debian/build-all.sh`. export DEBFULLNAME=$(git config user.name) export DEBEMAIL=$(git config user.email) ./debian/build-all.sh This script is tied to my config and won't work out of the box on someone else system. But heck, if you're not me, you're not supposed to release anything anyway, so move on! At last, a few `dput` commands will finish the damn job. Done with packaging. Artwork ------- Both the svg source files and the png files are versioned. To rebuild the png files, run: ./scripts/rebuild-images.sh icons Integrations ------------ This is just for myself to remember what additional services are used for Goodvibes development, and how it integrates with GitLab. #### GitLab CI Up to date documentation should be available on the *Registry* page: The configuration is mostly in-tree: - `.gitlab-ci.yml` describes the different pipelines - `.gitlab-ci` contains Dockerfiles and associated scripts. The docker images need to be updated from time to time. The procedure to update an image is roughly: DIST=debian cd .gitlab-ci # Work on the image vi Dockerfile.${DIST:?} # Build the image ./docker-build-image.sh Dockerfile.${DIST:?} # Log in the registry docker login registry.gitlab.com # Push the image docker push registry.gitlab.com/goodvibes/goodvibes/${DIST:?} #### GitHub Mirror Very well documented at: . In short: 1. Create a GitHub token. 2. Configure GitLab to push automatically, using the token to authenticate. This mirror is just there for a while, so that active GitHub users (if any) have time to notice the change. #### WebLate Weblate needs to be notified of new commits, so there's a webhook. Additionally, Weblate needs write permission on the repository. This is achieved by adding the [Weblate push user](https://gitlab.com/weblate) as a member of the project. I configured it as a `Developer`, however developers can't write to protected branches by default, so there's a bit of additional config. #### ReadTheDocs Just needs to be notified of changes, so there's only a webhook. goodvibes-v0.4.2/NEWS000066400000000000000000000125541337147241200143760ustar00rootroot000000000000000.4.2 (2018-11-10) - Paris / L'Imperatrice ------------------------------------------ Fixes * Segfault on application shutdown 0.4.1 (2018-11-10) - Parfum Theremine / L'Imperatrice ----------------------------------------------------- Outstanding! * Ready for Flatpak! General * Change application id to 'io.gitlab.Goodvibes'. * Rename icon files according to the app id (flatpak requirement). * Stations are now saved in $XDG_DATA_HOME (was $XDG_CONFIG_HOME). * Use GResource to handle glade files. #54 Fixes * Support lowercase keys in pls playlists. #88 * Add meson post-install script. Translations * New translations: Spanish. 0.4 (2018-08-18) - Sphynx / La Femme ------------------------------------ Outstanding! * Project moved to GitLab, good-bye GitHub. * CI is now handled by GitLab, good-bye Shippable. * Build system switched to Meson, good-bye Autotools. #53 General * Add an appstream file (ie. appdata). #47 Fixes * Desktop file: exec now accepts url (thx Elías Alejandro Año Mendoza). Translations * Updated translations: Dutch, German. 0.3.7 (2018-05-08) - Marilou Reggae (Gainsbourg Cover) / Alain Bashung ---------------------------------------------------------------------- General * Update default station list. Fixes * Fix notification crash on Cinnamon. #79 Translations * Updated translations: Czech, French, German, Norwegian. 0.3.6 (2018-03-31) - Birthmark / Deftones ----------------------------------------- General * Add support for dark theme. #59 Fixes * Fix ```` multimedia key. #83 Documentation * A new documentation is available at . * Remove the GitHub wiki. Translations * Updated translations: Dutch, French, Norwegian. 0.3.5 (2018-03-10) - No Ordinary Love (Sade Cover) / Deftones ------------------------------------------------------------- Fixes * Ensure ```` is a shortcut to toggle playback. #80 * Retry playback on error, hence resume playback after suspend. This is just a first attempt, error handling needs more care than that. #72 * Fixes here and there in translatable strings (thx Allan Nordhøy). Translations * Updated translations: French, German, Norwegian. 0.3.4 (2018-01-27) - God in Hell / Khynn ---------------------------------------- General * New icon set at last! Thanks to Hector Lahminewski and his endless patience - the icons have been waiting for almost a year. #51 * There's now a script to launch Goodvibes from the source tree - talking to developers here, users don't care. #55 Translations * Updated translations: Dutch, French. * Update pot file (I forgot that for a while). #49 * Document translation process. #50 0.3.3 (2018-01-07) - Psygnosis is Shit / Psygnosis -------------------------------------------------- Fixes * Fix XML escaping of station properties before saving. #69 * Add support for the ``NumberOfEvents`` key in pls playlists. #67 Translations * New translations: German, Norwegian Bokmal, Russian. * Add translators to the About dialog. #48 0.3.2 (2017-10-11) - Je m'en sortira / Babylon Pression ------------------------------------------------------- Translations * French translation updated. 0.3.1 (2017-02-22) - Where Dragons Dwell / Gojira ------------------------------------------------- Fixes * Fix missing status-icon glade file. #44 * Fix segfault when station list is empty. 0.3 (2017-02-20) - Pain Is A Master / Gojira -------------------------------------------- General * Make the main window resizable, and add an ``autoset-height`` option to the *Preferences* window. #27 * Add a giant tooltip on the station display area (this is used to display in-depth info about the current station). #24 * Add ``Add Station`` entry to the main menu. #31 * Add accelerators to the main window. #43 * Add GStreamer options to the preferences. #21 * Allow setting the user-agent per station (secret feature). #23 * Add a manual page. #10 * Add Freedesktop Portal support to ``libcaphe``. Fixes * Fix build on FreeBSD. #39 * Fix mute which was broken. #40 Gory Internal Details * Rework init procedure to report early errors. * Use ``g_signal_connect_object()`` pretty much everywhere. * ``GvStation`` is now initially unwowned! Translations * New translations: Czech, Dutch and French. 0.2.1 (2017-02-03) - Cradle Song / Tomahawk ------------------------------------------- General * Add ``play-stop`` toggle command to the client. #37 * Add ``is-running`` command to the client. * Add station name in mpris2 metadata. Fixes * Fix exec path in dbus service file. #36 * Some widgets were not initialized properly. 0.2 (2017-01-26) - Doom Town / Wipers ------------------------------------- This release will wipe out your conf! General * Handle configuration with ``GSettings``. #22 * Emit notifications with ``GNotification``. #22 * Load/store station list with ``GMarkup``. #22 Fixes * Fix desktop filename in mpris2 backend. #26 * Handle queries and fragments in station uris. #29 * Add a proper user-agent in HTTP requests. #33 Translations * Translations are now managed with Weblate. #14 Dependencies * Drop compile-time dependency ``libnotify`` and ``libxml2``. * Add run-time dependency ``dconf-gsettings-backend``. 0.1 (2017-01-13) - Positive Vibration / Bob Marley & The Wailers ---------------------------------------------------------------- * Initial release. goodvibes-v0.4.2/README.md000066400000000000000000000044511337147241200151530ustar00rootroot00000000000000Goodvibes ========= ![Goodvibes Logo](https://gitlab.com/goodvibes/goodvibes/raw/master/data/icons/hicolor/256x256/apps/goodvibes.png) **Goodvibes** is an internet radio player for GNU/Linux. It aims to be light, simple, straightforward. - **Documentation** is available on [Read the Docs](https://goodvibes.readthedocs.io) - **Development** is hosted on [GitLab](https://gitlab.com/goodvibes/goodvibes) - **Translation** takes place on [Weblate](https://hosted.weblate.org/projects/goodvibes) - **Artwork** is made by [Hector Lahminèwskï](http://lahminewski-lab.net) Goodvibes is released under the [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html), and relies on major open-source building blocks such as [GLib][], [LibSoup][], [GStreamer][] and [GTK+][]. For install instructions, please refer to the [documentation](https://goodvibes.readthedocs.io/en/latest/downloads.html). [glib]: https://wiki.gnome.org/Projects/GLib [libsoup]: https://wiki.gnome.org/Projects/libsoup [gstreamer]: https://gstreamer.freedesktop.org/ [gtk+]: https://www.gtk.org/ Compiling --------- Goodvibes is written in C and builds with Meson. At first, make sure you have all the dependencies installed. *Note*: The following commands are for a Debian-like distribution. If you're using another distribution, please adjust the package names and replace `apt` by whatever you use. ```bash # # Compile-time dependencies # # Build toolchain sudo apt install build-essential git meson # Core dependencies sudo apt install libglib2.0-dev libsoup2.4-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev # GUI dependencies sudo apt install libgtk-3-dev libkeybinder-3.0-dev # # Run-time dependencies # # To load and store settings sudo apt install dconf-gsettings-backend # To listen to mp3 streams sudo apt install gstreamer1.0-plugins-ugly # GStreamer audio backend dpkg -s pulseaudio >/dev/null 2>&1 && \ sudo apt install gstreamer1.0-pulseaudio || \ sudo apt install gstreamer1.0-alsa ``` Now you're ready to get the source and compile. ```bash # Grab the source git clone https://gitlab.com/goodvibes/goodvibes.git cd goodvibes # Build meson build ninja -C build # Install sudo ninja -C build install ``` You can also run the application in-tree, without installing anything. ```bash ./goodvibes-launcher.sh ``` goodvibes-v0.4.2/data/000077500000000000000000000000001337147241200146015ustar00rootroot00000000000000goodvibes-v0.4.2/data/goodvibes-client.1.in000066400000000000000000000007641337147241200205340ustar00rootroot00000000000000.TH "@PACKAGE@-CLIENT" "1" "July 2018" "@package@-client @version@" .SH NAME @package@-client \- command-line client for @package@ .SH SYNOPSIS .B @package@-client [OPTIONS...] .SH DESCRIPTION .B @package@-client is the command-line client for @package@. It communicates through the native D-Bus interface, which therefore must be enabled in @package@. For more details, type '@package@-client help'. .SH AUTHORS Written by @author@. .SH SEE ALSO .nf .BR @package@ (1) \fI\%@homepage@\fR\& .fi goodvibes-v0.4.2/data/goodvibes.1.in000066400000000000000000000025121337147241200172510ustar00rootroot00000000000000.TH "@PACKAGE@" "1" "July 2018" "@package@ @version@" .SH NAME @package@ \- a lightweight internet radio player for GNU/Linux .SH SYNOPSIS .B @package@ [OPTIONS...] [STATION] .SH DESCRIPTION .B @Package@ is a lightweight internet radio player for GNU/Linux. It offers a simple way to have your favorite radio stations at easy reach. It provides everything you can expect from a modern media player: multimedia keys binding, mpris2 support, notifications, and sleep inhibition. It can be launched with or without GUI, and comes with a command-line client. .PP This manual page is only a short reference for @Package@. Complete documentation is available at \fI\%@homepage@\fR\&. .SH OPTIONS .TP .BR \-b ", " \-\-background Run in the background. .TP .BR \-c ", " \-\-colorless Disable colors in log messages. .TP .BR \-l ", " \-\-log\-level =\fIlevel\fR Set the log level, amongst: trace, debug, info, warning, critical, error. The default is \fIwarning\fR. .TP .BR \-o ", " \-\-output\-file =\fIfile\fR Redirect log messages to a file. .TP .BR \-v ", " \-\-version Print the version and exit. .TP .BR \-\-without\-ui Disable the graphical user interface at startup. .TP .BR \-\-status\-icon Launch as a status icon (deprecated on modern desktops). .SH AUTHORS Written by @author@. .SH SEE ALSO .nf .BR @package@-client (1) \fI\%@homepage@\fR\& .fi goodvibes-v0.4.2/data/icons/000077500000000000000000000000001337147241200157145ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/000077500000000000000000000000001337147241200173535ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/16x16/000077500000000000000000000000001337147241200201405ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/16x16/apps/000077500000000000000000000000001337147241200211035ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/16x16/apps/io.gitlab.Goodvibes.png000066400000000000000000000015501337147241200254020ustar00rootroot00000000000000PNG  IHDRasBIT|d pHYsy(qtEXtSoftwarewww.inkscape.org<tEXtAuthorHector LahminewskilIDAT8o[e{}85iҖEU Bc HA(Q@ά ^3D?CBXRJqiEq*s^dpZE푞sߑZ8ߵ֮8ꛛRZ1+ 1EZ6'X~^Z?UJtvj1o ɬe܂R+ÛƘ߭cR cLsccWj*'^{x!Y6XZZau\gp?/7FDk^H*S=jAORZ+Rxl0|Q`3|٬:.Ƽ<99 HR@WȌOR(Ð0 d2(riidj0AJ1x/06&J%0n+Y-Xkej0`C\Apo 3r}dYR $Kd$0:5KA…R`0\.S(RlXGZo,w.9;O>.,FFF^d2GDžo)J)%8/>}zj}.׾藓>J&Cdr8XkZFJJ)#56~~J(J?ղB&eZ0(Xm*E+@X% Go:uT?CCC=zfN*D `ƀ1XZQ]˓۹ \Έl{*9;3%TKgcV2O:t?WVU`ɗ'ٗq$vI)A"b"4%}rMӹyyBc Sms$D cL΋1F5E"4fo;}uR%3 85חp B«뎒nVI.M",R,Y__guu(r{v11ZX)A`-hV,.7SA1>fk*h*d(iq?`&-u c0 q]"72M L:>FKؖXaO*Hm"ˑ!L؈ݭK<]kѪYZwf 7K1K\.XXXM14 0Z2iѴc꺋" /iM[Il#7"EYVVV(T*2KKK흓TiWVoO/'8-s|sGXLiAO~f|ۋuTX: zqGXci)sرW'O+|ݜb통 |bqxT+pa\r 8X,L&Z-4R4qq)`K`tQn1wk,=JwUIENDB`goodvibes-v0.4.2/data/icons/hicolor/24x24/000077500000000000000000000000001337147241200201365ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/24x24/apps/000077500000000000000000000000001337147241200211015ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/24x24/apps/io.gitlab.Goodvibes.png000066400000000000000000000026331337147241200254030ustar00rootroot00000000000000PNG  IHDRw=sBIT|d pHYsbb_'StEXtSoftwarewww.inkscape.org<tEXtAuthorHector LahminewskilIDATHMlT}͛=`C(Ҫ"M)JH ʾbME@"K~)MT4HAuLcbb06{s4!-{5ĉ$I^_?{@4GC &k-lA -8U8&MS* EQ)QaRl ^ rg"=hQAiH*{זCEywUmFEuZ|R?S *&7>QUT^2"B$uTåiAR<\O<{3:Bn"SK8Ȳ<ɲ c ah4((h4|{ϩ,p>@:kkjkA7h"9Dgǫ(jIk-ySUó#-ae63KO|nDh.CӔ666HӔ :xc$0_f8p ٿ/=8g>`5GZ$;nB* 88x8Ư?7o~o߾_LX+~iE6(pޮ͟ݸq`t۶mMLLRTF ;˙ϲ ONLocP^t=l,.;( Bq1y},[nR uC/0k)orKg?l-С|, ; !=!Bh]HviduHAszJNBJL[T}IӴuheibb"VVղ~0 ,.搣:􊦉r|Zݛ۶=v>Ï5hzEĄfYeݮiJ+=1<R=c|M@!dtCKtZ}jC-voYL˲:!o0ġR:}C!waYk !1Olu<Ë@ \p[teA{g*j,R=gx[_'f3o:С˖>4MI) g%xxƸO8'ࡦ;uCMLL'줔(˯4Xe:^5a_:}[9 mt%Mw_Lg׻%PTO{L:tI{MJPہR:HyRmۏk4?O5=%RGЊ?mNk/ؼ @d9ޏTO25N< L&th~ر[,A v0xP2~{ޖgaM'#|{׮]=>Bn\Y_)/[ b R`RMӮ]2DZ2}Z_(r:dL/byUgp?>s{x5Mw_ܲضYI)T[nA~ _SJ]v 2t(}6MͨwmI1J,-<>!mۏi;~/yxQ:$ -L^Ok{wRy^VځJy]}=3JCe=k׮e?r{M~NJgm~T{zei2I_PJ 6M^ O}S-˺L&J .\ o~-^-Yn)KU|.hO4Mۭ&&&& 蓟aovyV'Jv?opx[k*J/L۪iBm1 {RGӴ,ze6 w}q0^cNMn۶ϖV헑oYS۶Y\\ܙJ"/4fi cX *k_RO&A@6XNhŰ3艉;t]io@K6#L= ̛{>,W[κW,!$kvrRzضRHX|K_ $TeR]yo}Ν7%7~Hh۫4=OlmC&ˁV6myOJ߿'|Rvj1+5ݿmƿ0KV 5&-L>n.Efd^F_q h'1oF774?U^~0 2FY KkUտ(L>kG_> x2 b|׽ܹseƟXϘv)v}XoV3@zrʷߎ>SnY'N|IpkK?/SX,vi[7S,*.(-r_%*K<])?}E"`I NP1?ہ0L0tuDdڀ~X,}^F|ZKkon_N#~+ :8u~$xi*3 7xc駟 D/ZPoq˜s~f@P\4}@Tq/e Ki~mpر?~ n鶹4V/K{7 øEA{[e u@}ssso| RrJ({ ~p ڇUa/%4frܯ_~ pF'}{<`X4Ϻ|*RLOIM Hأ*-OTyIҠ>f(Pv=y S>+FEuPUGW{N^ ۶sr}K_>%@XP9X?k{{{&1V9ZQ'''qphlu&GA !=/^8%Si$,E0U !k++m8 %$Ul B;egf)L;(.LJ3#,\(~_c~M@ n+ _=}IN(%춂4 ]/g|Y9ж)HԷm9P@P6"q{`x:;<~0GPԇ1F7ekw~k_{^ y5p@[[\sdk~WWC\܉Ͽ-;ش&dSAw);H~\w iy|;J2=.m۶MBFdҝuOfߏ֋CsZp3cN*ᛗcsMΥ‰Zt_R'ov39~q״K VwlE wn/v#j5c][y$/Jy{s _&f?-_X5]sd -CR,ŋq@ltz΅}U|oXFm꾎3&́z5[;NY _O\};F>4Jw>6Dc2G MΝ;BY#c~χ:ч^~RuG՟ r$8-h \#=W6 FsN?:ChŊLQb*b(re e"{ .C3RKkCiAp:%FK a077o#>B_3*ו&"^iR_ PxRV1‰q\EQhh ZMXjH]B'6(@2uH#6/`}kzr ޥAk á(JyXZL̲(ٻq^ :?ᇻE_h4>N$O0 8l)emrźtp+ `Sc{*N%1*B/-ˈR ˲`[UDHe褊y06m#sJ}X(3#̊.&ɲn`N=ypXǭzǮ;}t}2PTLk'򺾾ն_i?~_@F6Ba7czE2ܽ U a 0 ,@ Wӣ)ˡn_Ze;H$"*lhq)lZ Ok%&X~+6J+pR }p?NG숨V~2W1|XF? WȜGU {i Q aPV=7%>?0؍*}!:"媎'>LX$͈Cq˹Iۏ @l{g(`i`w]uWd=ds)<4:c"wZ2WUoH~Q$Cpc?caQhw}u]"FGGw o;\%U^&q~Z|> +&g3}xL,-wI5"U*V)㍛"aVЮa÷{ |MX5~+^&_}&y-cȦMT1LeagU9P}? 0 HMZ*ěֈ@PB5Q:ת%@mB)(/]ZA-R&B! IK:F?XY..hG)O$qëc(jR F磽w3HE*D?~ZAuy] ֻU!d|vvyi/E!xoȪU>Z[ `yk% bNKX(dt.$$y@pC+sxlyFL'Pzwib&0[Jz: G$K,p[ Kp]npo,̨N=ySp3|$We˖7syX<>4ZVXù$-LjI|B6\ 1@N 7K%a/~ }l  _ đ8sP#]%ȫ"U% X gWg|0^#{ 6OGҀ\PJ/b~~BAO|?=7q>?_N|srΞ= ]בL&ݍ>DH @d>>jiI׀[gytwhnݺu y:b(͛7o@$ 8ܿ}Wwj 2{SΆ齠 \yj?i@!d*)葮%Y>4P* i$4t]G$vYِfQ(P,6d3g Lb``-^FT1qy aA{cN. I~}?8_4MD3R0u@.}QcHEiCϷ (+,2< xLz{/@ ]}^f4Μ9L&S: a ,'@Xy0tړZbvvsssA>_nYll'OD__FFF]uN%l4=荗}m5H=ڔcZv2u} …)D]ׯ`3|G-h:kkNrz8v~ehHY uI^-er3>`P.ͻ+y$R_Ba_Q7~bZF ړm8<Μ9nծhk׮02۝D,?6#0044!5@fff\P`ttd2 lޟǑ#G6h7o8"K]~ RUL͛L=k|9۶a&֬YիW#`rr.\p֮]t@AukuLec7WYNo#3Jr))I4># I$*@Ӵ.2,A).4Ž <#o=|6ī@ .S0\ /X"dc|.^@($L=0 p?DgϞ]viضmCV3' {3-H3)Ɯ*by ϩ' l޼k׮űc\S0;;7"HQgt x cf6T>;KA}v (ʡ q&s zhO%n+)MfX'|3a˜)[HwskQ}ej/4=<[ȑ#RJ122;7֜~)j~y2/vP9ޤ'̑H7oۑL&0x0==^v"zH~g(Ph-%s νbN =n .)(ًFS.}% D {iV ԧuLgˤcu _~֐d~EJpbxxqڅ%79kx=1yÀLu]ĉNډ'diӦ!#|=qhd$M+Lk)Hi*e@G ߲)~7'M "X?iUMOqM 4f 4,!SnN?s ;bM6a֭M/9ra6H1B0::^:tlann/eZux%ܵ74h9fyᘠR :6G`Th;eVV?d}h# Maq'Ս| w1>}6&Ja&n'Ұ_~lR,öm099ϰo>lٲʲ|6'JqyO/ފVFW/j ɄJ=r*gOq͸\ؿ5xtQPoIx>P$$"XTkFg>qpG7G#Ѿ4#)N+Bb5k[o26_ ab۶mFkʇԔkY0n;>Ȕ[~] ZhH~w"/uAujh<؄h:6u< /Tqz=-zJmb.*6Urncj"r6 bF}t|fwuBf{337!ذan&_b!x0L* Cضm8qgϞ̱ (zF_FP &՟')jC@3<lCPU1GlgyT6 [8jنUv[ ' )9 8/+R]"A`y kpBVbզOAēbNE(u]#^[!5 P׋= ZF7zq6sԘ~~ީ+֭["vayO@6f C,qAd2Bp)DQ׶gq?݃ xUxxU~'e=MXf;[đ`U) *VoN"ґ[bUI$z"VQXۜ@%t=WDpvO0)(/pq2@0?U@`F4t.Ii"$,p'ͣIiDQ۷.\pk}v/y-i'?'?'ǟSP[IӨT*mǏܜTZi[ۄs!%_2DOֈ ۢ]C$V*@jņnr00ÅS)4VY9 `tc1veCQػ0 sp|.Ӕ}}y[6Ez1#M,Crǽ(Jizc@5+&ѕ d~]"ˁ7|R^3L&Y.l5Xj`bPVQV1==Yhb|\,ZN/mȒУFMm 4'a?>HI It@9cBWWW =zj 7܀z9Qv===?ɓTj4 PCJ!C;Xp`'sZKj?_Ī+̔QXtTvFD<]EPb`M 30̘eē:"(d-hWam ^_c>Ԡ\; TϘRB{uׯ_f.uV166Rb0099 64 x$޶вOF\'N4xOu"dPS/Е6սrٹ ő[ZQRD5fVB7V-(-df0L|g>G4o4 ˦ZאA>y,%?}1[A4Ht:۷ْXPf˨ُ}4ӆLcll Re,,,`aa}\bhg5  zAlXS]">Vnuqt [*Q6K %tq ?.nVk>1P;9O$7.\ɓ'k% _ZvS61s3 ˌ_ُ_Ža9@L }}}(˨V::ť͞ux䘿,/vxyWd?go2$]#FKAj(HxJF2҃Q,L666zKDx}A  eg4rg O{ F ~]:1cUϷ+`Qėx8Tch.aJ* =}([/Z R؞vniviWW7ÍHjot k H⻺k=q]eZ]_{ =] $pڳ>h ӏ&>ŇyJGUدvRS]144ϣR… aRgÏOמP K@@5@[C.IZaD^2h,dU(6r5y jRUj}[lA4]Du7쁚DPUde9Qg=[wvxM h ͎YӃEED"LOOcddD:&G mhh`*v[x$X=]݋G#SNưD=WVi'vxxU08}k?1eYUKc!*3(g}UA X6A X]8u* Z?@"=8<ۃM}Wn4c.Y;nS [HP)}m괫p|>HX B ]x2+RYmt޽ƍ3>V1><d$%/hFty88* LsJ ]yq^3~qvBhK#cE`QshFwL`1~ Ug";ÇtF"@vLaaH*%_LÃ̷o/2}F8XLBЭX?Yx1Vz|9`%Ed_#YbeȦJtw3q.}fX~6ԉ=@Wrb~2Aw/3d;RD"H$j M0??oRT?R ݖn@wvЙb$cReYD"D"H$.Fuױ`|e}|hn+AkH$A)E&'փLNC.L h\&'1sۨU+xg|~Eﲧ^Y>@6L\&a2w9XX,%.V~ˀvRJn#y+2JBl.\aHrdڃJ `}e,?6xm-LPKfJ?%I[$XjL]3h#D]*ž|UnO) ]%ײSGSJj*'/Odq2f{Q(0??0044(gNH$T*˲Yr99-Jt>4!EfW&S(D|c&WP_3 1Wˆ/> U֞g @@]@ 7˥?MP!4?_⹣QtjN˘Ϟ=čJlTVuEuWdxQO嵈Q(088T**dۇ~ۋT*Y={}}}.&7UtU ̷Յ\.۶Q(EշAC1+BԼ*h pk~n_4e87! {Eѿ}>Cݭazÿl#Sen5իW#c/ SSST*O(|},~@":'2J%tuu)RD_ggª;!o{) Ͻ.,RϭwYO7®BKV11PK.vmێ*,ToYJ3`ao_SSSF+[%+/e]]]XjΝ;Jh { g6ٕWUFAH́Z,QV=4-o Tϼϕ(&p$) N]\X\)ax>쿜(k3yH4^opS4TU!*W&D088|XY=4M̙3utجo9id;T*9K~ˁ!oysO[o$̹*4wT0?Y"#ˤaƹsF=0<bѳ-Xl;bGxiL>*! m3{RRy@4ۥ(}CNt0Gt IDAT[`zAa5u{ѕڦ0,bL&UxZ ȀH "z{{qE^է7~F@tMGG!ZF;)Np&\O<& c}-U#1A9077Hd%TPԢ-ym0¶m$ iy1 ǯb===p 0@͑3ia5 #Â((4 4ذH !積ug` :(qT XT}G.F.JI]TS+U 3(α_Y"Cb>gh1]L±_So:%H_ӫdqi"P( Lz1͓Y$,` peYok_ɬwl6ٖJf&5On L#&8H}'kТWUR'O7ب+ߗI\V^_VQ.t{㫈Qض0`&[U/῿e(UtLkx S쓾0 <&\RU_v/?a)OP=͛- ߃y䟡M<>+{/\ve`U4oBE6lOH}{y~~>A %7Zv%DtEHU}zLoeˉ"@4ݑ^sR׶m=1_-WP(8?O˼,P=wt]GTۯ1>fWaY-û@0G쾞^_+ _Lgx>KPk߲\.;8\D&Sd &{WS=/"2C!JgYf7/or2ooK`%w84n- Rdz2#pU…kJ^JkϡٽLDf)/!E;gmdO0`Bor964耕0d +iaDiB+G2/j3+0 E(]`c`}>N-QF@ K fW^GHm+g?R)!rΉ+ @x XF!2Ƣxb$q>cb^>,>A`? PM%l/3! R!LJܚB;6XFo}\4״lҠ2L{J~WqF{-L:ˈ5X{ԗ!b1 ;'3 EMX,+ |R2 HyȏJ!RZ) Tcw="Sˀ `X {NT`SRq<=nQ1e2CCCλLJdc 2 H${"d$zPP?dm骻Z`.BL@NQcs|8,K_A쇢|׿ZvZ=*hH'Bj0?l|+daw}C$̉OLc\.h4|#q)cJof0m#ڢiKڠ{[!Qe瀠(`e&C=@({6EJY >=}mxuX c${=H`ݺufy^1?*-mP>%(oW?lJy4@DR0EjySÞtuzDeLOXV^66t-Xdlv_W="c嗵:֭C& uva66nl "/BhnXvPJ+ʗn hM=ur)~Qmeܑ| QE"~趮ew_+X]wgt+(SIUz,6|JiDϵx{\ F0Mǎa4KRd2q;cvJ40/+#룦i(x lq4*-r"LbS-_\6'P meuI&^ ߸ju4Z=f<D~د[nE<B[BE 㪫r$?^rczgH V-a~aHrk,HKCChD,wQcbFb#[J [Jb)efjHt0&W?_{RޠOZ;#5 =/e$nh6ׅt(#qkxvkFL&a&.\hf('SY<ƊRO5F4r|\ΑaLD"@WW$b*I_ b~Q/o3V(` GIaV.qr0B5kלUy`MXoA&bb#cV8Gx1>jKxb()wp]^=ijjZ@.mۘr@d/A "4Ӄdz.U6 >O!e BhL+^ @BxUM|`Rj@j5MטjQ>;i Ȫށ9g*4^=~16]3L^]*!3*rrcqDvؼ2"^e}D)-2/f?_ 0dNZQ BllYMHuR)}<<1b˪x(U>U\Po{b*K]a钛<=g;1\ZL-/=(u jQ0⯣wpM[ "+oM7eA.8&ioaϦPQixx, (bj=ᖭ](d!mu}ci*-@|| _־b*#["%\0CW!Q-P%j jߗxs^8\^MomWBn'F\.R`zza  c``xmaÕnq+E2Ms@b|r P0 OxtLaܹso&atӫo";ȗ_2qY]A^\ P\ĭ'++Y!Q;,XAdx.N1? \|#zWW2R ǏG:FPp\.Zĉ1VVB,gm~5~bHx&nab;A`4.Ad2X9WIw0-)c6i<) Ɩ1 !T/~NiɨG39(aN@D&fϧq̴*К=7(ߛo8cg_{ǁID $Q$EIqѦ9$RJl%J\#T9q^JٲɶdKUd E p{>gvf^t#5yn sss|2rBHd2N0z+N{)4~9d \zG>P|x ʁ$N#8Qy7o|/C.$+} `jts%˲Pՠi1n݊\.K.9*0R \s1f>7vۺB>+SO!$I^ב煅 !@["-#DÜo ɱ߄4Mk%~þ:!?H">鎎 }MU/v  Xض ]ױ5%Sv܉Y'F UTr4TUe-‡&=.AW\l֙?P(h* ia }//?U*F8j5xFx˲3PݿMg cLd(^*J 4s #b1_rw]i4T 'Q6 =֖!6QŮe42YeȲ quP-#(#"a@ ?md1B$ѱo7gP\ x/2B%&QmۆœUKh@@8hu-K'k`kw"wDz7[Z_6ZRD™0ZBuLOO{Boi_ ^ϋYd/?_ B,O43Mo&:7:vgoox`܁ف!f9WlJf-cm4]q(JQCoVCL p=k$l|s͎W8 YERz˘^sppkta3M\LTg^#>|wG+x@+Tx``fFj0 #}wEDk߁'!maG b9}O@[g cqѰs;N!7z =Sy~ƞ%δG SAm4 r-q5ozIDATk$ͦu#B @6E__ @RAVÍ7`TM ۶2LkrlAV !^T`\ū /W1+gl*'MW |mp{g Y*bY~4Mq B^cI4uwwC4ׄ@ƫGs۳E<4x\|ƒcC ڿGmRǟYS I2t]fgg}gj46A' a?+ARrǣXUC6`XH4m u»eK.p?~ΝX !T E:tHnqGjſ`rO>ⓝgو MvZmy3T&D"(N$|uk^~{=~g`>qu23ذkЛhZnD썳4f'tPBBS%g}5)f,C<wF",(ˎ0 e(d2L&Ӛ du!EI0557~m뿯A>5!C|GYXzHAMBww7t]G\F@P@Xt&!D~8KL^@k0mxuRiMeY0ؖ ?*F*V#!rh{ZVMU ռ|~<*={ ?Ϝ&ހ砦i'ux<YL&VxHkYL/?% "k$7흣K(,*/Yib1gz0Fu[n-LpmxY3opR~DAR9XXv~$w]|b~0e̻YxXLj혦 B~۷yEQF/^>gl#}'EN@r1x>'^֛ܽxI%Nsi4 dYX᏾{3BjKiɀ$!!!O#%**dXq*4 a8!@+^;_,=1iHƫ@1~ޚ>?YGF<5/8>>eO G*[!w`vSL@b2)DWEX*aCt'-W>?,}cll@ 2āH'Oķ'x5|c+U8x9Òsb9m;'9kAvT؉#޻D"ᴩWUR)g^B4P *YWe3(ل\o>:Fu h6pQ+9pUnUU 55="2K)24$w_ttuu9BI5,Pnqw__H30>쫹; +<6PP5@IvT%Ir$Y:#&jIH򀷊`۶"sETC34 ,//V*00:RfW%@wK5;5Rmۗǿg-f~l0&-D_K{W¸>D?_HH$~?kؾ:8ǹuI"*.\_Եsa_H7#%C-Kv=%=9P]`%&V;dYF:F2t u](H$h6Pʼn$Tm +?S߹} |grOdLh|ƣ'z;Y4\`Of9 eV`.+I>WTksHAobC=23M|ثw*lGvdZ,nLa&[r6~1#HaKzuIq<=N0p+7KImg8{Mm I&s6v88aǠXjw ",Bwz5uv?8 )x zf*]M$~u"M$yγV4:Tcv$[gŚx`j~ֱ_| @JఏG$s=w^$ _GtJxv;dOw\k͜u}>{i @ռ#-t]'b ֬8qɚ l$ lۆX,~&|\æΆJ_r3g܀[4AX1zӲ7$Izm U.yl=iA] ^R#a<<0*?,?r2`CPN/Jg5;/Oz·k냰u[tKz9akY CAm.gqhFM^O^gΜɣg|9p޽{$p7>Hjxl,"|X֋ƽ~{=S"W05d00$K|f[m<2GJ8}CC 7XtOMR% =,酮J\tF`&P/`u|ױ-{z->o/_h o~+dWVV^M&%"K@$ 3q H^G !ޒH l;Md2Vˋ҃='Soi2هKiez Z >|zS }/pk~A< cܾ}{~llJ P353''n+b@ ]ɷ[!>?IDl>@z»=Wų{ i`؊xΚ J, Vci{/ ^ ٢JT*;11qmKnts` |',fcB4L#̳ewO(mB8d< Ez^@+s]G1!қ^Zlpd>sD*\/n|ڎe;_1?Kh}Ν;㚦hk{0B@e9PAQV>NdEȭ{ E}IGv=З\%|P60mC6i$!Ƣ^}eZ08qh?@}`^l_]]4Os\bM~΀*իW;}8Zv0H޾>f@Gq:mHW1: 6Z&t󃴹+ZȭHف>>J}mpzP/q^wB`^(/48v񱱱I,(2$Ik*v,?v%}g #̱{ <‹wHz疂-vgWT9~X3U!c69C/\.W&F!WMTt.{GzQUR{Tj]Mf <#ZJ%gH߱!#%/ckZ q_\C۵꼴Iϳ`} >0W?-=pq4eرcRC%038H  [ly$I1MB@ѐp0:wMZEt1~5wkwB>~^PnH]ԓC:b_r'''O<:-Z;@---Bww1oxZ?HUKE +@6Td>BA?K5?܎Me@})`vv/N8cOO'엇 PP(fl=tXa&q)oџ֡-0 ?C3$u Z^tN|k04?߼vkaH]q"Tzѣv{?-h'rt _]ҫKfuz{񁁁b~&ҘXwp}@0B;@XR^L5S3ۿl`@M-f10K1c@QĚv!ʛ?4 BppC :1Rp 43<ާP$,Le &n?YF-:u4Uwb_٧ɭн3-1yi|_bnVyҶtSBCl ,n׳UBPc-yWVVοKh-j =- Z]ҫۉ#G<722Ktg wGz86S2í( sۀ u_*I|sIaĴd9`qqG~0*ZZh|" = 0; к+4 /~С޽cd.#DxM}i~:_X|ѣi_[h^ Ws []ͣMӼ׷_e-H#.0B _~ZٿVV@BBRP8944UӴ>E G!½Db3N>tFQ!9ه&HoYDOi4n+NJKK׍E mZ&-yـvO{o\tBIوAU9wo}>yX^unۤ4i`- ^}?Rؠ!Ь@OYaZ$DAOӿY}?'KN>k]o^ǎku/ݻf"CD;0ٳgx9R*9suv{ p=hZD`^D^UJ\,۷O)s"s=UJSJj6/߿crY1Zx{{c@)UPJ]޽{+jk׮`֭A#"sCWޚeffOa8:Eaevktjlov=ǷS$=("xx~v]s:ξ8OecKKK: (0044DE(P+"011AZEDHR|sx%u2`# _ q|~|rH\r1l6K6ED9GߜݬADq2JPZKZe2eu xkqdxǩVh{vC""jNqt$Yփ T)\=8H34?B>5c7(J3|w۷qGhXko _=Jd=&#49~y|K%jLD"k-VFAZٻ̦UzQ_^cƘF=䒎|k]aXwK葷y:rNJ5V, L.'~FSJJhe`h/iWkIENDB`goodvibes-v0.4.2/data/icons/hicolor/48x48/000077500000000000000000000000001337147241200201525ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/48x48/apps/000077500000000000000000000000001337147241200211155ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/48x48/apps/io.gitlab.Goodvibes.png000066400000000000000000000066331337147241200254230ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYstEXtSoftwarewww.inkscape.org<tEXtAuthorHector Lahminewskil IDATh[]yke̜a{l0ƲEcHn\)QCZQ'"K*d}*UTD1 QqJlu.\<3gk}}gg<Ԥ}k]:7uV;~V_Ƚ"SN}gU?"rsTDXkӧ/qq{Goਈ ɈZa<ϻz)oaa>9f٭J1ЈhYkeff浍}!:y ~sL)uצZ1c@Z3cKzO>4`7?N9f= ֺJ)0$ߵSJAŋ/6u~cN:w]n1k"2n%-tZJ)>/"/c^K@\h3*LLL|oxwRZ&yiM ں1vkK+ \DpZn/}ϾU69q S)(x6vu1zWO?ĉV(qi~zk u &V@, "6 Ƃ)fL􎵈m[)oB*Mr&[<1h3<\ "~[n8YL};0i ߾$%BH[%֟>cR"L pe0 +++nL@U}6@thX(BBV4,\/K V*z2-EӓiQ***O^*uuu-p p|qus>0q^MOѼ[~ta3)7an[s_b @ьv0(Ur6#9sPw +y@x`«6}ҚNjY8aP}<<\%0DkM?on6<][&JlirkEέW}}} |hnj1 w֊ 6@, @ ʪ62g?~jӣT*!w獵' !нu+K?}gR$b}[0[k)(y5 )Ӗa7vlDɅ ϽJ]35 uݍ!qoߎβsNޕ?pL51FcpeW&e!Jk6oG}ݍ[ܗfٱcٳ} -qSa:@VwJ2d?{p;\|]vWݯ722*T$B-!W_D`09|͸4Gur"m!\+W37KbXk@{s8{QQZG 1ߍ2!=v]u .I66Z H,--.v$b}Hbm A"6rδm*1[&>l6ql68֕R84M<# CyPW7dtV̺ P%($I-6[X,'q bq<%07 z~S:(776WCI;Cf%D++e\EDp')]% C lRTZl6}?c $cELl`CF͢PR EpŴ C!|}qb~~qSR*#"t-6kQZĄ(i ߑKi}ZrN ZmӅrEAalW !?Fo}=x-eX]]}FGGl ;VER!rşz\gTkksy0aʳT[k ŝ{tX\<3Cab2NbuF25dps6z1~h4"TyOF~vϳ?ѝ7F;З o_6K^V%3l,,#]ի8I)Ek=?22eWkp+ŚF,".>Jwk#ޡ+,Zŗ6UMv^;w(xvqyWJj.\r\}"wu?;}uր$KL>mgN]?.,R#GXv:{JRxr3S@#0344zP(;þg ODqZ/;gVOvjZz7177wh-[;S(A!l]bFa]%Rm źAFh [o  zm}sdd+lw/kZ-s}R ƀX__Bau JV[>[.`&Gd/ y@-O~4TKׇ>aVrpIENDB`goodvibes-v0.4.2/data/icons/hicolor/512x512/000077500000000000000000000000001337147241200203025ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/512x512/apps/000077500000000000000000000000001337147241200212455ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/hicolor/512x512/apps/io.gitlab.Goodvibes.png000066400000000000000000002362411337147241200255530ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsetEXtSoftwarewww.inkscape.org<tEXtAuthorHector Lahminewskil IDATxwř?ٜI + `1"ieWgl˝%0X0lp"ъ( D[&H rVwBNtWUWάNwSO=Ϸ ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((( 2(((((( .ԧL209#l0 㵲Goclj¸W\ѠiER xXӴk~o+EEJW\q,MSJ;!ЭRat˖--oPPPPP(9kY$ !|JtXEh PPPPPPUW]ՔL&/v@G:y|׎92 1H"  : Ø %ҲeB UKj{Ա'xM7qUZPC cŋO3 B BxN:P[~[":NVPPPPP((bX\B|O4vaꠔ~!+h(++ `aE !uvvյ(((((/n.$(ap\uX;(B, }/PJ[%>%9UTPC qUW5qIzCC"0 cmݶ՘(((((x¢EfJ9fgC|MӴ'4ĠX"H̥v̼\L0!y?@OvE=555MX,.TRBP,\P1ci- tqW azCޚOxַEERbD"1#c,X $ `/:74ا61((((("/^ PJ;ya,{|ֹf#0  <+[QPs1bX4Lk/pH~g9xn7=~M@ٹQh@@E~L2ߦL&/Pey^ɳ 0 eLR(((((=bJN^`s(满XBadYΞo4 8[Ь" %X,Va\JiG*0쥖B//D}1K[= dB L;T",bq囟f>X-֡܀iҊ(((().\Ϛ5kNqFP|s/fH"^2P@AAA&/ס^hFKnP@AAA?E)./1Q,$ eqcQoIbYiE@{{{͗QE 6@?lJ&jynHAkYeݬ xBȚVX'4]wݬX>tĴT|k+/վʲ5B۾}҇~8%4DF%OɎ'e־^[ǔLޚogxg˗/)4ġQ%KRځ5@8x?:XN\&+sQ4ML)vUWWv"(p ieee'uPJg,V'FB,`9L",'oϳ/QJW)]Tz(((k,]rxxڀ`Xx*[|;XΞwR$.]ן馛P@AAaG?QaPJBkv|ާhB#=Y_,>Wn3yXn淬.Z0~tҽҕ)pBQaɒ%-_vUGK/`?"~ jɟh־oK 0뮄8P@AAaLa T6X|:D\4o=gOݯnQ_ 6Ro\_H(Pp,]2Je/^ wp>E<~nynsx0'cX8@BA] ҳkKnx_>z 'J$jmC\zsN,%Uh־~T*7<_SJP@AA!0r- 8 k2Dc{.S[y=|9Ӕ}}}O-_|XX˜@p7u˟C)ʔ* zXY<^/xo~CXBQ@O,ORµ2YFBnU/CY}+TS([ne*0%*rz|/P5c/6kE)]҆" b1t:6=z22r^zn ݓe!IJiW$Qk1T@A(ƲeSOö6<YR ǖ!UTkɒ%/:GPP8BUUUgk)'ʔ+22^E!Wr`Af^xR,vkɒ%\+;(0q-L OY6|{2q||ԃ.6c<Bv>|i6_AqYuPJPJ(A2ږcMRE)]u׿)4Nᨃ" 儐sӳdk˷EF7v0xoX(((nT"JKT):9uޤ=|n峬Y|BfJi!dݻ|i(P"x衇={fsm~dDܞ/ӻ2|e)NkUk|A"Ɗ+ !VhM`m>IYE)}JW((-[֮i6󊭗/#x==N< {Vv˄ (0ƈb)8L)9xQzvsн|{>7/xRڥz5\+((~4#ԘyGK/ZN6m뱗3Ү蝹 zBp2 #͗>=Ulp(fx\Μjm˜AE;SbxxMPJ^Y,} }w 3"ë*6_(BXlٴp8|!tyxȎ绅E[ٖ/  l6 o}[jmBA ((䀇zH裏NKR3du22YZ'{љoد䭢C!k!7f" B.y-< {2rcgE/&EV)-PdP@AA+VhO;G) ymf"Ȏ0!R@W(i(@, 555I/t/_F&^>KVv2 Ed{BVBEPBP@A!'M_˿RZZ( /B8xY<![(PJjjj|q 5 P04}L$YoYkϱ+;IϏlr6o/k))B+U\uUjmQP<r>wygtnzC&IW^¡~?=}^{ng9q1z,us)P%]((c(r6?=?z2ed伐/#cܺ^FFgn:dgː /|3--O)} *M|Pxڽ@WXўgr׵A֗ ro|ϸ;+ϭ.Q٠$RRڵh"6_AAW%wvvF+**>i\J(Ue{22nNQ{WP[hk^K^?!9BȪWk|䜀ow}w4QJ/u}>"5"Z/_G֑ˆes͓m0bX\%Pw1 @adF>fU/ZO>K`HiJzk-Z&|QPPCm4~Pҥs)aYo]V%`2nrǯs^qB0Ǿ﫵 y"ubŊ麮#tPJQJx`^,(+us*{!÷7B 뮻xk((9PU,[[[O#t|+rPнa\zI* !/X=<d=)#'wyg;ͧJ*8[Z&2^zRVFJia]b2*PP*0v(k=X,zn(keF<_ }&J}+G'oڋ(]:zAyPyyL)]iZ֭[Z|ycRP'P ۔3p5MzsDe"'űڝHƫM^  K1)RD"`=KAPC"_ߋ^7Y{>S>UL8at>D6x5O蓕 1y<Q:lp˧\"X=<(p>J3Db޽{[fIgPD@`pԽ5FE/_mBP(4y 9 BN Z[/^57( XmFC=7|Sz8+~c@EFbǷYI?` SQy=РC5|2\}7t{e2 G cs{ Xb͍BKkW'WVvf.c,{,$?n bD"s=O?  g_"i W+k_fҾo B:4MêòiPeځOcq~ٮq0WS_[Q}篐+dx-熣 mǫCnѢE'O>_K5MyrZg!9c ރS;vX|#z2={YA{+Dd';SQ8|ۦ:4M { u~!IY2cvNߐJVg|?[Iw}1=T@s“ͥg_Lb<|%\ϛ7h4:22Q9Ǚi՛㗙<Ȓ/Iw27,~Ba!86Ox}=lٲ F^{A,SP7d~eny~oa</Y7rW7 xv'% x2н7ᕱ9ji}aߨG)ݞJϾ;O}v&\/GV(v;ρ˨x"84c[OJ;w!e{~DXquT#G<_bΝ;/HB1w~/@aα^e_~yg9GP(9g) 7)ۆ~J drÇW-Yd% ~A/7Eqy!&Pmd9z9]믿~rMMͥ_i$ o/eD>0IRO JRvY8/AyyN( '}AV_0"eDrhh~.i%O{9sY=%R%^A̙3:P(t)SsݵOT.k;O4wӟB/7/e %"4 c?ҭDb!d֭[7Z*gN rhPu~fS+k'ODyeD 6m(ag|`R!~z-WZq'& ]iŚMRc)^BȘpsn2ˇ>2!loo^Jda/m܂u^@ѓb'pRm^pa䬳κ4|EӴRJp}!~{2A!y[ kC1\6AJx?t*4*sS4(f et;Ahu>ve2_ǚLi2d CPEawfA)}.J5k r ;&Jd&.\X>gΜN,^o݋#F=r)"7O^FT{~S6(x>T*˾{wsE8%Fr{r_׫N:餯B8{^ycn>Pאf.-Hf෮E_1ؐ}cmC1\\#i4a .{^}D_|Qb"=9}{SLA IDATP(/2J,N\v Aɗ=OGR M_1 Bl~'{5RK/h@g߮1cƕP[ӎR6_|neeه^)Ey䚧^r/,';SJ骡,mu^R?LU_ L9b+应+gEs0vyUzqD NƂq<ǟF'LӴ/:3_-'鵬'RJ__6h/C)1<<{%8I^ v?{c(`ɒ%O8KӴsocnX:ee~%[Hx! ȗbpbWg4Kǫ6 }y h;vvqOG5RBV3 .l޼yE"5Y_4+,XrUTӞ*( I8fҿܥ2~22SJqeRu5"~Zw.Ǣl+;Bn m{8iB(//G4E4Eee%BB d@KlgK|)3i_d27!ڵ vΝ;q8A|tRue"o2C~q>!aqE_'Zw\ R:$stxW%k5qȖfCzr^d6 SP{82B*TFQSSʚz749F{>zzd v_04q/~usL i&ƍyضe#v:P،N15 u!K8SԫwQg=E&|ʫ44Jxye3`䵏kReo}4vqljg,e2fȴo{N={sb!`!`aEueͭ:mmmhnnMsy-c 0СC~Mjt'&:_iы: {Rlڴ 6W}&VSq8c8~JYK%ˍ6XŎ]:;{^5v8tE2fH8|ٴH\{#M6-+[xwH&3 LiӦ!tuH@f8 ]fב#G=[lSF^ѸOie2]eؠG47_׽!F .S ?QimRjm!LL 8we˅K[ًwKw!7 dg@c7 plͅ0ɋ"7xn!'8)~5@v8Vf\rezrqO?\D-Kmƿ1 ^p\Z,[KϩEUqm|?%j$dFst9"$hI?zI N5>H onV#X.!gNO t`1 cî]>1E,ci\%:/t_W Ѯ~^'cǎxgS+K6ZG05ߐwo1 ! 0g* $p'V/$`)W~'q1vGn hl>v۳:AO  ʥ k׮͛fUr^0_s5֮$ds˄\yryql`3>\F|J)} 21g' /ٛwU@2Fr">`rNH@}4s|,{ޗtP'~8餓iegD`ƽsϷ*-#^C%-+ Fr!g/SNy2Y=:yطoxmjG#C &5E-7n+Yg-Wƙp#/y"\bg= 4 >9 #9s.r!B loIWք1H0sޞ"woMpAo4 FK я~[]?gwI"u'788_t|7K[z&)9\ǩ onr2DA Ds;A.s~eW)j"|{s!ߛN2:ߞV( ?ׄP؆34~'nlۿs{1s` ;~yVU\ͼyi뮻htt^^:wKɭ[+y'^1Z%Uީ0gpmyu~93C>˃+hNJ'NS$E0)0=$KYgd z|3"@)}}A rW\qʼnmmmP8C,Gg_|ݏ 6?ފ~Y3~.W=^Iƚ( pYv4LP$ջvTIynVAPx`!`Χ梵5':GH&}.5,[ˁepYgE??i$ A[C\1;q鹵4l!# v<4Ϯ.\ygDn$rG>jE6uf5/x]wle3𷟭Op}3%?c.C=b m0 ϋv= /-)h9dXE8߻2y^VȬ^0sEvqq d"?{444+MN;M֟R>غO/FY\ԚQRpǓ."U%o/2b xy qʸoûn&xB&5 I=vu;wHGP@\@T@FDB">+P a,PId=/a{v]?7-]kj@I~㥭TV*"e8]f?S0zϞU}֟},^lgEutJxOÌeft~+9=< HKyt,M* H`oz8I HI9_Ey@}E5hFS_suUè*K< 랷wy,w!i?mo:P^^m!H$~_|m0ɛweA޿}_Vnx^٬4Nn%^֮8'-#w( YH=>tfҲRd"ًԞkt:v>3!l?Ql=֏S"i!]]3EM2 ܄"s86RH)Rنו'1S0nS1v!-n_fNi~9f?͜b#OǺp=o#Ծ}|'6*ҟ=Ye9  WWW8{4}~>Ϯyk8WLN5WW}P^ E-PDC2?N^$`(}Q*0OwB4= ]׳DJ)R @29N%H&35`joM8  d^å:@sVb跓G %ցI܋>E 4{ >p}}L|9G/ +x^Ӂ^ؿ[xy__އnc7˳f͆9<鲒٤ILQeIhB!IPxgW%L>֡aB̟_1!J!L"L"H JH7tBe'I=8qK9TfޱL#)(t03qfޓΝ;O_fVd;v^ PW_}3 ҡ8ޘ?{Bcܾ6t^1BӨ鞞y>xaA<ElYG< x}K5QAiB&H$0R $#ΡjɇqJ[7ºyӊ}B0}|4M8>SFM9E D"7b6sE~i3o؏i$v.-Dzoߛ(K{gg2m9{ODybȩXwO9YH^z$ذ;?}PW?¡0{A!J!##R,Dq8{G8calC9ߙIKf?/Ϲ&M |.sI⫯zَ=(W6o|b'VdUa(0 }-;5+C8$mesty/ 2 |Hx3}q.kzJ{GK=Fp HHdsD"MLQugL>9SI2KE4[åʑZ} %gyCUUSFv.@"m`q(ɤIdw_<OD2?(gazKIo?4"v&&L|*jg:?g="[ѥlSt!bPN2'+CN^ivZj@0;mowk߭ WP_FGPS3^,!T!H`xxoʶF(<#|v~tyɤ0_{y˲){u+a~>я<>sRuV^(G?Hk0<]od '4K*c,ɭx晸sҟ^>z*J-瞨k婜va0kWKzVQ+^ӳqӳX9E̞C.}Hkީ;;GVی82)0REs.8 7$ӫf11[ lk;&@'kv:4%mkUHc$x~t~ ֽ/d*Sۏc<Ȗ7CӴݻ7@ l1…mmmmKGl6mǬs^۱GM3>?e,춓O핖u~fA)6q`9e0dG[YYJXc9[!W?Ċ5-xO u8hjjjP^^}]Bai"+:4mn ,'8!IFmvu2z]Rd3N <.:ZZZDsDl־c10cUh^~ȱ{1ϲ4@ʆa/kƹV3̄KmOכH.|#eya7brtYY-geM˒!插u2 /=̨tY2593 B8B1((8ۿLʷ'c`IP]`XuְY Ao %՟cLE]]oyehCOֹ{E s^M6}زmXEQ7{.W|euK'./pT9O ksTk?gWE"=q媏sj%pKW+[_ UUj~@ၲ2$ {+Ԧ6ē7#3+e(z`߇csBlÎLnj_{7o޼ӧwY`Dį~sՒNk't>oϷ!KI GgL>旞]h. `#!z˹9 qW zQYY% MPVVHY*< f6 pSߚ=Y㪄e}Tِq1zaF8ݜzyyׯ_>g55 <+ .h4W#J`i $߫ӓRNM(?Y[dᦃrh2@|wc6u2Y(y X&ҙ)k8z9 [,͏`՟0qf_a%$ņXDD)LL0.(2!o27oBf4Mn-Y푐KyY/<\ww7?MFQ0]ʡ S̷ n6q\;d@:e gR%jx$d'v٬C %$@o?jµMĎCQTWW!SP(Z#(~ ,zb6?`7cMP,/Ԅ`˖-Byy֎;H]," 4'0Afc[G؞K/$ qbNCwaxNT.qrdyͪs/o`µe2FhegV ֏b@Z@~G}@@֞OB:FQSJ144! #Je޸g7Ju-J9#QJ:o(u=sQV6a4-kDo ?1;i'pҶ {EeaX8v,B|~6p}زe fΜ'+㪪s1+9>r544k7Z2ҋ'NNs^d(W0H*k+"2p2Y ⥃eYṡo`KρqFpuU%7 G?088d2r kG6S7 I,a %sm !@4E?}i{u8ܼ8|~L]* "!5`ƌܹ{uulyN@@VrL"ȜLb:楇›o_0dz2=} ǝ/g_;ڹe)/+fR(#?+- p `nX^Xl PJJ\ *++Q^^^!oJ)ۋbhh(kT7{;'K9znFD"TTTUUU*hiF4\}*x3yyl4`^ܹsL`st]? N,,^`'4LQ~{W^y'GWtH1IumE:--K.@(S!d2o2~fh:|r{ә0 K\o ED P.xh@C,GN$Ͼ]m@2 (kE2Dww7:n ;8Λf-=6ˋ,=k)C__ MPSSZ BF"Eeeega=OeE.#Ii{pè%"PSS3&6E#(m" {쩼q<x |z{{wy"U!Bɹ‹"7[5a7R@l~{V3w&FEYy#OC^?+=W?Lp xqC"k*L&qA8pݎn=Ypf&eVRb>q˔7 ‘#Gݍm۶/ PW? x69{Г FC-ɀOm[@P$K*Y ,Lns HEE,y]ĠY2~U 8 > TOx!rvNF(//q(++˼>7,31O4.)J&3 xf⡹7Dau=+:`hAܹvByy9؈H$"ysRofme'mf 'v)S )leM3, o d>d' @(:UHv_~:8}}*"4M tGˇ vvE,gqvW0Zސ@ԈIkOUC)]pL&85ݚVYYMP]]ʌu BCCCCooos``9%ݻQSS&֎霁p8FwbZ|wzFX,N ~Jq̙ Lݻq10e8c"CxyBB|+ذau{Z rs;euqw'\C4p?^iH j׹6_B Ac ֑7UUUHRطoݛ53 Q C؈444d;A`=D BQ^^6d&49r$3fk0,B`GA8Ƅ 0a„ <: ?+}3xӺ`/SXp)q[-$̪yDYvhO3^V2DlAUUx<[b޽\F:P]]SbÎ~sq,thCuu5;8̘1ݻqLIDR~lܸUUU:u*\bZGq͚ٸwqjlp%(=>> ЌA9r$kFQr!,uf*uQW!ơ=Z}:~޾Ox<XRzL9$DD/C$; Ȯw k Ν;}v w}󯭭 3gDsss 51󷃧+6X? ͙=ٓhm+؈)S )F:/>J0=Ӆ4 em<'j`˲u2~@Vd@ZQȪc۶mhŤ2Km~PNߏ4X ޾ȐLcFgwtۡ#!`{!r$lu6"Q[[~Ė-[000ȳ'd'Oq Cr޷l\zAD*;}6cE{{;ك;vdZ5iZfesu#iYp8ֶIؿo/Ky hi"Si/0Μʍ@i{SG57Bs{DxZPC [-dM GM8c6+t8M^|Ht\K%fζ(nxtlK߁|0~Bf̘3fd"8\&/: :$硫P(c9&MuV e륔bϞ=8x O(Ȫ'WB!M1>y69v Jfc|}{R"%x mߜSiV_/=Fx($~j#APƅ X#5̇' рlQ,3ipV .v2H騮.~ؼyn ?O:'x'o:f:·Bԑ~]ֆV߿۶mCcx<1ca$sZ[[:~0k7g3]HC"_8q"bpp0kLMӬ~0Nya"({3 L_0˰lyEΝ#2dG$x `9Irvn^YM .}}}ظq#wus{{;N8DQpN؄}W!| pFü;%c U@C Y^+e'@^~<;,kÈ8"Q_Tg^>q3yj;B2=~\kp`ˑy.~/$?m" lΝ;efO^KK N;4TWWg97?gbHs/2Ͼʪ"ͭ5=3;W#/ 0`m~4`0װaȋ`,ke]kH&dqk6b-~y"#\NTX%cIB9Mq|U,JZϽ8~8x ѷ?LzN'n t4Š/"Kg&, +gw,O>ű2-R _ dUE!IWլ 2H `%HKi$h<!w} ?q rFWɓA<$@U@T.'6pur8c N޽ n33ưg8|07`Vo ſ둞'Qїkcܸ[Q6㫑v`pn L!8^IiI@?W dJ@0>aB d!R7 nnEYIQ* )ɝ2~777ٓ^~gm[o/Fɼf^G9$eY^" 9T*ҥKX]]F0QPx<ƍ73g[^^}\t~&'Dy2g '<8>f/<_"jXe10Y%7O@[iIKu8)m_?DYe@qA|"7@oIz"”a_}\|yL< ߻5Ia`/!<0쇿`Ps}E#u]XRNS2 J8QQYr !0%y*N ~"WUoDМ_y6ZN vB;X,jn IDATq;wN(}.'opuSe͢6XL&ys 8yvO/biI\vY[[y[@4?|42H@Y> A'&8Ic\=V8(Q8ɸxgRR2Lad۬RDw(ir2Ŭi@g _Ր@Wv<-]070S9_ho}[O Si6Q,uDžU,tSzMʩxwCM| hsՉwD Xm6s93cԑlK1WJ.<(:?;]ly`xYL(Ϡ4Qz}s ~o QO1 0wcw,o;ݻf6\'Z3ȠtXYnSaɓhZ`ox>.]AФ 6o8͏/a׎oڠ ReP= À<FL|1!?oo=pfa#F}s_Ն$@Im4O>[oORR7_͛˿#TWi@街G[嵯IfݫJEjI!w~CX;_{ NLs&aT^!0F}eUj$$"gYVDz, '?т\wDO!o9 y彌mz*KP=σ89(={,sɟ͸ZsA+LӾM?{$K}%s޶ zI>iB 1J ?S. kY^! fF@8_Om5P*u&{?&wNd~~b\^>-1+,m( |2VWWC8JM#P[N|?zx xtDw'%;d\F'P-u҈N``[e<by">w?|?Br3*x_x#@U/퀊?IY&OvO*?_ ?O0;qa:Nm>`4tQ++ rEÛ<`4pQkpr wiؼ߃3pTpZ%;=݃{nw.}g>ЇZ\?Cdadow=%X~GZrcmK./- *MN\t~w(m4D8t+T=} gϞ|Tָ&/`Su;^1MF,K4^6u\r7n`0J ;w HN`"VW;+ښS58qA&,܂&$]gKXL2X䢏G~}ž1n~/8~޷" x?7sؼڹ EKuXEG~Ekgju;N0?xb"+`2*ų/zXX+cdPkp\ Aۭ h2 6 6_͑X6 SuF&2dDm?|cL;_ץ BE, ַBICe<뺹![Dz~_E7iCrmT*tR$@,Ν;ىx*Lx=ӧO^69O;x%gYV4.ie~I^Ɠ1~@*l,,--Pmxqښ.#dPm=:IZk0X!1\daX[_Σ>*tM_ޠvVIڹ?-|{3_s: a -ȁ2@NCKOKhZT*J[GqכQ&Nd % զowƾ@jaTnu&}%w7p= {(y /pF7*}q},ipgX(7+uoE;vxaɷ[}ia>[M y<;6Jϯk?.u]'?~'O׾,YWSuq3)&J +L'Q\;qAO+\+uTEx=Yd dZ=cpԟ}\g X\\@V??^Q+ƅ QUVT],#Pi9/{D@rf݆4+ Ο? 8>p A<r7_O>'8 Gˀ9IV$%H{DZ0ƘqȀs_C[CF.uK>{6x&V,,2SW_.m\%>u?7~׉u;o;U] ^z%g  sUM_n`{aυ]`9|*e CiTZ~b]UaqϞ=õkׂp=;{:SUd9CҖqmPYyAuQeZ3g_IݻHzW U0P.q􎋿r?T+e=1Ht)yΕAVQBwb |"TC"K_zo96OӋ f s+QRۨu}YMH}UbQ ÉZ AÏ~(౲{Ϩqm7>V^Ze*`,VbtkyM<&?Ix~tV"r%}T?v]~0*wssswcyJws]Y<,olaA C;lĘG@!Uq/\DwTo}j>Z .d֯Yɝf_YO|k$ "uN}. CY OS[1TU|Ma^6ptV;femcnntRXޭ[B -Zo[ HE'urıfIgf௾`^{^EaAHFk"/uml0`)~[B_)';'8P$cr_6̢SNuv0ݻJuxojoLRcڌ1}N8aJLbvGs/рk1y {Lyo*h6[jZk> !裏&eyqՐ5*'o_-1 96$lƙ3g`6cp'ة1]lmm%c.esbBY/'<=qޞRtBڙ_=&q1,+ xV^qcرc*B|AdܟǝvÍ7B_7d}-k2/wܹ.*ٳ7 34dcHLTH+&sLoz&7vO<Q,W=|'_ێ.}-#3=T96\6?~[Yz++SxҮȢ~ @fyt"MGXx^4ecVN!xх;pb=mƮe3};Dh̻`".>9@UF \|QEX ou^Ai#޳!ܑx?\|C_BH%"?Vĝ6N\ }[{7?OGJeWu]{Ҽч5 0/f evqr'/7~nx7>o"_sF CG6:GP> N{O_uh-oɝ NkF^"d@c߿_F\\bWj7n`ss321<,//'DD1K]Ϋ؆Ycֺr|4qox{%]bյkxt hJ{Xh*$jv6@[Ʌ~ ЮCߨug{X:YEY@YhIa e{6$hc*DnIgbݛN0;(Ykݛ|f\18oRM0xS܀O-NNO>JoΤCX l,˙YҽFcjD S`kkK3G'N, Gf0 dc 6" gSSpcmYG"7B5%OXZ|ӝ: qovq=yG{wݧC/q/VqYI%#3O{ m[1żxNy\K>vǨ5|]k}%Ku_;MoFQkޅ7:/ `A 8^;#|yzIA$C$_|\-'֕߿'OD:fΝ;BM"͟Sޞübx) XYY q*q<|0ЗPa'N@gnWA^QqC~y[I7 7wqTtG~A_Pm]ft.iwo\y!| >7:9?pz&N~ezsssJO?c4|_ Y*.~('EwϓCqfI2sX\\DO@7N + X];D^C6 {s3Ck>aܑsM䝗AhFか/oL^246ކ;p~kml=v,w໨OTZzxtk2x종ce/.&.1sEcz9u~=IzHD:~xƓ5#7!ׯ_G&ߑ~f@tEMg9NG$#.Τ @8uȇՆ{Jgr TĤYuǏc ?~8Ž?m6dj,%WLK]Te:Eϔ&Ω<>R%TlZ*0uc%.+/L 8_G?.koa4pp; rG ztʙ™`Z:YAbuߜ-yY6 L}DI:DUy O/dok IDAT> u?>X8 #缞ID~q`9I[rtaP}G0+&$!$m(JXZZ`8X,1N]χz0*β,,-otojx:ER7ƔȠyM"a1K&Q_ lYgϥX$iݖ%{֔&6G v4HIn}iOM@_Ga4E<QEFN$2t)E0UUx,ˊH]8c!Hr2>Ky28h>zVlkROO[zGWLl!aԾl5 P) wbҒ \؈y;5ttsgST?UFʐu׈/JKS䂺TEw)Ty*ϴlJ(E=Km;gzZ8#:_qR ӻ&8ZߌmQcYwdlPƘ+פin^|ƝAz-/JZU `Ov,( SK_?8d)˫W @o- ^PNO'8wE.:#'j "i q耪3?uL3>y_E F#exgϞa~~mk~*L[[[õu'QNLtgeQ|b k%k唃!xZEzX4?0Dʺŝ;w"@ٳgqBYUӖE.CU9O,^/۶a6iA0-3ɯjhSmwnrx(1y,qCx<*u$CpC}O 333& @ [{ /.1/70A/SץKB,ub'1/'WU 4gIzRl ė2/JCqp| afH&Mü+,]iǩuy(I%f舾Im%` ׯ_'ouuF~.Vn ׯ#FuS:pUYʢTag&.NuT+`VNt] <4eQo"%Ӎ6 Af}xIQUX\\D"i-&ܹ\랷FM\mbZ&qId3qITYyxC. Y j{oFR>b#噇4D_ qYDEE{ Hi*HE *Q$E>-} W~1ڵk@^l6aMH=H&l7Z(kRj̔8 IaUĿH^VgZWܸ9u Z5HPyh4 >#;sLvI#&i<&kݱ*N U]g=ә^S}8y;h9<{`V{5n أ(+ c zǷj߬\>=k%sD=/@FU0b롳8M<Fu?VR"*Ν;GOdԩS7CPӐi]dw]1u.j1U.FQ)Zu?^6/P(X,bOG yǪ為_@TBZE6R$G6M궰_qsS%VH# HA׋މiH@p4/ `ǂSA;<",hA`p i:~x|K"j4ىynIEL\±pړsޞ\=Թʂ&؂ Jx$!P9Bh%)cYY6YOH\Buolԍ7;w0#@Pj"L'rG-!3F`YdPDN, ضJZrRDn, ܋Oh4 @VjX,zZ*2@[L: TS neqz(:EQd3O0瀙뺌Wj&`EO;+@4s_LYTtT/\&?>kar" <8qMDr][ 0$V<8888APrFFX誶Q2Xrl6Bnqpppx*!:QUeS/^/1_3B=Gb\{VZNz'U*0aio O$n vs L)@X,^P&S՟v,tU\ !N >>txĊ6x,?mc 4)sHQuٚRŁRCU$?UWx~d̶mJ%ϣh(*@IvT 9??~=m CgZbA%r * 7:*sobZJ"/ Tl$ބdP` 2J, OrĹuu)C 8{b *U,_F> +++ױ:#UpUsq`ZVM3qD*Oxtܽ\.888J"JRKgkk+;e>n8,NƢ-rK~1"|8bssm,//l@O'%2<}'YVJOb0ӧX\\ v=&"ɂq\_>VoP4i,JAR`b P(, 677ylJ%-xtVq\\Y*p loop]sss@-tOqK4HIIrjFnq"XG/B!Q̱>@>&͓-aόLjmLxBӿ' `)]PVlֿ҄u2 Bص6ͺ@+K\qᬬ頬8CMY<' ˠ,i]n-x?nuu5 ix&$ 2l.8" %o+{HxbeOFz3ϥR`oo/ u0%b N=NUǴ">~Y}VLo7LڡD3pǧc7PU=+Pl\?a A~ֿ7X\^4T?󛛛@_.҉ZnU ЛĩڦJjXYYQZqֿuEZ?4Q`6pppC>+TwULr/  BFY+WE"0יvk;u=JCYs]+1$A$ @ LN=&-7LB8{)J W19OS?yUxZVLfr= my ERWKxwwwqK.[=[P.ɯiCTD|lYҟ!*Hn@dY%BtWfKC?Q?UŁ~ ^i֣A8” @TG-?y$H'.,lԪJ !okk X,beeE ڦd ) -nIPzemZ6J6u>I'f%T]}h4 _b_ZVn}A%9hˋNNůmb L* |2" CJi{GEuJCūxFOG BjZc9AT iGuָi>@SE??Q&rSbGRx<Ƴg"}N_ Rޅk*b\N8qW'BnEj( )c,>hHiы`HM'WP:]u__Z|9)R(`yjZ| 6M3+Iݔ)KyA$j7"qd ($%e !J'u =e:EZ;,OoggjZ&BuҳD0x.k.wZ}i@4)@]^!^_K&_#7 `0@CZn+մ^"ѵJ+Qu*m;ɟ[N*eޯ2]µ>z feAż<Wg @$9&Y.i@͐ z84Ebo@0g7rtBRN"ϔzH^ :=~ym*-NNFlFVZTT`/։g ]mh4t:EMc IDATGi'<P(`4KLlnw.Cx(/p \}&4s~r$=4< Pٝ 8pc 7h֧(NL<N:X$MPu+d7ߑdv0ژ)7cWs]P.;BW;C ]/G0& i`ýWA8aS&q 8 *JV$׆,TT(A>Q\h**`@@g)TcTOYrpXsw$ ؛յEӛXr&ϑeYVFhX\\8tHN1eYGvz+#u 刟思}SQn t/HN>[*O C#OqA-z^_ZDSk [u/ Q>exz,nCy甄P%!J\0 =&/j,H/$)#v1+Zr)=qDJ/?M"b9 ѣ+bRz vfѤ> 7GxC4k xDIGEXd|P@B  >x1Er9xQd&T,~yhA}:Bې{!L:US!0XYe u#LGΫ80?a".*=q*N,ͺWU o>mxN]eHb !(%?ՙ պK H 0-Nt$A/vj2ЉI_T^Ivڝt^SuPa|׻xl> !JtzUJoBeV3aJnP.5*\'>Ri'@AB>Ґ R،Ds 3xU䓈yɸhSf7,(p򊿣(B)؋UEq?L Bȯ]\&KzUr'HHpo=+х+ z&d@R`XbRA& 8 )'*GM;;My^h9ף0cYT]tM_Ne0~5 )˞ Wgn0S Tl~1gb'%$L% lw}f*G#M u"ip %T4%OPKV20X90We`S#ΘJg+"S`7$$@&:*DbUGLd!2Ku{ P4&ֽx8`$XD vx #Drr%dhuqS cG]t:b(W"&˦,ES=&a&Uܪ󊿺cQDbN: OKK0^7 ^OJ h7iOG7"XJ"Q. ~(2MOU4 em(M 臂S;կ eew!kȝT)uuz<&Qψ Y,aza&D@E$U"{İmX8]_5EQtP`m40@-$ "']Q⚕wȡ_TsX7! d,}ٓIH(J8bO5%(GY;&yeO7m.i:j :3J(/Z"L/վeݛ9ONP*ߗ@ Ou<L N#9=Y%,PED` dhC:4 )rկ 7X5}aSKY&eWwZ&aԯ(|?aݯ QZ~\3 HaI:>|L4^P$!I=) UO2$` *fY˰,+2LUeTf:?L0&$IkrO@dX P &'+ *MD'3H kV%~U|_‹i wb8=O'Ir:K7~.kX-m;$VwE/+ /˟x =?*V< mo+ZjtIXIq*'~W.]~jccf0m۰, (p>utKx;]%:|. P:ceP=:RȞ$Q?%V)!_{Ȥ$E#" Pi @$&7&R>eP^l 7! Nuqxh#nq%#֚7HuzP.vYi03TUmKRCI*L_e)O bFQ _O^y$BJctS1m$Vma+Ya-tZgKA%uJ!I>HxSH^PF(lj2uy\x%<ޭ cU^>:oNO\OyRJ'c jR Z-RoWG/r?SB EJ%ABH&vRbbS$&]Y\J,yL_>:0%? "h"^bۣ4srHG6@8j>F(˺)w…BpE7O#e.EҸuiENeQsIoJx]Tn;d$)V8^ojrNb9mJ>@:OWZ<Ue֕Dr\d#9Ay!T< K%9z>d~AKU/zU?L_ Bl.1 c-nR 'l@\qYʽ.NjRTP*y^dcBDl Og˿'~U>FQm]Yc:}(/?Oz4 MW@PGx'@ť V}xf?@pRC;~Ë#b&74:w.ED]*Z r^$N޴|tS.5ydp]Jz=(S D"fgH:Ϲ8Wz@ H~玊799 M#DOW aS[0I Mvs7InjO;d-~l^Yx%!3qF T}texκjCՊAN~zvTI@@EZE^md}.NjK0U( #^w^?òJ8!#pD=MtH7 Ld@N$Hr  r&}tف.;<Iu#+-',Āb5!2_$:2H*PtC*R@?%EYHn2F8Rm"bDr;!o5Ȭe#IBo"yt"%A,^*Ǡ(: itḤσ<'LEkic[7 *K>z(KPo6t:888PNL b)FK>6!q*t:mJJXb)(I:+-PePށ,ůR׷`;H < c"Ux<*֜V1PćdAN(rѤ H")`Gro6'L2bd0ouKs? Yqz;p2)1)ђ>6adQO\;T (|LxV b,lRvvG6yy*3XjXZ?"g+"4@<{d@[M Tbt ^eINdV:qX J|΄P&P,~X1/m)Jh4`iiI[O2 i](uO=;n1j54P[XqIǴ<,~n~y 5RסkY7_o !6 #iT''aH`zFLRkџU&idOŭ/i厵Zn@h@r"i'OT#i6677sd]2T Lu8xò<%xyS_9 G"mB"' F!^8x EY_hyFh@tp"vi;h1T*Cϱ[EVtY-i~">ՊOI]s[B!4] ܀ÐYg7M$ |,8X7i?ZVVV5bC< m%/N? 3@3ɤbդ@e`4 'i2N4,lFO ѭ['@[ Ǒf Ahe=f:QаgN0 CX*ೄ;2:/2>( r@o %^qs\tdt3%0M\X˘aoue>A,#NcvvH$D2iKBBб{欓 ÖlU v]LhEP ZH3e~!w_hq'ELaf4"Ft] uxXEl6fA۶m4 R)-9,p?%CFFF|jfp |30wC2> LOO\.w5|2LdYccc8p_lݡ[;T}Jt?eS_8l4tCh|0C>QU,>Ċ!L)&ЦۨY1n?~ՙ&mģ\Z g-U=~ x찍[iF7@[[[[E6:=yta&LMMP(&2M( \G}[&Sgo]-r K^8~+,B˟zK3K2h`oa_(bnuuU,TfLfneIb1D"lnnbmm ==RZo;;;۱؏9RʿK|\/F 3Ceym@ƑP菑S9Ƈ 0]D!H@t8wIM IDATNmJd lƐD'o􁮀=vemg~t,ߋ)J=Y4T:*H.{ 88"Q(B﮸8D033Ǔ,{9gg7\%Z/H7r.}|8 @IKvc&uP+vNK$? mՒLk=*=jڤo]:[/c"MG}}?zpOF .J3D"y`S~~;XѕD eJH$JjBPTPVQ(055;1R)G2_0y|o_$7;l޽_p흞 А|=Z4җ@G`x^J=}R x_gGkpbzKG߈y!K"p=L])KHz~An{I ;L+2@:LCk?{Ar8>-$ e!!,-ueb1`ttSSSE~Y; ~e=}V˜E @rG"4y9$!ZOvd<*+ Hҵň<==nq li{p\#xm$sH"H c&O_AӉNtpၟ=ķI ax==z[/lϙ- ) A^6_:F-2Nj@Zf_v/sB&&&籱\.m SXD066|> g?Jl_e>s?n_`EYdby??t ٖ=[ΔtDzRt^ o0+o/#j5=4 C󀆛C-f\/NjCİ5D@*RF:ZB̨0l*L:l:tRkbaT?=|h58L&L j4n^~ 4MmD4;ې FeysQ 7!x2 jJVVVJdJ<5Muuq!b||cccmJ=}D#:ųF,-,/#^`랷+VA#4Nr0 f/rҴ)t4mnzJ,C(P("oZMƆD&2FK֑c!7\TD3͠A~Jc9'zS|umױЕ) cP<y^**FR _ױrRAXd2ٓy, ehl6id2 ^9yRխ({&ȱnHHj7QP+~P(/ F{s-e`GNotdH+ krA XmƆu H #Nb|$O!q8ipf"1YFsQNƑ(LDz0V .Ϟh~ "2 B mcjYժtʡ2 &^vjhW%Keu`1 ²,T*T*4 FR08 U7c6\I$Ir96K[.v*WSG+JOQ 'z:L[v_lb0;?h B```w<(;Rl2X-eqqRE ̳p,R mfR,C,k*@q$fwYS XE = Ѱxz^ɡl,p"@&Ar{ U,,,rW4AĂm3&:|WQu}FdJ!dd, ZWl٭M4E<G*xN$Hd2d2BpG:E&zn+l{Q(d<|X !pMڱ|i(,eB wB6w>CM/ ^f5w3WY@5ݎSC8(L'%R6, 0J霞l², ۶}OaxUnz35lϟRˆB@j|O޻7OY>XxeLUQU*a;Tj6+U*崄3[ePvdw܎81la}r\ 7E;|>ߗO%:f=::Q\[ˡ6k8fGq#8wxS耾8qԐ|b+WӰmx4#H:_ G2/B.MJ.m<\wqYO`ff|'&0077'NAYܮy%^wY0 +:ȻJÂf=YMY=eA_'UD|2ڏwOϦ3 eu%+({Hf o >T` P `цwK_B=5bqq[ܪ>W/ױaM P @ d yX]]m~Ƌ¨G^%GU퐪F =W3&' &^)sPm% fx=$my7@7 \)cX1.;Xpq#t)Gp4^\V^.B O1/iH$0:: m:++?|€[{xk6LFй[ e,LX׫b `F!88hFǯD}B6a+h.XtpI: e5VV<x.R)9sa.1M#@^%2ڂv>w\j9aDXК @}l+EYOgDyȎ|#m@R[cE]- !ĭǰǡCBY2<,* rove2p`fˢ@t\jka(qa155% O7,c Wj@(^(B=Z "v&HxgKWj`Y71q }^$Zۨ@*&d !u`2ttE=?+˘mr|/fa=zfƷmɓx7p1LMM?|ILLL֭[X\\WFz8y$\_gB^6ř5!,D$'ߎE0Ъ)_p]ןfhYוV6޺@+@@׵ vuٵ, t@gZn^6(ԑ޿ S0dgu+feiHBh a:[>]+m,NsG0K6+9ܪɞhT,[o,oFoe|az4Ld] sdey<Yd(\tsdAcr3Ίk@zF]v/4[zVDΟw'm-e@4#@V)daq5SWH<+w}xΝ :u7`_j5t2D"8s 6Wsxj&6'/b"[T8-$y ߍ'Sէ)D###f~l6}[ZZRZg$`u35tu&tʖ$K֙aGޕ"$n(ʢ9Y,˲``q_:* @/2PJ,P8llVbg>éxװSN/H)X Ǐ?z׊8e,.l`~hz@I ~| 6f 2/ЪojZC:6"` ű{΋bfΐ"zsʦ}Q9p^DߏL`%*+{dL9ґ9o&pneJCb_@􂵿DxHIR)P&C`GcG166(5 Eeid"^atxB;O:1=VXA!i R q?\m2!m9N/>9^hEs5|Eu]8Cye ?{T*J#ɗUXBt[OlyrsfUπK5<,TiOA2*fQI>ZH@ D Pж$x>RWmj5;bT*{LٳgN[UrΟ?|;mqc:k,+ u>K"䕈SLt|>LYI`JA t0 ^,HruiPciq tNehHv^,)R!PDGbpo,FFFV/:]sԁ^C9 i/Uϣe<4s9Ţ>SFimt:ݶ: *JT*tH$⟷ip sc=i#+[XS"%##\|۷oM" ;篛&^F 8|0i~dv@v[t? IwځR&ǟ-]ˢ(w5W5*`lw r& y;]ߎ%R*al<ı2u}P*<f UTHۻֲ: !Z*7 pSRyuˬ:gy ;ZxpYʞGKqmۆiB`֫8:Z3J`?V!Ҩ,; ЉksBYQv֭[(JJ$c033۶qulnn~:ly"pPEӨ+ h4U\l|`Go xJ."PUQ,&q6Z juA! 8\H c /e1 IDAT#z| c3( ?vATрV-JXpQ!iQUa>!97ׅBTw j8(r3Xr:l<]/<}<{iXWV|KyZ8?ݷ=% %Om֭胴G5 邢(LT,WE@_AJ@Pxx͛畀r<0ӿy05“ EIJ@%V _ttX ĵkEg7GȔ ^Ӫ2 sss¡ڐ;5݀JiuU(\QdTB%@ux{y:id4́yؙ%^AЇ @`B!`Ҩ\sQD"mӸ2fl6'O"JX,͛B^aؘ/Кh{1^EyW𳍐(O(}o/bx^0MS;F=k[%pyyjk街> ('@W:|h%FusU~>/4:XZgYV_C }PhxFZRt\97,7Qy>,5~x'N@>iqv E@vL&Q;XUA& `I;O4-hKkK/DLMqcZ E$tysss6fA NqP -vۋ*Sm}OY{)fFgh4]0Zb~#N 4Mm}ѱ8r݀?/5 e@+蠥zuHkE ,y:{SݟHt^U0JR]ZyAЫa$(ӓR #x5B[[[xw|AˏRٽضG}Cܖ^o.-Ӹ9a i)}xb~~V:e2i0l6] iy7 WsBH[O]$/8e`-^~CEsҭo:J_8VymV/4}+̇&eСW bQ-.f;R):OI  E3Iݻchjv{gA_FWܺ/Qv3 G/6-ِhá^Ϟյa{Mt}DDrfٶ ^~SD.z8aX5^@¥l"^Z[m) L1x:ĉmnOjEaAzPX@T[c barTCb k*|jڽ>~eY$Jb水/&E7{ѣH$mfx;מ "p:qTaǶeʐ]yA$ 9Tߌhf {,ʗWk,aZt?ZjɌ_<{u` ?!Uچ*JQkRF!v*3t)0"JZs]~N7ݒ==4[[[az}t0pZ uOT?;p' Va Pt"Ys%&*pcibkke*'LOA9rm|6t7hV?ècGT*K$"H<_d0==Oh4 W*Jddc ɓ'C0N:%4a>"M}WO^ _Sq uXնO(ajjm׬cXx&=Ƌưk>LjtO뉆ʦy%Wx+^jOoNh^DR\ R!#uޙqB~~zJzeOrRO ' I:@_cOoC5Ԫ-~R> Z &|8t!\2L6Z lgoܴuBJB2 0O CP)FsQ̌E1pv ^__ȚRVC\g%B}]$6[L#~KOg޿ &~˰f4226-]3^onnbuu'PPZ̠ıGYYxKgޟlM i,K@qXW E#v?v4;khyi"Ni4|?ǹ9xq^^=<_> HݻwQ)mc,R‹ONīѰ83ɨjDo[QQA|%CxK|r|h+Z LXs/<ӈb;|:}ӴQ ĮVYR0/d8H ސUʼnX,|JZ)a*]ǯ?sFH{t6 J AKՀETȐGHUDmܸqCyo"[~"4 0L&Sd G ӣ,NtTOon?Zu?X?y(#To_@lA~e1Ql֟"zֈWU<|d3z8V޶,:rT||ؼn]y!ZGTq,`EARB~o"~F 7x `%8k]8򸇋/.3*\d2t:x<>!%B\E"LjY-2_[[ŋ6H'jPu+w"/C-Y_̈}Ey@̌BgT*RѣG{$xQx],?U8Q]ց9=;JOG\W5+oy{~SjkS$`Z. } N H#oH-7hY199xU` ,I$NJE;s_#ضjup9bzzv[Vk[ڲ,xrHUS$CC!1Hv;q2NF3Ip}kk,{d-rmRzCw|mf~vHdK p-$:FKGBw_C46av/*_XEa TZkLLL )t@V,/lŃhϟ.Ъ_6b"F)#H `Ѳ,lookB5MT*e<6_{oH ԝx( @m`"@@A'J(V6] #gRA Ϣ#Mo9FQЍFd2H$ 8#W2MzHM$H&-T*Hu:͈NYuߟ^~A_ s3kh48%:CDl6QVۜWM2JR ɔ`QU[W:PX?MGӖeï=r %~^& TM噄XSAP b\O6])a:q*a˟!;(m/)? k`u5Mvkk eؘ/* ]0\ǧ,![D=}e :c=\jۮ{ImR.b:SoU߹27F]uz^ WKRXڊ¶ 1# ŀI[L?n/oz 7N1??bc+m~ccc~9z|qi:ٞ?oxA+Թ^zAbg)k w$(Y@+ZD.5(fH&(,W6*dRTh4022;8)+a|:YdayfMT2ہe5^XmqxĿkITajnsR/C0G,..~>:=e?J0V\'ȥYCxGadG#'p6 omѣ]`У޹9yɟ1dgy(spH F~b&&}}&'Gc๣el⸾yH&mKHLaq`vۻ@Hki("w_Lz*3j\+-F/uZgLb<{{XzʕsN4~`x:w(Nl4}S({0 *xf1#EL|o^Dp>g8kꗭ1Dr!dצOn$siݽsoa41ѦJ)ڀ#gixfG-'AS2D*~/{5,A%5[ASa_.Ok^xDߡ| ~!qcB,0ի~ 0:WX%;y䋱X) *NOn^, Y BB"2\,}!fggqI苔0ڼEE?S$5̎7_TAYl7+E8^zmIB>(LOg#x0s*]݇Ix;pKI46ְ@}OJރ6z=~]4m IZd5Pm J-BRB אYڮߺ{$sgYm\zw.u'/U-=L?@Z &Nj>WNbq!&G8}(oxCuԋȬǃ>*8 _HUjLGr[xr{?YHgrg HD ޺8H${ IDATҐSB?=TX&UlK)#S x@cdxY)*Y:R^d DU<5M~iTXo0o-mbBϲ!^-n]-(˘dpc%M;x7F! wť@@o&M4Pi\:ÿ1o۷o㥗^B*>Y=bX6l ZgloopM3W0Hm70jtq֍Ϭo˟8\!KGdH$Qij5=σi, tTUܝfxyJ, P *wYHU)mۨT*jmT*e4xS">#BZ86T-k׾s)2ڈU9s:th& ~U FDŪXRJQs[1`%^Zx&LÀ[VQx ,/xel*s6L_$~~{;_W<ښ^2uӀxX~aJ3&=RkwFPوpcX,/ ["X/uua&lۆym%@aj8(|5 -+ET/<}sM{_ݜZxRYlލ7v @pT*A199%S$t8U|1EOGVGX<ǭE4XݨFV R_7*E a"9?3|k_Cy!ǀ DZ~3 2v[sL}ag4mǷqQkE0~L'&c'?;F1kh)fz_Y]fC7Iwϓ@'J&L@<FRm~˥mD 7CZaD<=Zj4_uօK=_=) `0l6=4L;т6WD<Zӷ63I#p<ʫoX,"ɴ`j?rv|_ǟcmdyώ_ty7p&~i o@O=xb +nEq*)S@> ¶m4 PEѼxbh8wCaZF.vZ Jma~h4(KL/?N<4J1<2O6Zw޽ _C]xc `ccA0 c*BR<dž= h#㗍3x.Eϱ03sIO[_&~} 'p9>}p'd;w7DRB&?L?CYRt/ۮ)d!1ۻo B;<x$WQ*n#?7΁,FkL&F/%*tT*o>6l4a&IȊvzиY냮AQEh46+˰>*gVi6:>dd6KNPRy Raw hX9?"/kh wt z766_t7Gիb׾W70YȡIpHd 8t099 R)aRVuΝ;w(~,~YBgلa?0M/ w)`Q{hh޶ve HRH$2 bVDfQQJaV>Ke ?,Ou]T+%4m_Y/կ&}Iׁ拈,fhҿ|yy]s<[ AOa~Ԫe$ o.ӥ<$ 0yeVE;2Ņ85v\# qlSr6>l$_*AEbMx}U&I<W7&޺iJ%. R7Ш4T HxD'̉4;]nB'^ .#5uۣ-;uxZ'c&ߞß5 Y<=X_ZT*)`{M K8wl{aAh/2x _Lg]j>ӱg/3<ϻʲ-u;4ZvFqtkK"iV]|xL=(,3G/`& t8?/|Uumuf5 0I84h)|D -/I)4 { "Eg*w]@"t[a]VB+'T)RX?aH~vݒgB(0mۨVJ<?}|&"F,weuTlH'۶yFa%Hh8xi2]}GFFp}mwWXQ!>C@7NR&i8Nn9ܮd՞_ (?;W|{c7APDt %,lHE}|dxA4d|3\Sc:[ot𛟹1EpX( =zv!zʠ+-)?_>\ֳ;%]O{ɶmTe_8{XK|i\)F*볲H 1Z Uy#Hh)B5ѱI|=yAR"`P@4d 4R型$F拘ұW-8[^'SW/ K=(,OSJW[ր@.2kWY TYNqԊ%ITP([W_{Q9+Dzk4+٩.{|摓 3uC .S ,  WhњLaYD"L![8 KDk3F^)bpq9rd0i)0Az:>{u~~j61*<ƒPgpx\C:Фp Q}p|wX2 ;5Gqcjds{@?H}?=OoH=D?jE:3f)ӤQJo}"'N8F:MhPiSM^8i#jbmޒ_C1)cq>Dnx!{j@-π/cA?ӊ4{guo}>H#Ya]Zv$cbqbd66a(pd^XP`y4Vp.!g8> v/ P`Ƭ5ɾ,"?to)YQF"9IT{I[B.o\*v܆yֲQ,n U҉;g>ƗNAۧx_P:OA*2 ~F#|?T 5 XZZ8y4-{D @":a#) hJcT2bh1PvFP+Utry|p:|f\ZJP,Z؜ rb|e'/⩃H^ө锿C=r<{J7+A_.oh4te8`SJ\Qt: L{h\cvu.fMټ+ڴof*{2.4 (ّ~= w1b˂d ĀeYۃJe$2^?y_{&a1@.\}V4\կ~&?6@ {'pggg_9q!QD ¾o0~yy Qqا?BwUvD7@6aKfҩLtݩ!59u :R$$u(RT6lHU֑95Fy$tT_\澀LG~*os]ww:=~["O:`>`Hp`yy5ଷl Nc0~(EfP{ZZxb"b T~:qF"-%b~)h$~SZ3ǵk0Id2y_uض ˲`YjVuSvy<1H ޟ٧锋?KT\.`ffƛYY?(*Ѐ/M`_Gtn[{f㵆#Q8󒅎J]2ǽHъQ>T{eUD_ P^jl>'}X؊` }2߀뺨VV5ڛ#@/l~\g;?jFE1*ccc/B 2D޿hkkk471ν3{2@"{U;;)!صV8 諔i0VM=Ҧg.♣by#\]b/&;P,nLCn0 ku]8S{۶Vͻ^™e_ƑNg0㕫OJRr'sLh@ کDXDc@0$޿gM5X8X[?}t_{F.PgԌ;#[( IATƕ j.*.LtZ&Wpg{kat6E1i$x7lqjmۛ۶ ǩ¶$MsqcǻWq;<4/ǩKLc]~K|=*?./Pw??>躎xq_ӻw4\a TH~͍jaD ZVV.mZ١Z siLtbn=B 4Y,Ҩ:?ZMg:\h4v.D}@׶u5MW0]p e7p{G;֐W fW=$j/ڗu}'wfz>䲧T*VooHnʈ.$ Q_^}= |*MV&ϖ#vʕ˨g@(T ? Ԗ\.WqJU~Gy,^K '~3_R/gT@~0`m:`.ݜxԩ/>Xķ @$ln-w5)bK` E%2%Au|~4)A>hGe7Na~o-zu+W|3-a[E/7җ2ԇ4~ſ6/8d2 >f*t A޲ep]acǮpE F|:M~]Voo잷ϴ/vE -~ SJEsglxx+CuP_hL > Tk8 Mk!}sa/.;}'~%"+>yĞvۋ~| ->3;)Ueggg5::ꅞя?cʂ`?whhژA`L btw7З*`[Gl/#-;aWcqWn/)SN}`_P ?@^>fo>yA2VFeY {v_wEi{A'NH$DŽ^FiHg:d ݛ)ꬠ'Q@ayk(@Z6eI'A¿)OfY _m{᭷/Ri l_Oe(p@tlǏ0AFfl4afpsyJufW-ߣX}f(uc諔i {P~How4(ǑP؇oJ߿=kllϦq~[4 _lE\@!=CxT*]>xzE݉H 4 T vbćJN &رyCp;*M{\~D/7m;@f 07cuuL&'~ u`ð9H09{bm}]*ˮw-m?\fڈY늸5S,/o_X&,2p3ϯ&TWp?(2їm?[Y\oj1" z @H `1777ݽ*y+`}Lc-6L74u#{U#8zH:1z~޾  řw${o|u]V=_`E_V?Dh >===9<`c[C&W"*D* Gl _Zo_R 'j8=+E c [:fPuX/ΝUl T ^, X/qp?ox^S[K`K/iwwsu0F,Y2JdPz w]ґ_C1#7gUZ}2~$/| 1?!f'DoU[EU*6\dhXh7'm $H$_|]]]םoe>TFUUlll9pS|+!`F).bgFlBLvvwDؽ}w.PJ(U;k\Žb?ʃ(H$JU?m [{wraa!mAg^lWA|g–1?z$/]l%jJJ)i(+UF_N =*S) c- ЪӮo7n?v^|p]o߿ɐ/-_FڊŲi-ڠgI`9IWX.+eRNcJk@77bQ=x*i,e}1???z!utQ@mhQ>&|(-Cu]TUضj q`6 V$t+c!n| ʀjE! eEm7\wKrᾳ-)ڒh!*L8*v %' MӠzm1 "|ZؑPTf~rpo[Y#m /gş5Xux`pp 'C B bo!h>=$bqsV=VAM;(:@_[\&''vvv8eV D~o;JVWW/) ُs٤?$yDboHEv޽)Mf>1VE B&|^p?';›o~dY}d>2wO2_$2#V- bq|xx!02 '_? TEWyq[~{?w''b3ʢuj~zzÃI& 'S(?싅 h ~/*f~[2::k׮]F8̚x/B"G/~C$RX'q@|LdA"~UBa<|ڹs羳za>oğoO*vu;wܬT*O뺞n8VI vGDQ_{kϟi\1D$~ YWLyYq///LOO0444NO|K"d_' 9TF҂gӂ܅ {1w~@}?H/zߌ`E}Y96{T*[n{(3CH ~%LY_"ڳo?\YYYE /e]'TL/2hYԱ}!4X\\\s΅d6=*4@A 'J tb8+WڶɋĞ}KHE"/e^?/;,˪NMMoll;H$( _ 0j~֭5?7D^6N@E 3Dɢޚ=h?Gweeek6;b[h^$A |}Uw]s?===n ߄?*>෴mС0`݇yf $fgSig giiiҥK/yռ<~rK۷d淥DUnd?%)H3 7d qR;t:zG8r dr0CӋ v(c8SY\\onE?/ ӳ޻'6k-AB?J@4t?#@ 07qp!u]׵g>622َץ7ArtQTrsssCX,Q@E^<ٛD um MkfAфB:mxxx|zpp4;B]/"  cǩ+o~ƍ7P/"_ .{__έviu vP@d Y+lH0k~nKhkǎ;>22xa~ p49x)S GGz@&l_˗MM۾4L~QXC@% (({3D >:iƩSNx%!~m~@@i8Ghh`Q@ K7X#@Hfu >GFF;q4͞ 8B0o^8'@P~~[dEa} 6P r߿G\-p -?Q_d'FpC9ff2DO Aض],Jbq&O:#e") ;tF 跨xM=V PP1 1пWNcEc`[fvg@www_6OT0D"5 CiiI #ڮVǩڶ]m`YVѲr\(חs+++yN&@{k/F^@ @?% ږ~AoFk@M~&D_Gtjdٌ,O>AD+FRW$x~>kBo[&a_tݔKHvol֞!WW3DXQAN% b"*Q,o;\ #|ݗ҂_۲kw픈Sd @f7.&(4qlh b!m}>_1;\ ~7@Ux?b~*AP]EQv ok6=( |~< vOTw|>{<1Ee**𲈃HE#6h:(* DH``}~[ (|]㯇ߵK#KY>ۆA 2ZEEk;^T?ZqQ C@!b$Ets=:P4 /Jcye޲H< q~FEM"`~ƂLP, :OQ1 A"&~QU>]&*Fv{줰\c@H CBዺD/@CO~C$d{|6Š4?f/f?HUce7DFEx6& ͫ2o^_fD8 ]VEDۢ2b#s;WgbgD%- kU@& ϋ ٶ\e}NY:6 b',Y^0ikUqraD>h-Nӛ5mY?(/:e#I/hyTW vv>qUQAx!22D/De]A*  ׯRoM~av a=㰞vBGUjA%H:ao]5WMUD5 deY=^P#bFx~{1죤EH܃OM~3)AK\"FDB3A5bGm<| #*mD{o6q {m56rh_ɸEYz$"b8 v #ྀD9ZuB~UL݇AxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu image/svg+xml Hector Lahminewski goodvibes-v0.4.2/data/icons/meson.build000066400000000000000000000012421337147241200200550ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later themes = [ [ 'hicolor', [ [ '16x16', gv_icon_name + '.png' ], [ '22x22', gv_icon_name + '.png' ], [ '24x24', gv_icon_name + '.png' ], [ '32x32', gv_icon_name + '.png' ], [ '48x48', gv_icon_name + '.png' ], [ '256x256', gv_icon_name + '.png' ], [ '512x512', gv_icon_name + '.png' ], [ 'symbolic', gv_icon_name + '-symbolic.svg' ], ]] ] foreach i : themes theme = i[0] sizes = i[1] foreach i : sizes size = i[0] icon = i[1] install_data(join_paths(theme, size, 'apps', icon), install_dir : join_paths(datadir, 'icons', theme, size, 'apps') ) endforeach endforeach goodvibes-v0.4.2/data/icons/src/000077500000000000000000000000001337147241200165035ustar00rootroot00000000000000goodvibes-v0.4.2/data/icons/src/goodvibes-favicon.svg000066400000000000000000000135031337147241200226320ustar00rootroot00000000000000 image/svg+xml Hector Lahminewski goodvibes-v0.4.2/data/icons/src/goodvibes-large.svg000066400000000000000000001124751337147241200223070ustar00rootroot00000000000000 image/svg+xml Hector Lahminewski goodvibes-v0.4.2/data/icons/src/goodvibes-small.svg000066400000000000000000000467411337147241200223270ustar00rootroot00000000000000 image/svg+xml Hector Lahminewski goodvibes-v0.4.2/data/icons/src/goodvibes-symbolic.svg000066400000000000000000000127271337147241200230350ustar00rootroot00000000000000 image/svg+xml Hector Lahminewski goodvibes-v0.4.2/data/io.gitlab.Goodvibes.appdata.xml.in000066400000000000000000000034321337147241200231330ustar00rootroot00000000000000 io.gitlab.Goodvibes Goodvibes Play web radios CC0-1.0 GPL-3.0+

Goodvibes is a simple internet radio player for GNU/Linux.

It comes with every basic features you can expect from an audio player, such as multimedia keys, notifications, system sleep inhibition, and MPRIS2 support.

https://goodvibes.gitlab.io/appdata/screenshot-2018-11-10-light.png https://goodvibes.gitlab.io/appdata/screenshot-2018-11-10-light-prefs.png https://goodvibes.gitlab.io/appdata/screenshot-2018-11-10-dark-with-appmenu.png https://gitlab.com/goodvibes/goodvibes https://gitlab.com/goodvibes/goodvibes/issues https://goodvibes.readthedocs.io https://hosted.weblate.org/projects/goodvibes io.gitlab.Goodvibes.desktop goodvibes goodvibes elboulangero_at_gmail.com
goodvibes-v0.4.2/data/io.gitlab.Goodvibes.desktop.in000066400000000000000000000007341337147241200223750ustar00rootroot00000000000000[Desktop Entry] Name=Goodvibes GenericName=Radio Player Comment=Play web radios # TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! Keywords=Audio;Radio;Player; Categories=GTK;AudioVideo;Audio;Player; Type=Application # TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! Icon=io.gitlab.Goodvibes Exec=goodvibes %u DBusActivatable=true MimeType=audio/*; goodvibes-v0.4.2/data/io.gitlab.Goodvibes.gschema.xml.in000066400000000000000000000103051337147241200231250ustar00rootroot00000000000000 false Enable custom pipeline Whether the custom output pipeline is enabled '' Custom pipeline string Custom output pipeline description 100 Volume The playback volume (in percent) false Mute Whether playback is muted true Repeat Whether to repeat when next/prev reaches the end false Shuffle Whether to use shuffled playlist on next/prev false Autoplay Whether to start playing automatically on startup '' Current station uri The uri of the current station (-1, -1) Main window size The size of the main window (use -1 for no specific size) (-1, -1) Main window position The position of the main window (use -1 for no specific position) false Autoset main window height Automatically set the main window position 'default' Theme variant Prefer a different variant of the theme (if available) 'toggle' Middle click action Action for middle click on the status icon 'station' Scroll action Action for mouse scroll on the status icon false Enable the feature Whether to enable the feature false true true false false false goodvibes-v0.4.2/data/io.gitlab.Goodvibes.service.in000066400000000000000000000001051337147241200223540ustar00rootroot00000000000000[D-BUS Service] Name=@dbus_name@ Exec=@bindir@/@binary@ --background goodvibes-v0.4.2/data/meson.build000066400000000000000000000210541337147241200167450ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # My understanding of meson (a beginner guide) # # 'configure step' and 'build step' # # *Configure files* are generated early when creating the build directory with # 'meson 'builddir', and afterward Meson automatically update these files if # the source changes. Therefore, my understanding is that there's no need to # declare a dependency to ensure configure files are up-to-date: they're always # up-to-date. # # However, custom_target() has a 'depend_files:' keyword for declaring # dependencies on files. It's unclear if we need to use it for configure files. # I'd say no need. # # *Build targets* are, on the other hand, part of a dependency chain, and the # 'depends:' keyword must be used to indicate a dependency. This is mandatory. # # 'source dir' and 'build dir' # # Meson has a clear concept of 'source dir' and 'build dir'. # # When you give a relative path to 'input:', meson assumes it's relative to # 'source dir'. Beware when you need to use configure files as inputs, as they # live in 'build dir'. # # References # # # # Credit for this file goes to . # ----------------------------------------------------- # # D-Bus Service File # # ----------------------------------------------------- # # services_dir = join_paths(datadir, 'dbus-1/services') service_conf = configuration_data() service_conf.set('bindir', bindir) service_conf.set('binary', gv_binary) service_conf.set('dbus_name', gv_application_id) configure_file( input: gv_application_id + '.service.in', output: gv_application_id + '.service', configuration: service_conf, install_dir: services_dir, ) # XXX It would be nice to have a tool to validate the service file. # ----------------------------------------------------- # # Desktop File # # ----------------------------------------------------- # # # XXX It would be nice to have desktop.in created with configure_file(), # so that we can do some variable substitution, like for the other data # files here. However, it means that the POTFILES should list the built # file, which lives in the build dir. Right now it doesn't work. # https://github.com/mesonbuild/meson/issues/1733 applications_dir = join_paths(datadir, 'applications') desktop_file = i18n.merge_file( input: gv_application_id + '.desktop.in', output: gv_application_id + '.desktop', po_dir: '../po', type: 'desktop', install: true, install_dir: applications_dir, ) desktop_utils = find_program('desktop-file-validate', required: false) if desktop_utils.found() test('Validate desktop file', desktop_utils, args: [ desktop_file ], ) endif # ----------------------------------------------------- # # Appstream File # # ----------------------------------------------------- # # appstream_dir = join_paths(datadir, 'metainfo') appstream_file = i18n.merge_file( input: gv_application_id + '.appdata.xml.in', output: gv_application_id + '.appdata.xml', po_dir: '../po', type: 'xml', install: true, install_dir: appstream_dir, ) appstream_util = find_program('appstream-util', required: false) if appstream_util.found() # Relaxed validation, our screenshots don't meet the expectations # (16:9 without padding is definitely not suitable). test('Validate appstream file', appstream_util, args: [ 'validate-relax', appstream_file ] ) endif # ----------------------------------------------------- # # GSettings Schemas # # ----------------------------------------------------- # # schemas_dir = join_paths(datadir, 'glib-2.0', 'schemas') schema_conf = configuration_data() schema_conf.set('id', gv_application_id) schema_conf.set('path', gv_application_path) schema_file = configure_file( input: gv_application_id + '.gschema.xml.in', output: gv_application_id + '.gschema.xml', configuration: schema_conf, install_dir: schemas_dir, ) #gv_settings = gnome.compile_schemas( # depend_files: schema_file #) # Generate enums.xml file. # XXX gnome.compile_schemas() can't generate the enum.xml file automatically, # like it was done with the autotools. We need to do it explicitly. # https://github.com/mesonbuild/meson/issues/1687 # XXX It's unfortunate that we have to list the headers here, # maybe that would be better done in src/ ? enum_headers = [ '../src/ui/gv-main-window.h', '../src/ui/gv-status-icon.h', ] generate_enums = gnome.mkenums(gv_application_id + '.enums.xml', sources: enum_headers, comments: '', fhead: '', vhead: ' <@type@ id="' + gv_application_id + '.@EnumName@">', vprod: ' ', vtail: ' ', ftail: '', install_header: true, install_dir: schemas_dir ) # Compile schemas for running the application in-tree # XXX We use a custom target, because gnome.compile_schemas() # would run in the source dir rather than the build dir, hence # missing the generated 'enums.xml' file. # https://github.com/mesonbuild/meson/issues/1687 compile_schemas = custom_target('glib-compile-schemas', output: 'gschemas.compiled', build_by_default: true, install: false, command: [ find_program('glib-compile-schemas'), meson.current_build_dir(), ], depends: [ generate_enums ], ) compile_schemas = find_program('glib-compile-schemas', required: false) if compile_schemas.found() test('Validate schema file', compile_schemas, args: [ '--strict', '--dry-run', meson.current_build_dir() ], # Requires meson >= 0.46, commenting for now # depends: [ generate_enums ], ) endif # ----------------------------------------------------- # # Old GSettings Schemas # # ----------------------------------------------------- # # In version 4.1, I changed application ID to io.gitlab.Goodvibes. # This means changing the path and names of all the data files, # including gsettings schema files. And it turns out that the only # way to access the settings with the old app id (for migrating it # to the new app id) is to actually keep on shipping gschema and enum # files with the old name. So that's what we're doing here. old_application_id = 'com.elboulangero.Goodvibes' old_application_path = '/com/elboulangero/Goodvibes' old_schema_conf = configuration_data() old_schema_conf.set('id', old_application_id) old_schema_conf.set('path', old_application_path) old_schema_file = configure_file( input: gv_application_id + '.gschema.xml.in', output: old_application_id + '.gschema.xml', configuration: old_schema_conf, install_dir: schemas_dir, ) old_generate_enums = gnome.mkenums(old_application_id + '.enums.xml', sources: enum_headers, comments: '', fhead: '', vhead: ' <@type@ id="' + old_application_id + '.@EnumName@">', vprod: ' ', vtail: ' ', ftail: '', install_header: true, install_dir: schemas_dir ) # ----------------------------------------------------- # # Manual Pages # # ----------------------------------------------------- # # XXX That would be nice to use install_man(), however right now # it's not possible to use it with generated files. So we have # to use a custom target and zip the files manually. # https://github.com/mesonbuild/meson/issues/1550 manuals_dir = join_paths(mandir, 'man1') manual_conf = configuration_data() manual_conf.set('package', gv_name_lowercase) manual_conf.set('Package', gv_name_camelcase) manual_conf.set('PACKAGE', gv_name_uppercase) manual_conf.set('version', gv_version) manual_conf.set('homepage', gv_homepage) manual_conf.set('author', gv_author_name) gzip = find_program('gzip', required: true) foreach exe: [ 'goodvibes', 'goodvibes-client' ] tmp_man_file = configure_file( input: exe + '.1.in', output: exe + '.1', configuration: manual_conf, ) custom_target('man-' + exe, command: [ gzip, '--keep', '--force', tmp_man_file ], output: exe + '.1.gz', install: true, install_dir: manuals_dir, ) endforeach # ------------------------------------------------------- # # Subdirectories # # ------------------------------------------------------- # subdir('icons') goodvibes-v0.4.2/docs/000077500000000000000000000000001337147241200146205ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/000077500000000000000000000000001337147241200214735ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/Makefile000066400000000000000000000011361337147241200231340ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = Goodvibes SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/000077500000000000000000000000001337147241200231215ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/000077500000000000000000000000001337147241200237115ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/000077500000000000000000000000001337147241200260145ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/font-mfizz.css000066400000000000000000000241051337147241200306330ustar00rootroot00000000000000/*! * Font Mfizz 2.4.1 * Copyright 2013-2017 Fizzed, Inc. * MIT License * * Project: http://fizzed.com/oss/font-mfizz * * The font designed for technology and software geeks representing programming * languages, operating systems, software engineering, and technology. * * Fizzed, Inc. * Web: http://fizzed.com/ * Twitter: http://twitter.com/fizzed_inc */ @font-face { font-family: "FontMfizz"; src: url("./font-mfizz.eot"); src: url("./font-mfizz.eot?#iefix") format("embedded-opentype"), url("./font-mfizz.woff") format("woff"), url("./font-mfizz.ttf") format("truetype"), url("./font-mfizz.svg#font-mfizz") format("svg"); font-weight: normal; font-style: normal; } @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: "FontMfizz"; src: url("./font-mfizz.svg#font-mfizz") format("svg"); } } [data-icon]:before { content: attr(data-icon); } [data-icon]:before, .icon-3dprint:before, .icon-alpinelinux:before, .icon-angular:before, .icon-angular-alt:before, .icon-antenna:before, .icon-apache:before, .icon-archlinux:before, .icon-aws:before, .icon-azure:before, .icon-backbone:before, .icon-blackberry:before, .icon-bomb:before, .icon-bootstrap:before, .icon-c:before, .icon-cassandra:before, .icon-centos:before, .icon-clojure:before, .icon-codeigniter:before, .icon-codepen:before, .icon-coffee-bean:before, .icon-cplusplus:before, .icon-csharp:before, .icon-css:before, .icon-css3:before, .icon-css3-alt:before, .icon-d3:before, .icon-database:before, .icon-database-alt:before, .icon-database-alt2:before, .icon-debian:before, .icon-docker:before, .icon-dreamhost:before, .icon-elixir:before, .icon-elm:before, .icon-erlang:before, .icon-exherbo:before, .icon-fedora:before, .icon-fire-alt:before, .icon-freebsd:before, .icon-freecodecamp:before, .icon-gentoo:before, .icon-ghost:before, .icon-git:before, .icon-gnome:before, .icon-go:before, .icon-go-alt:before, .icon-google:before, .icon-google-alt:before, .icon-google-code:before, .icon-google-developers:before, .icon-gradle:before, .icon-grails:before, .icon-grails-alt:before, .icon-grunt:before, .icon-gulp:before, .icon-gulp-alt:before, .icon-hadoop:before, .icon-haskell:before, .icon-heroku:before, .icon-html:before, .icon-html5:before, .icon-html5-alt:before, .icon-iphone:before, .icon-java:before, .icon-java-bold:before, .icon-java-duke:before, .icon-javascript:before, .icon-javascript-alt:before, .icon-jetty:before, .icon-jquery:before, .icon-kde:before, .icon-laravel:before, .icon-line-graph:before, .icon-linux-mint:before, .icon-looking:before, .icon-magento:before, .icon-mariadb:before, .icon-maven:before, .icon-microscope:before, .icon-mobile-device:before, .icon-mobile-phone-alt:before, .icon-mobile-phone-broadcast:before, .icon-mongodb:before, .icon-mssql:before, .icon-mysql:before, .icon-mysql-alt:before, .icon-netbsd:before, .icon-nginx:before, .icon-nginx-alt:before, .icon-nginx-alt2:before, .icon-nodejs:before, .icon-npm:before, .icon-objc:before, .icon-openshift:before, .icon-oracle:before, .icon-oracle-alt:before, .icon-osx:before, .icon-perl:before, .icon-phone-alt:before, .icon-phone-gap:before, .icon-phone-retro:before, .icon-php:before, .icon-php-alt:before, .icon-playframework:before, .icon-playframework-alt:before, .icon-plone:before, .icon-postgres:before, .icon-postgres-alt:before, .icon-python:before, .icon-raspberrypi:before, .icon-reactjs:before, .icon-redhat:before, .icon-redis:before, .icon-ruby:before, .icon-ruby-on-rails:before, .icon-ruby-on-rails-alt:before, .icon-rust:before, .icon-sass:before, .icon-satellite:before, .icon-scala:before, .icon-scala-alt:before, .icon-script:before, .icon-script-alt:before, .icon-shell:before, .icon-sitefinity:before, .icon-solaris:before, .icon-splatter:before, .icon-spring:before, .icon-suse:before, .icon-svg:before, .icon-symfony:before, .icon-tomcat:before, .icon-ubuntu:before, .icon-unity:before, .icon-wireless:before, .icon-wordpress:before, .icon-x11:before { display: inline-block; font-family: "FontMfizz"; font-style: normal; font-weight: normal; font-variant: normal; line-height: 1; text-decoration: inherit; text-rendering: optimizeLegibility; text-transform: none; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; font-smoothing: antialiased; } .icon-3dprint:before { content: "\f100"; } .icon-alpinelinux:before { content: "\f101"; } .icon-angular:before { content: "\f102"; } .icon-angular-alt:before { content: "\f103"; } .icon-antenna:before { content: "\f104"; } .icon-apache:before { content: "\f105"; } .icon-archlinux:before { content: "\f106"; } .icon-aws:before { content: "\f107"; } .icon-azure:before { content: "\f108"; } .icon-backbone:before { content: "\f109"; } .icon-blackberry:before { content: "\f10a"; } .icon-bomb:before { content: "\f10b"; } .icon-bootstrap:before { content: "\f10c"; } .icon-c:before { content: "\f10d"; } .icon-cassandra:before { content: "\f10e"; } .icon-centos:before { content: "\f10f"; } .icon-clojure:before { content: "\f110"; } .icon-codeigniter:before { content: "\f111"; } .icon-codepen:before { content: "\f112"; } .icon-coffee-bean:before { content: "\f113"; } .icon-cplusplus:before { content: "\f114"; } .icon-csharp:before { content: "\f115"; } .icon-css:before { content: "\f116"; } .icon-css3:before { content: "\f117"; } .icon-css3-alt:before { content: "\f118"; } .icon-d3:before { content: "\f119"; } .icon-database:before { content: "\f11a"; } .icon-database-alt:before { content: "\f11b"; } .icon-database-alt2:before { content: "\f11c"; } .icon-debian:before { content: "\f11d"; } .icon-docker:before { content: "\f11e"; } .icon-dreamhost:before { content: "\f11f"; } .icon-elixir:before { content: "\f120"; } .icon-elm:before { content: "\f121"; } .icon-erlang:before { content: "\f122"; } .icon-exherbo:before { content: "\f123"; } .icon-fedora:before { content: "\f124"; } .icon-fire-alt:before { content: "\f125"; } .icon-freebsd:before { content: "\f126"; } .icon-freecodecamp:before { content: "\f127"; } .icon-gentoo:before { content: "\f128"; } .icon-ghost:before { content: "\f129"; } .icon-git:before { content: "\f12a"; } .icon-gnome:before { content: "\f12b"; } .icon-go:before { content: "\f12c"; } .icon-go-alt:before { content: "\f12d"; } .icon-google:before { content: "\f12e"; } .icon-google-alt:before { content: "\f12f"; } .icon-google-code:before { content: "\f130"; } .icon-google-developers:before { content: "\f131"; } .icon-gradle:before { content: "\f132"; } .icon-grails:before { content: "\f133"; } .icon-grails-alt:before { content: "\f134"; } .icon-grunt:before { content: "\f135"; } .icon-gulp:before { content: "\f136"; } .icon-gulp-alt:before { content: "\f137"; } .icon-hadoop:before { content: "\f138"; } .icon-haskell:before { content: "\f139"; } .icon-heroku:before { content: "\f13a"; } .icon-html:before { content: "\f13b"; } .icon-html5:before { content: "\f13c"; } .icon-html5-alt:before { content: "\f13d"; } .icon-iphone:before { content: "\f13e"; } .icon-java:before { content: "\f13f"; } .icon-java-bold:before { content: "\f140"; } .icon-java-duke:before { content: "\f141"; } .icon-javascript:before { content: "\f142"; } .icon-javascript-alt:before { content: "\f143"; } .icon-jetty:before { content: "\f144"; } .icon-jquery:before { content: "\f145"; } .icon-kde:before { content: "\f146"; } .icon-laravel:before { content: "\f147"; } .icon-line-graph:before { content: "\f148"; } .icon-linux-mint:before { content: "\f149"; } .icon-looking:before { content: "\f14a"; } .icon-magento:before { content: "\f14b"; } .icon-mariadb:before { content: "\f14c"; } .icon-maven:before { content: "\f14d"; } .icon-microscope:before { content: "\f14e"; } .icon-mobile-device:before { content: "\f14f"; } .icon-mobile-phone-alt:before { content: "\f150"; } .icon-mobile-phone-broadcast:before { content: "\f151"; } .icon-mongodb:before { content: "\f152"; } .icon-mssql:before { content: "\f153"; } .icon-mysql:before { content: "\f154"; } .icon-mysql-alt:before { content: "\f155"; } .icon-netbsd:before { content: "\f156"; } .icon-nginx:before { content: "\f157"; } .icon-nginx-alt:before { content: "\f158"; } .icon-nginx-alt2:before { content: "\f159"; } .icon-nodejs:before { content: "\f15a"; } .icon-npm:before { content: "\f15b"; } .icon-objc:before { content: "\f15c"; } .icon-openshift:before { content: "\f15d"; } .icon-oracle:before { content: "\f15e"; } .icon-oracle-alt:before { content: "\f15f"; } .icon-osx:before { content: "\f160"; } .icon-perl:before { content: "\f161"; } .icon-phone-alt:before { content: "\f162"; } .icon-phone-gap:before { content: "\f163"; } .icon-phone-retro:before { content: "\f164"; } .icon-php:before { content: "\f165"; } .icon-php-alt:before { content: "\f166"; } .icon-playframework:before { content: "\f167"; } .icon-playframework-alt:before { content: "\f168"; } .icon-plone:before { content: "\f169"; } .icon-postgres:before { content: "\f16a"; } .icon-postgres-alt:before { content: "\f16b"; } .icon-python:before { content: "\f16c"; } .icon-raspberrypi:before { content: "\f16d"; } .icon-reactjs:before { content: "\f16e"; } .icon-redhat:before { content: "\f16f"; } .icon-redis:before { content: "\f170"; } .icon-ruby:before { content: "\f171"; } .icon-ruby-on-rails:before { content: "\f172"; } .icon-ruby-on-rails-alt:before { content: "\f173"; } .icon-rust:before { content: "\f174"; } .icon-sass:before { content: "\f175"; } .icon-satellite:before { content: "\f176"; } .icon-scala:before { content: "\f177"; } .icon-scala-alt:before { content: "\f178"; } .icon-script:before { content: "\f179"; } .icon-script-alt:before { content: "\f17a"; } .icon-shell:before { content: "\f17b"; } .icon-sitefinity:before { content: "\f17c"; } .icon-solaris:before { content: "\f17d"; } .icon-splatter:before { content: "\f17e"; } .icon-spring:before { content: "\f17f"; } .icon-suse:before { content: "\f180"; } .icon-svg:before { content: "\f181"; } .icon-symfony:before { content: "\f182"; } .icon-tomcat:before { content: "\f183"; } .icon-ubuntu:before { content: "\f184"; } .icon-unity:before { content: "\f185"; } .icon-wireless:before { content: "\f186"; } .icon-wordpress:before { content: "\f187"; } .icon-x11:before { content: "\f188"; } goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/font-mfizz.eot000066400000000000000000001744261337147241200306460ustar00rootroot00000000000000`LPyofont-mfizz mfizz Version 001.000 font-mfizz PFFTMucrDOS/2Q'^X`cmap|Jcvt D(gasp<glyfHheadvU6hhea$hmtx$loca.TfD,maxpRb8 nameD%H$postEoy_< QQ@.@1@.LfGLfPfEd .   j.MLo     D( D****6fR  x  |ZDhPN2X`&x,.>6 0""z%((j)***+,/^048L8t899H9p9:<==>8>@B@ALAnAB(BCGGHhI@IIMNOOPPDPQQTRRRRSTTV\VWRWX6XX[t]^`@abccdeh6iViijnjk kFkvln4o|phqqs"tpttuTunU./<2<2/<2<233'3#wffU3#5:>FNV^fnv~ 7'7#2#'125215#=#04#50#'&1#1#1##&54#52"2"&46#"'6"#"'6#"'>#"'>#"'>"#"'6#"/2424'3"473"73"2"5432"721#1"'&1472&2"2"5462"&4&#"'.#"#.#"'&#"'.#"'&#"'.#"#.2"&<2"%2#!"43iouqԖRN;   j   &  L       a  X  0 8 8 WWW@LW/j__j[+    6     !0')e * %/ RF$ ., !'73567&'?7&'7r JE#Y9t#'X(!51 GIX9'X(!/8.'&'&'467>?'737244";7674&k HJ8:&"9ZSGFo,7(`%;;O1<   2 ('(W`d35 7'''##'3'--55e-`0O[[t[nn7@@@nf& !$8L`x7#3#7'7'7''77777?'654&"&5462'654&"&5462'654&"&5462'>54&".5462'654&"&5462'654&"&5462xHV V)"    5 +++\D9   ($%+=,%1F2$   (H#AZA"FdGH   ($%+=,%1F2e?& ^#;+/6#   +#%% ^;  - %++% -#11#  "8 3-@@-3 8"1GG1  - %++% -#1160303022121212101101#1##&'4'0&9&'5#"5&'&1&'#2021001181001211"010"011""1"101#0#1""1"""#01"010"0#0#"1"1""#"#"####700#01"1#100"01'672567567061606503670>1>1676767261061476741410&504&41545054745240456507473676767670507670610650>141>1674367460707167>50?4741304>1>716767265367070707>70163>7070161061074646503>760706167>05262>0725676743063403161275367261>2706767632430652>7214167536767>121>32146367067202567673672767310136140645671<7065674652632721263763:12004#3##3#1#           )"            #Jl  EbJ076?30'&'&5476760'&'&00'&'ᘖ'&'&"676?3'&/76760'&'&0'&'&'"47674'&'&"#676762&'&/767601#&'&0"'&'&47654'.050767>'&/767>0"'&'&'.'"5654'&'&'&"0476764'&'&/767600'.1'&'&0765&'&'&'&""67676743'&/76767670'&'�'&'&47654'&'&#"#747676744'&'&/7676760'&'�'&'&'&06564'&'&'.##16767640'&/767600&'&'&#21&'&'"&1476'&'&l1 +)"7 jD"  Hp j0 (`  $  ^C In /V'    &] %  pI!Nzq-   (_   ;_' :P h/ $&$L / '  nJ JsV6& %N % vI "3H U-!'   ,'#9   !N9 =Z 0X&   '^  "& $P ? VP  *I8T  =@  #*! G#]  %FEV")  # ' ."_ #XZ8J  -?   ?$BhI #G H1M   D  " ; ($X@  "2 jMEk   <(  &(93VN(P GV  D/ 2 (6 +  #7''75''757''75KQE&kmcKPF%kncKQE&kmc +0(!*0(S +0(g+K%2#"'#"&46;1>32&#"#1";67#1#1"&'332654&#"#67>320 #22# 4 ; '$ "" }*<=+!4*@*!81G3$1 #0#B>,,=."!)4$ f6212220332201223230267676762626127632607672767676767626262'&'"'&#&'"'&"&1"'"'&'&"'&'&'&"'.1.'&'"'&'."'.0'&#&'&###0"""""#"&#0"2306%&06276763067>76'&'"&'.'".'"'&0&""""""032301207674262606062767>5&'&'0&'&0&#"'&#&'4#&'&#&'0&4&001232256%&162612726762764'&0'&#.'&#&5"'&#.'&                        Z                      LM M     & C? &#-6?+732+732+732+7327+7232+732+732!%FC &FC!%FC!%FC &E1 &EC!%FCfXXXX&XXEX("<6'&'.'&267'&'&'&76767.'67&5476767>    :''%!4 +  0       * %%7:.) 2+ *   Go06:13032#"#" '&'&#"'&54'&'&76767>1676&#"3267676767676'&54'&'.'&4767676'&6332#"&6232+5@,XvX,       MNQQ    !  8([  33       i     &) ,  .&w(.'&326?#.'.547632  ),/'h3S)&#E7D0I## 9a@52 #H+j9.!.. &8 |uWZ]2.676&2654'&'36&#".#"0&'C32=4'4'67&#"&'"13039&'0'&54767230&562.'5"1&'&'3"&167212=&50'050#&5674134001&'4#674'&756500#"'"&14'0&0&104#67327726?736?116?>?0#>?6?76?7'".67>72?72?7726?06?76?7676&''&'".*&''&'.1&'&&'0'&"&'0.&'.01&5&'05&"&'&#456'&'67>'&&'&'&"67676570#&6767&6765&6767&#"'&'67676&#& ;S[c\C +>>W?02/       +6<    w   T/   g          !     +-O02O*;8b3   ; .    )                ." QS>h$   &!7 'B8 '88'         1    (   '             + 84         S      *        (     #   ! -        ED '+/3?Kko{+'=+373/#5?'%3#/73'#'#5;+'%=+'7;5';+/=+?/;=73'73#5?3?#?/73';';=#'7#;$&W)-V6k*``j*a`)v+W-V <<<    X,a)i)a*j(_-%<=' ,+V,*<.Vk*`_))``)iV-)VV* %'=;;<&   .X-+a)i)aa*j)_*.T<< ,UU,#9MY6#>'..66.'.677&'.7&&67 &&>76o-7;"6-+"k/ L '.76 =UY> 9=UX? Y;9+,vSUx44wSUx )( :<0&"/<&'&#"32767#"'&5476323##5#535;3##5#53539!C#$$#C!"Y2332Y"@DDCCCCCC7 )(MM)( 7 66^]66 WCCCCCCCC&KT|4723>7>7"#<5#<545>7<545>7<52366745%6&'&76767'&'&'&76  / /// 2*51   &0- #&6%"'9E%'?A . )"  . .  9 1  )" 0 . 5S Tu"$-5/<1 #7 '*A@/3Ch3!201"/&'&'0&1546"+32;5#"'&=4&'>=476;5332+326=46;5#"&=4&#!*/ " "|    B        CC 9 e      %/3?#'37#'!%!'%!Lpq7>=(E""A Y-J9:8_333t)7!'%!3#35/#($=>7qa_333N 8:9J-Y  +]`eg700167=%1+53267531#654&+532"375+5#6767061673264&+654'3'3264&+&'32106'tS#pB-.*ET<--j+$Y= 4" L,L&EA[n:2P5=p-% ;TqjN@,8?#"'&'./3276767>?#"'&'./3276767>?#"'&'./Sccc#&2 *' ##(* #%2  +& ##'* #%3 +& ##'* %%N   Y    Y    Y K #+2"&546"265<544&"26"&462kuNNuN˅ccc^__FMrLLr$./%%A-'',,'/.'KI++I+b&$$&$)5=AEI2".5465"&'26="&'26="'1265&264&"42&42&42cV<BFJNRVZ^bfjnrvz~ ",;G332767&'&?6'#"'#'&?35353533#'3#'3#'3#'3#'3#'3573#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35"264&#2327"43&'"#"#2 4  !7![h-,4h>----,--,,,,--  !) 2M5   'RSN #)543s&&&&&&&&&&&)--0%%%%%%%%%%%)--7&&&&&&&&&&&)--&&&&&&&&&&&)--0%%%%%%%%%%%)--7&&&&&&&&&&&)--0%%%%%%%%%%%)--0%%%%%%%%%%%)--k&&&&&&&&&&&)--u   V$1&6!2#!"&546./67>.'";26=4a   C6. &AL](1( 5Y`2:7G  *$+IPS7Z ^7 j .'&>7676'% 1(!/*,3) 7C 5  'B8'?$,@! 9).ygO C  %'3%3#77'37%!nozxyzlopyyyk"^ 04632'1'%#67'.723>.'(   M>'K*/ 3C ( G920($ :-& !41"%' 5.'.'.54?'.'&4767676276767676'&'&"'&676#"&54'&'.76&'&#"/76'&"'&'&&'.546?'.547632'"'&/6'.7>"76&'&76>'&'&'&'"726'.7>#"'.7>"74&#"%4&'&6$654&#"3+''  $U-  + $A,$(9'     !  ,     0   W("45$H DH/9/!   ) D)  ! *A#'!         p(: (*&=&C. '       ) 8.EM/ ."'&'&776.7>6&'.>1>&'.1&547&"  OFGL  $  )7o   #L     Z'#P#*)%/ #/ #1"'!'3    ~'  5]2767"&46%2"&547&'&547636632676&'&#"'&#"2654/7676'4;3%&~~~ :3:?, +>91: z{&%29Y~~~  )>[[92" ! !!08V{{V='  a ?N7&'&'<567676%&'&767676'&'&'&'&76%&76762#"7&'&54767674'&'&76676'&323'&7676'&#"'&7654'.#"#"[)'    A "   ("    wxY#             4I=2 -<  *7!5JC8!  8 '      $        !0>G.'&676?'.'&67>762>'.'&6'&'"&5676'6?G%% L/30#"^va  (% # &    9 <*-  $(>  i2&!_:1C *  21 '  3OW_%#0#*#"#"'&#"#"'&'.'&#"#"&54765462'264&#""'.#"3267626"&462"&462"        oo    4  \%     NT\\BJ++3    >  #&),/237<?CFIOTZ]`aehknqtvy}>217037053'03703'0761#711'070'175'7'1?50'50'43723+21321703#1/07611'1703-30'17535%5%57;5%71%571-'0/1/"721321#6#'"74170'5730'30'07'2/0/0763'0730'0;21&'.'&'<767>0:3676'&/5407676'&'*#/7654'&'&/767676*" 'EO\iij PF ILO`jjKiJ_ H  [iii\]((' %:!   "    3  : 4''( &  DD CB   *C+5?G7.67>&7>76&'.>7.7>7&6767&>&>&x>)!,(($!t!)!J  #  $ -    5<  .,^W  > x%) $*+         !{&/:DN[is '5CTajs7236&'&56"#263<261'676&'&6">7&%2&'6"676&0'.#6'74'6&'4&4'6"'.'&7>"2>7&6&562'&6%6&'62'.'.1&767&''6'&'&476&7676767>'.'&'&2'.7>"7>'.'2'.7>7>'.041&'&6764&5"&'6'&676'&627'.76#67<'>3'&'&6"#"63226'&'232'46!  .   $                       &6@I=6  #"Q,0 17>756'#&21#'&'5&'&736767137674656'&'1&2031'"5&76"727.'&'"16>703161101*&'&'#&'1&>7&7&'&7>6"#"1&112367676367674&'"5056/&76'4547676'.0#"0#"'&12321+'0#&'&71676"176&'5.#&6&7672&'6#&&54'1631'5&54617>76'1&2#"'&'###0"1&'&5&667347676#0#'"36?30762735>'&'&1'1&'&?3671336756'2'&7465676'&62&5&6 $  %!   h     r:*  #   1 H/#  "#F6  .G .  "  (      % &" " B         0 W)    %!            %124K@   2"'C>#0@C&!-  >K641# 4  Jw   %! T         V# ?   C6>7&'&"174&5263#>7263'&'&767676]E  , ,5 *) %h-SPHy^%"K%3+ !:U)I?i7?TJT/ .em%1/&'&'&'&'&767>767676767676323&547&'&67632726?30&>.33232363>'&'"1'#""9!"3!2654&'2#!"&5463_ +     ,#!     $$$$ $   $%%%%g    4      / )0 !;$&%%$&Dis{26;2+!5+"=&?6"+32;5#"&=4&'>=46;5332+326=46;5#"&=4&#353264&#32+ : ! 86   p   t$ A C?    .50  &%J6#*'&'.'&?63:32;2+"&7>767075670545'&76 M ? = = 0# 7BI X "  N?\ } h  dM; 8)  f   "%4&"265267##'7'3#'#'632\6L66L6\_RK`J*,4jUH,,._&66L66&|\H$h54'&'&76767654'&'&'4&'&/#!1767654'&767676767676=#S>  ?SiG   1;V%       !Vv    A    >3. =  W   % (         ?            %   ^=n%'0&#&'&547676'&'&'&'&'&'&'&76;"'#&'&547654'.'&'&'&'5;'&547674'&'.74767676?;       (   $$     %&   % % 7        . w3MUmv|.'&'.7>32>56&'&'&5476'&676'.'&7667>327>327667676762#7>76'.#"6'&62"7&762#"&5676'&#"'476#"&'&267654'4&'57>'&6767>'&'&'467>'&5467654&'.'&'&#"326767676'4&&'.7>3232&'6&'&&'&767632#"'&"7654'&72'64&#"6%676&#"'6&'&3767254&> #        !           &=A & )]  c    #$-. +  &. X( ,#    HG !  "     '  ! # /'  _   z   #    p  '  $!               #,   /  ' =      6  ( 4<0 -  +      /  ,#        $     " 7 k    "  M1!632'&/7676;7676'36?5&7676'&5&'"5476'&&476'&"&5476"27667636767676'&/&/32767>'&"5467676&#"'&4762'&54/& K4S $;5F        $'0     ]  #      [ <#     .""     .  5  = - .  / %' L28z,]{2322'&'0'&5>7276367467>7>0#""3.47672721672>7072'.#&'"&76767>7676"'47062&7>&&764#'&61>7>76#"&54676&"54>74"7627637>67>767>5465676762"##'&'"&#.'&'.'&54&56232767>7>76'&'&7465676216'.#&54'&'0&637672#'&'">3476'&76767>6764767243>7667>767676636>5>56'&7"7>727465>7''&7>7.'&'"#&23333636762#'&'&7&5.'4&4+1726367>5>7'"  00: "5 % 8#'9   :  P             /       $1-.*!            )*      & '(   ,)     &      v )                      3             !   # "  ,   V"   H   "   $ *DSw032##"50567"541>726>7632'"'&'&'1#"'"'"''&'&'&767063'0'"'&'&'&747'&'&'&747>7>7&5&7'&'&'&76767670321#"'&'0236767676767676&632676327>76'&7432#'&'&=67&'&'476767037256'4767654154327041>56321#"'&'767072727076?4767676567&'&543220&'&5476'41051"#"5>5676726321037676'4&5'"'##"541674'"#"541674741#"547>76723"#"54727&501.54367176023&'&'&'0'&"1"547025&'&2"5&767032#"5&7#"541676767676"027261612"16;2##"50747&'4&10#"5476767>722"5&5"1"'47676326760231&6760##"'0&5032764062767&%323:323674'"5<5&'04 O  %$           !<  #!   !            (       9  E    j L       35                   &    )*       !&*        %    ,)$        4             $P      & 57'337'3#'7'3/3xxZyyyy[[KK)(&qqj<?30'&4=3>qOTW.-O.*3%@PK2*;e )NAA )4L201"/&'&'0&15463'764&"3264'76=&5&'#'&"2?z " " Y;4ML4:4  CC 9 ;4 M' L4:5 !'%!5?#'!#3/3(E""=qp7333t)wI9: X,!'7!3/#?#'(8==7pq33369I-X :/7;CGS2+"&5#"=4;5#"=4;5#"=4;546;54;2";24#*2264&"7#2+"=43h#V -  wf     "  0%  F!>Wk|707'&767'"7'&76?>&'&'.>7'&76?&76'&/0?'&767>;6'&#"6767.7>'67'i,/ <= :< NAY      r (#  $ g,%$GB FZ-# <  %2 % * VH<1v"h  0  S                 Q  F(!0"+D @7Sr72367'".'&6527627'.'4>7#72'&7>'.766&7>74.3#'"&7672"726'.7676&7>76&"76'.5&'&7>7>2763'".'72>g):5<  4HKD  " (   Um  nw V U 0  W. 9    : %' !l762#.#"&747#"&7>54&7>7>'676'"&#&&'&67&'&76&7454>"#29232767567652418565454545&'&"&'&'3>3232>76&"676    "%  J   =     & * )   -  1HF'   *.f M3 %       #-#! .n;:Scs%60#&'&#"'&'&460327676'&'&'&5465;'&/7603276?!"&5463!2"3!2654&#'     %  b  ?  ?        DDDD  \?  ?  :Sc%#"'&0767654'&'&'&'&5676325260'&'&'&#"'&07676=+'!2#!"&546'  %     }  ?      GD  DD?& )732+"&?63+"&?6%32+"&76;2+"&76>n  nn  n-7 77 77 Q Q  Q Q  D  D(G023223123000121031010210323>?'"'"#&'4#&'0&1&'"'&'0&1&'0&#&'4"'&'0'&'4&1&'4'&'&'&'0#&'&1&'4&#&/&'&'&'4'&'.?00102103210301213>?'&'"'&'&#&'&#&'"'0'&'4"1&'4"'&'&'0&5&'&?2023>5'&'&'&'.7" @`@' 'A'i 2G/^53 [6I B3p6 ,-2"4}; "X$ "" &(9C7+34  .K& F7'267326737/+"/&'/&?&/&=4?67'&?dPhgOdKa9 (G2":8* 8$ >@ # 8E%<$2F$,"% ?? "7* : "=Uh%.1767&'67'6"1'&36%"6763!2&'&'.67#!"&5767''&'452>32"A6!"3)@1 M'6&7 E37&'7,`7m,%|b(.,+J6N, X4 Dr7'i'   H(6&77&`>d u!!'7'''78'p'n\h!\88tIyI.<O2632+&'&=3;265'4&"#54&"#546"&=#532'54&+'32;26&"'))&Ff;MGf&O8M O8O'o h  YY  YY'^Dp^EK4IC4I*43#264&"3762"&='#"/&>'3#762$!  _X# =/yM}$$ _Y$> /2U/HU;a6360'5'&'&'&'&"#&/=7676767>167?5"&'&'&/=7676) @  M L ?    '*  )zz|| 11 {,,zz(   }} }~ '{Iqz61&'5>7.&'&>7&'>767267>7>7>"001"00:2427014=45&4'*#0"3676"34>36"3673"3476'"7.   + !   *   I #W  )   +(   z   A / %     (%%        u &,289:>CIMNOUZ`dkqrsy   &+,245689;<BCDEFIJPQRSZ[\]^`ghioprtuvxyz{|6676&76&67>67&7>76&764&46767607207'667173#&&'&&''&5&'&5&&>5&&>76#&&76#"676&73&'&7'&7.'6.'>.5&'&230.5.76.54&46'767 4'433& 01670> #6'&66%54&'&'#07666'&6&7"6!'&3&1"'&4.#'&7"76'#'4523&1&776'7&'&776#&'&7#6''76'3&&'&72?'2&7#67'76#'763"+#4.'&'3"#';"&7'&'473"#"#33'&##32'&"'.'3#'&'&'&7##37              %         c  [![ >  (Z;) %; = 6  R  C   ( -q I Q)!  G pX   )\V   ! 6 T ' f#c  u# /l  .Yd:~ b>~N=6()  9LI5  aBI + % "       #*   (               )  -                     573!532654&'''7'77 o p?`2F7  " 6$+@$--F25`\K+G $,;?CW2#!"=/&'.7>7>2543"2"6#6&/&'&?'24"74#!"3!2%B&,\  8KK5 PG/  r *3Og!3L #/;GSco{732=4+"#2=4+"3732=4+"2=4+"374+";252=4+"3';2=4+"74&+";265";2=4#74+";25;2=4+";2=4+"34+";25+"&5463543232J5f+ q  q 8"W+   V2g}  =  /QO  Y K KF):K[7+"&=46;5&546254&+";2674&#""5462"'4&""54632"'4&""5462" uf  J J U<,   5  XX"&=>4.  "!(&" V Sk:'I.2W05 33M$y>11Bi`v$HZk*:IXfu$2DXgz00"#&'&'&'&7676761#676'&/&'&=0541767676225"'&'4123276760#&'230322230034'&'105&'&4'&'0'&'&'007676764#&'&"05456'&30076?&'&&2276?"276?'&'&00767614'&"007676?076760.'&'&02767>1&'&&76767076?'&'&'42767"0767>#&'"067676?5&'"&076?32'&01076761&'&2767<"103076?0'&'.'&07676307650327670'&7474061276?'&'"076703276760"76767607674'&'00276767#&'&'&07676?33076' C)@_ 1.#   G # ' 0D *  J ^    <  )J   %,    9.  <),  I    "k0 -    z   ;m 7!  F8 #    =6  9 +   d &    %&1   :/&#                                             &   '%4.1&'.'.'&'.'.'&'&'.'#.'&'&24367>7.'&'&'5'.7>7.'&'&'.'&'.67>3"#.'&'.'&'&'.'4>3601&'.'#56  %B             #   +<     4  ## M-     )  5+ %  B *     =,("     .   o|2233030.'&'.'.'&'.'5'.7>7.'&'&'.'&'.676"#&>'&!  +<     F+5     h    =,(!     C *   9 3&/#'&76%.&'>7>2.>XK<  ;DN#O9/+?M$ $$."+@6$9Z1q |    '    &;#'#37#"&4673+/?r 6V!CBhEC  Z ZEzu3">&2321762"&="&50546& ## #  D&-/+=?/3?#?/?'#'H9-.:H6+,,+7C,E ExxE E,D3!43!=j7"/&7676?6=4/&6=4;2#"/&=4?6#7"54;23254.'.54632+"'.#"# 6  *-+ 2W1%#'#M*%)!*' ^^^    _ _  ^=9  P%&!!#5#735335#335353#735335335#r9q89:W8VVrs;VqUUUUqC3!201"/&'&'0&1546"+32;5#"&=4'&'>=46;5"3267'#"'&'.5467>325#.#"326732654&'&'&2"&54!*/ " "0    !           !      CC 9 e       #   "   K +    D  ."):A77&7%3&'7'7&'&767>7&'%7'&'767677&'ZU% Z Z'=BqZ >U Y# sXrC<'Y> VZ!b   !6 O?/0'!#2!!D,2!0,&k 32+"&46";264&#C__CC__G+>>++>>+j____9=W==W=!!";264&#32+"&465676'&'4'&54'&76'&&'.5&7&67.'&767>767637>7676767676126"676767476'4&'@                 )       -    !    '   &                           (  @         |-7?.'&'4>?6'.'76o D  v* ;*35! |! H   \ !<O* eH#7   =f%#"'&'&'&'&5476366'&'&'&'&'&#"27676%6"#"'&'&'&4767632#&'&'&'&>764'.'&'.5476320#"'&'&'&7632'&?15'&'#5&7236#"'&75767'#*#"'&'&5&=7672323'"2764'&+52#"'&= %   @C   AB<    "  ' Z      -  T3  9, # &K@U-  ^Z" L   RaFF  6 LF; Rk,/ " Gs & FN"7>'6352'.7&#".7>7>7632>=2264&"1# 1>T1" 1>U#'5)0(5(" %   *   B//B.;) -4;) -%%*6% %6 $.B//BEl!+3BLT6.'.7667>'&73'#73>74+733#76+73'#73>74+kQ,9 7jm) BE9e'"4dh*C1   4     :1  k&J+U" P U&<  :Gl0  HP201"/&'&'0&1546;373654'#7323732376&'#7#373654'#732z " " *"4 -   C "4   CC 9 @+  @5: ?*  N2323"#"+"#"'&'&'.'4&=47>?467>7>37k'.\,"7~ d736766&62323#""+"#"'&'&'.'4&=47>?467>7>?  &.,76s   @# }  642"&462422"&464&"nq .!!.oԖԖTxxxxoo .!!. ooԖxxxx D3Kgz676'+'&'&'4''&'&'#'&'&'&'&76766203676"#01#&#"&'"#3676731767767676732767654'&'&'67656'&2.'6'4&74654545&'&'&'60&#01'&'&'&'&745'14'&'&0#67>&'&'&#"67676760#"'&'&'&'&7672233&'&'&'&'&762"&765&<14167676"#67>'4'&"#7>54&'&'"7#"'"'4767266'&'0'6767>76_K+  "   &&    /).#"$$ )          !) 'G@$&  "LA)#   ,    h#  $       &    2 :7  @'  /#-%=   8%,",     1 ;B / *<8 1   ,-   !'     ! % )"+$2e     # $   [{,FYr676#''&'&'&'&5'&'&'&6767670701&'#'&'&'&'&76763632527676"#0367654'&&676326126767671&76763&'&'4'&'&'&76&'&'&#"#36767&7654&505&767&&6'4&5465&"13674>'&'&2#'.7676323&'&'&5763672"332767'&#"2767>'&'&]K*  ,#        + '%%%  ' &%      0   &,%    $  !     '2  E> 5     .%.&;  :@, *"  1.   45&.#, '"# j     &$  (0Ya2#"#".4>;5#'4>"264".57546?32>7=3:+36264&" - g $ g +  )- g $  h +   c 5 /@. 3 "  q b 5 .@. 3 "  ]g1;GYfu262636736>2>6626+"'&'.'&'.6?467&'&'&7&7&7&7&663666&'&'&*#'''#7&'>&'&'&'&7&7&5&'677&'>7&'67"'67&'67'67'467567"#'7&&7&7&"2656&"76'&"56&"676&'&'&&67454&'&7>&'&"#76'&0#"0265414&#676&'&"76&'&#""326'4&^                             0:4#      "5:1      LV *&  '* )    q" *    *n (  &  v"!!         ,  ,                l   ! %!"$  ,5 4! "-* #  # " @'  '" % S  @IX`h6462"$#"'&'#"'.767&'&'&546767&'&7>67676'656&'&&'&'76763276767&'&'&#"67&'7&'6':3&'6767&'.&'6&'&'6767#"#67&'7>74'&'&'67>**4:2  +9%$  /5/  .I'%!$.  -)$             -   $)#:  (+.*  *# 9"9#+!5  l*   ,0++N521%' 1!  E1 / 0#&$ K4 {"19 s Z@> "&#$($" .4A "8#%&"(W2&#"0'67>767647467&#"&#"'"&#&#.'&'.#'&'&#"+&54%""&'&#&'&7&676&'&7676'.'&'&#&7>76742'&/.762726327"5056723232#"'"436'&"Д)             "    5  $   I# ",mLO "!     K     iJ?        ')i_ &,   M 1!  8    /  7S]`hm%'.'&=67>767'.'&=67>767'.'&=67>76'/7'77&"&4627'             3& $" "w$3%5%%5FFF2 I  I 4 I I [3 I  I 4 I  I ^2 I  I 4 I I #  R4X   #&.753 %%'7777'7?'?'77'7'''?r"v1w>{jQ'.=gf8OZGf`M#p,j_U"PM#  yhz *o3$n#b-<$"Nj5?Z6 "&*.26:>BF`x7&767676.''7'77'7?7'/7'''3'!3#%#5454&467676"32767>&5A )1W:  D !8 !<U7 "    "0 .+-+c*"6?M\  >5#RU"g  =;  ( & )*  9 . tu!R((    '(R!}g0 B "&,D[_cgkosw{732##'#7357032#5##543573#?3#7#32+53254#"&543%&767676.''7'77'7?7'/7'''3'!3#%#5454&467676"32767>&5+ C(*1r!    )1V9!C !8  ;U7 "    "+ ,++*m)<  7 6 5HG6I  5@M[  >4#RU!f  =;  ( % ))  9P1 z{"W*+    **W"`1S !LU^kv267>>3>7676762##&.#'&'&'&'.&'.#.'&&'&56&76&&74>4&&&7>'.'4>76.'&7676'&67>'.72>&'.67>7.762364&5&26&5&66'&667>7>&76'&366'&626&'.'>7&'.'."76'&&76'&35376'&'&"626&#&726227>767&#'.'&"76'&3&76'&                                &=+/  &   ,    b    5T6 9  w   $R% 3"  4J                                      +  0) ' !  !- n     %"w#V-  {  r%'"676'&'&'&7&''&7>76'&''&7&'&7&'&7>7.'&7>&'&63237>76'&>76726>7>7667476616'767674&766576'   (           4<Q^&%IK  -8gS/        <%t+ iPHy    $ +  .    )0&# # 5+,   2, - $  a9  *.67632?#"'&?62#"'&'&54>Y^SQq qH|   5  =P^ 3 G #527$#567$#567$G " ! "i!iri!hqi!hG 2@Uc#527$#567$#5>'23'./%636"+74'&6'./%276"#74'&F !  @7E(#|i38#}h38i!iri!hqi!h _' # :W `( $ ;X #(,3!!%!3733#3##7##7#537#537#373#53#'5N"% (" " ( !)* `)))O'; 999!- 9999 -!9Z--[+d66d">BF3!201"/&'&'0&1546375#3#33733735#735#7##73#35!*/ " "&2* 1)!'!1* 0)!'' '5  CC 9 eO99O4%4444%444R%!11 !!%!73#'57'511NRRO';|((+! +6+!%%''?''77''S.TS5SSS5SSSSSS4SSS5SSSYE3Qp%7>/./3.?&6?>=6&/07/"&"1">17>?>='./<14&/./632/./.'#367./"&/./&0367'.#76173567"/d 4 h        `       V    U  $    !      5 1  $2 '&'&'&760*.'.#3'&6>767>74>7>27>76'&26'.'&'.&'&656'&'&'&54656'&'&7>&'&'&67>7>&'.546>'&'.'.676%&767676#'&'&'&72&'&'&761&'&'&761   /    !     #          ', 2     4  :         M          Y   9  @ .              n                K  !". 62>."". >2 2>7>1 >2>.'&'.4"%2>763>7>0">233          !      $                      9             M                        @Cz25'&/762'&&'.'&#"1/&'&#"27676&'&76&'&676'.'&54767676"7>'&2'&'&76"76& *2'   # $ ' !!    5) N00        C            *! 8'#        @Gc+51463&462462622#4&"#7264&"564&"'4&"3#&"'#373"&=46234&"26= 32"&5#264&#"&46234&"*+<+<+<++!! P9 9   9 9Q !&-&.A&&6&&6&{& &&6&& &&6&9+<+**+<+ 9 9Q !! Q9 9 [&  \  &&\&&A& &&6& %%y"&462.'&'.7467676&'&676/&76"54765."7676?3654&#'7>367265.ȍ"     /       & "     &ǍǍY *       *32   &&L+       & !%)-F[^nrx7'7'7'777/3''?73#53#3#53#'#536'&#"'632363"31''7'76733&737#7&'&76>703"':10"54'&&'>#&'6#&'#546767&'&7672676&'&7&>7632473&'&'72%67&'3&,!  /U 44444444T .  !:/:0i +/0 3-@B/"+4' 6p  +)9-# ) .1d1.b  AS+OA W +# - 0 ]  %C#   ,      ![?d>~. ( %C@<[ W D!$#! :  8" 0+UE % <$b!20001"&50405414105414105423276010101+"5.#"/&5241&525&5241&6762'&'47&147&147&14767&'&507&507&507&56720000001"&5040404040404142111'&#"#"'.701&7&701&?63267432011100"&505< 54x**8*,MD,),M)&&*44++3(0,),D#++ ) 2B+: A*A"22"A(>>(n$G3$@ :* %7'#37/#'73?EY++qp6==r(g(r456547 08"'&""&47>"&4762"'&""'&""&4762"&462 TT  CC p JJ  88 N  .. _4%%4% SS  C//C V KK  77M   // %4%%4!)7;2'&4>235#6'&65#>.'.'iQ/&F  8M v~3  w KIUS 1P2k  > DPiK&    3B- 7373#.B-B &J       L q   Created by vagrant,,, with FontForge 2.0 (http://fontforge.sf.net)Created by vagrant,,, with FontForge 2.0 (http://fontforge.sf.net)font-mfizzfont-mfizzmfizzmfizzFontForge 2.0 : font-mfizz : 13-2-2017FontForge 2.0 : font-mfizz : 13-2-2017font-mfizzfont-mfizzVersion 001.000 Version 001.000 font-mfizzfont-mfizz      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~uniF100uniF101uniF102uniF103uniF104uniF105uniF106uniF107uniF108uniF109uniF10AuniF10BuniF10CuniF10DuniF10EuniF10FuniF110uniF111uniF112uniF113uniF114uniF115uniF116uniF117uniF118uniF119uniF11AuniF11BuniF11CuniF11DuniF11EuniF11FuniF120uniF121uniF122uniF123uniF124uniF125uniF126uniF127uniF128uniF129uniF12AuniF12BuniF12CuniF12DuniF12EuniF12FuniF130uniF131uniF132uniF133uniF134uniF135uniF136uniF137uniF138uniF139uniF13AuniF13BuniF13CuniF13DuniF13EuniF13FuniF140uniF141uniF142uniF143uniF144uniF145uniF146uniF147uniF148uniF149uniF14AuniF14BuniF14CuniF14DuniF14EuniF14FuniF150uniF151uniF152uniF153uniF154uniF155uniF156uniF157uniF158uniF159uniF15AuniF15BuniF15CuniF15DuniF15EuniF15FuniF160uniF161uniF162uniF163uniF164uniF165uniF166uniF167uniF168uniF169uniF16AuniF16BuniF16CuniF16DuniF16EuniF16FuniF170uniF171uniF172uniF173uniF174uniF175uniF176uniF177uniF178uniF179uniF17AuniF17BuniF17CuniF17DuniF17EuniF17FuniF180uniF181uniF182uniF183uniF184uniF185uniF186uniF187uniF188=QQgoodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/font-mfizz.svg000066400000000000000000016746421337147241200306640ustar00rootroot00000000000000 Created by FontForge 20120731 at Mon Feb 13 14:14:41 2017 By vagrant,,, Created by vagrant,,, with FontForge 2.0 (http://fontforge.sf.net) goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/font-mfizz.ttf000066400000000000000000001741401337147241200306450ustar00rootroot00000000000000 PFFTMucrDOS/2Q'^X`cmap|Jcvt D(gasp<glyfHheadvU6hhea$hmtx$loca.TfD,maxpRb8 nameD%H$postEoy_< QQ@.@1@.LfGLfPfEd .   j.MLo     D( D****6fR  x  |ZDhPN2X`&x,.>6 0""z%((j)***+,/^048L8t899H9p9:<==>8>@B@ALAnAB(BCGGHhI@IIMNOOPPDPQQTRRRRSTTV\VWRWX6XX[t]^`@abccdeh6iViijnjk kFkvln4o|phqqs"tpttuTunU./<2<2/<2<233'3#wffU3#5:>FNV^fnv~ 7'7#2#'125215#=#04#50#'&1#1#1##&54#52"2"&46#"'6"#"'6#"'>#"'>#"'>"#"'6#"/2424'3"473"73"2"5432"721#1"'&1472&2"2"5462"&4&#"'.#"#.#"'&#"'.#"'&#"'.#"#.2"&<2"%2#!"43iouqԖRN;   j   &  L       a  X  0 8 8 WWW@LW/j__j[+    6     !0')e * %/ RF$ ., !'73567&'?7&'7r JE#Y9t#'X(!51 GIX9'X(!/8.'&'&'467>?'737244";7674&k HJ8:&"9ZSGFo,7(`%;;O1<   2 ('(W`d35 7'''##'3'--55e-`0O[[t[nn7@@@nf& !$8L`x7#3#7'7'7''77777?'654&"&5462'654&"&5462'654&"&5462'>54&".5462'654&"&5462'654&"&5462xHV V)"    5 +++\D9   ($%+=,%1F2$   (H#AZA"FdGH   ($%+=,%1F2e?& ^#;+/6#   +#%% ^;  - %++% -#11#  "8 3-@@-3 8"1GG1  - %++% -#1160303022121212101101#1##&'4'0&9&'5#"5&'&1&'#2021001181001211"010"011""1"101#0#1""1"""#01"010"0#0#"1"1""#"#"####700#01"1#100"01'672567567061606503670>1>1676767261061476741410&504&41545054745240456507473676767670507670610650>141>1674367460707167>50?4741304>1>716767265367070707>70163>7070161061074646503>760706167>05262>0725676743063403161275367261>2706767632430652>7214167536767>121>32146367067202567673672767310136140645671<7065674652632721263763:12004#3##3#1#           )"            #Jl  EbJ076?30'&'&5476760'&'&00'&'ᘖ'&'&"676?3'&/76760'&'&0'&'&'"47674'&'&"#676762&'&/767601#&'&0"'&'&47654'.050767>'&/767>0"'&'&'.'"5654'&'&'&"0476764'&'&/767600'.1'&'&0765&'&'&'&""67676743'&/76767670'&'�'&'&47654'&'&#"#747676744'&'&/7676760'&'�'&'&'&06564'&'&'.##16767640'&/767600&'&'&#21&'&'"&1476'&'&l1 +)"7 jD"  Hp j0 (`  $  ^C In /V'    &] %  pI!Nzq-   (_   ;_' :P h/ $&$L / '  nJ JsV6& %N % vI "3H U-!'   ,'#9   !N9 =Z 0X&   '^  "& $P ? VP  *I8T  =@  #*! G#]  %FEV")  # ' ."_ #XZ8J  -?   ?$BhI #G H1M   D  " ; ($X@  "2 jMEk   <(  &(93VN(P GV  D/ 2 (6 +  #7''75''757''75KQE&kmcKPF%kncKQE&kmc +0(!*0(S +0(g+K%2#"'#"&46;1>32&#"#1";67#1#1"&'332654&#"#67>320 #22# 4 ; '$ "" }*<=+!4*@*!81G3$1 #0#B>,,=."!)4$ f6212220332201223230267676762626127632607672767676767626262'&'"'&#&'"'&"&1"'"'&'&"'&'&'&"'.1.'&'"'&'."'.0'&#&'&###0"""""#"&#0"2306%&06276763067>76'&'"&'.'".'"'&0&""""""032301207674262606062767>5&'&'0&'&0&#"'&#&'4#&'&#&'0&4&001232256%&162612726762764'&0'&#.'&#&5"'&#.'&                        Z                      LM M     & C? &#-6?+732+732+732+7327+7232+732+732!%FC &FC!%FC!%FC &E1 &EC!%FCfXXXX&XXEX("<6'&'.'&267'&'&'&76767.'67&5476767>    :''%!4 +  0       * %%7:.) 2+ *   Go06:13032#"#" '&'&#"'&54'&'&76767>1676&#"3267676767676'&54'&'.'&4767676'&6332#"&6232+5@,XvX,       MNQQ    !  8([  33       i     &) ,  .&w(.'&326?#.'.547632  ),/'h3S)&#E7D0I## 9a@52 #H+j9.!.. &8 |uWZ]2.676&2654'&'36&#".#"0&'C32=4'4'67&#"&'"13039&'0'&54767230&562.'5"1&'&'3"&167212=&50'050#&5674134001&'4#674'&756500#"'"&14'0&0&104#67327726?736?116?>?0#>?6?76?7'".67>72?72?7726?06?76?7676&''&'".*&''&'.1&'&&'0'&"&'0.&'.01&5&'05&"&'&#456'&'67>'&&'&'&"67676570#&6767&6765&6767&#"'&'67676&#& ;S[c\C +>>W?02/       +6<    w   T/   g          !     +-O02O*;8b3   ; .    )                ." QS>h$   &!7 'B8 '88'         1    (   '             + 84         S      *        (     #   ! -        ED '+/3?Kko{+'=+373/#5?'%3#/73'#'#5;+'%=+'7;5';+/=+?/;=73'73#5?3?#?/73';';=#'7#;$&W)-V6k*``j*a`)v+W-V <<<    X,a)i)a*j(_-%<=' ,+V,*<.Vk*`_))``)iV-)VV* %'=;;<&   .X-+a)i)aa*j)_*.T<< ,UU,#9MY6#>'..66.'.677&'.7&&67 &&>76o-7;"6-+"k/ L '.76 =UY> 9=UX? Y;9+,vSUx44wSUx )( :<0&"/<&'&#"32767#"'&5476323##5#535;3##5#53539!C#$$#C!"Y2332Y"@DDCCCCCC7 )(MM)( 7 66^]66 WCCCCCCCC&KT|4723>7>7"#<5#<545>7<545>7<52366745%6&'&76767'&'&'&76  / /// 2*51   &0- #&6%"'9E%'?A . )"  . .  9 1  )" 0 . 5S Tu"$-5/<1 #7 '*A@/3Ch3!201"/&'&'0&1546"+32;5#"'&=4&'>=476;5332+326=46;5#"&=4&#!*/ " "|    B        CC 9 e      %/3?#'37#'!%!'%!Lpq7>=(E""A Y-J9:8_333t)7!'%!3#35/#($=>7qa_333N 8:9J-Y  +]`eg700167=%1+53267531#654&+532"375+5#6767061673264&+654'3'3264&+&'32106'tS#pB-.*ET<--j+$Y= 4" L,L&EA[n:2P5=p-% ;TqjN@,8?#"'&'./3276767>?#"'&'./3276767>?#"'&'./Sccc#&2 *' ##(* #%2  +& ##'* #%3 +& ##'* %%N   Y    Y    Y K #+2"&546"265<544&"26"&462kuNNuN˅ccc^__FMrLLr$./%%A-'',,'/.'KI++I+b&$$&$)5=AEI2".5465"&'26="&'26="'1265&264&"42&42&42cV<BFJNRVZ^bfjnrvz~ ",;G332767&'&?6'#"'#'&?35353533#'3#'3#'3#'3#'3#'3573#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'35'3#'3#'3#'3#'3#'3#'353#'3#'3#'3#'3#'3#'35"264&#2327"43&'"#"#2 4  !7![h-,4h>----,--,,,,--  !) 2M5   'RSN #)543s&&&&&&&&&&&)--0%%%%%%%%%%%)--7&&&&&&&&&&&)--&&&&&&&&&&&)--0%%%%%%%%%%%)--7&&&&&&&&&&&)--0%%%%%%%%%%%)--0%%%%%%%%%%%)--k&&&&&&&&&&&)--u   V$1&6!2#!"&546./67>.'";26=4a   C6. &AL](1( 5Y`2:7G  *$+IPS7Z ^7 j .'&>7676'% 1(!/*,3) 7C 5  'B8'?$,@! 9).ygO C  %'3%3#77'37%!nozxyzlopyyyk"^ 04632'1'%#67'.723>.'(   M>'K*/ 3C ( G920($ :-& !41"%' 5.'.'.54?'.'&4767676276767676'&'&"'&676#"&54'&'.76&'&#"/76'&"'&'&&'.546?'.547632'"'&/6'.7>"76&'&76>'&'&'&'"726'.7>#"'.7>"74&#"%4&'&6$654&#"3+''  $U-  + $A,$(9'     !  ,     0   W("45$H DH/9/!   ) D)  ! *A#'!         p(: (*&=&C. '       ) 8.EM/ ."'&'&776.7>6&'.>1>&'.1&547&"  OFGL  $  )7o   #L     Z'#P#*)%/ #/ #1"'!'3    ~'  5]2767"&46%2"&547&'&547636632676&'&#"'&#"2654/7676'4;3%&~~~ :3:?, +>91: z{&%29Y~~~  )>[[92" ! !!08V{{V='  a ?N7&'&'<567676%&'&767676'&'&'&'&76%&76762#"7&'&54767674'&'&76676'&323'&7676'&#"'&7654'.#"#"[)'    A "   ("    wxY#             4I=2 -<  *7!5JC8!  8 '      $        !0>G.'&676?'.'&67>762>'.'&6'&'"&5676'6?G%% L/30#"^va  (% # &    9 <*-  $(>  i2&!_:1C *  21 '  3OW_%#0#*#"#"'&#"#"'&'.'&#"#"&54765462'264&#""'.#"3267626"&462"&462"        oo    4  \%     NT\\BJ++3    >  #&),/237<?CFIOTZ]`aehknqtvy}>217037053'03703'0761#711'070'175'7'1?50'50'43723+21321703#1/07611'1703-30'17535%5%57;5%71%571-'0/1/"721321#6#'"74170'5730'30'07'2/0/0763'0730'0;21&'.'&'<767>0:3676'&/5407676'&'*#/7654'&'&/767676*" 'EO\iij PF ILO`jjKiJ_ H  [iii\]((' %:!   "    3  : 4''( &  DD CB   *C+5?G7.67>&7>76&'.>7.7>7&6767&>&>&x>)!,(($!t!)!J  #  $ -    5<  .,^W  > x%) $*+         !{&/:DN[is '5CTajs7236&'&56"#263<261'676&'&6">7&%2&'6"676&0'.#6'74'6&'4&4'6"'.'&7>"2>7&6&562'&6%6&'62'.'.1&767&''6'&'&476&7676767>'.'&'&2'.7>"7>'.'2'.7>7>'.041&'&6764&5"&'6'&676'&627'.76#67<'>3'&'&6"#"63226'&'232'46!  .   $                       &6@I=6  #"Q,0 17>756'#&21#'&'5&'&736767137674656'&'1&2031'"5&76"727.'&'"16>703161101*&'&'#&'1&>7&7&'&7>6"#"1&112367676367674&'"5056/&76'4547676'.0#"0#"'&12321+'0#&'&71676"176&'5.#&6&7672&'6#&&54'1631'5&54617>76'1&2#"'&'###0"1&'&5&667347676#0#'"36?30762735>'&'&1'1&'&?3671336756'2'&7465676'&62&5&6 $  %!   h     r:*  #   1 H/#  "#F6  .G .  "  (      % &" " B         0 W)    %!            %124K@   2"'C>#0@C&!-  >K641# 4  Jw   %! T         V# ?   C6>7&'&"174&5263#>7263'&'&767676]E  , ,5 *) %h-SPHy^%"K%3+ !:U)I?i7?TJT/ .em%1/&'&'&'&'&767>767676767676323&547&'&67632726?30&>.33232363>'&'"1'#""9!"3!2654&'2#!"&5463_ +     ,#!     $$$$ $   $%%%%g    4      / )0 !;$&%%$&Dis{26;2+!5+"=&?6"+32;5#"&=4&'>=46;5332+326=46;5#"&=4&#353264&#32+ : ! 86   p   t$ A C?    .50  &%J6#*'&'.'&?63:32;2+"&7>767075670545'&76 M ? = = 0# 7BI X "  N?\ } h  dM; 8)  f   "%4&"265267##'7'3#'#'632\6L66L6\_RK`J*,4jUH,,._&66L66&|\H$h54'&'&76767654'&'&'4&'&/#!1767654'&767676767676=#S>  ?SiG   1;V%       !Vv    A    >3. =  W   % (         ?            %   ^=n%'0&#&'&547676'&'&'&'&'&'&'&76;"'#&'&547654'.'&'&'&'5;'&547674'&'.74767676?;       (   $$     %&   % % 7        . w3MUmv|.'&'.7>32>56&'&'&5476'&676'.'&7667>327>327667676762#7>76'.#"6'&62"7&762#"&5676'&#"'476#"&'&267654'4&'57>'&6767>'&'&'467>'&5467654&'.'&'&#"326767676'4&&'.7>3232&'6&'&&'&767632#"'&"7654'&72'64&#"6%676&#"'6&'&3767254&> #        !           &=A & )]  c    #$-. +  &. X( ,#    HG !  "     '  ! # /'  _   z   #    p  '  $!               #,   /  ' =      6  ( 4<0 -  +      /  ,#        $     " 7 k    "  M1!632'&/7676;7676'36?5&7676'&5&'"5476'&&476'&"&5476"27667636767676'&/&/32767>'&"5467676&#"'&4762'&54/& K4S $;5F        $'0     ]  #      [ <#     .""     .  5  = - .  / %' L28z,]{2322'&'0'&5>7276367467>7>0#""3.47672721672>7072'.#&'"&76767>7676"'47062&7>&&764#'&61>7>76#"&54676&"54>74"7627637>67>767>5465676762"##'&'"&#.'&'.'&54&56232767>7>76'&'&7465676216'.#&54'&'0&637672#'&'">3476'&76767>6764767243>7667>767676636>5>56'&7"7>727465>7''&7>7.'&'"#&23333636762#'&'&7&5.'4&4+1726367>5>7'"  00: "5 % 8#'9   :  P             /       $1-.*!            )*      & '(   ,)     &      v )                      3             !   # "  ,   V"   H   "   $ *DSw032##"50567"541>726>7632'"'&'&'1#"'"'"''&'&'&767063'0'"'&'&'&747'&'&'&747>7>7&5&7'&'&'&76767670321#"'&'0236767676767676&632676327>76'&7432#'&'&=67&'&'476767037256'4767654154327041>56321#"'&'767072727076?4767676567&'&543220&'&5476'41051"#"5>5676726321037676'4&5'"'##"541674'"#"541674741#"547>76723"#"54727&501.54367176023&'&'&'0'&"1"547025&'&2"5&767032#"5&7#"541676767676"027261612"16;2##"50747&'4&10#"5476767>722"5&5"1"'47676326760231&6760##"'0&5032764062767&%323:323674'"5<5&'04 O  %$           !<  #!   !            (       9  E    j L       35                   &    )*       !&*        %    ,)$        4             $P      & 57'337'3#'7'3/3xxZyyyy[[KK)(&qqj<?30'&4=3>qOTW.-O.*3%@PK2*;e )NAA )4L201"/&'&'0&15463'764&"3264'76=&5&'#'&"2?z " " Y;4ML4:4  CC 9 ;4 M' L4:5 !'%!5?#'!#3/3(E""=qp7333t)wI9: X,!'7!3/#?#'(8==7pq33369I-X :/7;CGS2+"&5#"=4;5#"=4;5#"=4;546;54;2";24#*2264&"7#2+"=43h#V -  wf     "  0%  F!>Wk|707'&767'"7'&76?>&'&'.>7'&76?&76'&/0?'&767>;6'&#"6767.7>'67'i,/ <= :< NAY      r (#  $ g,%$GB FZ-# <  %2 % * VH<1v"h  0  S                 Q  F(!0"+D @7Sr72367'".'&6527627'.'4>7#72'&7>'.766&7>74.3#'"&7672"726'.7676&7>76&"76'.5&'&7>7>2763'".'72>g):5<  4HKD  " (   Um  nw V U 0  W. 9    : %' !l762#.#"&747#"&7>54&7>7>'676'"&#&&'&67&'&76&7454>"#29232767567652418565454545&'&"&'&'3>3232>76&"676    "%  J   =     & * )   -  1HF'   *.f M3 %       #-#! .n;:Scs%60#&'&#"'&'&460327676'&'&'&5465;'&/7603276?!"&5463!2"3!2654&#'     %  b  ?  ?        DDDD  \?  ?  :Sc%#"'&0767654'&'&'&'&5676325260'&'&'&#"'&07676=+'!2#!"&546'  %     }  ?      GD  DD?& )732+"&?63+"&?6%32+"&76;2+"&76>n  nn  n-7 77 77 Q Q  Q Q  D  D(G023223123000121031010210323>?'"'"#&'4#&'0&1&'"'&'0&1&'0&#&'4"'&'0'&'4&1&'4'&'&'&'0#&'&1&'4&#&/&'&'&'4'&'.?00102103210301213>?'&'"'&'&#&'&#&'"'0'&'4"1&'4"'&'&'0&5&'&?2023>5'&'&'&'.7" @`@' 'A'i 2G/^53 [6I B3p6 ,-2"4}; "X$ "" &(9C7+34  .K& F7'267326737/+"/&'/&?&/&=4?67'&?dPhgOdKa9 (G2":8* 8$ >@ # 8E%<$2F$,"% ?? "7* : "=Uh%.1767&'67'6"1'&36%"6763!2&'&'.67#!"&5767''&'452>32"A6!"3)@1 M'6&7 E37&'7,`7m,%|b(.,+J6N, X4 Dr7'i'   H(6&77&`>d u!!'7'''78'p'n\h!\88tIyI.<O2632+&'&=3;265'4&"#54&"#546"&=#532'54&+'32;26&"'))&Ff;MGf&O8M O8O'o h  YY  YY'^Dp^EK4IC4I*43#264&"3762"&='#"/&>'3#762$!  _X# =/yM}$$ _Y$> /2U/HU;a6360'5'&'&'&'&"#&/=7676767>167?5"&'&'&/=7676) @  M L ?    '*  )zz|| 11 {,,zz(   }} }~ '{Iqz61&'5>7.&'&>7&'>767267>7>7>"001"00:2427014=45&4'*#0"3676"34>36"3673"3476'"7.   + !   *   I #W  )   +(   z   A / %     (%%        u &,289:>CIMNOUZ`dkqrsy   &+,245689;<BCDEFIJPQRSZ[\]^`ghioprtuvxyz{|6676&76&67>67&7>76&764&46767607207'667173#&&'&&''&5&'&5&&>5&&>76#&&76#"676&73&'&7'&7.'6.'>.5&'&230.5.76.54&46'767 4'433& 01670> #6'&66%54&'&'#07666'&6&7"6!'&3&1"'&4.#'&7"76'#'4523&1&776'7&'&776#&'&7#6''76'3&&'&72?'2&7#67'76#'763"+#4.'&'3"#';"&7'&'473"#"#33'&##32'&"'.'3#'&'&'&7##37              %         c  [![ >  (Z;) %; = 6  R  C   ( -q I Q)!  G pX   )\V   ! 6 T ' f#c  u# /l  .Yd:~ b>~N=6()  9LI5  aBI + % "       #*   (               )  -                     573!532654&'''7'77 o p?`2F7  " 6$+@$--F25`\K+G $,;?CW2#!"=/&'.7>7>2543"2"6#6&/&'&?'24"74#!"3!2%B&,\  8KK5 PG/  r *3Og!3L #/;GSco{732=4+"#2=4+"3732=4+"2=4+"374+";252=4+"3';2=4+"74&+";265";2=4#74+";25;2=4+";2=4+"34+";25+"&5463543232J5f+ q  q 8"W+   V2g}  =  /QO  Y K KF):K[7+"&=46;5&546254&+";2674&#""5462"'4&""54632"'4&""5462" uf  J J U<,   5  XX"&=>4.  "!(&" V Sk:'I.2W05 33M$y>11Bi`v$HZk*:IXfu$2DXgz00"#&'&'&'&7676761#676'&/&'&=0541767676225"'&'4123276760#&'230322230034'&'105&'&4'&'0'&'&'007676764#&'&"05456'&30076?&'&&2276?"276?'&'&00767614'&"007676?076760.'&'&02767>1&'&&76767076?'&'&'42767"0767>#&'"067676?5&'"&076?32'&01076761&'&2767<"103076?0'&'.'&07676307650327670'&7474061276?'&'"076703276760"76767607674'&'00276767#&'&'&07676?33076' C)@_ 1.#   G # ' 0D *  J ^    <  )J   %,    9.  <),  I    "k0 -    z   ;m 7!  F8 #    =6  9 +   d &    %&1   :/&#                                             &   '%4.1&'.'.'&'.'.'&'&'.'#.'&'&24367>7.'&'&'5'.7>7.'&'&'.'&'.67>3"#.'&'.'&'&'.'4>3601&'.'#56  %B             #   +<     4  ## M-     )  5+ %  B *     =,("     .   o|2233030.'&'.'.'&'.'5'.7>7.'&'&'.'&'.676"#&>'&!  +<     F+5     h    =,(!     C *   9 3&/#'&76%.&'>7>2.>XK<  ;DN#O9/+?M$ $$."+@6$9Z1q |    '    &;#'#37#"&4673+/?r 6V!CBhEC  Z ZEzu3">&2321762"&="&50546& ## #  D&-/+=?/3?#?/?'#'H9-.:H6+,,+7C,E ExxE E,D3!43!=j7"/&7676?6=4/&6=4;2#"/&=4?6#7"54;23254.'.54632+"'.#"# 6  *-+ 2W1%#'#M*%)!*' ^^^    _ _  ^=9  P%&!!#5#735335#335353#735335335#r9q89:W8VVrs;VqUUUUqC3!201"/&'&'0&1546"+32;5#"&=4'&'>=46;5"3267'#"'&'.5467>325#.#"326732654&'&'&2"&54!*/ " "0    !           !      CC 9 e       #   "   K +    D  ."):A77&7%3&'7'7&'&767>7&'%7'&'767677&'ZU% Z Z'=BqZ >U Y# sXrC<'Y> VZ!b   !6 O?/0'!#2!!D,2!0,&k 32+"&46";264&#C__CC__G+>>++>>+j____9=W==W=!!";264&#32+"&465676'&'4'&54'&76'&&'.5&7&67.'&767>767637>7676767676126"676767476'4&'@                 )       -    !    '   &                           (  @         |-7?.'&'4>?6'.'76o D  v* ;*35! |! H   \ !<O* eH#7   =f%#"'&'&'&'&5476366'&'&'&'&'&#"27676%6"#"'&'&'&4767632#&'&'&'&>764'.'&'.5476320#"'&'&'&7632'&?15'&'#5&7236#"'&75767'#*#"'&'&5&=7672323'"2764'&+52#"'&= %   @C   AB<    "  ' Z      -  T3  9, # &K@U-  ^Z" L   RaFF  6 LF; Rk,/ " Gs & FN"7>'6352'.7&#".7>7>7632>=2264&"1# 1>T1" 1>U#'5)0(5(" %   *   B//B.;) -4;) -%%*6% %6 $.B//BEl!+3BLT6.'.7667>'&73'#73>74+733#76+73'#73>74+kQ,9 7jm) BE9e'"4dh*C1   4     :1  k&J+U" P U&<  :Gl0  HP201"/&'&'0&1546;373654'#7323732376&'#7#373654'#732z " " *"4 -   C "4   CC 9 @+  @5: ?*  N2323"#"+"#"'&'&'.'4&=47>?467>7>37k'.\,"7~ d736766&62323#""+"#"'&'&'.'4&=47>?467>7>?  &.,76s   @# }  642"&462422"&464&"nq .!!.oԖԖTxxxxoo .!!. ooԖxxxx D3Kgz676'+'&'&'4''&'&'#'&'&'&'&76766203676"#01#&#"&'"#3676731767767676732767654'&'&'67656'&2.'6'4&74654545&'&'&'60&#01'&'&'&'&745'14'&'&0#67>&'&'&#"67676760#"'&'&'&'&7672233&'&'&'&'&762"&765&<14167676"#67>'4'&"#7>54&'&'"7#"'"'4767266'&'0'6767>76_K+  "   &&    /).#"$$ )          !) 'G@$&  "LA)#   ,    h#  $       &    2 :7  @'  /#-%=   8%,",     1 ;B / *<8 1   ,-   !'     ! % )"+$2e     # $   [{,FYr676#''&'&'&'&5'&'&'&6767670701&'#'&'&'&'&76763632527676"#0367654'&&676326126767671&76763&'&'4'&'&'&76&'&'&#"#36767&7654&505&767&&6'4&5465&"13674>'&'&2#'.7676323&'&'&5763672"332767'&#"2767>'&'&]K*  ,#        + '%%%  ' &%      0   &,%    $  !     '2  E> 5     .%.&;  :@, *"  1.   45&.#, '"# j     &$  (0Ya2#"#".4>;5#'4>"264".57546?32>7=3:+36264&" - g $ g +  )- g $  h +   c 5 /@. 3 "  q b 5 .@. 3 "  ]g1;GYfu262636736>2>6626+"'&'.'&'.6?467&'&'&7&7&7&7&663666&'&'&*#'''#7&'>&'&'&'&7&7&5&'677&'>7&'67"'67&'67'67'467567"#'7&&7&7&"2656&"76'&"56&"676&'&'&&67454&'&7>&'&"#76'&0#"0265414&#676&'&"76&'&#""326'4&^                             0:4#      "5:1      LV *&  '* )    q" *    *n (  &  v"!!         ,  ,                l   ! %!"$  ,5 4! "-* #  # " @'  '" % S  @IX`h6462"$#"'&'#"'.767&'&'&546767&'&7>67676'656&'&&'&'76763276767&'&'&#"67&'7&'6':3&'6767&'.&'6&'&'6767#"#67&'7>74'&'&'67>**4:2  +9%$  /5/  .I'%!$.  -)$             -   $)#:  (+.*  *# 9"9#+!5  l*   ,0++N521%' 1!  E1 / 0#&$ K4 {"19 s Z@> "&#$($" .4A "8#%&"(W2&#"0'67>767647467&#"&#"'"&#&#.'&'.#'&'&#"+&54%""&'&#&'&7&676&'&7676'.'&'&#&7>76742'&/.762726327"5056723232#"'"436'&"Д)             "    5  $   I# ",mLO "!     K     iJ?        ')i_ &,   M 1!  8    /  7S]`hm%'.'&=67>767'.'&=67>767'.'&=67>76'/7'77&"&4627'             3& $" "w$3%5%%5FFF2 I  I 4 I I [3 I  I 4 I  I ^2 I  I 4 I I #  R4X   #&.753 %%'7777'7?'?'77'7'''?r"v1w>{jQ'.=gf8OZGf`M#p,j_U"PM#  yhz *o3$n#b-<$"Nj5?Z6 "&*.26:>BF`x7&767676.''7'77'7?7'/7'''3'!3#%#5454&467676"32767>&5A )1W:  D !8 !<U7 "    "0 .+-+c*"6?M\  >5#RU"g  =;  ( & )*  9 . tu!R((    '(R!}g0 B "&,D[_cgkosw{732##'#7357032#5##543573#?3#7#32+53254#"&543%&767676.''7'77'7?7'/7'''3'!3#%#5454&467676"32767>&5+ C(*1r!    )1V9!C !8  ;U7 "    "+ ,++*m)<  7 6 5HG6I  5@M[  >4#RU!f  =;  ( % ))  9P1 z{"W*+    **W"`1S !LU^kv267>>3>7676762##&.#'&'&'&'.&'.#.'&&'&56&76&&74>4&&&7>'.'4>76.'&7676'&67>'.72>&'.67>7.762364&5&26&5&66'&667>7>&76'&366'&626&'.'>7&'.'."76'&&76'&35376'&'&"626&#&726227>767&#'.'&"76'&3&76'&                                &=+/  &   ,    b    5T6 9  w   $R% 3"  4J                                      +  0) ' !  !- n     %"w#V-  {  r%'"676'&'&'&7&''&7>76'&''&7&'&7&'&7>7.'&7>&'&63237>76'&>76726>7>7667476616'767674&766576'   (           4<Q^&%IK  -8gS/        <%t+ iPHy    $ +  .    )0&# # 5+,   2, - $  a9  *.67632?#"'&?62#"'&'&54>Y^SQq qH|   5  =P^ 3 G #527$#567$#567$G " ! "i!iri!hqi!hG 2@Uc#527$#567$#5>'23'./%636"+74'&6'./%276"#74'&F !  @7E(#|i38#}h38i!iri!hqi!h _' # :W `( $ ;X #(,3!!%!3733#3##7##7#537#537#373#53#'5N"% (" " ( !)* `)))O'; 999!- 9999 -!9Z--[+d66d">BF3!201"/&'&'0&1546375#3#33733735#735#7##73#35!*/ " "&2* 1)!'!1* 0)!'' '5  CC 9 eO99O4%4444%444R%!11 !!%!73#'57'511NRRO';|((+! +6+!%%''?''77''S.TS5SSS5SSSSSS4SSS5SSSYE3Qp%7>/./3.?&6?>=6&/07/"&"1">17>?>='./<14&/./632/./.'#367./"&/./&0367'.#76173567"/d 4 h        `       V    U  $    !      5 1  $2 '&'&'&760*.'.#3'&6>767>74>7>27>76'&26'.'&'.&'&656'&'&'&54656'&'&7>&'&'&67>7>&'.546>'&'.'.676%&767676#'&'&'&72&'&'&761&'&'&761   /    !     #          ', 2     4  :         M          Y   9  @ .              n                K  !". 62>."". >2 2>7>1 >2>.'&'.4"%2>763>7>0">233          !      $                      9             M                        @Cz25'&/762'&&'.'&#"1/&'&#"27676&'&76&'&676'.'&54767676"7>'&2'&'&76"76& *2'   # $ ' !!    5) N00        C            *! 8'#        @Gc+51463&462462622#4&"#7264&"564&"'4&"3#&"'#373"&=46234&"26= 32"&5#264&#"&46234&"*+<+<+<++!! P9 9   9 9Q !&-&.A&&6&&6&{& &&6&& &&6&9+<+**+<+ 9 9Q !! Q9 9 [&  \  &&\&&A& &&6& %%y"&462.'&'.7467676&'&676/&76"54765."7676?3654&#'7>367265.ȍ"     /       & "     &ǍǍY *       *32   &&L+       & !%)-F[^nrx7'7'7'777/3''?73#53#3#53#'#536'&#"'632363"31''7'76733&737#7&'&76>703"':10"54'&&'>#&'6#&'#546767&'&7672676&'&7&>7632473&'&'72%67&'3&,!  /U 44444444T .  !:/:0i +/0 3-@B/"+4' 6p  +)9-# ) .1d1.b  AS+OA W +# - 0 ]  %C#   ,      ![?d>~. ( %C@<[ W D!$#! :  8" 0+UE % <$b!20001"&50405414105414105423276010101+"5.#"/&5241&525&5241&6762'&'47&147&147&14767&'&507&507&507&56720000001"&5040404040404142111'&#"#"'.701&7&701&?63267432011100"&505< 54x**8*,MD,),M)&&*44++3(0,),D#++ ) 2B+: A*A"22"A(>>(n$G3$@ :* %7'#37/#'73?EY++qp6==r(g(r456547 08"'&""&47>"&4762"'&""'&""&4762"&462 TT  CC p JJ  88 N  .. _4%%4% SS  C//C V KK  77M   // %4%%4!)7;2'&4>235#6'&65#>.'.'iQ/&F  8M v~3  w KIUS 1P2k  > DPiK&    3B- 7373#.B-B &J       L q   Created by vagrant,,, with FontForge 2.0 (http://fontforge.sf.net)Created by vagrant,,, with FontForge 2.0 (http://fontforge.sf.net)font-mfizzfont-mfizzmfizzmfizzFontForge 2.0 : font-mfizz : 13-2-2017FontForge 2.0 : font-mfizz : 13-2-2017font-mfizzfont-mfizzVersion 001.000 Version 001.000 font-mfizzfont-mfizz      !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~uniF100uniF101uniF102uniF103uniF104uniF105uniF106uniF107uniF108uniF109uniF10AuniF10BuniF10CuniF10DuniF10EuniF10FuniF110uniF111uniF112uniF113uniF114uniF115uniF116uniF117uniF118uniF119uniF11AuniF11BuniF11CuniF11DuniF11EuniF11FuniF120uniF121uniF122uniF123uniF124uniF125uniF126uniF127uniF128uniF129uniF12AuniF12BuniF12CuniF12DuniF12EuniF12FuniF130uniF131uniF132uniF133uniF134uniF135uniF136uniF137uniF138uniF139uniF13AuniF13BuniF13CuniF13DuniF13EuniF13FuniF140uniF141uniF142uniF143uniF144uniF145uniF146uniF147uniF148uniF149uniF14AuniF14BuniF14CuniF14DuniF14EuniF14FuniF150uniF151uniF152uniF153uniF154uniF155uniF156uniF157uniF158uniF159uniF15AuniF15BuniF15CuniF15DuniF15EuniF15FuniF160uniF161uniF162uniF163uniF164uniF165uniF166uniF167uniF168uniF169uniF16AuniF16BuniF16CuniF16DuniF16EuniF16FuniF170uniF171uniF172uniF173uniF174uniF175uniF176uniF177uniF178uniF179uniF17AuniF17BuniF17CuniF17DuniF17EuniF17FuniF180uniF181uniF182uniF183uniF184uniF185uniF186uniF187uniF188=QQgoodvibes-v0.4.2/docs/goodvibes.readthedocs.io/_static/css/font-mfizz/font-mfizz.woff000066400000000000000000001177101337147241200310110ustar00rootroot00000000000000wOFF `FFTMucrOS/2J`Q'^cmap`JJ|cvt Dgaspglyfhead006vUhhea` $hmtxs$loca.TfDmaxp RbnamedTD%HpostExc`d``ʻ_m2p31a `ixc`d``<3"(sxc`d``e6dhf& fd90"xc`aч1Jedha``b`cfFHsMa8 zyAj(00 xc "x,F U@l`TA ||t>\  L @r =p9w$Sf4" jxc```f`Fp| >v+|dc~z `dcs*`X1[ nD****6fR  x  |ZDhPN2X`&x,.>6 0""z%((j)***+,/^048L8t899H9p9:<==>8>@B@ALAnAB(BCGGHhI@IIMNOOPPDPQQTRRRRSTTV\VWRWX6XX[t]^`@abccdeh6iViijnjk kFkvln4o|phqqs"tpttuTunxڴ mWU&kι>g>͹&In:HrwD ф6P D!AEa6*BPQQ)J{>|U~*UrƘk{o}4k511ckT 5M{QcԶ?Nݧm)FV8`OIAa)pa^"rGC2 M{F~L5CjG=2v"AauchoޡK{R'OjOk~[ڟhOHi<|. :Dh:NcL|2,0-RBMd1GI'e&bIQtY|ǟ!t"῟;r2i2?ItR4bAΒE2bȅ 뎒b3{|!?}R>)5F\§ ѳ6 #IL{|~!DHjׅԜ w!$Dc|3垭C,|ԮK>~RBnWyl^yG٤({G%?{[{*6#fxBy`nנ2LP8)ۅ0vrk$LprيRT^9}5SK\.q6l8 9yG}4NɥKU~ERo^]?O;xgͿpۯbg? Ҵ|{;qg\}ҟm:I֐Hl>i40|puPңs8>8uY6?jZeZMkj Uګр㸘eC 3|'Ϧ^V|.FYw'$EhmOa<>cX}&ҲpeR:KK;:ϖ2js6mXwxh7 Vm]tE*kt.Sӽ8FYaN86v;皶em^4VκV7[pX8C[['Nlƞ O3Ҫ>_yQc'迦?I~S/O_L~> 3l;JGCdA6y S`' M0I @`&B/,wӢ36Q:  f$?DZͳ<%ºÃT #vN&xM!( "憊%aGv A\,EG ;V@jLD]aR0Av!|v0}`C@$i'Vlp6Mcݣ=2w4« d8Md8L|2M)$O2G!M&a2!8dA,n5P67X(g6ZB f5SY hT[l<!hAbc#|?A#L\`1ͰjPft^,ūe0χPU g@r&L}""LɟF]0vzz!f1^P~GF :͉eH|bzm%aUY5&!D(\d)tA%nRP<#P"6Q51+1In;hQRET6F Zd RQueCtĂtܤ@0;a2v5,-$k972u2.UFC`6![t- j2Ʊ!c U {]lQUʠ= 7`kÂK'[jbk)Vxebv@Dn:b.<`'Ju/ʅqty &N UMZ=( uwAz W˼bZd+n3_5UD=` 0g!3ڥ~4 S+5-r +]S-ĤzhSh&Ya\&.]GQ\#T\`S("L @5bEy{*" e*\ ?wZjhTOLY}[ut(6-"-;-`Zgd -ZfTvIhX&η 9*|r[L\.ﲅ**9TCb`S'V@X6xp̍!,s"T$ylt]A{Kla -"DCqu|E$|5Qj8ëIBmCAzK|\av"`B7YEO|J};*矽2X ] V]p^.aX'Lch͙]{T0=usqLEmKKvRE^EʉLi)*J͵F-d穜 / ]{{޷~ޯRel<9x fX ^@(' XNkZQE_tzQqR< $Q(fb8P\ o22͍Ѩdh *t*QJT>cVZF]O,w/O6?ԃVURx5+59٬|z(mܜxywڼ9 ҃K>\Fwtg],L2w藤WzIFBJ@0~ϴy`a1Wx缻3ǏEOqMpMՌt` ԫ^LASfJE!~q02x6 l*Aaêz@2@FE#)O:ƞwqo0;U^Q}XN;U)`: 0>6(Eo6. SɨQ ٫$uG?&*Uloɋ&[)' )Fٕ5 !xa`eBZ5Crݍ\ߺv0vh*tN`L<*E 0%Hj ŁRtn mFԃKXtEN%A`֠Up`SdX[5 Kܐ+M+4(g¥ u+5!Ycj'Ɔa0L)'y^m[& ۝r'^VDpk-ϝŇ8@WSx?aA'!!QE60$đ\TXEZj-DV Tcc{@5oQ_S`.7!"pl)aVQE0ءIH3RӱL;pc6PȲ+|MðlI-R߀yȡE-lcNLQu+/r7[4'%A7!e=A!hs:' )SO7[PBccTk#u医V) )j}ҁlXae?pt'>PiTւ& ]j[Al14iKQ1KK f0w}sZm H~y^0Dk $l|8JTL~wJ >ӢS$GI.1aKLڟ%>c|q; w ʱC>/iQ \s@1,vtG޻imufl7=ܮ=qѶ3q%+ןϘgsrcwu7o>_c[?]7m3c}nnn:yfW"7kk:;Ùڙ3fG!;ztÈ;NYX6=]߾ykhv*n׷oww6oq ,?]݂y݋=fR zrAD^mpG-ښzζF灝vn791/_4#Lolkx3KܐGTz*6P$KGOַMs۫4& zXw&l\e,k27물 3-\ N6bD]vSz`G^ f[gY3#SG'蕧iwCFǖZ%@~+_{?gkE Sh4C{de{A: J'E*Egf:Dɠv]ܾ `>y=#NGdtedi>Gi_R0?G#%}n]]6i,vh/>o|gg{W=_xO1/&ڣw``)h,jN7u@Ixxq` !U-XnD{_=W1U3:MP6ҿTSZhț$-wwVll0[d:N(aL[Jqrժ7Z-xtKݮOLᏼ{^uի^[u<6;f3ӌoR1PGu@?2F& (@o$d*o l2׻-3R@xWĮeE!ܴS)&}*xUlS 'YYv֧åjRoQfKK?|i/e޶G%-Ԓ7Fm޶Mݡicq|HRpƘ P^ V$ Mwvk~%N{Jo;3o<ʚ(|: (LnԀc)N z=@0= h|:ȹ3x;SUby1-ғo K:4z͋ 7 l#b3sЬ7jn 䳑CINrKrT8BiW8FUU38xJ[q,3pu8)y_.3qNzKwL<ʿI~՟--ݲGݵ?T{+.(9[䙏exM2Co及c{x=Dew Y:tP-K&#~iBTa8nڋtÐB>Fy:dLQ2 0P(vs6SJ{||=/@DM~s %om_ݰu Bv[tpϸ*V#Z-w'/(iyYj+VYՕe8~Bv)]Rv0@OOah+Frq*Jz!>!,=޷{ۿA RL`obu=h)MYEG%}nޭ]h D!&ovKjH )MߡބDCĶct 閎@Waɯj.  ӛYGҹiѿk^y͗to׸/eW]2+/DnD77;?}/譬V=0v״vLHTl0L<AQHwN?J@D{]ݷ/x >g<럽p%Y3zs67hI&iNj,ϳ n VtxYд|GgmOwqFO6Ety[4=vá;}ΉW`uЩnԗ(<=hTNg|"v%I:-{hգ0R=[xHٝd:m@͑>yHiڶ6feڕ53ڭڝ igikRcڇ~V{JESkP#O?Bi  *LR#-` ꜂D3G;뙡 v#aAJ?OĻ*]Ok\gQ)FH;q ȯ4WJLɐ("؃u׀"b$ͼEA3rC΢k '[Z?WRcZٶEWݱW[Xbvy{$#SZ9R+A{zuܺs^& & o@5P,pgEEH6[YOi5'Ԛal'WlX#/֮sϩhǷ@Jкqw<ȐnL`7ͫy׿W$gy|Yo~_' Cܩ3HMD|4Iߣ->]X(CӫaW bk^MI4iu=o&$nɪn ThRmB_ߊz̮g˓aJA/3!> ohO:mTs]`cLY*"j#N)GΔ @xĎ+]=DfG?CKFY-vYO&*EV[2F7 6^׽4}ЮpGGo., mF7;0up#ymNH[a!÷uO'jn{o;o~QfGe#_tiRtn5)x]h@7.p랛 KVޱ[&3QCSfrbz-oT!4Τtb-!]Ym]ݤJ9%\*g hQfʰ{@!ׂ S nUڡՈQٴ}ZlݸV\=d8N0qB o咽G낼nZj`4+[c޷޼䯞ci Xkڦ2Xk@N~ ~} T{Hhߧ=A{diJ@;~J'_>oEڿӾB*gc9L}9|0 eṘb Ӭ?O84Ox>o@Rb8Um"y2&][Iw >n>$ځ ;Kg+fy|?Q PZ UR`CR,Ogyڸ"ȏj;r- DFi擃1U]'HO>T~pwֹ^_F>^r_גHu7ٳiɻu%! .=\1!%2!Q/E;VW.1 q,'e6ws; &M^K>ZЎvDmˁhG-_4œB@!gcȧFY&!ʣ29>ڼm*DmЕ={i(!R ʕ~:NP9飥m?0>TPB2c (ǁRjpb]+{jThǍݾ`%JJ[GR(WT/; yT΄8~xww`+ 4Ds`|Tq9 ҅ ,$pK{x^!|!Sبl>.`!h@l W)k rc7$qW LXپ63y#D3N>̨Ey("3MIj HH$PQ ~ךUJvcEyA`Q{qui4:e$8PG@T kųCf?*Z%RA,N>Ac|2ZVJSe *)R| Ds6G4Ayֹfcƃi>6QC.hp8C!Q^m5Nx\'4J٤[Me5%a$0,lԌT<XSoQ) ,E#׉Y $ $P+;H@᝸5 u- X\XKۧ-)ȿRv$d|C-3i@e t-hA,P9? 6|g+!ctkjYǴIGZATؔ NaL3` n*/fO 5 ZB*@Eyz)0LտvH_Fs8FI0/c.5 &.IX:CR[DhzbiK M %>SN4O19Vrl:W21瀶a+80 T*,y!VQ2ޫ0=*JNY4}<0H*'H|U4md4M!MÐxh?1)9z?1NςQ3I4J\$寂 4j OJ )|yX#rQ9=VAOT+D:`B<-Y2)H?p3AAJ&q䀱܊qO\R1JsN2\mj\v\i>x#=*TցCsͥͩcѵ}["ZHK`)Rvȝf'NOW]4˺A۰lh[>r=H#֘GoKg !h~Qu4098jt4;8GGcT(D:W{f կYL@'R38i Vd a_뜫A̐˥xy4?:& 3psa sSTp_ԄY qMi˶  a&qs,/  w++G[)Tg(+&Y I_K:Dh4,ݼو0=)PHÙZ6ԉr@@Ǖ4LغٶEneHRqq>R:Hp_0mU«f;<53xE$Pxgȷ`>ZU8"C7/:E}v nqɓguܾO嬷#]r'^{{SG;8z(.67S$*ӡt~ wMN(s OǟrS,T w0٢p^5uu7]ۭOqGh~'؝Բxp_Bؒ)=٢ :rdC=l.Qq_q` "*C/2 Nr<GX(ObM>Kj"j[0M\m[Q]K]*y:]N<e9I)o QPk!㭔V8̣ ǎ:d.(:HX81H駣KOFf#TVÜS;=+ z(g )9 -󓰳A 9JqTm-MubQRy0z x,\!˔mAk0]Cr D[O33_M} =*:h-#`g K3"Ӣ++I*)MIЊ`T$k²(1Y30*4pr.a! zajzs*mɛ b se `h1yx"ѝ#׉2&WL=IhnXr-90OXN0j>gX=QdiWS*0Ta_$)J!t+j@Ӣ3Dz-СORd)E^&hFC{pw3\glRcT72Q9!鄢E齷8tݴT@$3SJ3%USx=BUpi uQ[tke&i>#Pw"E)$:L[rf4tz,7`Z:O7][ ;nf :0<@;kӓt۶fhWiho&?Hn;k1L9oF([paE`1=St(9WG)#h8⣦]?dN<8s˔,$  Z GFxL'e9ٸ8:@9A)yKFMY,xt6%ңXh*㬅x 5n3fH)2pui[ta3e+&cԀ0A%[s%+jX+O|z=2/=#/7R(r[nv FPh8 jBܽ)$&QʳRBj$ɂS}r"䞃A}08y@ŏ;S95hay Q(.6J'ѝH+ua^Vm*lC7,zRƗ>9iZS y*- ܙ$1X`F x1cQ,LTwmPP$֫ŨiCZ"C < rsb-xAfXz"e瀨T&,@5#}џꎼ60A$֗+>-*C 'SE6zDFȜkDnM:@z.@mO 8È*>.reuL\t> wy&Ь;+@? Ӹ=?A,Ux6"!9_ Ax-]wK#6`8[MlRΤYF7}LBC((]CkI`ʅ7lt}Ǻ{:t+r-cq|"BEeIC[!jKU/ߣ_Bi,f<bbQ3Av3JgSHkǣQqf&! ;fP6B'5E?J$K뎅Kd.ݕ,pBt~G*6En91t%g \+՗ |25qJ4xu._*;ΫCԾ\VPƘ*zo ( d[ E] LPLgtl1A2L;d؛lG[EwZkR$c2oI;|OjHt4`mp:T)w\C3M1Qj >j6ϏO'-Wwj39:8ezsj~?ř1r:M2vj {k,n;Sz^&vθ(`JU'A TUdhwi"-3qmf0=RkMu; =FtL \S>H< 0ڌ"u$e*@>SG)F3x$s -egYˁb3 ZFK[hOܠ[M y[2h`1A/jXV+1{ UPC"%O5ne "?;sƣj[M##*:Eia ,4BOQ2m|dĒkYZ`;85e0 ͵БۥZjd/\c4Ht&qt%[qnq B^e\`\y\/bN6-t*ްHfjԨ`<  k:tM;Z_Bί͝ 3t3+b2foS)aP0h bULH:zy֐ʡp+[<"mCy`| qLpsm,4P=MMnDj)å:,M&ތq(g@/U*j?q ``$ 8StK1MW=5@Nr頀dNaRZ(שl V2QAxf[(>ХE՞5W ¦8c~i:6~|a~L2yLБ2 ;ďeƍzv*py13RO맞JKs_%;lb[FOiOER>֧7q1̓R~d>1әf.M+ۃ^nuo?h!mjMj0rZsu]fQm8*_̋2m1` yλ\kPtVMJi}Fϩ>n%Ŵ?636oGؐh86!?vIIdl>rL:^d4Bq@(]Iti[]~1i-]uoq9o-]qNvoYo=Ȕbs+2ݦ0pԫ-7}k|c[/eQ>%M @/l%o,Gr MbE'7w=V&-g7ˆȅEb6<ɋJi*+IԬ5^mwFx3Iihc3o1~24(i >L'@ۖ:U? s8~>in:/]W tIIEZԙY fC{M sj)*-Ϣce17AG Cta{)rĬmF@`+e~)q㞀߃1h*W}@!6o h˜CoЮ Hnblb2[gMόd ׹¸.9FDSy-?eC[T* _Ў"szPtҪQS`nq0)#qNs^ϖYˬYWXR#SD8 |L- cX$LQWyj-"3fCoRYUkUV~QUp4'0A v,9o@rFC/_i2A@ᙌ%LAPi aw&^>Am ք9='ea1\ZPzQ38ӑdo~"m-֑"P2;6XtpTh 3b|@v9Z:N[A2Q;A`&ibx\7'%^{/Ԥ)NѾݨ7C{bfk52t0kQ~`ޯ gz4k9/PEs583ggέ[Y)|XAc63ZqrfwҘ݀{^OOwq(=#Y7YZYq|~ o>8|>lS]^w= Ćq̸0H7s=!3J1}'SIC.'g9W\cFגϽBb& тKPմ#XJ]f)VvK_h{"U ?j̲j+~%%wEyh+ϪPGƜѴMl,Ly&L@,8V_28t)\k_bS%NVXh.5׸W;$~Vz+i4R5 8*GT<;MLR4y$}Rpڂ7L}zC0|8P~zRַZLUԸ xLSن:B~qxxx=wFe:\A4)'1dhZ!3e0$|XGH=ޞmn_ U Hi+G]:/Ng^&xz=D-_!, l=/\݊1 l;~=,?j pCKxgyJ;v$sy5UILHsKg46ȵs\oCq+)s]uw82. 1YNuVB0TMLh2EÆl8gX^V/G42x^)uBǎ؁I|a+P(b6/a?7 $"UR#u!NaC]r9C!גYrM ɋ+kțAMK>H~;QhaOOϐ_#N~./? J|'W!1zRNiFkM;t@:ݦc:SzУ^FOiz>>^Oo7.z/O_D///WGԚD_dtCB'Zh2H2|</)7ggaߜ2COb0*/gBV>yRO[%t(V?fStS`vif8% @R -@3:t6\e n3G\dt[.(؟S9]l2.:ˆE}PH4dPp fx]]Q-7,z V}V@f c`Vf 5M'?56y1S Q1hr_} 4h>fbءS][# WexGIO"LkyL1p64+x\\@ޫ!tԗxڊ|i;Bx$N i+Ű.[BD߁/F\Ʌۊ<JRhL`(&(h+Qsfy$`{F$&f3Dp7vaz0!'ˋ?v݀nP`ba▀HWׇ'c'O~]{ JAI PtIؒ ޲w]Ta:Gkl=^-qq05$Z @"1ƪ D@8Lwp 5,NkcaR1*]8 a902ڈ?9WtӪHXE3 Y*8 r^F?&z& +^xƀK+&j|I9fT ٗ#QK |XB\Vh&TB !LKb9Ƣ4)0,q .)H$@*mXp0v0Dm,|{DTTSXeȶwq@\.CFpd3@Oca(-KϞ3'Kҁ0AGa<&RcZSҼwNru'sU bhy ΋O}ӭ ̀LK:,wi_~} g%^"H˳% gKcg;pW|h熝xً_b7p?xp%[O";{eSbYNmZVۭXV{}m!6~/m!}h) 6VYw Q:VV˻:lȹn|b͛YzmK uq1k{sƗd8Cn!ny=aÀ)7} }7#0|\&mڭ{R{Z6'y&BYģm'{xF G9ы]rСr*e_Kzx+zIOlt>"l,u~+_6Lx7ib%ݒ~9D6q9^ɹ _qsiccGxF& R^gŀZ{@75Bts_/e6b mpq/5n-KE0[މO617RM84̑o~:|2i_L":[EQM#oWd.!CagHEts)SB!-N0k{!=Jg!qj)L;k jVsvU<'pBEӚ3Hbk`/P4ږOkDgV Q!KrbᲠ;Nُ#%`xI)ʺ qpa8Yq@2#,Q%DTL4J@PnR% kkRC9|nQ\G)##`G#1>$L c:0К4>፮ ^uwkm _*kb Ε[Ԍ]H<"4s`jb~bǕ8bAja`0xmG=T ! ZZyxsV"_ CSW$џ B.ؕL*L7rbUYm³M?,~R{NpM€5}VioQ]"=,or.ǚD ۨi̿(͒H3V.(frJYlaV]ePsYVC2eS6lUb?=_{ɧ I18h.oh 6LQ`$.p.󖫎 ̞GOk9v%KV孓ӥ۶{xzZg=Эp]s#aJA?c@~KsQ&a_YE(k(Grݝ.}C^>ty2wFUj!o?-u? o,Ui:`Ooj?rr?WB?[)T.qƁmCIqh bq+؝kUj?: P>]d4se\^/0@^N5FtM>n3 ֥|y3y8n<iOs"i~ |(%E牎kG!X;1k֘x{^jBA?c5UF{833^-l-Gwem38ՖI_5f^]MWCfv`+B=rOpJͮӉ}ei'OkfԄk+U;`8Fnn+SdZ&5HElgԖg][nv\WS[;CO;GZ7ϒ㦛ƋGoz,{h>}Ԁ97OE@gQnDKN97RW%'"l!nXefn[/$y&|c@fu]_Z'cU<iз27lHf̳Bh\fWI/;HQ`ض?Ա'J LNSO]v睗kk7;_'82_c P-ZGM.:3D7~j8 k2M/ Ƴkgg7#AǮ{N` |0җRR^&g@{䏠5)tEL>>N~j?.MP< 7'c ,B, Lp:(Vjj$,b@ sOSR'z2or,Y%[ Pm}Q^Yڋ[S̬rmr2hR"!L`V/u~F&Fؚ|l 0zQ:[@2 8Dl)Y4`cXfЈWe? KW30lpQ3=Xfs$<" vJLd&8w9=q,bRf` 7j5hb^(p*cvRjcF*Db5-QuB'"L)mX̔)EE3Xc[p.9˔P. zpƑ8.K˴\<0rJ*B8h,<,ەGِo2ɤLff= m F-nRh:S0bˊFfC  ˧riY/ W=qtuFw;nUW9tJJ&uȹQySl'gUkVjJ` )LMMveU3xԼA؃Ǎ{`|WD=Ǡ߻MtO /".B{疧is4ϊd+Nմt@dۅD}Jh'NCmaXh4:`v[h=-}%v1ʆZ#E2%5AP2+^kx芁j𓗹tL-P\r7 FAmT0Iyb=ŌD͵3MPRh,Q9)IW30L 򧩭ۨ=QQ7fsځ|D$؁kr4B넹&t0cFIen7${એg% Wq[gDJ=\ E:GDqw3~`dX8}Ʊ2?9ǯ6#(BO^> sc7<$Ճgw';=>+0F/F eKsh.kEj[NSMGjA0ˤ4cŶwq]ɿz_u:JcY[U^>,'+&z!޲]̍s-O(tHŹSܿСK7jT+>Xi/,~X"鲠2UmT60!q9 5w^j\me[}3Vz/ ^E~L+JŢ$ڠrX=e}K ZVSx(tQR6/hحoaQP:iDTd34_=2.FqE;wgx˿՝?zыQ{>r86=+pSO&1Ƚ6䗠"ټ"5^DB>.D_\$0-Pels?X7hA چ_`)im y`@f.s|>3>] D Մ{QIiN gQ\1|qpk\1Փ-snf tb!m=%kER㨴@DY15.ʱD }m"OW%qfгf+%u<XΫ\b=ǏS0ju0T"me9X 0K|0Y~heޏcFMEqeQz^3q~ .0$ # #ʭ]D{x%AT"SYR_EW_+ީ1Ĵ54$fh1o{C<8Ɵ4# U8W{VQuy2˚#LM4`*>[!@o5p@yBd`j8 6Jw/86V ՠa%Y\3[+`!jxpJY(nb&RPQjhjyZikҲӪ t 9ppm!i$t}6MX緿  NN[Ø{1Jt qᷩ.Lq31+wZ{]R鬏26u2cφ/rU_-UaYZϻҽjdS /Ԥnc4Vn%X!#\Q{2",EDJ` qJ+<.jJGRVpmpX^ 5ZC(,!6 Rݥ 'xVV`|ȟ6*nZM:Zz\*@G8;qEQRrc:j k>IB=_Ch ՝xXE#kZvj[CObV5B.߂.jz(b̶6Zuݵ]~ W{&X[;Д&{%/=WjmҬRO pM2A܆V"OeIbT Yw|m3^u_2&˱s&%l8Zs 0mFWfL:̘E,f^STIjדMd6~ |j`)5'y9C8K{V9Q'`=@ g+AS|Fְ҈ xUڸ̜(jK Α^Dۍ;zxbe+Wo0 :'8!@ڙ~%[Q?A2:p^ŵ[QmouFؽ _u]VG/*YU`}mOjKjb/ VҲC#8y^O]o&i\y&gRI;TPm7&'] ue\֕.Ǯؑ{XW늟+^3ig1ᏼwW$ zs﮷"UϷ|r+TH\MΒ;#㥟| xZjcOkŻ۷uB.<.Bh#'Oؘ41RP Ơ Oa튡R$Z`2Ֆrcw!-b~*BMKuR+$݆Q$`8dV>s[  XZjO/v1*Qð ⇼Eo 4dI@SE1ݞ%H>-Bgl2Da rslDϲlLU @۔fuƙ F#,R5wC3xp k4P$ 1gk?sɩ8j h#\8234-LU RL+i[e?08+`iF`D&TL}0Ǫl+MIn%`.R/sHaDp$q{ls jil[1N1v2Ob@MlCde,NYil e16\sR# Hxf0( fAA`H\RA! E+d=5',/wm| Բ`$((kHgoP%lq\FBd81NóDL0=B Sk*"nwAi_NAUkVUld>\J)tࢨA=͙#hU9O(PyR]Hlf(Hx6՝VJtXeT[2:־Et*ʎTlx f5yĵN -1x{SE"LUN+L+-q 77|_0A{֒C $F8s(ۄfmQ]-C`e+ݘG6׈#]ԶiäNFe@jy;h;j)Qb'1YZ\IàewwWlèV&k t+9p!_[4hɇ} =dJ<&8$4)ڧD_[9;1TEH[΂[ WcE=[*ʥK }5Tz9J& ZגsIaC}ܷ'/Ϝw 1k FCd꽀tIhf" h9M}<9{L3*q9Z|>qdaE_u(#v#)ӍK\;%U*uKLj_%(\Ӓe>*-#՟d_~N0:|yڧn ::tWնqZYYN|t!9tv}ss}Wgͱڿђ$ $F:צx 9|4D ۫֊Zu6W\·=Qru渵ÿ׿? $$6t+\倖Gƃg*  Bdt`zbl (:jgN7mJʃ]Cp!蘹5̏"+P[G! pt&1=Lbz-"ħ3nK-LBjXhdU,sƫI0ކ=e盙m8햳򭖛o`M㞿9-fnm*3KOy1\aC`vjs+ۂ3K2y7? w uC'UNhw KJ`2Q^ŶTQ4T-K+tcΣNbUbہF ۉVxVAK uTɃ`Ddz\[kx~ bԛi h&`hpeȢ,1;.okG qh,+$Ӄh󪯜 בAf96;0]K֓marحq +i-s *#!jEZ2s \ʩB^bi}[ ɒj?JHAڃl,11 |)]ٶNlϷ?pxpWpґ7!n9 e#w?cZb2gБp].c5Hy8S!Q0sR g4?]rX(*t?u]r换,G9~֖M!N9kV0&i$s0bUCenEX+`Ǔڊ2Ƶ֟c%6O-ߖVA.B (Q.2"m1Q*)8@Z=DGBdx*Fyc5CO5|Ѝ$VMڭA8<O?d )16%ZhW?GI~l^ +dk<UyJ=˰LN"ιԋ>E*ScJx9N: ʟu0@-,";Vv ֲckyk[tgE)vxsvww_kKa`0ſW ;cf)Z:(^?ϝg;AڳpKx:,˜ho/WD&b+MY!ʹ]Ih͜ uVݝhgn֚Sz:g1e|ЖO|'>{ϲi\ 1Y h'.4u4+:hQ;][rE\ H ::FIx؀>M\t&E*d'N}6+WCǵm*LGnSY"Cwsmr&X.`pO9;Fp@za`u:25 ۍ;/7d_ c60QN jH cs8»Cq,GQ,Mg#!G|d;# hC2LVB@[6дuE%]3qvfQ*bsexV`(F:ihV7<ɭCal{_nojL@p@?7vMUNƊ]t3qDf#G6]6͎FAS&'/pꭍB{z#V(d`*[ȅDja}xN\%/yݍkym\}w0t62Y| & wNʭȄuQ͈f0vx&q3'n~F,x 5r j0.<Uo][7Հ&m lZrۑeǪ,%v8˶A. C$zK2`evm-؀- =u(Нv/CÜG*NEg%| 96OY6Є[flKIDԈ#vɍ́2 3Ҭ0P^SB 19nB\My:K#q|D žP\^Dkw$h*F p:!M`DK"Sj\DIu."^" *9 ?$VjMh"6 M^H3f[a1W7@BxZ'TOJL2V`q*_f8V r^YyΌ#U׭:U@2cFҫ ަ(dkKQy옒*y.d"G8ME)m."թ9 1+t^2o6n@g‚IpU2sGG֕2`3Ŝ!y+(f:$BW'N*!˷?wo]wsewvvy~:zOI(ct,/,)Ѱ ~<J"o,mf߶ ai_ǖnbֈ6_,Ek#X/M\Y ΪfuC'rЗL 6\c=Y{jCQ)fڅ 'Ls'i=n1YdBQW׌cfF8a Mr}ּ>U$!vi[Z}k*%3h Pˆ`#bvX+:F~9d>2}?}Nk xڍN0'1<"%2RbNIIV`Ax`b$5re\!n!NqX'54⸎cq渁vđsܢHh0*Y5v\9^7q=(`rBb-5+12V*$6HI#U< :  gX"B8oUX1ST `Xؚl:qf=ϓ((/fFJe%TRUDeƲ5>;JH߾"O5f6`oFj&AP9PZ%7,'4ϤցZ˿{uGxmiuu!, |:,Yُ!#یz=s7wכD>LC3f3S X">g1_,K+X׬b5kc= Id FF6-߱daFe-leNv_^Wq#9INq3q \Uƹ\p nq?_yCySyK^O_ 6hͶjF;hv~A?~A?G~ԏQ?G~ԏQ?'~OI?'~OI?g~Y?g~Y?_~/E_~/EW_~կUW_~կU7~oM7~oMw~w~wݿX2xc```d3΃+hSgoodvibes-v0.4.2/docs/goodvibes.readthedocs.io/changelog.rst000066400000000000000000000000551337147241200241540ustar00rootroot00000000000000Changelog ========= .. include:: ../../NEWS goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/conf.py000066400000000000000000000065021337147241200227750ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Goodvibes documentation build configuration file, created by # sphinx-quickstart on Sun Mar 11 15:15:49 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Goodvibes' copyright = 'El Boulangero' author = 'El Boulangero' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.3.5' # The full version, including alpha/beta/rc tags. release = '0.3.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} html_theme_options = { 'prev_next_buttons_location': None } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Favicon html_favicon = 'images/favicon.ico' # Custom css def setup(app): app.add_stylesheet('css/font-mfizz/font-mfizz.css') goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/credits.rst000066400000000000000000000033561337147241200236710ustar00rootroot00000000000000Credits ======= Code ---- The source code is generously hosted by `GitLab `_. Goodvibes is open-source and simply couldn't exist without the various open-source libraries it relies on. Here's a non-exhaustive list of projects that make it possible. * `GLib `_ * `GStreamer `_ * `GTK+ `_ * `LibSoup `_ There are many open-source audio players around, and having all this code available made it much easier to design and write Goodvibes. Thanks to all of them for sharing their code. * `Rhythmbox `_ * `Totem `_ * `Quodlibet `_ * `Parole `_ * `RadioTray `_ And at last, a personal thank you to *mgo* and *jcdr*, two fellow engineers who taught me the job. Hope you guys are doing well :) Translation ----------- The translation effort is generously hosted by `Weblate `_. Here are the people who translate Goodvibes at the moment: * Michal Čihař - Czech (cs) * Lukáš Linhart - Czech (cs) * Andreas Kleinert - German (de) * Vinz - German (de) * Wolf Berg - German (de) * Benages - Spanish (es) * Étienne Deparis - French (fr) * Allan Nordhøy - Norwegian Bokmål (nb_NO) * Heimen Stoffels - Dutch (nl) * Алексей Выскубов - Russian (ru) Artwork ------- `Hector Lahminèwskï `_ is the author of the artwork. Documentation ------------- This documentation is generously hosted by `Read the Docs `_. goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/downloads.rst000066400000000000000000000041441337147241200242220ustar00rootroot00000000000000Downloads ========= .. include:: icons.rst .. highlight:: none The easiest way to install Goodvibes is to use the package manager of your distribution. If your distrib doesn't provide a package, or if you want to install a more recent version, you can try the following packages, or you can build Goodvibes from source. |debian-logo| Debian -------------------- :: codename=$(lsb_release -sc) sudo tee << EOF /etc/apt/sources.list.d/elboulangero.list deb http://pkg.elboulangero.com/debian ${codename:?} main EOF sudo apt-key adv --keyserver keys.gnupg.net --recv-key FFD52770DD5A8135 sudo apt-get update sudo apt-get install goodvibes At the moment, you will find packages for ``stretch`` and ``buster``. |ubuntu-logo| Ubuntu -------------------- :: sudo add-apt-repository ppa:elboulangero/goodvibes sudo apt-get update sudo apt-get install goodvibes More details at . |arch-logo| Arch Linux ---------------------- Goodvibes is available on AUR: |source-logo| Sources --------------------- If there is no package for your distribution, and if you have some packaging skills, please get in touch, your contribution is welcome! Alternatively, you might want to download the source and build Goodvibes by yourself. It's about building a C application with the Autotools. At first, ensure that you have all the required dependencies. Here's how it goes on Debian, you might need to adapt that a bit for your distribution:: sudo apt install \ build-essential git meson \ libglib2.0-dev libsoup2.4-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ libgtk-3-dev libkeybinder-3.0-dev Grab the source code from git:: git clone https://gitlab.com/goodvibes/goodvibes.git cd goodvibes Then build:: meson build ninja -C build Install:: sudo ninja -C build install You can run without installing, for testing:: ./goodvibes-launcher.sh goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/icons.rst000066400000000000000000000003631337147241200233420ustar00rootroot00000000000000.. |arch-logo| raw:: html .. |debian-logo| raw:: html .. |source-logo| raw:: html .. |ubuntu-logo| raw:: html goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/images/000077500000000000000000000000001337147241200227405ustar00rootroot00000000000000goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/images/favicon.ico000066400000000000000000001032761337147241200250720ustar00rootroot00000000000000 hV   F00 %@@ (BD(  HffffffffffHYZ}{~~K[~FX~}Mkx^\\wwwvR<3&r!U;  (0 bbhi ""$;- ?<4) w@<e[@@<@<f@<~@<5@<sd@;to?'((rxUf<3%rHU88( @ CE"$UI Ieu _\nk :`'1I(`uln ,FS#+wMUf<3"fU U8d-?(0` $$333333333333333333333333333333%34NR+/^vXF]du .{yFkyiU0VyQ<Wy~j yDvyG=%yyyyyy3eyy6fy2y$@vi}dD(+Og38abOOieUUUUUUUUUUUUUUUT> D3+wMUf<<"fUUk9*h3 L( <?~~~<??(@ @+mn-Y] fm~?$ *K4'/P!M|{_(O,!O?|0gO)D4R J+ $?=~ /u[wN2MN2 'N2 !N2;Q5w > Rwy$.2,+ RP 3D^f D3+wU Uo<D"f^ Mw*8g K/z]^ ??????????goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/images/goodvibes.png000066400000000000000000000533161337147241200254370ustar00rootroot00000000000000PNG  IHDRRlsBIT|d pHYs  tEXtSoftwarewww.inkscape.org<tEXtAuthorHector Lahminewskil IDATxy$Gy'<껧͌44@ƒ H/le{5kx920A H[#i{黻ʌ#+"##{t@OWgfDddD}}8X5Z5Z5Z5Z5Z'"uhD>]B)MPJG>O}Zc5z=?(m?}ӟ~y5Cn }Һg\O~rl5affkFat]wmN)I1tBw-wSJ;Y`.y֭[/pv{)B|1|sVk=*u]pU@d2y=Vy`VLwuMӘ @ b]SJAikZaYֵ)?C) Wˈq_:M`ժ˚ FMp8jF-R*]]n朁R ۶xq<4 R:Zޠ`TDl/!v۶&./s^a?BǁinkWW|> t!d8dkΫ9"U9Zu^S~Cf۶OWőIy(U4M{Se-WTAk=#Gp;t8cT0FvU5UKOEJof{?4<좔^zp=&Wm€H‡H)!m[csN$ǹRNB 2 ݏ{}xP(|{Yj .(1Mi4;ǹ@ŭxea2iϧi6O8;`~˲>GzkLk :_Rʼ6oZM㕗* *4J>jFO|Rz!M#Xky)H2}Rz#s=55x铟FI3Zjek3p !j^?Z h. ?ClaaB~R_Rm9#h湌8I?8cJG.oդ5Xe?8έy ϳV$!J~Bw<6<<\mo4ZcU8p=jުH])?$w5X}MT՛; [MCVej Iy78Cù5hͺw[(Q>~5T|%+;$C)}^Ӵ=x]w=GQsɏ 1o333><iڃүDzя*R/A{ogR\@8傞O/S{Ty ~yBNitTzxxxtKҏq!d!+`QKH3͛GtzKo%+'>X"AӴYB7TPm$ U$̨mJ /4Y5RF~O}7]Ӵ}@ڻA9b.Xڎh(RJO_2ӝ~ ٕht]Hd_m@zD4e 5®s^wgpqji88_?ó o\P!pb``ǹ]uoy :9}0t0[ ;8η)o gNlzTVLO|bs,0km<߼ aaazwsy?cQAWV\[[nd2y !{%kLsxۊ+I>`.?<<Ej tZ}ȶ?=X~p!#Yİ.K}c횦#bn hNyJFW0 =jӶmcnnnG?#-mߨ # DžQ!LvUh5@%HH=.z4o4 ^eDh Q#`+;v{"U8r^R U}j^%w=n5M6M~"[ʵ 47f9a[qAv/[B {~=gU&..wpq qRJ-../|aO*J@|)3lڴIM|=^ju"lpk9[ #^:^0.Ρ8crGy'N+aw aQFn"IYaKk|I;QJX.?~vE!eQHMu]"0e,WiT+ V* jZ?rc{/HAS4MaL4{{}4?@@oiŭ)^;o$aa2qRʼn>VcS T˿i.a*LX/H;Xzz޳\:U.{}u;ܑM1L"q?@۵k|/cا5MA./JT1LCva ;|^L(~Ivp繩& a/ |`ݺu"䥺̽pCj׎`vvbbUjuHx|\RK%?B ;gSB/N|>J'IH%Ro`i fT xeT!~zaa/~'k3@S%1 }ܾ}75MH%eD&Ǒ摌}/c#אIh P4/  eyS #I$ ˯vppBy) 4\}Ŧ() :NF0GތuumQo 333|+_9?4AKxz{K.?u]*U/X)ƞ9Do2fe4$`j暓# @-Vid Q'г00S87gp&Rt;#NMM׾1 7M__w]odjJ/Jx7~uu݆i͒@xtP0?R!_k 3 Q`%N˽($vw|AJ)l~ѣoBU!x?4JgהRN_t'd)2r>񠗨1> e0 ᙄO$jt){}/1u c^??~[GGǿBH p䙯lE ?O,rIۿ =IE/S{^yط~mAEIRsIn\OwV)Z&M,u*ْTB"RE2RE[i/]B[*&yM`;V'3oC2m_򗿏g=A0.|Iq}'ݧb,@'CĪRvᙁ+MCjpxgՎ|1^.7H(%@0hJP1Q(U Thc‰7z),$ t׬\Mxʃzj ֭=H# '/p5:?a}œ^lܽ1HbX(hd/&hC5 g%'%T1 Q$ Gqb#3A결0flI2x\ vy7xRrS@T+-Rƶ%u8֥ {%IoВA̷]:M'M_W l$ 5vux/%Yȴf@dT Kn>N kR; yGp|PPP%X6|Xfqvzu"ԄB)LWqOa4n:TԪxM3[@A{? N}[oO+H)E*z?k&+|;h)"C<hDg-aIЊ$T'J(0a2ͧUytBuk8dO`d&1|xMZ[:݋^ &>V/kǏo{'J 7|OB:x~~[H$x3Lݔ^:wjLŞDx G_bA & |^JjN64 etcҍLyܶ} M{7`R~{foy F 644s?)/}@bm2C˲`'$^)۰v-ܒaKy*ɽےwnHѭ_,_W85pn!T@z"[Oc^^߽Gg2>(#pO{oE~?h܃EAbOs|1+$&/)/%&ߛE> J <~42:\N38;T8mMhX, 9%BƬgmPr qaE{/U!Fpy.G4 fv4X~p@dO _ Yfë> c`/UϦ)&y(t⏓20\c;2./?BDF#l f( (bpdF"q$I#NYv.㭛FpE#<u/OPW^ jӗ_~cccPgd }}}o$y;u%m&'^nM(<'D"~~1  {91Pw{Hl- ֪*fff0==%V0 D" ]a&*lۆ8, m6\|4 dL]]]F35MCE#m8x sALkH?^ڼ^ p^ {x<Sp;R%`Ţ/fncxNe_'+h3 @ Q[S1ˎr&iwB٤4??IP{{;ֆd2t:HD7c˲Q(177l6 ۶Q(P(0>>t:.tv6K!xe38}a\3+FX.c7649="Ø%mx.TjXbǏca#!}}}KQkg$0&K# aisEall cccV6mڄN߽<=M'N/z"߽9S%/OrhgL|uaWx4ɝ*+:p &;PF!<8ԭD0@|/voz-%qp;wT6l_D"Pk!`giآ&a,h4m۶aƍ,Յ[4Me6,Tv㯞0>nK0,3TM$$P ė(o3 2@",1_z6} _p_V~S"JA~lLy-0pwh( E>|\Ri\q){qaHFSF,0 lܸ}}}8u籰_~۷oG&zy``ՠ`: ~ M-egXV/~x[!p)anhT3#]LΆ=e׮]袋jP%[M̳4X \r fffp)TU9rׯdž 2P=&eafQ%{jL:e kDž㘳lNڛ%;4LmQҍzw3rN |N8W{N>gϺ=i" ]]]nYzͤa]n~=v<Ν;b/X2'ȑ=-XE4 |@T.Q*hOø47r%^ 'Nݷ(] P 5xUϷѣG199 J)qWܙ* z!In&M$]p9LLL`aa¥^fߗLu@ N'dpZd IDATz;N㢋.f9rRokz>U~g($dTO?`:4@ K А_ʘY[HfLd,d*#BV:ǡ+A m&(R&4h(RXv́yO@$0 Nm gJt&oK)c'?xQ~#/kf}܅6A!jA$3pmdrRnrU:~+=SI$= J)6lS{ճt4콅qh6m(166eqYJ=e¢ M&F 1 MCMYј.=U#5W@Y#W?GPwϲ,<쳠{ C&R@3h! #`2ahдWG[-2#o:#ydT*bꪫfW+fjD}}}b(J8y$, #;bqt{>ٴb,=@  A2rGX=,¢B2*xVٕHy 9 t9J(,UAVX0sfuY|@@Ϗ@CRǏcffa޽MԌgʤi6nJ)JFFFi{7'.k|2-#Y2E&pu|0Mn\"f/X0"|Uq$J1?Yce-l,TP(,ZZ(f-FDs2/{nȗm>6RK/ضm֭[G ~F RɇwiL1M֭CT"5MC5fCר쭒lWsTF?`~`U3J}T): YeJ"`2& 4 1?z 8:jп)DRjxQVp饗0S<4<2HQ"& L|ԭdR gϞ<A|Lp|dHsdCs8!؝{hK}.N@pRZ@0 =~Z[T*ϵ!Օ{122MӰ{n?гk˲U<4|#l3V|,\!vyٺ̙3(JBooW,;cEaa</ dLDFrs3^7dWlKo<ߋ/࠯40)DŽA Cl` H$.,..bffQhJ)C^F $^ tPpsuG;qz KE{Dax› g(\:(z^Gt\DpLmROOOcrrܹ3p7?"KyԼuj5'Yji}bTnf T*CgggnHػ4u: @F7 $VR W lP[T&cRcH&앋r }3 DB5XD=gttt[n_2Q&ZF"heYo S{0jn\sssޢ|\^ݓC̓ۑόH 'u<ܾP.i]bꇷKLw(M>DF)!NtSv1i2z;K'Jr&ya(d2fT*XXX@:h|O=߰-ԍx=@VV"l"A'!pSESA)\ ݃Q鴇Ǐ{s[z{{Ȁlj T;%7aת^ S$"uKB@!Vho Դ**' |t|gRM7$=sdG%n0 bSZG*)8v`ƍ@ă!E/3 C)J(Jm+4{qlSD3CXw.Q54 TǴ\.I)Ew6e~-R,T@\wA"=)ϺJb~)`j88@_*'&&B&V%y%K#~o1K#?spZв,R)on>P%s٬k&m\fcL욥&?!dsss:s O<e({ D^Ń}P=B (nSd>d R0>};2wTޟ/$!j۶r)۶mBۺ P陴Fd2(:::2Qōx@!Jaii By'DhNy q|՚MPcGL~ 'p@R@m KTM xccc ۧ:0oj&|:֜/Kggga&֯_?bV>D"A,..bzz݁},~B42k!brJiЖobJ>}"@`{YnHi'EPJ~ly* |; Ը=*2\s}3/{kYSSSHhoo/~@$nkka x+Z5"_OATH$W*3Z M3 PK6^W_r`P .E <[ "QEeuA۶0EF{:zxB($=Td*"4MCRQnuV#aPI\"))iGP?zaCLϫf2@7 hX D DYf'=>{x& Qukooz{{}Ꙩi@'k2ݹ2I1uf 7uZuA{@M\Lyxxǟ!1*Wh!_QcjHge:%}m#HoƪÌ[e8ٖ|z!dc |z>H$Ջ7by&`eye[`T ֞oE#->(7(7bnaFpDlV3X,OPvIU="!x< x[dxl&{0; d02 |T'c;c q23B$MӼ^PU@ԤH{PCٵVq[7p # ZٛgOyPB@<T*iOg(4]B,MX ){?Gt-6jpr9R)@M=>'19c(A!Wjj<=MF2X75k &ѽT(4bAxCWQؑRxL& qVp|#O>"PdFʀ 23`02&/_.3N>,8URVcoؙpnM$/XA1L  &#gn@XdiE9?ADI؋1p30L3eYH$y&\Ǐ$҉R_T=Yj(ټg^57 U2 $ 2{Nka8U8pԲ|x:@&nKk2dd]"J' <+oi'/y?oDՎe,;Xs*"Pj Fm0HF|\ P@\HO+MiBT#L)  c {Kevl/"0"ĝEuE +QT0 A0u Y-{e[T @7 Xְ 뒆ʸӰxYET@ bҍ]3KNQuÙ3gIf[lA>S4C ˡ6mϞ bytaB7.b;MFs{U |s۩~+zW2fXUTm _w^J -A+aTu*3I K~Q[[<ccqKT*>;d2ؽ{7Ξ=%DQ;_ub4}vOc.KլV|yG_'vnYVhTъ Z"["L K8_؄ EӰi yh$b#ʜGO`[4W0 pM}eg8LlRv9QԸT^񚥉D"غu+ Ǒ@T,Æ  kT*t >iس*Jnkg[pa @)aa5{ r<9كY\HFڀ 8#|eBjq\1t 4vz%>]zz mRD"R)X{>zǤ!j g2&`~x~A"^(P*PTP*`(bɤo?![+kg"2˰OVyZ`kIn{)i5G1XK,X?M V & tATЇ$mUۑM Q7,*/g ?,LbzkS/#Y < xހ52/ \FA*(fX/zJP*П\‰6$QanX 0cGY*iAW2p^8F<{s}HI8Sloha Nv0 ~~j} kkpn;L0<2fffPqDFdQqoC))2?Mfs~R9=t|<_7JoQ1 VF#@A஽MGSs6TnF*@lOFP@$A<G"@,!sss8z(ٷn=NǙ @).9Twܺ0`H5/-p v22MLjR$H5H&Ro;giD;E&d+;T86^c`]Ϝ݆T31==x-{V\FRQ,2E☆m۾4dgqáiT֬Ma8TV:`aAizyRꎨfYX$[z\.{dEsFϗC[.yDۋJ)ϭ@7.3;ﺔDGf+T=xpϟN>IUޗ<22b}i+W:SԥYE-`mD ͎m{NۇO1ˣA3ôZt3;wjuZTk1vޙ!q?}j7,(u$I8qUT*yR__^Mbə B^[e nܹA(X\\d(H).!k^QDpڑg2c"U+7$Л WU!<5erŁE}Ɨmvm#N'ODXl ~|k&(lAg<'H(:o|9_oS*Bggg 0T1AX2`kTfԿiF1{rѪ/ grdLQ*pGeE/<`hhΝ6 cz$lV2iC< q$ATt1jZE?4}яQ\nP #" ԠGXl/2=9!ׯ^@Cԧ TvJq`d́^l+b[Tߗc#Sxo \vɄү:cquI~nqImNBib``JBgϞ Hv^ݑIwqLboH%IV۶"ݚûvNc_!HpC/ fN %(ds8vTA4 4x!dT0r^ҰS$U1|T0Q"@__iΝ;3IDAT׃R[ Y}A/0ۆ߰JE>.` 1Ù"bZYk MT`~Z~N㩳q@bN8fgg:Ξ=o~ _/a}WmXZFa҈A׭ހ194ǽ-@SbyC8-~gj{Nۏ6E+?X?y.ɇu!(@׷=zX)qS(A;'F<#lgdR])ꮗU*T%+Z}P c :Scfb)juNBܟj(U#86;NWͯfrq}f0⣈D"m_Ŏqi p  zʍ HG3?؊2 GQ y*bHRH&W 8-nji{EݜX,6/^]ێt_tb.KOKuHEZe }Ν麾Io5bಾ1tßx F_r\}A~~i.[74=qb1$ $ 0 FVf7v\ΚZڔ¬Smӣ FLU,j ypD'O|B)H܉fçB`/UubGg7P %?~;.9^E@dTiTwv, Oo‘FsJ)#LZx0\"<:)0 _T<7sɧxgll@)}|bC2ff0 (!܈Ru"nZէR="/.O_a _o B,a´\'fOlŬ e 0// W0mILTpН"&rVfT~5ȮZH:qEI5;RIO^|:?o[رcnW<#tz%> S  ST8U 7k1ޗdۺx $灲m0: j!L᚞c=boWm+dF*۽kkjjCgϞE7.011ݾ}[5M[K~4f7WK8֘`yOwN޺e%L/:UX4ڔ|ȶm:(޷y\3N7,T!~#)\J/q̸->F |Q[6 Sux֕J|(A.^Svu׮µ말-A0كB`gm&!-as,nrk{_-/A5wjT*Xf? `7_Ӵ~^?3c[yxY![oї8&7jez_ЅT>ىx8vE |@Ub&Hh9lځ Zeu!\%/\k/&^x~̙3p0\F] qtP(ܛL&ibk8Sx}cե}3$N)BEp76n AgLq˖empj'3%LlcִXTNlԆ9Fd5 mU]Tu~e~F2_.̙3sޢ|/ex____-]Y/ 2s}xnu.XT𕩦AU6DC&X` 2{ (YJU #;UD*U*UK$Ӹ{9s\_P7+:ڹ@ZOOd2y+ 7}LEptv~TD5K"๋0Ћ׼˶,&m"tDHEʈUD4޼o *:}J-_/,,|?R㗑l$Et"[nT Ċ-=f3"cor%~ӀNՠ1Aj|ހDz> ַ}BJrGǩT CA[\\|ypp5MKxŁ1_aڋgđ1% i/\dI}Li͌PExj *NmgnDEE"af_i/Õi`k/穮7 *6AmxNNVѓȡ3`uB4m6f‚~~Q^p%lOSnP&$v1W{zzn'2^"lFO,+Y@gTԂI,D4n'Nb솬J@"ErZ~@O=C]z}ERZ!$U{p>BB=OƯcglG93| ? 'A7QRO~%AD;HFd<$$ BDz;$XnlR8! .⪹&egg燋B'? @fSg"F6EDͯ+bT;P7Hvv<<<|}~~amdog];t:o8 $,gz\1>=ԧ)pN[~7F! 3EB ,+[^|l۶ P\xwnj=ϣ>"i'/fReK_k5_jW5NG'|TO_oy[^W>|~闞go~fE/+~]jW#}!}~z=yo~>O8s'5_jWվpkOAS:O<O|e*;;qMq]jW}\}|w}wo~K^__7e{?mo{v Wծv}2ʯg?< E/z_TB{;/O/ _jW=c{J]%ɿ꫾{W|;7M~ԯv] ^W}|x'g?9<ȓO>W_'_ծv}A_O_,j7׿ٟmݯگ}7|}<#78Nonn:ԯv] ^|ܑ6k}]}MozSU/^:wݷ/~OOvO~dK^O^k5_jW7~7>|'<_^w]/| ?o/Mo/GwzW裏h:ǝ9_ݮcbv]߾[]zc=qۯzիur>`'}sۿ?.Qx3???)~~o|_??,uծv}+_׿__[[?c?wϾ/Oן|7}{?#Ok^OU/~//??Y{נ~]jWڧ |Ck^|#K]jW>O+mWծv}^5_jWծv Wծv]sɮ}Wծ}oWo~^Z_jWծvO?+T n(iĹiwqsy'E詧 mK8ryn6dN9QM*kl<1Ӊ{Ćͭ*# e˵$Y5ϧ@,fgwݴ YJ}a 3CesOJsPP* )$%rRQ6e<\5$ݜLb&4™Lg@:'zpSy1B9'=*""2FLmVUc <b@eR(GDee-69ϊMU,w i: 3*3mLjIs΂;'1q}% 噹mɲId؆wwH16U6 Y"Op38̧,{6aJQUvjȪ8mn0LY5SDHș3#6a2ɛ10)=ItP>uʐ朶vs10Ʀn%i `Tȶ(DCga@IQF [BM* U& 2zD W%#v #lYAt}HrΈ{k ;{(Ϫ2nJI]$Űky=hm!0+Y,DE,#FfJW EVj@I Ѥ ^J^_)ʝǩfzt#;N2"PVC1"FνrR W& ҶmAJʚڍwOe&sR"UʔEi1',`I̪Av"rf\ Wig1 G}F(no2wd  AcpHIp,3d"GrF E,B(U,IPJeQU&N!'4M2FlO42A0mQmfRĂA brf J#P2%nNUÀ 1 3s1rIbVY8EA"3S0 r"4ʧ/,0R? Qb\a#%>9 WhSc=m$@'i'h[謄+F8]3YDh*] PG40!;i[R1FVffh{}V@mE밼 :*'KPNK•AJ:-Ebwa0(dU"r:?֩}D*L3"&&T+a;|X]#vfpΌJyEU@*zĸ2 (0rى2#X!+?3~]E)Ϋ](f&#r:d'FѮ:eܑ0d{3B}I 4RP}[ZNv{WDk+pۤB-zǗ`Aw'Fsv)DB5ǰmc?BdoH<{T̲#:w9YF0;VAPr J@HDɕp/lCN%H ennFO Q# A 8aKdg;S" 0|-mD(W؆ #XtW9Ymȑp_@(,"DCiYAgR ed vΈAvs ˂U22@U ^-i\:ȡ)*WUTW@j흩#hإ"Nl # Y j et\WNI@?gIuJQ(E taŠ].r8}kڙNH:0`{])Z Wt!qws$$~: RY;(*VZ߱Lň.a Ҵ޿l:"g1žT#Dan76*!ۆgi16vֶvu0y,3r*[F0L s[?;chnb6G@]AF *,44-a.Gdlڳ[xG{{]v1pZ&],oJ+H͂mp6dSކ)!elcCWYEi@D&"PYzYd~l:&L:M?9<&i6ݹĈ؇ЬU=D]j 2k#HD EDvdPaZrCB0ө`c ̴!em`Srs q7m;przHP`!AX.UGL)4̊&sZ`D~Zd'5V v|lUaF "BLUdWƁdhn4XhD3չۻ'+yD${G*iA'+u= '<(#⩏ͫht%;n!9Ef!@H10ƪkC +STݾo1cfirueG#mXw5,̙mYC cMgՔQ8R-ӰAT919'C1FwRF 86h8R3Y,Y}r9QGE`#fa0tiD4&+iBfZ{jBF_WJ;ZH#0]`(g)"%'R(;&YP3hFálZiA1Jk.& I@H`*q"UXlO 7'۬.F쪅`=; +SPr]vLcǁ]6kӝ*K*WU<%D=^Zt4PBuD+bVWWX.)ld ,`(D8m+&dPS f9=]C+KPh As6Mj<8Y8m@{l;KDFD1 em)d+mol.= ݓ W~^OA:n,9s^2ܧ&|m E\D>^ yūtJ moN3+Y^&\y*ki2N'ш c Zt#A@DUK4FV13Έ@"Cl9=Jl4*u-HaċjYwu*,y$myguF.Ad¤X(9A/zhd!UEm 1\![@t&ps ?S,eI׷+4huu*XZ@g#- wj$ Y 0UL`8"Fd{B:zܠ-$yg'뾱dtP bۜh]![UrOP˶byFU~Pkk@2͡#(jyX՝cD  nC4xA]G!/>4,DR4QhDUQq^RqcA_ՕS>U^\홫TUh}V`.CrUAnpjmm@hVX2 I%cR59 ; / cBՁ026%upunx䏼4\g`G<>sn-}:\bsy JUd\M*8l`tXxll(N>WXA!nw.ܷ.y기(z5IɅS™{].я^ .[jaR/MeW0.= -G9Kq?CMCD5^ٕ89>q3 DUl'DxN cQC,h]#t{;;*AwňNV)xH}%퓀pR#L:'ϳ@`"aCB{hj/B}6l9!f7jiALVh1yzSGB6W̄ɱSP~&졂 l:ֈmYu$!sy!,lO{В5kPFg6 BNrAB'nآfc6R[J&Z:D:0'AoteaD΢BsO#1O5l1+kb yOT)tYA 3r;"HFUvNZ% 薊 JD`!L!c;ZԒ>&$LTD*Y @,YQ,!4V_hvPs"Z$,o^pqqrtAWYڐPB7Yyvա]TBꡐ"WIXjLwM.ݤ"@#k加x(F:!HdC-n1d̅{wIt!]vrKջ\yyz4Bp2 ?{//(FV]>0F[utovn JH`g_r/^ҁ+4۰%^"5-ڵ IDATZA-vCc++?-|Ƒ-a)Ex!p_zBMO\Z.˫NMje-o´Smʭڍ x=`rfe;,1b9m>=T" ڶ*j`{^Y]|לMdՒ@xWi3A6jg.O$Ai%}rD:+wN@qs593(n*QSO88+E2Y3ɥx UDywOdU+#ZN9n cPdmSvX0Wml9g#ˈU8m6f lrugr4z><- w۾}tpR1:$KF+(islmf,C;Kz0ĮL֊KU0Yf>dC r p$ =  r[rMJU6Z\,jU=zްy bfoXEfӇ«I= H3oE`wocAo@Е4E4a^JF VzQ}3vW7)jiTX=M(mTcw:$st@ef (P3-Y0Mo M u rUHݸc0A 7u"ZҞr`6ͮ\ eCDK`5&-j)ȼ@(٭w~-#l8G`ģӱH[ѷrׅld4E]i*%[\/@W`u-[߉հv[yy[^]n8D=>P,M|CATq?ݨSp޷\dhp T+ck.]:?!\rfŹ+?t ?dq4-N2Kc{C8 l.PV*5 ȹdۉ7Sq{m>Jc,>r#i@ۀ=c@־GYۨ*̌mˁSA"# %{l'3bo74P(*q#[J! e&NCZ!upN6N#m6:WAM0YcDUF  jׇ1 :FF< @p1}<ư=$9$Iwg-P:[lY)p bO(pݮ1#$ӆX-"]䈚;`d7) geU(hʷ[$tL3@eerOlcإ;3PlM< 5'Omsr99K.dUS{60f\u¢UY0!mf"݃Mj  f)'THKTPN-=ŎjLJSQzf=rEЩ`ͩ}$a=Ĉi>gE>W!<Х` ߏQ̚rd{Mdw߸•UywX=N34Hz v G ř1΄ %Hb}ЭhBTo%[S:4X-m{,A s^B7n1FzXc W1BWk XH(tD \3PTPddӋ0 8&iU=|*g?c<nK1nk}jVum``Lhl=L/ؗXRPZJm4uy*w~!(kQ{AWWd~e9c:a:VubČ>Q ^أ,ZSFۅ5R/ff(1!Oӱ;+[ўsoPaazQzXGOPUXYk3CYhP]M5lzQq [;X9s i)ʻC欣Ы<.,@̹sځ{.ZOc/`B8l>Lk,{D@%unWxa޷@m=nTu&#|sm* O{߮9GzĽCEs㸁Ѫ8%idk- Cgdօdh5)1/S/m2S\9?LJ<-?ػG꤬摎yGg{}P@c Vsjh0z9yT>f$cV#fqN؃e:m=3wn|eb5Cf3ic;ww9M93@h8xc|w:g8 R3hƁn4pOk}LE*L;(խqC4Hx?3w3PF&ݟ>Ychu:iV9'fwR~nU9{6jUVO) `/Y^U4IAmľ&Ʊ?܋(Os1g/! Lʹ5]j|Z眻BMֆLͤ]^xqD2F =3NCi.ifYU }։:\,`9fȪg(Q⾇QB*;ҶЇCC?k@G,u-mԚDKn:[bW.f&?.1Ƙsp Jk+fV bռ/κVvVa iyTQc $29[#M䪥otPwV3s{KRbްaE4k؂;ƎJ+XA {Μ㾷K@ş̾};wsfΜ={i"I午55-Vؗ]L} eTPIMq\.eJYDiec8{muɐGI8TNfcY:ceD-1"#D쑥{˅X )QtPLGYPGC@,OqJŤ:klOtƗZHw-SBɲ`acjr"J'әhI5Ӊ%c3!.ʲZu,$ɄzP-u2e2\ Vd/,4gH1 uˤ1FީslY Ê!,!F;OYls(sٳR*!,}H+=@'8l˛Esd\u()fɅgd]ٛ@Ң!=I]^DĚ,.L*P?lHQV,"0{fi{%5.RVbWUDrH |veI{֊$KĬiJDlػi @ԫL˻KV6s3 /"LX( 8/Y!"D4I`K!9$D0P2B!j(4@d b%y3BqAi[zfH K&ڨ%aԋ*" QBd)oω{- 3iJd"e/}reJ'u|%#s?xɣN"ZJfFIFXĆ3E βFv<\,ŮYx9hK}T\4u;9kpjrN-2(Ĥ$^ʻ3Rhɍi6xCd:@U#fb/JU5tR޳yVR˥\2LAf(ܩHMbSTʙ۷kcVŢׅUUV$ӂYZ@)SNe2u''yzD3 RZu_f:D峑ee4_Yf?;Pr} S-冉u|lRERW:}GI,w_W/뷗%5U66[ 0K/ö%[b:_O.%;xl$˨Cqnd8'br e&V<>$h{ɌMXfs"Vz29LC &8a؂yG8b/i)g<@Ŭ=3e|7I͜@Ԃ@I"jVA2M)(Ϛ:=>fPE}`v7kw~/lNLs>=X4dg {I¡"D${u湊0@8Z**ڴK~6e\旍zIJuyKiŔ 5 Q/ΈIJ{cGN?xaWܦJD 59o\KR~b/%aqNC=]SˑY=![9%M:*ޙ\xFr9*zq)Xɥ(4$ GU]uQjENS罐m>V"UU,J{)9z XũG.'y,F*K2bKFND2_RCwAQ`R%hc dT>~]z et;\)gzjdڮ͒@o |Ml g2u VDz 0g :U\]*Au/[=A.߃L6XV[$}j%'[JEcdڟ\-p٦$V$Zkي.XZ236t=T^:iYC IRI{x$A1i˅ $4ԕDb2xyY X  dlIY~sY_~%uީB$R"d"a X5{wYJ&.Ddɤif,t^@ z64ebYͶ҇r1S`%$Rb߲4}Y3&ʹnARUcu9=@u@`L:ջpV.G8!(a ƻR,΄EwY̬@C*re0n@;7*:@)IXf(KQ$Dd#ūcB] X8;c $j&5d9+a@$UQ8/E1Q4Nݙ1ņA̔zqք@ABPq fVf'"ˊVKݱz@ ʻ㝙39YN[8"czg yu bwj2G~1cTҫ[͵^17< }@vg:zȭw-j݀ (KleN٢]Kd] wF7o#v^AX)_a% zc%RevE@ vY#qV,[NiD$*ðQR O=X7z%GĶGS[F.AUrdmoz?{'hYEcݓx'{HwЯ鹏 ۽FC~N`fgzigiv~Y_L|Yg/&uh+/_]R"KDs!bԫ9{4k&@ ,]Q6RfE3ejeb<9yo("O[S?Z5ɥjrq'ڼE:ឳμ{ypO=vEϸ?m9:瞩9^ ƍu`;o0Gv][l4m͖XSSh#x3fػ'yyNQ'~n?wa/>#ףmЅ~+̟=k_-]s,͛oׯw??77'4| /[~OG Өysf|1yV&;ؤ G8POzuw]6ƟM(_!![N'hE]v٫kunڜ-ڶ3}v]3C􁙕HTsD (b"bR2f@ ʛr@t,g+ 04"l'^@+: o<7z|{vgmބp-5XCS|ϋ=g*BmIOYDa7/:$/2 A%YKwtA ܩ񃆏nvѽr[){Aq.`v\]7"j$@ҚaC;s]"9mHW샏 Zwo#ͥ_##sևuk}W l}7p@ߖ˯r 6Go5;WN=z&yfͪ?h^ݸC;wv1(W@8U| P6&框X!" ^cc2Xx$Uar a $xf;RM]6x!WQ>xRlԋ%r;Utٰ=Oɇn7yqzNڕQm &Ois@֮y[7/2u䥙hQls5E3:_1svox|z|HU=sk[A 9luxvׁg_]սc޼7>5%xKg5z ߍ4ǯzvpn 6  nCݗSdSNr﮻fXVVlLX%ژz2FDJҰSɦy T%۽g!,"cy&`"r9CT~gq _ W歷e:ʕ0qGSj݄~6q нG5W0qݻu~q|GWwT Ѯ;x,c+۵abn\2Ki?HqV)^oo>ΛK6+Tm2^ǮakW]p펿;+Ͼc"U6z u_\tm,** ^o'j;1=~^,}^]cm|4W]Y]}!gnԩg{]޲aVQ"(&#"B 6 a@Sl""D$JP!ab)ńH=~wDӹywwݳK+`Dlz߸sEgLJTS|c6o}~QCcgRnV{ysƮ<Ƭ}ӦM?6wuhaN}{w(.mQ0VP_A`:u[ozi?m-7닯>dA+:+it8i/fs.!Uj <0 ԫab(!cz)m'\W&4g(QN' RՁNaSNzwuLIlX*B[5X&O+Y~]4рn׫gyɂ>z'3~vyocq~h)WǗ>h{?pN_=jțS9牧wJ7מ{P%n#rxVϞOmKͫjV}:|m? aUSS Δ1>c-F֫r!AZ-5ާ `'TiXQuJSj1%" v?7J?cK揼{O)PE³숧"71;pzVM?B"(~ij>%/9^T+jE8&WF$t].~ k[w1ne-3wkȓOa;6a[Ou%۟>6Xm/w[Qa_/ƵzwlgN|g!iAf z񢯦~_?ɊA:^g'mNUqI=p/&K|z4ZdK Iv t9ncqpG#;h+tL2.~/?#D SykZ*:TRH\ؚ(ޥlV РUWrqY DMW嘑jA^s R:o SS5tI&4Br0֞3u=h>`nC=ލop嗞y>IswW/+tZGxo37v`fkwO&v ܅mgm\ӤuvkױܘF/4YdΗ/tO쫊e4v ߘXnd;z7mަm]׊إR`O^z!tn߮"[k|FGPY1e3 bѽ{c?3gEVkQUeк'Ԧh+M|EdGp*L=h$%r>RU -:#p C&8U1ݨoѭk3fLJe]ϵj~d?7>kguE y÷V Î\}f,V[?.}7;.}<Ƽٯ>Rl\r3oNš; vO]rEz.ڏ%M g?9/m}K35r57<u7d7|\ƽyo,>+ vt)~eLN$Km}ȶ͆=[a]*7:ٍkߟ)!;oG9?ŤSZJDlDкR.+j5J"IJq$Fk *r6Mښp 8kb({Qli1%1;3$Qzʶ%Cݻu+C:}4ثN=9j\qw(Eg&}<0o̥GQ󮍵۵ >[^wO޶ᄏvrn Bِ4wŇnܢ |JVhչ 7]p39__|Љ寏OܬO>i66_jNjo8AϦeob+o|g##,gݴ8nL}XOcO-:wv'F_koM|W^ "&:qL&̠L,GĞTXs.)\hΒ )m@c1NUrd""o[pGKSv5{nrbwű= $mwkڷv%G @?Wi'G%-掼s/>tNE Im9$s~;['5Q-EzR7~8|lmddVvPR~Gڨ0 ^w A>=W0#XL-ˁbψ k)FR'MPϝ;7ܼ@bVhZ,1`⬍8V cc!]vviW_۫WI'7ɫ0yIt÷:[nG7qg~1pU5&~wǍBCw8=kOdu'9[9I}Ao/ѓpѿ;퓥3&9f`I<{Zq-wd(r+j_?v?

OxF8O s/HZ UQT-X#q>6d8qH p'E7i-{\ '>܃TIdɃ){Ü01!fV'+^~/wƾ\KnkRӐO\/U0 k_OhS%@ທze`+]YdS*$;X?i Gt#S=bfgk֮f t]+Gi{j'txދ;uU$sFzջ j6ꪤC&Sz&% 1AE\$ e .dNwBlIUDL$@9˄PIm$J[sqz5+Es>-,f-nE+Y2oI_~)]So'YDӟ=lfǽ3OpbM0&N+,3Y')l) H J٫1 >*St "BW'><ְ\OZ kQ4)mlIύVo-|g=}ʯnTr_8mu񍻌8O }@sEGuϓoOĶmhvCp3}}ue\u[99iR%.]reonO>H*ڮ׺U\_WX\L&VS/9cDl&N\z%#N؊8L@`gow O6b.SB&̆>$YS~ճKA'=ze' ϙ9iʜMvدOշൟ=\g^+wr=<hӈrÁ|%tkQ+/8֪~٧F ό o>NJ?vaftԴR8 .|pZz~pzYw8׬iuw:|7F-{}rYzwvܢE5$Y3c 6Bt%9γ;y!ޣ UUe,y|1᪊(7S CC\Mf}uFCY/mo{ro[']2W>tYݮ]㑯7C5{o[>Mwoڄp-N:Xᮮgv϶G<: vglRE3ߨ#sdG|?<׻Sݏ9ZCZ?Z1jq |ǬxLe@հN,cM@|$2X UwYTf@5ۼ uy(F%&M+cu}Zeb:|rg}l.[•S<{ϖ'za>Gl =7RI`۬׫),jWgMt~$2~pM]Z.||_{MykUgb-U<G;kD1LFAw92lYUTi'Y%rRH85O$"qD"HR2L}o;Or}~oM3ܗ.$vA|R}0s7%@[W5 cڷٟ(|{[/Lgyc|vyEo,ha=]q}jAT ׸wy9D+EŢ*!ISeYc@;gm@ lԭsJ j8ȸZ65EQ|bAbq3_}cAξdc/yQɴ/j4[7ܱcC=ga]߾C/޹c<1 w?f׵mVDM8;sS3ṡurT}aG]~tK '8aCk}"@q'MA7_~pUw둼؈7M}^Sݸfּ-P&b#9QAѱ1T‰$"EHDc Xutn{@R8-9T!T"nvƿFMŞSٵgm=kVsS^5z|mlYLC#F:C/?_>x>{e35 +$Π?ך7ો:v;_Lj'cƼ?ضG_2-=}zW}ԍ5jJ ʿVc@#>;7Ը%C^OVb " "C (MSxqRԆz X 3D#D:uX q`PJ^HU MI* h N 27i捁 \THAH# N-my:u VO_l9bH*BdUzɡ, %B k"$!".v@HRՒ8g9HTE 8ck @3*bwΉj)"2ZcTC+?Tc(4;S=)ϐ:EV,.9_Skɇҫ@0D9'N]TT&HR[CE2KD| K=c3fJi֚(ֆd?eދ1FPT9o,?[l1B$DRiu-†@CfCZxH8!MS1Y΋HdiQ0gшCJۏdΓBDQ* bc%BJűзTǑބz(l/eMT iΰT{v@.rJz K$*Pv O&GLyCǚYH @bPnbmyJ{G((r%b|C4"̥XcHn萀r+҅K#6hiڨ%[4WUՈ{ʼnl.I &4q(:EռAOӤPr97s8IRSS(r[{t&E^0:pye8IZV5biQkSB\ 9bR0ǖԑ!r /Y1E@Ll FR,4WUM☍)zks|Kud @B.ER-pjTđ\1;A*blR&xQ:dKe1l8ԋc#QR)q.fլ[APU@)SQU% зoou@HT ew*[E՚" @<U@FzM" Yc ׈5T k9-&H4.RQYI?**+X }1X~H)u{tsr)UYUBDM}`MBQ. 7{cŧH|.GwD6 <`8U3* ʢh|и4UTT./Q¥io?N (#fCĚ1E Q"9,y54K?yݏ5p>U?`e'O?۳wp6*ROy+ꜱV"B H!e01( }EbEEs bWVjj1+}n} 4 dLIR&ocru:u9Dd+$~篸|.<3*i:Nda;s4'\d='Y*zoZb'b(TII BJ6,mzn(* ooF- UUYb"2|Pn'ykPL5b)8ưaNԿ/7\|>Ŧv B5eUW1i cL LJh1"0\\.vέLVw[ '+Ҧ BDRLO5q SLuquV?guЧEZ__~-p^_\tEg7'̪{ب{n¦;~VI vxݻVPc,o}bŦ;:ݷ?f*ڷff|sXLL,Γ*e̬q~0b1h3\.)W8}dҁ̮L b ӒܟӈUἔDkߍQ<}摃O#OYZ|6Y<_]~?.w}g}gvP\X7>~W:wg/]?[~su7O7{`ԣ^y6m\|Ǽ1Gw]Uk}Ν$!J(JG)bA)R(((("-P$ " (  B'@z{^k83C33I?>e=s^{J32:(Hp3g,#u+S5Sc,]9!!81D[pe;3ߙ[ ސ!mviiB} uU9_z77my~-cfw2Ÿ?|~ RپoKw<8@!V]!ϚVhܤϷsu^/#wZ~}uUߋ(ÿw~[+d21֖P *9jbFh%S;m=1Z M** +.Eqmexo91#_yh_7<7?ޡ\{H}թg{JjKW/'XT56eif!,]M)F;0UY6&"`-^ @j[@@{A9 s{UYU56Qd~_Z_zgNzk̟{Z>f&>Xe]]oʩcF]yMJ ,>+FY髇n֛h|OG>b f)|ȿU{c\J$aa??:[l2w[kBY5QBV9@n+ H+#o& cwi=;Z6xc8b-KH,%<>tgzį_ŴuSW_{#63‹׷Ă,j$19)395rʌ4sga*#SDے6={nq&[t_cӧ/}}[ga^=/~uɓ-}K,`# 0 j `3S.qNDRfpl"9í RAf&2swKN*#)n4\y:m\?ԋڷNnlu}]ŢDBv{߶j6;Fr߸}K,hKԾs5 ݈H;q6.ԣU\Ծ}3ӣ]L>c_[>q(7%@ DpsfqAD(gzy并֝SkeD896$]R٣J.1JlH` { , I12f%MӟӸ@;[/^:XnG׺j얾%΃ܫ 7wnfFDU(;X*X̩^٘, NP!fd #% (4dRu҄TyT>?ܘwI 8?u`m7nuSO/tW:;_[bVݪjJDUT[PQA1h^ZdID3XmDĕu=8$i4Y'5EP݁P.4&1Zֽ}[xѸt--ݫAIݫ00W :eŊK YhVOuuN "j!Y,p%X0,cs,Le:S.pw7$0O}[%!UAiD` #ToilY3FpbÆ kjjZ!Ui7|3 1 30nEgĪJ[Jj5X鸩o׷BpXϕNu%fb,sTz%:fݻ4L6 usdR !`NpXmMC&T)YK͝(*W[-1MeQ99Ƭd]0֠ Y^\+:.WFu&f$ 70XD"XFxڽE, MMo=ݷDWBE*Tu[۞#w)ԃp ND̒4Ds]d!`-bu & !#f8&֔&{s^xO67獍d$K}[%L333' Ljm"?x5BL҅쮭2,]~ke,ݬ,TPH:j$s,Kf&"3#HHOɲZ[Z{BO{kssȲ,K}[%#NȝvKdcw(Tyh^Rd"S& 2*Tc\SrgPӨt47w{hwoinf|qñRMCB9ZTf]SWgɴ̌ *I4DgF#2PUʹ%%ԉ i> (Z̩56v HԷ߷ćW~eٖN.*@h8օųPR-@\jE(h&D!cVaRCp$T%mNZ<ϖXA,(Z-n۩o%2L5BRU*"ur,D5ҖnvmcsiӖݿ{Y()1UѢ\叭j+-^Ze=K$dYcSSTm3JxhͳgkzudJ}K,Ìg"o[Q4fTT/eC7akoz{Z9lnc~;]W!r'aJEA IDAT,WVseKܽ,9sz^5>۩o%ZsqLJy!Y$c4ޫB%5S7<zzҳY Gv?1}N/߿߶__ߧ'|SwmvaC'}ƝzY]|q~+L8`)s[rI'uo5jvmZYr଑L53$!8ر(Z[ZgϞ=s3gnmne V5ظ$S[bQ)D sS'Z[],p-ʲ <*~̳{‰oV~`L6\#m贱yN9l>nAi { #n?GG?{ϿICVGxsC/GݮwCq=~pij{oscv+K ՝âS35šu"arw7tlr+uRUL\JW2%xw'il Z/ u+EzoO=k_zҥBU}7o=w{5|ؽL2JC? V^qŎz _wy76p@KmFog{x%YTn!Y+K"b3SՀJjVe1UiK$#>*96v1IֺK'pCsagoO.yZƞG l\G0eevmnuK~ `)L5K8|TT*7c)B%b=jJH$DW m :JKDڂ1j QX  텐KwKmK1 !+a\$ 5wrɂk3ܕ{"Xt̴]$;3WAmm*Gs+/L̤fސQ,zϩq{>C/ =縣ի{x9g/6})g.۷sĩ/ M:tG &"r !@30g5G$D":U+Mmۉ3P/Yi!4'd@lZn3yz9 Dd=vN{lo򐃆9ic2h@/;D? _{]_zJ8pϿbGRSw䭷f֒M2P raKtY&"EQXTf2Y8P M$NUCe*ΝJ+C`h]jpZzk]efnrG[kͥ9?}eׯ|u^#t7tg `#g̘y3~<ëv=Xk5;-~)sMũW'N:3{<\fc]_̖[,ޔ % F3,kMus#$H$x:T$^9@G=”nw`*u|kӍ7Yƞ"5·4-~DtL<5xРv~ozp=pO{͐!/Eqքޛo y~V[Rk܍Lʢ$ 1"4&Yp2O:Ϥn QDҷ^4լ^;R=hG&LXawʛo:pݯͬyKaGu~!٣q/ǧyz70|l<DSr A<B ^D# YjZD"0qU5]BPU*,,s"ڋ dZP 1 p'wOY-?ۺYf&L`fpg:c_}u„ Yz(/G=>dG\3Oi6x~90sb*D,Uͪкf\q{.Db1Wh`ҹOKEsshj>\/[[( 4*̌B%l]wwhKCVƒ%MըPbaJҖH$ucf&6U1EuT6yp fZ3#j^rɲT!"&MJ=BYe"-5`uTO=H,:SIaIVte*,ZX"(+,F2g|˃`.5@`:d!(U}MD 2}͞93 `"X4E+9*q8֚D@yRDK.BU蓮``abۘ%}L$ YTUe./"c0{%"m' *20|LA Yp"7bSUZ5j:3.H,֢SsUsٙ(,MtvBcĢ[<'D e eiLFT/#DUA,*CshL$0ڒXÚտv˲j!,"`H8c)5*1CPDw3a3%s3 uM˂)g4H,΂S&N3Aۢe yXKbAaN( ZVEV[KPQWCrjѝHF̬pw&%̦F J$Dbѩb̈H-?,rPc(u0siY'\꫏;^/e/i?$ݜ|,DnV{w3I(H$CEy@0DwTi $p{kI\˃-Cm /3\аkl---t̮F0)̬*Ct0kێH$uP7#s EލPEjV+iZhiiYkZš{4MDd3ab8Ug(@R=H,rC a737q6*ܕ(={ -;|SDSL̘Ea"+ʙQ!sPԑ =H,cV[uWuDXP * 4&@DT(-Ykd7"W+FsCD"0Iui޶]ƙj%ra5Ja)#i"12HȜY$ h sa{߃F=xmѿoln}v=޻ra*3ZQg}:RJ)w'rކ99OB]WN@Q3@7&HtŢ$,*9E, `v7ѷGyO8iz?sW}>YA {oMYC"=HXUL8T]jҐK"A'XkM$pgYMWD T ^rUefh4γ{_r|o}ϳZS2'nbccЗik;_ᘳ>㹶 }vcvx/}wv`A5Ϗ?OzoztgL׼XVBOeᎪQFDf+I_ uK*"Yr D&^"ѩJ,Ac$q33Ubq‰Y$Ww, KBRXiڱ +gق.晳&G}>{;rI\5[Ǟxj}^|g04?sS'N1|5~稍[}o Myoz'_""ú+4i3j"C>ܕʲND ,2QRZad~O$:d@f.Yf45"0/e=KTΞ[}e<:Z&u/:g>7|Kuv[s}/}o+usŝʖ >qzn”6nlvvY"DU?4kwI=NnUlYmk@LDy紘4d\CS N"sDC8D@p%1,MrwWzn7yw.+n8}:Zrw8//xs=[FA.T~u֕C=򼽏.>5򪟏ze.e 7qƑGD]ݽ*$:w0! 1(^FK.zqDBpW? fk0sPv կ\U[ln-5 Gskط.<ߞBѐ5&Vsg"NF72Sfj̨[VV]uy;k>o]$}e{6 I=\UmpeFlLeYHk a^:[W|>\-Ojmۦ}_>9EU]^ͻaMOvԙ)]3A&"Q<1K2eXhVKcw>$z"|@"VDT2sA\5,537NdBfy9_3Z:xίi=cg8cǿnX:[ouQt`XJr;`'8\M+J;HIva95gH,'-˻S E5،Yr*K'޽HBu:ȅ6˯=8hf,eи^'!=K5գRhscf8bY@(W%11x9}Y#GKjz"|uR."U P"e\2aeIr~Kd1/e>:2$f=5>|3{>T Z}zY,nAL™Y4;XUA UIâuy_ }3&W"<9UF9ӎDmY_:zYN؅2=Bt)ƞaS]n4?Y᧧4m-eԴs}'=˭ot3yeб_4_7VVe|oys۰)Nn53!"]5ƶ.w0Kf8jeJјH$DUz6"8 hAըl<hF< jR|Ql=v)0IOl`ýJ>l>'Ɛb{όiϮٖuH%=0Iȴ,P:-Y"X59@dTňvÍaT\#ˌ) 1`v1;o8 re~]S.$&5sr'x4/U*X᤮fj"R)iĢDnedj;#GUMͨ\0KWFdNok|7/xoOibIca Tkr F1IHX(Er&6H,W7~ݿ0@*aw>BC^ 4"F™eV Y Np5ԌŹ,KX&b;4 \@VHDbw$0#gWi"FА;S54lFѫf)&HtFRDA$gEQQ#v&dB iB%E|0iff*d[kQst55h w2cp3W%'Y@TY8 -* qK$Km)۪|HF;Kw?6gWF}ҕeܱw=y9_m#~t9peˉbzO?3{a(kr7'83e#GiK|,0"!A3Wlչ&2s DXkK䬛Ht:@,Aj&Y&\#XJufsDRA0#Z<̢t~зZ.9o\{6\l/m;Ψ<86FھV>|ĬJ/ (9 wge^xƁ+D?1}~_5_z^}w;'x~.'_87cZꝶ˝>gk|~3u?:k}#:})X5*V+vrb03=NݽC1ѐYCCc T[W|>\-Ojmۦ}_>9]pzD5o/?:+.oNYm=E_<~ؠ^_6/RA@p]c$c% E"|qQqԡGuߘ]|ѿ])?~5ؽSw0']VZ} 0{V?zq['7ϚYsʇfc=y6/|GXH}k~ױ9g<7j}GFg-wUvO_}o~[G<oN/mLbɅUb%06W "2W9pqN sCm"u?\=Wgh3CwpU?zU{ 6yuܚ 8k)}ACk'QSK. ERݠp#5f eЮL,?gł1wΞw[lҋ8==Y?ꚃeW޹O_Mi$4hUZ;&W#aFaQ3+$H, Zh;|46MlGm}|D<[_|qG0,|LAwꑡ?}_3ɀAD3eb"P a8Gxf-X[m]?g>'4x~j۬_,9"uS݉Ag;-[}x<8w>9]lA_%cG3W=0>mr@#Z:ɗU-^}6۪_,_I3qf1=-9%ug"fz{J(fb3@ ,(3$xxp>}>_ރoyμ?ֶvƤve'"ΩЩ_{@M?|Rgk{zo>/l3ͱۮ;ƵA/]d|K+`=lƫ&jZm#n!;mˋW|-t$^c!!+#Q[La1|2:{ɓ&O4y+PnMv_os&Oכ<nwr S6x׾{Y]yC7N5YV]AVz󮜰שW.o4_~_-Z?r]iy';ƒQ=ۮX0`tI,j TUS#HYzAaBg)ƞamγ}vܥYgNig/j\LW}V78=8qWvڝ>v/>MzϿ?{pO U4O;lW?tS; os#c2i ov[蛻o}wkr~x^~!_vYgʘ.k˄uO=1߹z= gϿeur/70:Nνvo^1Z,z#^f9#H`wB=㗴HK9{w_u}yy3~zyg;3?q㞫U<+:cmhhIPH%^Æ: ~͚ .-)`=c7kj*X_ANuu{Mnp/fA{Kt9X]eguF4o8~s?ݾey~X< 6#wLɍ3.[sW=ꊛ/lyCY{6t`BX j:@XPlE+ƈ&ֈMZ)afWp쇦D$HD)#Xy!xL%8lcb+j rʌME?a/_|},yy$F(7n^s7 `O+MC6!:js{^9R^_ݮVZC{NLfO/<3_#67IO9?ٸV7>3zer,(B$vQ64$lVj0 *RF0xΉp8CGE#5Q@ H;LX}H< H!0@IK̒+zx(pP~\KD6 $C$CF$0bwr_f70F]'@Z$Rs fWB7}_rp{UGJ^hq4"a:_ "E$$bB B$Eh,Z|If/:*n:9wH5@R(̖"_{ٺ1`!0UVS-:w;fYP}S{|Sj[v܅5͵?yqޥ8o G|7dIU%^=>^Z*8:pTMi_Ab`E@`QDh,߽KaiZˁ%oaUo5{Ekr4O~/wWh[xy\kZx$Icκo?pvasM|;]:VMJ)k1F)uU:a+J2shHP*RoqUCcoj,-ny-]\gu$X<~ o ?Y6@df 6QX@ВVi[M[V/]b] HLE2 F gV?s .z;SɆl0Uv߃=ir[߼hs48v}4ViYk5;:qL# q` "XFDԿC:W\d1KEC>=cY;׉nj?åtH?nZOᕍ/CbF#ξEeӫ'"f}afY/] }p9UZmzMVv< +yuc:񉽟~;^^\xp윣L@@B2h@4lWF׻wĀ^5[І*= 9wb8'Ho\;þkwnr6}y҈oV5zZmW9\r,J"0MJL+#4W$BJ"*-(51[ C{)ްvuPu'kĄPuy!w zgx,nĽ2K~SWw hg|?M^0} 9yrZsW@C;遅UnrCf/f^tMc+zϋMkb-/sPⅯx-TvәuӶs:Hv+-`=Y6@+fЯ~̓ҭwuc( [ZVǓ.3^RڑַrspۇOvOexwA}UL'4N+޶l{GM/d}Nv2nΧ']?6i߾a6H~mu}7ulҲ~.Z;O<ڲMni-{yOt~őH;D;bffFu[PGBq I)H+HBT,6 .%e?:[ke[kz\sgS:/_6][>i_olFyH|F-uE*<:j\:QWCG ~;1hثZ6}2P벷xZB?|7 uH?jhձ|W@ {O٭t_d Q4 V Ƣ&akQ@DNUwt&.G1L|G~NsntȒp_8»bNĦWq\{_,3)%:.wM;m4|u}_67dam}ޞ7"U>68Ё3lwwfxgct*뿿EO^{yKyV\Aw,7 V4W$rG^@[A1#ŋL2(MX.1g>=Н˛e7^@s'd:9V IDAT߁H,KiX,AVB+LWDxuNgRD(oS^q낿88%N brKS4'U5qwenf5Zc~^7үZ:v-i9exG֭}6S)Ox"r:va GUD@Dvk4,Q008T0O=\2}+oYd!g9oM`{]D7c*,jQ`{7֞:~Cn9!㻞vK?}_Y)ϕ=7zQ8['.X EnV  a_rgܡlq%ϭu fWkN,A,3(EdecR *04kw W~75K?̘֡񫂟cmj,aXύtR7?\.("HRvרEu@:- -:UM^=iT"lrԆ'͛Ms{G^zs:S.wl=px,$j>a R;yp}Jrs?MvH,q꠿6o&q"}ޛ#}QpE]MF<_Ze̛M Qw'{{w#ϪE#n.$?~gկ۵CԲmZٯ~fT!>8:~'d민eԼZ'+|u`~!C rR}"[(BRʲ%GA+-@+ZkI]¹<= ^3?\|3y yy6YW%`A^8#Z?zb1ڢ%NUK.nc$j7=o`< ʷ&>n|jͦ T[yjdUYXⳕU`-[O>Pvewh_܃ǁ_p\Df@"E"VlX&4_~ILz4g,h"HBD.^q:uWbƍ{irM!┖fP(BTE, )f${ȯUF>?#=,*tqR=s )"`fVvZ$Nef+LؐA:Y,   _zRΡ%$ rLC2p^9?p{ުoˁ5oů_V~{~LFl>~Y  5B]0KY J9Zk0)Hp8{A b/K"B1_Fdk-i/ lCXx< ?-NWG j%M|ekţrMZ~}W=<[tw_De$Vȶbc4 "0V|Sw8"%"" wPv`1 z %V}OS[NuGWvӘ)+JvɨRy]>ܾiu?*ui]Bv-`|\Ո)/q>&|lܵw 4) :99992&TcH,Xk11mt]Zv^Z^X\\\\\\Jvq7~u0 j ̌(ICE%!Z+ 2"R܇?Oð7ukhfw?{L{}ijן.3Wگz[ߛꟽ'Osօʼ1;Qt8DYCa:@GdњH1)7 ;eոToѱOϜ .H@<}v Ӗn_G9{{Y5ʙs%7{oG?_Sb|k:yEK?)v=݅m+f/M 3O~xͦvkuu#z 6f'[75b̙߅U v|pֵk"@Z۫NĆksmul ! XC&",$$b-3R Bi%@`~zt)w kUt/_uߧGohSf3rʩUvhWűCk%vjwӱ';w\ߺ} R0"h7H(@azL@Z -n;9N|ZKz4Z u=ZTѶ߿۷-:L:vVsZ`{gojgf~z )/O }pgnW+='&kh୨*w-߹MvV^uעNb̩9wBV5hsQ'M[vC9*h()hXb@k4YaB}`c mIJ ܉ QqwF >ݹ2)!gW/fUӮϭ~[Z\0t5XLJD'W vcF*@Y64PDfk󶆹KW>wS{Ŏ)6nܸǑn9M! j-Q@3Z# oC#QBd9sԓ6a؇ͺ=5VoY9LlzOl==8MIF[_k7Um~kȺI T^} :խz'p=';xOt{pߞ^7r]$7>v`.o筳%&?51vS ]ZeUT*t}pӏu}GT:5ץO }L7YIJpWAE֨%PTBUb/ ISXDԞplXA+(I 4=)Jr"q IK6v|aruL)l")OQ/ɛ^gB49M!`Z3}pN̟Y%o<)î9=kԯpoӰ`9=xwZ¼5[9bᬗǬ=lԱfUJY|(V4zšs^;sA:xS7_k- " *j׆ C[ВtH@ 耢l%HZW(Wx$5 `h h3%R)o1/,*/(6ʩV䚗^%i3N:jbEcm0Q0wֲ7~jMՋF;M|jyr rQ }'Imմ)DQ1ڐ&ߠ#D%eA2i@+EAAW5x <  "THR=҂@lY<@(1HP+C?ڜՁ7>ՠYQXύtR7?\("HRvרEu@:- -:UM^*u+ ~|{ϹE[7v>ZH o\oou "F &P!Y)-fgXFtH,&̂Dm:10kt,lB`=D+N6?so&\!/oS^ަ5>jYs0,ȫ#gUkSO_Vm,uڵ^ﯜ4vq<&Wy/O|kBnn1;l0,M(EZ7_Y~7cx9U\q{T@kF))+~tN6M!Z46r瘵 "- kOKhD$")%ƲF5!.4s.A+?xWZeӟ~A:'4뛟VD9O7EsG>xx׍PWoY+{;O@a~~f(@hSP W}5p~͚mf@E,1@_wҞ jeeO!!Fآp @ic0!aDh%"bcYa`84D$%\8I$pr "Yp2T  chEHK2!p8*R MKiL3Q<%Z1&MKz  i6r6΂J3 =acJk, UPIC˚uݾ۞(\܉]stzW N丬y^xLuAݟ_<֠7~?(?ayU%g0{Krc_kKj{{ѦU?]֬~̷Kg<~~v#ҏ^8ae:!'U+[C-Y(ߺuɫg,Scp^G¾..Xg#H?WǾT_xhf;F~Z 鶑7zŅ Kêm:wlUfv&vbN.9oyM3XWz;{n{fn4UEU?dUk~%aU2x (OCI`]>u5nP+D(q-~?`%4DXXjj5ȝL2~ZijZ 5Wfo%[ٚr]=.z]M1,–˨ӱ4VEKvchT<QFD@D$$S%I%IyP\g ;e8'E>=sr' 3'L[-k4aWΜ%()(և{>X^+Z)/OI׵[P!.ܜHn[1{(ҙ gWwwIS Ik{uCpЩ60m_yQ{W=3{w)"!耈`Ih#c2 aa@Ƙda0@` vww >s D\ `2V 8KS2gT-D/5=qHd9VWKG5)095SiNj zUU3,M\lɺsJ+G}4hTvɸU&mR;]<r۾6}Ok^MhkOr!^{C㮺V՞OiG7fpw{QD(F\9cVf J \*A-2H0TULk'TUkeԉ:UKf&yąJn"ZZPL,AG5k wlƚ=J/5$LAMw&*'|K^yUF"edw:3;sB0PSv!/ƉW  EQ!8H7q.DLH bUFyH$)ɛ0(ZNx`qkQKy 1eBP#Į2r塀EEMA  8P CMAwT-} C߸ѷO TMàҞ>W(T K!7A$ĤH7j:9`"$bPe,!SxvD8O>m 1u^У&Nw57hӧ<>M]woed/wSo7~5=ϵpOǤx=7jW+|.#+7o[̈f֑sP-|B'+,f$S969aj,0\#qCSrX$)]W }wRkG "nssEK'\x}yq.),Yco nUxY 8uOWg۟u}rƃނZq69:2dPcZ4¸Z:MMmN{5ıD>M .+V,R zסɴRRʎĊ+=s~}x}NvfdWN}mI+kѿ}qN9q=dM鑩;ڼ]Zn~gw=4hn]tI8kft'鐵~~ҡ//3637dvZWgSGy)}kp|W0?~ Ug~^=k N~zʙO7y!c=o/w7X}"+$13!"fRS29&">V.afPx!2eLL*Jޏ=rV"-R8W@}0f+eLHr XEu&)̟k9xዏÒZwBg剷v[+W^V71|b{ńös5-k{Ͼ|͋}>Ŝs۴GB}iw>w?<CoxPylQ#+je""jf&l=޹Y{UM[g͜V\w1'  l(ri&B%Vo3{N[Ⴟ-H큖ncVqxmږ Ym@1y3^-=~6wGU}/-Oa5W.^36޴מ}k^sά Z$-CdN$0ٕR-ΫYQ E"]Kuc55Uf6&Ě8AT 4Qv5񟎻 %zWzv!qX߭f+[Skyju\24jPh޲ӻWߝ뇮3Zj;sv [Jt)wh3e#w ^pNghGVZ+#3y|`RHS]ƹj#2#.Z0 ;g0E@E0f"2(}fhNzޛ9g9sfϙ}OsƗ7]so|uHcۿ*ˠk.MMk_sܷ64b4wzoPnvʺ-֥I#VGG]y7?qzC~׽=e.Ҁ^Zy-'c8cr=Bf_ZZ™3RgK[raFC) 9#%=dIʀT:r"J=}v&m7Z[iawop7?sO~Ϸ5~&(٧|./I3ם|ms\w?ŔKfn{E-OcQggqν}^+ YsRRQLjH7gC!fԘ,/xoj;Ӣ AN8rHޑ"H`;&xa` D05D UlI-LDcfDdqmy= 0k#P*mHǨYQ km':$hJ,j4-κ?}5Op޷%ۤF|{.kX)ޜ-@( 31<$j CP|KD"=f&D0SUBMRjyQ׻Z(1CT-KN:g;yҮ۴,s4ѣyM ulJϞ62ů4|/LV6nʅ#>O89TU!Cf3"&Ǣb" %AեqyD"=M3$y0%JGЄ1A`H h C!/-Bꨛ6u !7}',HI |  0X0ՄPPc<<3!S"\EgH$cPc2BS5ŪQ\e2#Ͻ{NS2'KٵdS>k\g\p{vа= fOvF?'bL[.Q4bn27VS-=DSzɐß(E_F*dΣ>z^i_w+mM;}MFL|}想y~>1Y!3`#,q*D 7'!\DzwQ834#3{ndaX*d#88nuoH𭛗ZJYw5{>c:m/nnu~ôS7S_K+qgvͿg7(O{~xi޼ׯsj/?|qͻ势@>eҕsؾk7/e Cw[kWmzAc/W5ӮŹ-?cӟnCX?sddLYJ skmAZ H$0އqV}likkkkk+V%^7f5{5j|jC1cP(̀ĩĎV4yaZB=,ǂaT񟩘vz'\Sq̼|4!3*}6r֡><,ouJK6ص1?>}[_=ܼ>K7;|}5Gn9/rOޙw8,EӻS mNZeЕw 3`wn|wD>DSsRTC͓Ԉ<LE%OKH$Zq9Is"jI,J%$ 1ՕPJQs3>%~Cnwƴi ia:#w'q ߰o֏=bLګu5nzo8PW?oT {ߠ݋n{vFj1JC'?&c%L r>VSw 1;ιD"=XoL JLD #1uVvwoȕPWE8!/H͈ԱJ9f\h/MDYb  e5 Zcт+F\tHD"˳0CD$"H:]YT$j!">R Q0đD9Li>aH5JsYJi-DU՘<D"] uf,T)ue'o :\\'h~D"=_pTUC3L Ӏΐ60c 8jYKS㸷t '^j+Y$ bH0I҄LM'g\V"rh5Q \ΦLY u5;# A*-H9S"23I(`j ` f*qG+,ZR3cB02OyN"\(B ,nF"ݠABBP3rL0)r&B5 0QJ[$L'*DWPUIR16 'B)#.ߛղ9a3bOSnl IDAT03fNbHg9vHZ֢fs9GD"]Hͳ/4jSeB=j{x 1@\h^ ".M͔D`EQ'$ѺvQy˚GoyKSt&f>vzVxʄvu]|m]t3ǑC1TP 罆NZ8RT-Hk]ݠd`JB@y'y %W+7I~nssEK'\x}yqƒygQgt'~ ho.iY+#@D=č{6#Rf8fP)f>D"]žY211#"Hq-¢m6 bݴ+Y> Юgoo 2ْ5<f2ZvXӮ!T*Eձ$"eq0#.1Pruup*DT[;'"Q5Qw|]o9kƃ ~)Up5>5q L0;/9ݶWO7:g>qeόz7\s5?MZs%^w'\_f?gہaHc%թpHU`DAۅ: $BB lD*EDdDfDd 1@BE `sLlExo}wOɷ{樁K;.YydW}-.rdb-O~ v yj_9~.;Fyno?٪kWtcw?6}6־nغ mwm/l?go;K;G;o[CZ/x׽nqN 05XMe V-&>uZ[.TjVq#.f"edJR0 D0PSѼ;sEXMnmS?׺{s wo^zW\ݰ/`3[k~o:[=wqC`-3S<0^:=Km:xe=rכo#ޚv ku+>XŹkO$B95PP\]jPPǬ .<}T_g>zbǑO5 I d!֒J \\MdEZs-mMmd]霗|؝6u[o9}UW_ɍkj/. ]@,C{ y@I.|egG"PS DDP{#OTJ]"ERJlTrOi-j- ~G>G'-Opq1@k]+93}XWC|j/6L;oͨ)< Y)QO]@S5OIdԲn-^tOqn=\lj3oL\23b`¦#Z 5O *InSg(9g\TȱA;Q,0yڳ ʝ<ZimZy[N<]y&E^yղͯemz'^ѣ^"]<)PP^x33N՛Yfg99MktJk|_?^~[V/_eX^suVY^%u_~ڤ*Xo9N>=G"u!RUUika몝읓jyMfcuw[joF3[}WvwƙÁ?;ÿ?zi3=m m]w?INrDmw'n1*rs$хz;5ݽvhUPS. TQ:o |M֚0N]wq}6m;t7^t{.uWa꥛}f1qL⍋6^.u?l9W^:wq?̿[a[z%Ȝnڏ<2a {i=dοC֭R൶-ۭ盷=+x_Ŧu]tCkqw׍pf͚'Ï<61IiJsISoAxGB1%%?YyUF"ed v>=V&D\{7w.*$Tr0ñI^2)kߐ3[7^Z iz'^eҕsؾk7/e Cw[kWmzAc/Wubګ۟}K~/s%/V}֩ :絾wU~=3#'ԥk+qgvͿg'ӵ 1м@cAE`Ps:ԪNDz̵dVj|:i `}H<ץPRBUe JV?Wv@Q?o35;_3[h'kv_C_gO:'8rZSn _uo֧m޲gߩv a/%}k=թϿ/.KhY0)Aԃ\HWZJiB53bH$b"RBcj*C#4y3# jX&7OJ2lذC=>z>5\AیN՗g~ɵEYGP@N[J2ob|2uM$u EխAcYsFFpidfAF3 E0# )8rHZԪ%0 ޅj= "IJ^h% WP*}W6tӢ[s{Z[bJߡөg˂az.LmFV;}ljd0J"W|EUL 'bV-FCE"f5ibfةm-kkkЀ"Ђ6crYƪ&.pPYj{ޛG{>|#@@9b'};^])S--?ڹ2p73Sg6EpϑO\̀Pd첄cޛ#q,FE2Z#Dz;nAH6fvޙte2q*Q.@נ(T *+*SYd.78xa3߬{Ę>u5nzo8&z#qLf>#wʇӰmƩ='9G' HpP XUM FfTZQBcB$Y$7יVXU뮲Q%V JmjK1D狅E"|0m#kig }zyBU$3` 1z_rn iD"Ό|\ΪSbΙ}e{V0I\PO&`BCy5H H9)@s 3&RJ]91 0.c$1fZ ) bjvZLYjmXZJTPaH$#Sm-+J* xfy`ǷH$ 7f1]B7S`#KZf΃sY7= B'd@-&Fr0GHJ"C"r@\S@D9*3PKkY3fgMmZ]֎S S4 H+&fDPb"W̓".!<,(DzعT ԡ]][`O,䑦i=J՜Lq D|q`RbAMDPJ yjQ}SB bFH$j=0&" {UF&岕"<eiInF"]dرPԳ -%0hP os$"Ha۵2:GD# @.KT+$oP8HjށEp8K%ϭ%Vs`DcQ=kDڹΕ<䅥mAC%-!WW>e+B?t3ɀB9 *R ; d !DVt6H$yGj3`YOUT۪wd"{mi5(Ro!f0Dc Sf@ -Rz˃:HT`Ly&VGQLDGu""Sk7/s}46hQ % - ts! DaV-NPePI #(GDz xj6%:̬U 6JR&߻Z{$Y.I`B$Ifx@&hQDW$Y.`QwLF~3oBH^u1-JQgn:,Ȁ,GԜC0#،$D1Ec$9ӦLDfJDČvk(jB: Yj1#`bFa>!@p:C` fmdF$C"rIu bS8W+#ļ\sYE)q D*(Uvu%瘤f0 D %B>"(D]C"HQ38fUEj~s랃mE^rܘ3F.znw޲[mu']Ru ;6~9Ǡy 3kw7P+#qԚ hUԧ{ UYPkaMV? }ቓPtm[>|݇NJ}6"980a8PdP!rq>[+7I ik;]~k.8CiuE?|sL3 H#/8LDM3#&-( +4MFK\+esw8LR1 )THUP_yu͋RnO̜;no8m^ D>ƌOTjz.rDTޡ+PTwއkQ+hn ,ts6./' 36V3 E:5IRP83Ϯbs *L>c* iQ;-W_kHo/ɍSW:<9kG'8//g{VW6:ssPWH s5YPg }ZO\c/s^Dez+_;K]O?sIsK7LD]E_FJNi"oo옦6$&`JU2I< T &> "K3]y7>ֻ'z[=sIko+>斵~}m'S~8Jh:dk?t=_vdn'p=o=H'LLpi~$~I}}UUeZ%>jjQpvʱߥ;>~z"RyحN&?w>rvvŇmf'|/E/S&]:v{]5'>ݔ_}ȧq1J@)\Ĺ8EP8tՍ`FE01o}cVӼgN^#e +qL3Qk!KBJ`v bg!$I?_ sg͝W-miBڲ7'oO[P/B=Y7quGQ'sWUf2+兵U ʻL{27=n_r腛\s%G?} _+{l9iӤV>to}ݘ(;)E5$[;*P ȅԱ0)RCȕNKu7?>姷*}[XJnjy&Z~#->~"KtmF=AʰZ-zH%O,j31V[?4haگ6Yk^8u wwkraOԒτُN8K|nzMv; ڨo`W)}ܶכ7Y?d&c Oc_f͚'~_kRrIL52o"T;ggEPxjW]5i$YFfL>;Us3s S;s핤 \IUg{@rYD, /MG"BéwL i]0I(qʠ<(%j"yRrxEQG" ;l3 S B}FbΚ"4PsbiPY]jE"]HU\}]5J#R_sӄIqPH13Aa5WxQuP\)TQ}0\t5ǜ̫eD:\L58jZpCH4]$鱦"̉FWoi8!,)BZi" %fh~DV@L8޼ 3CU#Hajq7ů13;_S[eaDM IDAT~5dT-Z #P“&LEH!msJ%dWYĥuJϊD"! 1`n/ѶpO]JAzҢL>KB5j{$5{% lYK@!^AK]!!E8p4G"J3@̬*ґ8vOҁBQT4g aAV$KP=(MXBĻEP$^ f6@§%WW'4PD"=˵LMEu $A\c#'iuqBIAC;dS٣Dhj+24w]kÆ . ` 9{J.ߝhXl]=,qEtp{л^xׯ_pO{>;W0WG_W=_7o} o~> hNV P YT gͽcRG4 6\njR230 չ/+aT"$,b#Ӂl}X,)w'3^K͡yy;CK۝6_7[`_$:ћEmi,zTJ(IKx7lp 'Ӻa{lފcu W1)x)m6 ^'s7Z%˩7Gy}n}飍W7[f}W=↧^\K뉯8ѿ{/}.Kc/׿=٧wW]oqݝaq tKtr;񙇌\\;r8;n/?~xw}e *:E-5L3Z֪$ 64&YZwk Gbڡl붷[%NN Gp|ßs__O{{7^}_;qP߻>m_w|þN55oػ] y췜==vg[K^r;-[ԯ{ӣWm}89;/W W}3ff$c@R JII>f6lp-x9ğxQEF^˫^i0窓΂~E[~ퟷ8K;[&?&_w_W<^g۟583/WoǾz?{yO77h} )Ol8tW o}k_Ug?-S p7);o֧݋=g^? ?K&w}_%B/?6/<]es YZ]JI]VՓtqKxm =ȎM-EI=)˰W7K~6?ySc_ZM3?ڗڇ_x 3ny+<)ڷwk;Zw7f-gnKt>?gqcXǢ>W+(8LN8V=͸@O 55 4"BdZ={'eV8|g}7<GZMݞ/{=3Hd5x=>-~;}_s7|F$((Sj9Li3Sw07EJ3ԩ~E \3op{|;~Mkhoy+A5׾w}uo :W5/ 8U?Gw}3<3ϸr.y|oooooooqK `}> ᪱onf TGfZ&VSkTg +I\N>D˩MzۿnͷؾD]x:& *r7|F"ډ6ws:cӎrԱ#4!OЯͿ/.{͝\og} ?t[|w˛οa/yq/~'/k7Rg<~x{;_??z\ʎX~&pxhG?T.Wř<+Eo{^??rgvʱ75cbI *caDCB C\ fP;hrm<>}ӟ#}:O>ɏ^ɛҟ=>;q./_P%ԗ>>{;:|;>wyܱ_yҟϟN7]'_>cir+H Rĉ2ݕƊޒCMz;}ʣ=IƓ>;_tnr?C?t-O[q%p}G\ه#7?^>?;x~9?rq~\a?Kk_~5ʩ7=%xsnz/x}i7c^1Koя ~~8I6\5Lu!3Jef e$- R>Y/xγ^gg_b;_/zMz_u^{Syk]Ƈ|7}m\7Y~ߟ~~8y7z _7}C}?_oexrw_o~];k~C~6p/^'qL>wpUՊ@ sE9~)byduއ9jkڪtk21:}[Nozv~uDO}ns~7u~]EG_g]2T|V1 WIbif!@ѹ(`wOǏǩGxhǎ[%(VZQl6\w@e\@<+'d\Y3S,`"㿋􊨍y_wx^ko:)]B xa]ns|nB::|ϿG|7zb_0&;*mL f̋Yr|׭%zfli6lEUKXb5ZFDqWo0̽Yw3~wtcY?r_N;k>Sn+}t5nqP>%s^K !?OyWr-G+Ӯsn8u>򡏜^ܯld՛z?cdė>c3Jf0BG-uWڸX"4}H3;SCl7l-O3N/B=͌f$RU@]<ys{r#o{S>W~1Ozzߵ?ksi Oz_fK]5syX{^s/|G+g=!;/gy>r0W)vqZg$nae1l9\(1!:,. $z*6lp24ޥt˫ݕ/e{H /Pnp `]{a 7.nIZ7CaÆ_q7LMo;Clbˆɡ`^glp٦:Hb`@)E f53[͘fBmذJ mNq" y)]ӨBrd"&FdÆˢP D-i5kQЧcѦÆ bϹC{x5kh뉤m7l,<="eK')B4f>mذJ,8^ dj6LZ1ZAn-@ւS$G. ZazM452{*† " SR$:t+=Iwġ0L$:i(aB-H8BRĺĢRЪP2!C@SL#d IC ABf`H]TQ4R@qI4sE^:;sŒ$AsH,HOd$hnD1D`fe8Uw`V!& f42&@$#a,'BpM!2@4>;A5I)#`JtCBh ,NK"P̜Tkr?8Xf/3 cN@O.=ᄙzHIf jD NzABH"93;$e*Rh 6.^E6f.0+xRgun֛(Lٍi:YO_OMȴTR`4cF{=*$TOZN!MÀP1Ԃh J)HpQ՝`nւٻ26+Rep (ZfZ%D"anٚ$/PHOכY9@#M fFg%`f@ J22Y̲w 2RRf&ļ0Y'H* *&=,0Łnּr*)yʄ`F'41$Efg,$RDFI 82#  Tt2EzfiˉHKyA:˩m^h,Sd=`ɬj\ ˅y EdF+{9$SvBǓ{qoذ8aSsy9"'~ho-7eWŔR0rA:.fdd1u+%Ѓ0(@ш)a|\云!737WXZO.]ET1h1MPhes(ZbY)͖h %sY7@HBW$;dfP=# ZbLǔf2n4ҝU)3 X2"eF$Acqψf5yf#s*V̐SGv n5Z"Fλ#s#za03 43B!l[{uA-8S# Ȳt`NB4cuex1Ń.C@9,[dٲA`4 9j*#@;hU$V͹MHP mJL>e&z*}Ŷ3RR%Ӌ-IH"I6YWqS|fÆ 'ͼDtw'8ۼ%N`-Y;[:w4֌ub9{jX<p/Ԫ (fP&I(f\ÂhV܄ōC ؝rp.p-cݸ,5&*ҏ,32Wű "C˱D Iz a0 VҖUYpagq.ƲžF5$XR,ΔVM)MY bZ{fEA(j֣"3נ3#R*ʸ ,A-ݲ& ְj]` D*zR螀"eˆhY1z@&:`0X| bfzrSG Gmp%GM|I1K\JH#Z9Tٚ Xb@NcQ)lݻaWȦX2WRwOIX[ Ob{Mhb :MAV-e`݆qJN[Y-o&)S(VMJۆ NLW`*$}}V,|nE9ÝuguaAO%Di+z/BDȩèJŸTqh V+KOrE)Er,fZ8vQt/^xL,5n=dq*dGRiK$RYyED87us9Þ))*@ .fKi FBP1!wͶ/MB0+^2Ut0#"E,C*4TYr",NwH$a6"9k 4Nn$ ;H;&%"RDU R -=3iF*C2(蠂 *LRIfjCEP慳+J@+#ԺCHs1 ,j.#wwe<-04,GzBC25O]*fF2zȢX@Xn֧ 6]h ]RDs'6v`--}䨍ڱixtD[,nvܚ$-*ͰBkX+2 wEr|՘‘'0z*bUX8~MȫCI84*ZN}/P IDATBp7|gzUۓW?e+=[Un:PZ$lkIloT,F$mgKޤ-RP ZXT8o80fC{N$ q\rR`5 [C%M[ DrCA Fj9)i'BBa``1(.lǺ!;vy@ΨSX"!U[?R2`!Ej˘D#؞W[K{ߟ0sXr9~s::Ejsxv0#h ٭)YNQ{8!%i Z5%4 CYԶ ZKl/_侵J8eȭcXҠcfm_M̀SNjm2egBꪮLDZ<A{n)aÆw[QB^JinR\Ӟ'Sz m6uSǺr﯋ K9x6[as26cRӤ텦)XCZKL %{*95J)=#a L ]YKOI'3}P|V=a2D1Kh]gBJҼVA26Ǥ;'>T* p,&^" 5{ӌu33z EH˜$֚ztO0%ԧ S {"%房 ֚=5=F"J" 4%S7T`f{— ؟p`@e[`Dœsm94 aE͖ZO"' 1BZ*# `fޣBwN3#ZfR!XwDZ@F;92OS#iaÆ`nʃ4s|1O *6\O c)AZQ1#zkŋ% TBܼgj]h=/eV0"-Fj-2ǚ6T/ )[C)À::FSIl{5McŠ)`nSoU/VFGh2J!s=4:Ɗ6EiVvd]nDuuY]n{|15saebpjtcpOJVG!:Z蒬:`NwldQKDCM3̡X1Eik@ [,jEYWnUֵ|g=ed֝6{FZ!W4gsX"hZ1e۟("a9%4w30R=#"efu!vF)h8,$a+V۲I=5<+fk}#}c@_z}Y-$wPzf s$DSfG1W4A eF7+jÆ W̙lG/^|띾{kqY lY[cZs8Vr=KnOŴ5` М{zh5q9`hQi4v`c#SSp{a|sKr,FËg5qH'{w73ldzx'ݭfD)\ bp cp0b$V.*8/>s3w;EA^jP@# uI3ѼԂѴ5f)-ƁT =͝Cm]Rb +uޑ8ݗe9Z{\*fiPjPL[ ƺXw6V$,Z8XqZ`g9 E sqZR9{A̬E3S%V uN7@V¡T8S4p3 f..ւIr,6TբI*iXErQ K_lѽ. BL}T$KKJjQP R.b9tzJ20g5.ow,R6l?vl}/?DeĒ[ @`I̅nX#(MB`c h{ n%=S C;cǩw(}k@R1UeդlʖFaliPgO;^x۸N!@@)swY,JZQa# [3AbjhY Z. j[Ds9Zrm);]7u܊اZ+s(*^kLA6*-Xw|X9;l]i4Ʋ\jrݸ0Vu1җ (sZA %(O ˅Zky$%{v)6(nH2}N꧀f If"t"'>b@aD:dbs]%Ε\^"fDjm4m/)PcUHtiEՂ"*ER+*I4zڜ ё-ÍohRytIp"g\t >,\"A: 6$F1{ߞsE*6䑥uiݷ XߟMyu;p`V!U`DaSコa՝ˁFN[rS]V.\[)֍cH n$ {SIpâ2]VGxx2m9`Q?5v ZT&'T{kl1xH-ҏZL b1Y/&\VuX%aZzbT4 &Q= kud! ]H)Z Pu $c=݌ų"Vz[[Y|GIiDu -8 uJ+X"92j2Djz[hSrpu!SL[LER[ZafX w9Q2-ZVrXP}=E*Ʃ55$OLRrόq{[VbzJu^k*`5V%P1.qB"{-%ml(ݙ)3ÐuBխ̥jf}Æ WNɹvI=H 6pQU+$?rԑ]( jDZjӍŧ "4 Ij  =Zyh #s:7*ur(wj!6rlR\-Vwե>鐙}ke! rr#i^mKQfYM.HdP{x)-:Td&&Ʊ@!#X3^Y-XZѓ0KL:}ݲ-J_-3l N>0Sq,*I{C'NwPl i5etc͆A@(},ޚZ)K*[)enW&0xj P-F;5B X\̺jV@ ӺAp|V(auZ9A Ŗ!%*+L ۔bfJno :ET\.d%%.*XMf;;m%6IlHR;Slm}\ms\-TݰaÕ <39 +(uzrtԋq{єmF8IEᇶ0V_O,4c(@ާ:UXcƂu堈A5u3VZ]qSg Eˢ:Bi{lتu9b깷f5 wl{iIq1Dޚ9wLaH\P8{k3uPe ܫcŢ\MT{0PZf ѓŵ5t¦.$Lk(mRS.gW8(SD hsLEDd[g9[]Q,u8J&V VSdxHqՐs;0h}HPE-R9uҬo j.}o1BjbeTYd:mک="}k{ fkӶֱU喫f_|3+ jEڔ\uM "Sn*H+R9uJ6Fnmذ1z*}_+_xы1*;"A*N,#.T5.ee[X Y%u%-7+Ylv]78gDIy8(B]|@kUx2/ 5yFU6?pVlc2zcTKћe8W(Ա]3#Ӻ;{z .WghGFe`4DōZ} *V K Oy'W/Uś1 [CTV暶*Topz] "1mp%lK :B6=>ծұlv띐1 \648[o_?cyѲQ Pl#J+ mU HrYFu񑐍^+f^9cuǥ3 oeL:5ᗡcmW'9ncmTuk)Ul^bUF2{/oNN[E`E)}o{{9 _$ 83$ ֵVcIYU=ڑ(" f}p*%nct}J汉u_lea۰MkK#@i﵂׍$U>>XꦵiçƵ11:g *.ymC<#T6җ6.DkL 3i~_:u#D+=1K @jkլTfEDe9szRr#~j'Ѯ#fp#(a(s1<+5y_4KyLt#57o ٽaFUBtQ X[d3W[#2k暩VIRke|v]ZuG3^Y\YR- f ^hϼܚ Gs˹jOG2kffjyJ#ZXqmbˀ2ن8$sk9RR,sФO3SR7Ǐ㫿TZ_~?k~eoGWl/?_??'oCb{ы~AT*32lFKW"j# U[rZ$U,:mΝvasAk?5> PEh,Wa8nx$569!xX yO(㾔rG9C6œFZU!Tyt}ŭ' O-'fҼY}@YT6ۙa#Q%h!sV '\ UOO=}Wٯ]_h«_|7_[~/ɷګ/_~zٿ}տU|N/z/@yB%`1vh< P~BKfLs-.^R-k7f6=%ҝUeW=BumNݮ {ZxuP3tf"t}1=U6D]gzԢ5s۰Lw뫙Qp#6zT3V1ʏ`s9WeKB}&˦Ys??klgٿ+f__ٯ}}g?<~[C/>s`d 괪;m mVW)+஭[&vd}'ŭyVPr[35gm>+X᩾mJn#*,^#vɹR wC{{de.%Wrke0rs937x޸Z2=4ia]I4YTY8лbSU5k crc% Zo)8*Bo'pM.FMh%53yQF =l" 8yTBj!̵Ҋ$a%s-U5s:YlzKѬd3j.]khgUyEUh%QL~ eYiH903+8ڏnYs2Uznd ܢP|VPx0mWq! xzS y3֚sU%yM̬23 *ޱ>[kk>u}h߮?ϸ~'~'|o__үo@w|EQ퓎uiZk|5&zZX9?xWɶח0" }u?XpZ>zWI]%j4fc3Δ4%4 85fn㽺'y#U}6pifw7m UFiug,令+*;o.=<Պx}}7OVιVwJzcsM!)U5Zʬbk yxԛ{V9 $٨2[A3 a2V; IDATicwbz7]E*\l!fff6soH66JU8uLgYB5"ֶfxs2 kV} jJ:l֒8]"!6wP`3fJc>ޕ%fYkV0,FԾxꌓܻHYx46j[[[V7x1Q:!pgFɘfzSe( pD<3|2% #Ls\wM'?s_uϏ s>?s>=[@o|_/X??[_|ߏ|[o~__l1н5}RGv}rkUQ2w/o fEN* P 6 H]2Cs %U8J@sn5E}fx}g Q [fEYwKǬ3f0C v, "Y fBbdܻ`f[gs;E3iW.Hi),6F?WNI)xHP7IQb3Мm@dkV 9Gk4J $f(Lұ0*J3I8A363D2ٛTjdGI@gClWFKH~^og[d08F?f-4f>6 FZDq.@o:gyy3cB{G dv;ln"ӐfПdu}U5Ye4死5 Igb3Vڧ^dgox;W}M!cſK菾so#)›G>z\y_{ы~jOtwIIH{ߍ5o=!+=*õrܳ9@[ں~`1vֲJAyb[o=H4hrm4ֱ/]^_7{us+}]n 5b飏vL0g뫚cO4mxBYnVuRLK4_TMYlN32qpR׫5(Hʹ,geibs]{pѷn1ftEA.{}ֺcta x1WKn_2DD[WU)Q2Z5J5+rl40ېojr}b*']EY3 8wl''^]u66=a8kv0jU52KQ>~jvbjfqzmIk|}c}WYɺ{o0 񐡏nOsU ɶ/#6o[(WIUh|ę{صw~O/~7|~O7|r ???:ȷ|/|7og쾿E :;+U$7;XiZt蘡[8fRF4[K=p1gTU46b#mLc0RǴ1*QPF}g,7G*#|tJq,tV2 w'[S3ITʕx<0FRT;hU]<$=U(V[aAA0t$q\g(wU,E䜭w[)hd"76/:(tg nOHUW0sSUh( gl?c\<13)Q +WKEfidTk.|UHYF9_I "ER5w窱Xt/#f`[9e$PMkCUi59{k2qB3EzsdU [kr5l}O I7qrh%3S@cSP(+.T=K>^E{?S3 1 f ?rZU߹9GW$ n3MkAi.OETeuV`Ș29A 35ifVd \vpD{xu5fubEi-Cifo sNtgx7ZuSfkEA2*wn*HI+ֱkN$,PikqF̕GTwuFa:Dfc hY=*f."Ajf֛U\MH8Y֝hci_el}xJ@ ? f&+k ͤ1 xz>Tc8EtVP4Yd/}7Xfwww\nڶ 2zC:Lp:fBuLu'ə=\h4#L2BF̰#y۰uKQz%-dL)Cn,¯MI9yH5DVI# mQFiI/(B+K級ŠE/z'Rњ.m~W?Ai[UnGj^?ΪNKD 8+r4`Y>zeՊmYg|\s2 Xe>@\PhR t@QJzLt\po9шEeeэg2 EXŇMX0vݶ3eC3d/Y 8W6|2@RRtgBZc$ s.KyA:s`\Bjֲ'5C- :*R C"-3heHHnˀ**5$ Sn H%[ʍHpr֐#.RӞ A*[E3sC' ^ FQ$}PU&$hHC33K㱊ճJ1sEXRDB0mU:C8q3SU2=;J̍g' }{9^#7u- 峊Ie_yR|sHlM5g[k:<\I;cP./v0G>3%&uz8Q p9lĵ[0c5î[OhIb=NA|M0ȶ/}W]7^OSؚEFՌp` Ke~FsٹwuZ$`x+TBh0N2nu]P Z]"Y1$U{YAPh,yBfy"[Ke>AmT3 Hv=*kE3/X$eyH: l晙o&2/g̚TsUdx,D,š{*#af\:f hR v?' *h~$ޖ.aO{B!<'{0+yo, 7o:IF8]ӶWs?ve{fVχ6E/z'#]D7CRE9 )mvW5g>@Z A/vǖﳉ$f ;.*z_ !ܮ,~d}QNQ.!ɚ'^YlKE1PX+J+eC"k_7άH߬*AີdcZFe[Ge>:dއDUVKW6R0i<zmSV0qn;gYުD7ZѮWJ@kcfgb Ͷ:O0%=o1zfm[B<֯,Ti%VAZSH L]Y#akG&Yr Uy9u+Kax@J|} Hmz+(y楳[*^D$juy_Gu鸌*r~h^KmE( Jj,2iYpIIsPBETE3ҕ{'p-'YgVǴf~Xc&F*4A벩9Cp8fR~~^`U)rƨl+IsYwV*5 ~ OGl4N{@v4B+hpwҚye4ZOOdd6J5"H0z2 IUXרs`3E`sY]m5e `sEuoT}Ԍt7fҷ-V z IҷNBIzGF3*HnㄪyX>s/:n23 oȅ2!Ge5WR"jw9VEz>zO vsH57k\RnY̞鷇ZE*!#+#b9K0Ҁ̘T세$ 0 ̴B3TB:q/zы^I2+ tS YxWMmR$Սnx&ʙkѮEu1**C m͢$Ñg{ފ㓺٫[>MTevA$1Z\q_+#wvvQ$xyZ]pdWےձti"PѳٮP++vDlRO;OzUsMThps3lFy,Y$DuJ>[Vd42w}UO@֚(62, Q J:t/GtcudaXūV*(#KFCsaln%Ts+UuT:ػmG>٭2~m*ܭ qY^F=̲׷{*5GDGv[=\I7;Zխ# Kxa+t ʷ5̛Q&yNc-Jl&#\€`8Q=/Ӌ^OV5+f\ܼ?_6 P$G/u4@7iVqq8Vi_*3 5Jf*h;֊ygUq{pOgsa]o$3BÈ. nUɭ4:m;3@xip 52>q싗_6Pk2˅*V@y ܼ9RYv^nhNpGYQ[ut"[6|tJr9C8saw%`"NYRz&$PA^݌Ơ1B4ݫjχw2ZLYfjԱx  "b(7ΨȺ8XK38Wx}y]:Jݑ:7o,{Q cnT"v,N_\ij`^:XX)P*E/dufX\(Υwznk?rZ/Y׮fr?,f̕E8,mn[_\0QFE!t6uw덽}$UZgӛ< ud]?|:bkUwT}R}$]*]:ʹn4n}jF{mQY:|0ngn#ukUuf!Yp̙@7QP~WA^k킹4ǽ]NBFT9ָudU&fY$un޼-W(t[dd8Ivwۆ̔Uy:t?lt]GEs3 y~HJFykD Wۆ}}վǐtwյ[˻0\ [h5(Ҥ[JSІ*іjMkRllzRŒMATM *B8-|߷|gq6;`ǜs5{fb1LPeET oMHE5̬Ff3skQ4)b Jwji!R@XF6gW6W1J޺ Hdj<#X֜8CsoĘ3+=ݚC.;vL! G$T2p;G7_"+shkm!Alio 3C"ӵVرγ7҂JJ}+. 2o*x XһVYku/sԮ Bvܗεdʼn_+pUl=D}\{K!Zg4զV?7nJQAÃuE MUp XӅ+R9Bv5\nY#]Ec.Qޙ:ؕ<֒f, 9$cffLa \SUTҮOӓwmfTĽC =O zU͋U+T<]1Q¥# Eݺ75֫;gXMg0"LU"&\KGwO$,A`3ߺ1Yl(U*Pil ttMV @w1A d3KTT TY(6[flQwh-r啊(¢50[%*g/q,Π+f2"7ou2+O($RMNLIfD'hkRxNHUd`aɶR\tʌn ,}YB.I*j}/@JȪԤ% Ԣ&Dy5%E24J$+"Ȳ Ea({H&"%rePU%͔b4)Tqgf)BUєEyUejh'З06PAB7,RUTW'ܓ^/_8~/SUQeډBW oi"Q}7Ws6JiU(Ь֥jҭ5 3qQgw 6eu@b.vD7-(Yk$e%pDUNaP،U*U\ \aiRԬpq7]H13Jeze*šABf5Y%ޭCg" RtRғNOʹU|~7UlLq!26 ^Q.UJJoRZ{LnY),[!QqCn6U5})L^۝QEgmy_aRι]l,36ٷ\»j#<n:jR!6k-)I ;B6Ry 5bV8itW7+@3׮.A1Ja11TպS9I\+j"3JLeo\`\ HAb}vM%edmU$R.Df9<n-!E˪,6Qt̮Ƈm $f S"$D2 1符"} \yg"H 3c#,fyv˺@̂GIU)D4=c @FFU "PST2RE O'=I? -^zƐik6\XIS E8f$sT]8Ek!hJ.v]̇!oyTJoZ+[WsJl( Z-xLfgwݺ[Ev)fuލ[z/Tȥ/nͺ P {(/Rk Kz#@G`%4"aE+Q\A̹tXfFzs>s5*yjX!n↑zDuKqó] {YJ{%m2qT>Ef3b 'hT鋣3JWĜg*W).#V܆gVTEaiΕuAZ%\*xvA;=yf(4űyHopQc:{+DwsgW1[knG֒X k{6BݹyUOPa*.31[]; 3庳YZs-qYl Hu8bHUefZ D@O'=IXgviiY$D!=Eբn(!fEɌ3 ǻ'6JTL5}gUÕ*Z3ᘲ0F 9.]mEJSl dVlg(QugULZSx5גXKg3ҽWX Z%sV1ভ[2,W%،+%[o2C$K\U4YҚ*{cw]a,9BACq$21C\T&?@DQʓݯ"Z^$ *nۖQFTfJZm4&D 35rMFbޚ(gU 'Ϳ2%8NX1+T (zήNEUXZYnQZ]EY3&U`HDQ%2L5(UьTmGN;?ilv4BlU%PT(`d P#YX$) -j=MOOzғ~z:ID(@ՙumvݪQM[RֱdV17r-͂ )E:f QiĘ}Je[KӂQʹ9f &u;loS7lzyLd؛!c59mUlz#8;MĶt35.gX[\H"(7W 9W ̭2k-t7P# Q:ZCZp= d,Qu T}V3!ʵf[#8)=9n&񆢜QVujzAU[],mHA&B֛.͵Xyic"\K7z֬*lݚ}JQ.=2S,X8AڋA]HԭJ)d{ycnm}274,b3VnK8v7}Jm!+D,51W2bU8c$UVȧ^E3Y?N{YDDU[eap-1C)|66ٛcoy;w=X0.A6Z^\r#j֡c;Mݻ\7y ^@h׍\kz֪*tm`%6F Yrh.̪(ꘜK^`B0%RTU՜+25UHYu?T-H\>_.If."U똕IT@mͶ^ԲLtH%Ś(=b`8fXᄛZ(6-ATTs,9VT W KU֚9McaFd7>)Rd+JL1[jޠ9^Zl^Uy dys\2 QH"1&#U5VW(ikAyftͼ2"sgv _OOgIW|֧Oz:r|uF<3pޯ*IIuî3+B"ܒv`"+U"x-Q҇\7#ڕĘ9ys qba~$شiD_yV$vn{C䘓&f+9ư\ ٶLmS_זUr,^)UMjmkn$Jd-{B&*q(us9m]37}w-M"V.ƌR8l. IǾoԜR4̩1b3+bS[+P[s\1EĈ$ukJX%@[KL3&JfhrlV\"Ik7!sYg&UlTU:mH!M7o~m:VKcPU!s,jhgoY1&;ܛn 1a׭r NGZr1Pa4G$hM7stn=3TDDe#fg?,4?nB/_?~W>z`3>__ U?ǿUaK+}_~/?8_S>~OzcwԔ"nciƛ/zݯa,̄2SL]]d\,\d1x64n!dxSv[7|e~ͷn.s.vLʳkym".;zyCw^d؛b&}q|º[97g/wyYcz^/n8bޛ\6Fh^cJLG3+9/=˜a<#Jr,V+HY*_@RM2hjhd>&"B6Ys]ε4R%%f׭<Л"7n2"~9>xWstpMVawo=[ZY2(jcn.g|/ۤkky/[zTm񨈺vQR̥r loDz3EcLM#1*T3$cLn8B.dfݏ7`w/JfTfxPm`fo-7\յVrLq",Uz:EZe~ Kk~g݁˻>c>3|S_׭ /k~'}ϵ=IO٣bQIY,1UWo߽JH̥ԜUB뒰D2 T7wTՌjjuF2e.~uͽD8E&&|isk-:FGȽm招v]#y}1ľȼ-n4|p?x|ClMuw43JL\ L1Y_.ɬ 5=T4$7socShܮxcAId 3FPBe-+ 95s@k[ !ɂFW*X %*U%ԭ+z`t0Y"AU )\tD)]IfS&+jP{EH3y>(Փ`,F'hO [JFS,@E!3) &qhV,YD];'Y+]żhpʲx gVd@܋Z 1V 59/W}A@o^|_{WˋW^|->ԩtpOz3G)0P{{9KG{1WS%R\y12=tMk]"wȬcɥ{xyõQ k @鋱^z2zT۾{+y?Jkwcrkv;V)Neu ^$ں5Ue{W Ub&IHGBKFXZ+[u}{BDܬJc57h&d, f^1E5R\hp?2 Wɕ['VD\a]* gob_]_|`DIwa{6J;veñJ]FոgSqYěZ23"S氽UnU,ȼz[77sdyGdFJk*3w4䌒C[bw%QѴu8R+TOj]n(#?~'??~ _{>>~G}xosyw9ÿ͟>#/7[+|_?ӿkw>___O!?>܆o#_S ws~2q_~~8ց u!VUF{`ITW4kWTnMRKr6sujBǐ&ul*w˷#6o=Wpoz~,C6}71kzoTmRқYi]g6MDqjV=UUuǣR y7~G|_|~w|{oW0F_{y+~}}=G!ſc/_'_o/C'?O_k;~}o~|9w˱'=UUJMq!?n82@1eU+QbӦe|ygR/]ʬK #.DŽy̷dgWm-#xAS Yf"l[gל#Dۻ,q+3@t wq}Qba4j+a.dj};bZ ALqg`9Vre PGz̪cak,Be J qio)0ejsW&2"#U ްĝ8 k-Fj%+63( UUF`%LQU# bw[hj,"y.~AJ$ݥ "j꽌*9FMK ٙ[hh梕U0VD8BZU(౐E,*KTݙŤ&͍&5B$#LUB7D&JݦVsU!*d%HUCV)e*$T9$RUOY+_ M_M7/?r|w~7~[~{?ǾЊ2/~o??SWS^%o{Ȭگ/}_}o?x?#o}W~m͏Oz{}Cx|EѢQ?wpK%&^XJ[Ŧ @1u IDATcֶUsyk?[r(  D@CњVD#(ZbhQjDM(1J0≣?LӘF 4,;ϳZ}9?~b/?k=?zys! NJ9p]\.|w̸|~w;cl{?qy+Vuzqlose얠k?>}aw{̨=㌇k6 F su >Z{Wv|;LvRmlu[/_Eě}YR=-]^My9Z~urdfr~w͙Z+խKě8|GG)enrCJmT0­H7`>\n(y8zS]Op+&nL0gN㒓&A۫wl`saӑ%2|L{̶au_peqȍn]m I+{>Fl-8ݓ9lu]ԀoPQV;Qv;U2ljDԫkG*YYW69; '; kU0Q1_vc_{r_+WK]c_\aIt'r}_G\~oO۷?s~Ӆ?5;q{~'?K~ً)f<#<Ïxŏ%HrrYU G `&fhlo;LF}7*R!f5Qpݎ}"-nq4+S Gn^Ǎ6/uq9=ס;opse7S*{G^k9Mݪ9mʪSgwm$pĪuN2fD9q%aSS"RU9P?z'O;/Oߧ?_>?g}ai_s/o9}O}/_OoHk~w{7'O)/_詾g'_7Wm/yOy~~{ۚOGK>z=>^wVu*ʵ`>p1KK}^̫|_'#̄YqeQZn.[yuu'3kH^GOx7?wKv ՙ.ct7k3h[o鞙xq3[r}Va9|_ {2~ebƱהwxd1"%. .%Lkp+4ZHVNo԰U=b&6Vю>]-1b#ʙl l0ؙi-GesZGr< Jc w:UQV|nY9L7u.?lLsn̵X=ÓjFb}0` ]F]W# ^8W4U,Asf 4 ͅʲ|HàMUƂ)V]b[mUa֦9@UZ4Y$-4Ά9ʲ-FFh^/~[??[/O謹o{|ƯZ~_{}rw__MŇwqտ ~/x_Woz~~ׯ>]/e?g]߿[[C+w7xŏ2 FCPOOܤNEHp 5uұn{eSy0ߓ-i8j?X7R3|8>  $@2seM'A=|{t{dV uٜ0}_uVW~}Zvf8=nFafiyr_8%҄7װ~ws/x?q}dnʮ}p<\0ݽ> }Ue~xȼ3l쫉q~0þܬJen;\lّj6sӽ:Z)AUxy̎6vtwhְFl yιбh?5tT"$pt$Zˏ2r _u{Jnܟ ,=^t%c\̬t8ZAxa/ķf#"L h(m +lZuwOM:8L<ոNI ([c|;ɗPx+~?~= B@I~(aA5x|&DUpUX/û۷9Tվ 5%KiL*RG1z?LU4]U#}lLleо@iUl}d4j pNsk"`sBblR)q:=c; 7C Cв4Eҷu͵ Ɠ-'oW$sjD&l FCsU8ݐ#N/^'$eh U{fo} #j+ @69&hnfl>y  ]B ZDvkСSӬIU72IHZ `7<# N69 kF/U#"A؍@-Lf]UFkIY G6i7P۷glИ =ݔI$ܧ}hݲowl!RWwoavە 6햸q-FS?\$eB_.آh>T.ü>|ӶPx|:\f2iYm?\:XI 1!T5qS ͖bX]rVf>aF>1nR bHa0~60m2WQ{TLWд_6ztCG8NF6z^W|l4M ,] }d={a:2b汪z^jQ $=q{[|eotXlӴKx^oul>Lׁո \r?iGj'.yxK[}zy=i% Gky\6=:B_bgʶaۈ{zݭaU9oݯy P6k pA\Ʊ2͜D\hy^2/Ȯ>0GUgkK*~JL͞vǫ` 0B==jqѸqIn,;w͍bDC4Mw>߰/]FJ]2ZV'$43m#WĂ#vd꾇@3u;- q $Q6ݹ ec˅oF~]5(lSqrijw} 6x?( +6'yWʍa[d2=";sEmUt+-bn 9^oWNUoA9Xhe ݕ9a\ApZe½$L{sc! WLKKe}G5x['fȈg?aOӲq_uϳw{WVacxsɪ\Š۽:V`k|o#)7uwg c"SFT9h(qzfV$aӁ[a hkuY ɭT/_)NpH7d:*WVɱ +]4nal! -IZ}| +ȤjH tʷ؆TN(5ϹDw7*+Fv F Fn[T \UN` )Œѹ&բaHsJK\D.EENWW7FwHʩ6fYxڞp9ӸO˅Gm8 J7-)Xcu2+;f4%ј{%=X*{XI0y"_>Oks7L{ǻAsoχէEJtK|N=\)ﺍ3ZZ5iGѝhY՘#S*8Zl<`a׋"^h}_UӳK;ڭ,,b`Ue"ܔvF=ltb_2! no#EvUvd=\5b|TJaք5qd;0}f6\/\Fd%ncu,Org`h7܎ی-*3+Sp^ nvcm43$E38ۋ[?p̖{u髭[Q:ƕ !G|{p| /cUX>Z2!uČUgN$Gĩ7hX^-_W| ]8|FW`}/F`?"B[ k-w9ʺoaod-m6w{x/$^#yžr]{Ϟ~8xu\w7 <50|x Xs'ϾkEfpK[nS W_Ⱥr{fv'+-$@cYs >9ѵs?P5ȕfln AV8x S'aa^(&$U Ox_"* @WZ `5$nlٞӯAY&6*K80| sC.hU0Чo=epJ hb8擎OEA̮c W7Vi:ݱ'Jõʄ2ֲpVިH5byW]vRu*2$@fJ:bQ=}{mAR K:Uc >cr76jaZf鶛0齪 FtU6NVnӎ8|Z۶qD>unò`lb_؏tdu_VOme%=^ x6:ݍmjUU]~YqWWi kqt;Ě}2(Dl&p-Q.hUg LgPU:R[VyƘ#`G4`U @adhBp),\De s VKNJLSe>EUp2%4-X4:GbFz_2(Uܛ>i,[m$J up\Xt04it>c$rtL+-ׁmJ ~` ? Fʈ:[xW սy Y25ZV1gkm0K8'k-4u~m8\>NLJ|P@ d2dC7QFP#xO1sqt={}:B:4/;ȸ}ٶ1't5=eџ}0L 9Hܞ^߾ ÈjcFމ y,;nvܔ2vh%.St[xf5esV{ j>un!8N&Z#P vsn7ݍm6EȈhZ|߯[ d׹0n_BT/~vl,P9biro.gߵn.6Z=ãl[Lumks QNy렛 䁮LѽUE3{ܴ0}ΌmzCO{^ְX]j(nUۜsVVmR}a:(mGkI`ʬՐ/p I`kØVB cpt+eq<82ltK()*fg1'(9U>%Ku;ݤ e`2Е05[nm\9 J"S?KnFDgOvwUUֹ cYl5r0X~}2lim"0V3""8c1N)ht 6-8dS t֪AL [puf<} -e;9ý!VmddK.\bzĪ<( #~T\^W/}6;H>hCZ=}<f2 Ųno.J7ϻ$6Q|æ-?d|^*+OOqMܓq_vԌQ#߻j|w94Da3>Jv`+q9gQm`8ڶJAۨ}y_7],i?M6:6j՘ZF3O28t_6l><;td@9(1®{w6rVFV),UUfn4{nUBcnk?|^aǀipwǾieUra8!wQZ+-|8u-B5TccNw_TpeCKöKh_l*j:Obfv`[5 $ᣍm*Clt2V+k4'C]jۦvOl-/0y,b(-D) $z?4"t IDAT8j݇Qx+>VשuAfF^KB7 sy7ΰa1/wnWm2+$ m> kI@#fo|O<6l=6EJBZzVf]]xiM gQhޗ+Xa+F32n W@VzaO n4ٌͬ 72XF]}z()k1b2} D 6N4ʄ /7/ouËͨnaG6E@p3k-ww3έǗ (wt[ e݀NU\UjD8GzKH$faݧ0lہ,=aB͙]}>J˶UH`zR#̈H&hfː+ݣϠC[Dx闡cU&++f L~ *|򓯗+^!޽}{^NS{f[c2~jL_ӗy6}l!}6_]'3NjOI|/&Yms~܃ 3ot7=lYy#gWڛ+[fG?2yjx4+*|0ds6["խŽu(sq zUU\䇷u&/*Gv۴:~޻:^, f>ݝӍ$a9 EL@+~0f%IApgNw&wu"h*& *"4#M#`ԙрydg jufB:}- $t?J}$203 \c;FCU ɍpt$JP )'6zWWL3TjCPUˮa$-܈:9@U^'I+ ~ zl#ZDWWz": ̬Siu?udoJ-~CV=^nG;)\0nGʷOmoqci_~mI^cu;2t|q_%h @VtN/(ngo=mAX'0W/*)7\'O s?LacE@nq|̵%#؋ftU ApYCU\#$¬0'Y )2U86gN'ARA.EeQ%{fv=L=lsN{>V&6g%3(AcD+"dX*p+ޢ ӷY icX%urUe-P8VL}Gx 爑`V^(aUH 1dG\Ʃ֙\u yӬi$n-00npKWbq >WumpWFy5-(갺D{۞}?U,"%l#`/czUg Y͹Z4].;_6 ثԵ̬1UUi}+hL$~JwV&2Nю]gr椹slL;nWBn?d2ۃйsP-X#7cXw]L&GLYn]VQ/lӨ+Qxdžp-U7HF{k, EDڧ}=411@f\^@B ~wv h{I->}vժyo w&@o<" W}zU_^/it9k| _O}'Sr6M@|{.T=\X߾Daε6oOFokWE!NWp2 HD5KAe b-W+3Hb7_jG\]4udWu0kE"_IնA)~^_ Z1\0]7FZ0kuC_ g(8!Uyd&1+|k{됺֚H~]vjV[[yǀ"# xXUQ#pY.Gz53f\ݞs#DHܪM.9х$\Zډy!rqXP)[ۡW*dZ>BZz3<Ϝnjs`HoSk~8D۱MqC*cT.;?z5yEa{R81BrZ=~ 1qK 5)As61|"jI&rw`\h+r +d_=Q3j]]ݵ6G›W0{8`TbP4F/{Jslۻ_Gv56!{@<exwF*kZ?`/s+Q׬Ő>^ŠѩzNC89O1a6άje !vvW㇌gDc\k\~:|A\ -kZ1W#[Y}$,u&|^%?_ƙ9[W-k,`1t>U}E! x^S$S& {O8yO\ MmsC^9fUs. 1Zu 0}DUsǩ#ő^a m(rH n;|Hï+$4#r sIڲ6s4p]zc_$rVkXh2#X^=6T=%6Ps^[-xp\!jÛՙ##y43|Uۈ k֜P]ar1IcYkIсx5kФ)k@wb@G( {FBԞ&$A"oTf]%]^+hp3M Un82Dw/wҿ ]~_ey?jwC/ǀPF|h@UZx sףϫq5/~uMH>b[B߯t!?_:rCO#z^~_8`su luzv7?zHQz>r=j_b0#s!d[|-?<8,_5۽}_"!Íz]}~B*u ޤE*ץ+:宾P4Mq_ h)z$N]8VpÌ2ּc "9ZN8XvUchuA 9V3Z;]nxq6c6JjdV\J2em7Yki-A}]皀m\HtyWu-FnMSvՓP9_~P~Oߊry``z-,!b91| ~1FYZэ:}Rėki-\ s*|=Ǘ/׷"jO8V]3ۓZ_Dd=ZqVckb^x_4tD H̶ ovJA(V 5/qt˅! e< pVZׅYCѯ #zD,4AB1PY[ ׵G<S8k"QKՒ{as\qv2W#CIX !' V+l5{Ueq|Rɐu ]9Fd֗,Iv(f%#C&!, 9lvB$ϳr#;Z]iUx__:~_\OsvoH?}?'o5GMvJ"d5RCW_s"T^u]ƚc Z!e9&}7 =g1z 4b'c#P|" l |Fȏ!Ɍ1qת*E8U.jmTfZU"8<9F&PF?|\1k"#gM5S_n}ox|p<m&񜜵gi2ǖSxNu >~\eso}Ρj5F4\Ke~ Ud q5aljclͪ!+F $+֜3TUv#QdU˹B|k~2% kIصI5wӝ1cg9Uv}13)C!y917̭^ْq1ρ9^׌q1[G1w8׬:Y5g '}-VĒ>áb̀TFC^p|нjxMQȡЂ]2ݝkņk-:ѐ͖0 8W I#1 MgWgdPݻ'_~G\n)J7Eu |lpie]#z_8D^lrO߮y`9BoOH{g"/'`|xdu12V3WZÇ|)ucGLL,CAWśapZ %_K j6kvbԳj2\ˆxmͮϞ u8S#H݆oRF#k4x47$[ZL$) Sl[f7)U<Xݫm1Xsk6 1\k&WBs^8T#Z9Lb=q062a WDUe^U|jTǁnCl.g5 F(3PkPvLn("s09;C6_LWAZ ⶓA~E~cmt *@-g}F'Gn? &B8Ԍo95'԰nnxV ZKG`_b:DUu]#"5;"syٮ22$9U6s9LX8B1 H!2\UpȪd^并ڼ&Vuu}q eXMRٮFۗZ$w|njb[:=F ;۾ǨDˁeym݆jbyЭeVN0XEMPvMDvk۾v8ƞkC#8gws$}OB-v'b VpR*!"=r ~^ ~bA͛ہv! g0QB h{<47;/#"\ne2mHuV);S[ klc.`ua2#âA`ʯAkv>3]#, k̵a+~:f!ġP=oOic n 9Ư~|>o?/s8'x#O@59 :VuZ8`-lԝ״wؼfےv`լBskzZ=Le!U#hfu<5 ׵m?R2/oGI>WHkժu]ysu8(5ܯ)`٠hF(%'z^4&"FU)Ub$kő6Gq&,Ht= )nsהmPĺ:}sw;-sF*R ku]"8,5ն$ܵ{kuw|y`V.fqtuϗ"D*:[ׅd&V6G! v&9zZok `UoE}˫t 73Dm魈"]^270Fڮ-wt"=7a&3ekZע_}-~ok1PGUzߟx^ޭ\8|^i2>^:F\s~5xu'>^0kbXkavH׏zxnzGu8Kk!W+ʝ~O\_^K<ƪR#ǙB!dCxxN^+3'sF@naWR6ygUI_kUytd|xHGV1ï 2*q&Zy:k0DB=K(>ɨ_ůIgLXE#@U}l[Bnkck |Mϵ%c 3"#!|9wC`z5H_^G_aIDp|0+|xN%p$٫"nA0Aw7~5HIFA#B&3Um!-~ꞅ#0PeUkSr^, !b{O\qlv#\ZmXڃ;)R-I${;f4Ւ֚F$#cj„ڨ;:`cFULr+)l2AZjGJ 8۽&DI&jZz ZݢruAyYf; t"-o؝$f-EAF^o2Ph!sY젖eT[N FنBzKE(Vvʈ=bM ށU5fl-fUf;\O&G]gow&pgv6v#gYt ح՟gMt_sܽ=]~o/`jر{dٌcsq~}Z87Se+ ]MLjUJQ\" UU\ǨdtXk$‘{9?c؏ 8FJ2cX^ -_:~H#SǙE"pHeޒcl]ȄWyFnGU{b[Э#EuA0{Fx[1"HE4!#Ɓ#y f"HNHX%ЁSg3,{DD]=jcQ]R4Pw_jH*236U2m FPU9oY S D&7ec ?1E79%"FuH{-k$" 1(PpzLUo gA*I6u+c_,%Z6:@ bـ|Pj6CWe Q0S_"j!vi9e+ A!l 6x H)GbܨHͅ8SvG[;<ݷ}M|Km]4;ZG賷w߯U2Io})O$}KQ9 R;;S4bgv9.1y?iwMatȪBw*M ֽ36ߑd]llp2 at}mEҸOjTeu՞.)V2tMky}MWB6*m/VF!owHۭn HwSQ}qRrF(?ꧽʮ- InAC[?k?O75W\:"/8Aߞc*u"+G,T9an#<|]#q-Wy$#V?9[H^Iko/.쒻6Z+2]p-womd6\7oe;) Գ QU[QݷNr!=}M{g޹t6 UDV& L}Š2g͠j& MМu,69wڕ͎ {bBbw߭UxvyOH!Ue!w1 }_] o?i8&Dю7&w, RiF㨱lqCad7I݌Эd`P}ޖ_?Bf p5OfƆg@a Aݽ5k(HrJ W4T^dPxϠ›;F( \VͲymZ0t PPb4m ~ mr o7m vKw:3捒U(’=wDxvǿ%z$RZnc.a%H,*h6sȨ?#|ǡ1jUwkd*j- PxPl`dÁ7VDը,eRJ)sو抍zUN٭znK4s !6۽O lɱvaUK>2.P#jޑhk'| @7QL̪r \ Ū*9`mQ˽7Q}rk%w4zF2,תTX)vݺ/LFk i#@\@t8i2An}Vmu[@쒕UvGfA3Tm!}a맶)ܫ]]u]-n0c'MݕY՛}Ցc۵ =_-? $r53Lx5 y őD5LP^/A ݇K (oؖ!骾̸`o4T .M!U lxm@v{Joƥu~X&c n7&SnG{wv=yk[mD;DD~ӸQ2"VUEw}|ԯ$+ Q?]> v~+--Uo~ÍK6B 猪ɸ=w`+΀zФ}qǦX6fk2o;'~טTVoشПNfw 7o7x͛Ǻ+2H w ^(qCAމ;h6n+ZF{5oGFȹհ-|0;IFtH;I]R׻B 7%Ap.J09WFFDWy-oDh}U) fU, G2yڱJx~-]5Kpn+;}Xc=FyFu22g,g Fy.\A?/# T̾xaz1zv!R*_䖩GU xg۽VPQ4ȄWD_smȷe; ؓV6)MHS$سLlVk犆d͝7=Fjw#t,$g펉Z1R"Rײf60YdW.E֙X0c7sNeHEV?c=}*2#B]G@.hSe aInX bDQTI˵[2+Qnׁ9]#bX 8{fvuPʜnT}ߘ9UndݑwҼf ,)Fhkud7P160 ^Q"U#$ޞa<֭ erQHӰgpCL{קu*xHPu%ĕ.F]5'8[]#DژD6v4츟CsO|=GéHpٞ=ox)<h .:bo)OYq)i2 yrÏ>ucƒ">]Ak$4t~9m7u:埣y53c;G?tu?eQ k#H-ݼ֜}?] ޷p`wo{k:L&nv.~6fתw E+"޻oF+xZO[SȃJܛxwƨ2͟& EE]l#ת*W{bbT&k˻ aehB TnkGfٴC9%=5SQB{x] B<6Fr+q-RxJ? H =y櫞U }?\9؎ h0PӼ WIʈꆡk9¶Lf4 i#keM82~8SL6"~]ὧ-uVa `]yo!.ٳdFFWͺc݄4Ͼ۔DWMdsΪzٷ Rkn+fUA22 ,iW? K{ g,db43V 7Q̟ $ji #vqԗq8 7 'il(} gE6d F##E@Տ94|&COb!fjOjWCȧ)̯a~%C<ӸeK󈨪i[UXs y `.3CcE;ONnȻg({Jo1R9$|]w^kr#m\T ײ-~vyeǶrD̯W^iw嵐12Fݛ_׾b}+|iլ {M`w|]BY@wu-b( ӳ |ȼwgd`^ ۢBr+dxBKwsRGN''5?gY{ʥtNm돗ֵ mD]Seqw1:Vʨ]&4^Rb^•wBc+`wwWJ#mkӉцVêHO&hY3 6(d2$@Dqd.ZӮj;5x C &pQiws'2]5]IITd~YU48YL9<6^)nZ#Gx-* yϱ=ڛ<h3z6ARi)B*UTDdv u㟮# 8bsgrUExMďjQb`^1}Rf|d҈E1cGF~DJi$w s ݍ47| #@EU4/qSnzGolDžG$Q2;RKp~幜K_|yvDLxbTh5!=OǣAhؑcSky1}ij<?vyă+8ʴ&ѿGJ}@/޵Wx%b+56n)W/~ߍ.:ZVa_mZiR2pޭ2f+ ƻZݮ bV#R][?W݄ZnGWP*] o#+S}͐.E ~~hLǙJ4W}+ў/j`7#IY[!Z+2ͻuYSQ)tGY#]n#L}Lǘ}+g2Ecș}odTus({ l(mWJe{Θb>1PhbdoN5.OGYm<ϥh+"'=uU{L"#ѻO? OG|=`t1|Þ_>^+N |FS !T? 9?p ~(_*\rFؖ{ZZAUCGktu+ǤX }?V"^J!ĕ xcF}UI;Ƚ7D%7ɢ« YNyIa8̦a#jG?[Ǻ=YgqzاGg*OY0OG8&'q\i}әGH]ݝ%/ۡWuO1צ?0A7P'%߽TP REs8Fwn'u~t:AG^M=7pЃ\xѧ9:FeG_ʼn_ǟw7W]\໕ff4w,յZյJZkt-}-[}ץn;ڵd 5~㢍mebGlL?r%^ɪƑ5 pIDAT?՗ܛUx]j[{gw}wW*&P KKKR;#4|~oȮ[)v _UtTr PwwKp߾gZ*d_+7?[ù +{Xmfb7!] ݆d#1¦Ŕџ!mK(ϟLR&˷Ȁk+$U .aR{xUCLbp93Km.~Hî ݧڶFڍ:f[_i㾷"5ꔼ.0<-Df 5]p3TQgUu0{ِ"jVCkAs|`^UkfMIhP&S.U!mtA%nĦRnNF!i?\0tCݹ)].uG~me%Bl|)\=V4X0éލRkь|ǻ1 ]ݎ QR.wll'aȂ3a-%O'ȥW@: z6Wڿ\p"@O3,@=X(gS|2ۺ2$ #]! '@ x"hvHџSA̟|?yx槑.M1G*x>9y"vR\s"Phr/6cU>\'op2N'T+]$%d1ݷGx?JY}pa'OпnO3HY;8ȍ5>El[+<NwXp$Dz)D=f3A<9i <̝&ί,gGo@dq $ʽ? PYW{gjX?/BF:Hȕ?_w%_?kqoƾgFSKY4ޛ`|JbP}.^~"/Wmc!|f骂+׫}J4Lݽʋi'ɱtvkzZ47J+퐾.UVsT'"2xB)}3-V Ke.}]@N}J5K5bNqE4+E>=7 LbQ'nx"g"˺^ o7W~Zb}0Zn{?]Y,)l; WhտNC.+5X}"0]Nm1lY>b}]sldPn=3xjJUɛP{7P4xn$Lw8Ħ4lwDRg&ާy.H <]3.)LJ_=Џc>eu&`c˜4Ϥ]nifJw^0 -϶3R 8}>jТv gFSAz(-0wdBhn]=soILl7sDoO$\G_9b/>`8yCqL8|2G">'WgDZ?QfBhXnۡqޛsVdObQxpe}}d7sg'Iu{ݿk DğΟLߙOz0K3z֙qIENDB`goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/images/screenshot-2017-01-mate.png000066400000000000000000000421171337147241200273610ustar00rootroot00000000000000PNG  IHDR6s;K! pHYs  tIME  I61 IDATxw@gˆDPTP* WZmu:[ֺBqu1q d\# DdCÛ޻{y=Gt xEETATATAPQAPQAEAEAEEETATATAPQAPQAEAEAEE `ؒ1 F@E)$ڋT%YH˪fHB.\jqAcӢrWY5OT*XQt90Le[ʪ^?K-EOZjT>QQj1-+RV2U~j*bYʪ^??6HEUE}t*gUfiuNP4R>QKJ\EOeYuŊl.QUGVb]ܪiTB?O Ub*ftKg4H dѥwtKVzƶXϮwd5]7 'o]odޛ79bդ#@[vȶ,0qgҊn(]{ٳ;k6ɨYD>F5,dۮ }{ zrƀ ߑ?7e`Ӏo_I^ L:Y7zRܺ^ EӜd]qO.Ӑ\oߢ΅kU䴘}yWeIKmD.0@v~S429ϕ(Ս tF"Mcp䇣 oŒg)7hvOUlXŏE43f˺΅%gw=Dm$ ;6K AaJ Z07a:$Khֵ¢)Vs0g ոuvmАVJ.=m 8jI nLP1N־a 2ՒܨKFSد4RT^ QGW0}R^<"ddž]mt/yԾ'^W'7)/А$zβiե0Iuω 6m\ЯuO,2lh\c9m$Fυ&tJ}]]]u'N{1\qIev1ssZJŵ1A?K^ ϲlh^EcⴎB]O+kK_SN=?%nWPE?w9d^́&"ydє֭#v\) H7XA"AUGZ땈`+ * ( ( (Tx3atDR*XBԴ*_ڇ{\766V*xQQׯWX߿xb+iӦQ[6mǏI,VѺ*6B MgϞ>}4={K*p+ܻwoTTܼysݺu˗/p8.\(#;;Ν;؊V3=6mZΝǎkll\gddݻիEߚ5<|[p‹/î]wG&O Νر#]VUƩgM6.-{N3NIIAEk^*f^ԋR dfffff;v,kժU"PHsrrJ|gB5($5Zwj044 L}:k֬>}A;vO?Tfߔ.f,:rH˖-J#G<{lnn.?~ƌ~:899ibiih ѣǡC 5```= њxyy>Cҭ|rQKׯ_F˗o500?<==CBBrsso>d͌ׯgΜY!3͚5+"Jwӧϵk3""*2vt?VZM>N&UqBpϟ߻w[>{LWbprr6mT*-,OA,?zɓ'Oܶm=zT[6m4bΟ8eFRrR9-Z:uŋc݊ݾ}o߿_0o޼u^]/СCTpppdRiV3 jZ*:::޽R˱e2YNNN)'Nܵkד'OT*0Apܢ]2~O+L&ի%//z\g$÷~+_0Lvڷo?fJ5tДyxx^bllVe2YE]e<~aHkԔ@||]nݫWN:r<==] A|J-r9N̙3O?w\ooԄwVܭʗM.<q޽i+9s郑M4Yr%K[j[tE\JeVVָqӫ`s8 )JRƎ+~$:>E JY >xhg[-ͼ999بg Ţi_V*Bp,%bbbT*322]MSPl<Ψ(RNJejjjep8SSS+++<Ψ(RNCcQAEAEEE5tF RTlWlE;""*d]eRԒ']73O^g]NP[s l[M\:вnouC7ƂZz@=CчNoL:֑ ׺3cLY0cOS8E-[[]JBjo iye|+?{gKݧ2 usk<N=iU3nlKKM(q'gNxD @v^ܜZ6&=zR&M w%n| 8"?7y~mHU˨l}>* HF-(xѲx+ww`þM[t9 9vtKx7607~Zˋfy[F^9B0c~ڣ!{rN"+Cɷ0Ҙ3٥CVyT lFζ< EPѲP730>Ĝ{c>ueY# wo[aμҚܼV nҢf%CaDꨡ//G#5x `GATAPQAPQAPQAEAEEEETATAPQAP/]fcJ*[t߃b)j޽z hܿo'&6d1D *Z0o?i˗/[08"0jֽCAx7~t9FVӯjƅ 3Ezb:uŞ}lBX77c9h[550>[;WΧ] n}~>ӯAۣE-Aǯ>vC{*vukqn5)e˜7w]7J=vNP]WsOlݶmw{G#܏ Z> ɾzD.F\!om+Oik&ZӧICE+c,݌Bøp;am3Zt67>~EaTJ +-IP72gF6\|Wں {xٰfVM9Euӻ΃aҶcǷ7@T$d Owe] FE+i,Ԁ&~pF{ql<"NGEvhWiT "i|LF+@() VDcaDCfS"U{Jͧc͑sz9K^{k+U,<{\F]&bXۮXb;VxS"l/t-+gMx2v{>>;~9_zuޒ/ܦ `RBZ-h5zH_ {O=9dA>h_Wȭ9mz?qB}ϼ|Mp%KU!p%<=yjz'h{ANW-8w= JCp}oOZqg&8w֌a>6kI''?߬ˮϴ r;qbtcb"qn:Lz,CE;x8!\Wye3vKVndk) FyXB3lZW4peO#eyPF|05 (""* * * ( (~fLj3<gh^nEB] 돩ݚͼQֽe?AWmSTYO1CMmOe-&BUm(ec w vV7n{O<4{b(L}(Y醾=ejTAjoG !?N}NLPќG 9R|I~gua__%:sP6<9cߙ$꜈skaS/Z+O:칵ۊ]FJ}ANgrЕ=۳ksSEIܛsmӦM6.bo)XQZyntǎ dݶؽU/5$-.^aB[A:wLM1rǏoߢL;w8,lgOd{T8tr>f 6w80ރn94v?_ uV45U24*Z]ղe˖-[zM7ػ߸AŰ_ -RUՙ=?ۊc=_2Eu$ q ]t72 [xY 8oU IDATݨaճ7qf'I3'=QRˊg.>b1}I/+Jg P{t&-qHk=Ԑnvs_F0*zjbK1P[:+If(?xP&ֵђ[<I\X%jv F)7ش|T$.[Q0swpA>6"E =W.~~ݙ<:3dCW{ &(aPK%tQo2c y(3rF,❎cМtR[<ѶTεn֤+_^gu \; Uo[|ªW/Q|k{ޛ[9 0سG"eEo2;uvX<9kQĜ{cJkO^'cHwFZ%}R6e^sw=;ذ`v/vrk?e&GFsup_pAZ: ;-Y7em=F  eY# wo[a<Pu;5bWڴiӦݱ)sC],{vaCCq`YP=1Ho1t*y܎835o." D|AEk$@$£|> ( (""* * * ( u|Ί~9ɡ ub>CzR{t]@ )ew}&Kl͋/Ό 2e=OLuY}I O=yUa.j?g_[sZb+Z %>ڱze{Wl@ lշ[R<yћ{ ~'?v\p;ɍ<8gGceFEZJ}Eb*R<&Db\޽M=0 =}8ȅxavׇ<M1POuMg'Kut|͛7w-:Vڊ%u>f=0C; C?W)S'J7MXSLPה9_zK~[Z=6;}鲾{toY vmdj Ȃ~|їkޕϝ>&$%0ENh-W["ŪۭwZA-1o'g5ѭfj[kIM\w[I^^NJ Fm3֖O?v2۷@t`_̦QeG`?`>Pq3pK ]=ZwuNn%7#pV6EuއY8[SPO u7"vtAEEEETATAPQAPQAPQAEAEEPh\Pך}i=}||d3x5XTBXmCGyU[:rA^^Ӈ7rrƞb*u-+зY9&ۇB}mk"*Z%̳ݣ#{Kdw]WV[kIN~4}h+V}Fjk &^CFOmf-NwF9jsAwFm94KuR rV~_DހYaMc_O!s!PݥwMɯ3sm]Ak꺠ū wsHw_M|2zJEOC3ZNgbkT拾Pׅ.VJ7ON}{ a4v()n6|;9s%zRl|3;Ե6>.xhiyucqIkW.0mU7ش|T$.[ࡥ;LUl|3UqaA[.䍥Y?~uỡ1Z"P3<ѶTεnlUH"v">EF"Ko>&46֧;wۈ ZP-, (4L`f~I{lF&/R{gTtuQW}X ~<(o Z#PSf0߽W5o}94[іaVcR+f^;0"b]ٰ<1FPH%AgFF88]̉4ÀcQAPQAPQAEAEEEETATAPQAPQAPuujtVW"񹇺ֲϮx}o0t5oBEk>m5{Oƛ=zjZROyћ 6=%޼~`z1oSwئu7R0Q`5o1qoi-!%Ã:W/bE+-V`"-!LK HbBdR*=й>6U2_Tі 6 !)KB)a(/TѲC[ t!j@`ڳdQ_ki=Wl*Z|de:O olZGn/Yƛs `kC]#"* * * ( (""* * * he#o bSO 054 (.AAEk$!|;XuNā95wvv0tr푬1glܽ}QR  C uGҢ9wLM1rǏohdm6w80ރn9 hE;JZ-l~E"ɢN\cP@R"aS2Ν}c]"(n6|;9s%jZk_N Zw5pXΞfl~}Σ4y{gv8tqoCԥnQ_75߸.{.M/m)mѫeg 't,v5C[\RA368³yl@vPEsл8qbЭkEH{6a'-uksEkX-iB F%)ra3c?h.씡9_zK~5 1ђO更{cRk#S203"A ͨJb~e[opb oѯz-yamMkuݜ771[@^ Q.qm k:׮+9;}LHJ`f%  ,Fl}YMNXIx7-w$04Khon͜/<:hgmѫ{`DɊ#.vBɓ͛÷Z;(Od6 *;>.ڶ[܊aŞ=)+}VdbUw\?ɟs^2z5aiɚ)k{hvu>d#.v;Doͭu!_7fZar{]NP{ִnQ6e^sw=;ؔљsH%բVpe֧ Uhk uJ0`@P[s.܁*BUq#_XR6U_o**j#HҪdM8R[KHL/Z^?Mh>=ʉ|E^J)Y{+2lW M}v*^QlZ*R eY1'R{;%ǟU#-~"H"deWi#vzRV>}*)b7kWE?hӏL:t@$EQAPE@s'}+A"""XXx9nz0EjI-dqC!T^ra:Ls\]V߿'WpKӓdiQQsR$jLn߇ VDy&4ۢL_n윐ޝ6m84{I2>>~鶶۶mSTxPF΍sӂ9P$ ͷtRy+Rݻw[*M2Ԍ$ɔԔC­bUSNUTD" gWjyWVɸ]%R|A)Er .*~}Rկ_߿Ou SRH,--l r-ѩY3s3Os̙A4klҥP }w xyBBBV*Ԍ,Kdfl< r. KV%111*:"ٳ&ѳ]zF$'a$ 3S3ɕgsV>b>}J+mҬG~5.˗se .ԌE?OR^z%Zh0,K]=jN}G[$* *L;)L;+ Q˦IbgB)H$ $ Bav]啌vnKYdK|?owm۶)JsΥi PT%5)+W 9tPLLLnnn iӦcǎ 3p3gRFr{[]g]XAM(^¬ܬ+͵.4A¦Mƽ% I(($H F$z7p8yrʈSL 755MKKKMMSNܸ\.I/nРh]>}:{x#M:.|.ZUFEk4w-?RX2<  0$lrueVϠyE={{LZpI$IIE>wk؈>Ra4wW3\ܜhyKCE+/0uoA>>>7o,J#rZǖ`…k֬uwwriii"d2oЈG$͖H$[nsfki,٥K'';vhe09=`ʦ\L|~)qIf46S=]VzԙOUls6Phσ Tb%XK9:p2*Z4:/ ʟ#Ln,zyV%zi[g\fԁg~r?V:+"fx|innb .^ءC˗W^͚5eTa3}4NӍ5200̴r)))tM\.… |>Ύ ćFDD(^zitrrd:ǮLE_dk6}<>Fceȣij."x:ܺ XwJԐV?Tut طKSIBYT:=Ur_A𓋎=335v Z:<kxٰC+564޻wQCg_lʕ+52Ϟ={ƍ?jܸT*-}5Sdgg HT*\qYiċ}ͦMի7c 0aɬnn\ ؆"?`7+sbrs. f`hǢK21 ^™J-eb$SO̞5vV&yRҺT)?XS(uְ0R <}tՋ/n׮B(Ox2qㆁX,p8M4ӹ4NNNbi{{{$9Ś5k( 6]C˖-uIԴxPݖ~fmM[݌HQkE;tL&MI-g_:*:%~+MG]vRU潬342򩥅iiS$^t]5j }Qf?~իQTWë]Redd i۶L&+kܸqJɩ(%*)T_~8vtKA]ʤ{׮]-Yzy慍Jeݐ͒砷 HLF_^;y!=5g-m^Q^#Z5D{KIDATQD䓆M'v*uqٻ @્:ބj.c&۷Go JEi~---4H1hHk佋13H۷?%; LP4J?QAp8oU{-6wbh`z9O`hQ[y+AÇ,ό D"CCC#vڦM}źrss5kɁa\6ݹs߼yakQ={|Kr>>>Gqfp/ooF#GԴgNL$ٲt6}I)r \A|= o._l233ߤ>SFfVZ1RF$I/]а~\^ǁ L|>yB(jGvmrߐKoulEku!9eemcL%{ֵzOMfeR|'A+V0337ndeeUдE)J4iR2(H᧬U4S @:뀁{#JIAeA&:M[{a2^߾abb?tW=eKKK'''|Y9,UTâѭ rlU|.)QG?nmu0(OHm_V.][WLzܫ{/#2n`'N`_~xAE?8TPjy.MRF1ed^[*2ۨ/hQF4C R)~&XlZSW/)af$Q@4?)IENDB`goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/images/screenshot-2017-01-openbox-tint2.png000066400000000000000000004120061337147241200311410ustar00rootroot00000000000000PNG  IHDR pHYs  tIME !P$ IDATxڴyuYQ'{dJL PIADJ1ۮբPZmRzP*p-]vײ@PEi+ZMUDM)3E {{?b}b}{bGox"`zBz?F/"*ҽ_ !fB!춻EDO!0M!"Ɇa i7@Dz#a33;.^<Wl9=pC{DB 0n bKQ! >谈>~a3|ttt/^82=s}qeakgzdq?"b/yq;L^qvqa u} SW 75ͫHt3A׼vZӬ&m6a"jE8QA{C CLڼDMY5q`L0XϢhb!/n<x@{MefF{t?:Ayjv2 D-R>zlGRޏ1T jʜ;,#99|q*k'S'U]9|}TkDJ%!рI[〢gG<^4NK,&"LXv08` ~tlW|>lw*K'As9ƌ=}{.8 ˝ 7MtzV[mN"U!Dk)k fĬ"f(0;D#bfޟz=~= SzXX(G[Wv_@;>=!3n8Bn'Mm A ϕPSɾʣbgnAC?yg:K0g4KukQjok-ygyǸӐZяRB "qə?àv'եB@9?š{}G7#3'_R'a|V+j>Rvؓ*k=_kѡ2N])36'2PJ%WA)VRvSٽl /шި)}z">q_A@ !Pe7M9fLhgct1C^6U˯F1G͑)HbTF:K0wSDWM;t营 蒙Q{$;U2Fֺ9:p0bծ[4M؆g]'+f0f Df p +cSq`\줚a뵮mfFd E7X1i \9r>!W`fDWyKysz":Sӟ5褃ўvRJ`GdC5gYޥ 궈*6E<؄}<W-Q8N0Ygf@^ ~OH I"LAZ!cnfsJܫ9moո"p$[@Hg Wʞ0MqEq>~ɉ* ֖=WP78"dN(7 +ĭAFGK^(VsG~S9qIsBq-"p>Qu8eB71^ṛ=c"ߠ$!ȩůΛmC+u XYLZ>oGP5yyNc(!DPXPWK HYyB6ȃ#0{JF0G 9.p6w U(gCۈo`XBpѰKaNu}Ƽ"nbuGVjB*lQl<3ђZlƎTlW됫u cA91f '↑dd,(])(v[ReKh2ŠO 3324M5^ ް -H-,)HLp>B'6"9D!sNLDfcX ;\eg4/В>'4?w4Mg&v0Х.;Rs{0 h5::9SDXY1yg 6 `:&JNsR>dI2/!4K:^q=mnwӤ{V2]2X4NNً Ǡ #.`Uk 5J7nBHdK3Md/"g-Ía|H0k3v93T. Swm.\7XxL%=ݘ1Bi@W`?"S_UP7秈4xtݣz|x(hfϛڵ(`U bJh*! 䠡s-md5F_V*N17Z>jD{Х9Ly^Bj8? 2{eWtiU'ARX4<+k/x5>v~nߕu@| ]sՅ!'>xӛnco7|=1Wr鞻7֛}\Mwp||G(br 08:.,=ٱJ E?`ɮcP"4RbtbVGV#-?@!bSGZL8$o Lc|%O2 uK -?#8 #̸ /gj,JgaX!5),ECwyy{EF,ccD*`6<%  Uҹb ֌&= A -[;#"nPʔ-,T K@0\<דl/}#\|µG|7_w.FggUnx\S|]_OL+{³__3`6CgKPD!N@ uXS4+? diB|NkA#A]iV:vpT-&2)nadQsrZ * .I3kDƦQ@ Y.w [;<4eSe. RQ8r8g9gb}"4^ +" &PS0Ϳl68X>d3GYeY㳂@# SK}}-w=coX~ۯIOĉ;O?_ 7=lQ/](u7}ng?WMϧ}e D:)]V(/ 7qBT4ebuŸ4HinVjF"DBT(K`  EB3u|F.׏ vwna. X!FXu%Җ^dX*]ld$D"]ŒUsAVjMDC5" ym[DmsP-6PιzlT %I1bPh?2[=۲.+ >[~ 芇q&A`᫾+A?w3|[?vKxK:ۿt?\E_+dC8Fo{?O|3.MBI-Jxa@A`rW3ݺ )bB H%&$ K,V|j)1@<cR\c炵MiUn)jKBHxaʜ 09ք*>`QD%}%2V۳ k\|(̾uh= !'4Ls]eLuŽ" DreCzhvӤ+88++p@F"쐆!8RсLJ !SLfc038:&純-qH|(rkc M& IfD2g@nYU^Rh/~V9~=E#9hDX]>3zi%}ι@ n@X61<Ozš GM'{nG>R 8nԯ'p> wm\kl9\QZ x8@5*HDk2[:H%kA[]pFNuX 6/L,v詤R d4!14gDS9N\6yU]SAf\'s%,A^jcJ[yUȲFgg Qe-F,#}Yʼnh)'w\sU r*oy6Kҳq}u'$1#dṼ]̩5O5㊝`´zGÓ7L}%i,TS$}kpI9kCWy24#!8nRW\ÒhT%(TaetKT)v@0:u(-{w`Id.Æk*Y_T& V6f HVBv\P"+ a:Mj9i:US"$0:J:)sAʮ uǕ'!z&שd:W[o îя<W?s#}dqd gZ7A`J2wM8+w`b:U;5YF1mB`E'XTFR+w{EF[7DA){4GLSu"V]F*a W#7Mۦ(ݹ=2A}܁@xK~>:b=/}|{| O{ |}_y;>{3nkGMD,svm:L07܊eϮ5x|X(Vʼn ho[M`rRٯsR[vՁB`Dq`JWF f a9J'K!5o9ZEmv`tfl1рP+<#AQ1Gdp9+Jˬ֛xq.Mf>NIo8Dn&a1D=8Dq?/ty7?ݭ'^ſ=ioXa|4FN(dq7-Wmxv #78-70XH,}q~덕 섉0Ao8^寈Kpae`@`d!M` :̴ DT=hoj&Ԫ"BAl|D}aEFAtài>mjx"0Hp1cIxl%(vuBY):(,,J=v^ث~觭^`>X0~֤c`0ٮV6Sġ@=+XE|@ՁUjJR|o gJP8╖Q1>̌\#1g/UUά.YsW^y8w_9yy"&&.H9Gc&4؅Gwιsf#"'''-B>)Ǖv>99}>ir0=ᄱ(FUmm5ё*V};eũ 3,_5gybەѕW^y3sj#˞PĨX70 R R1t΂3uLYAs_TS! lUeu2^4^mdTK}gיʉѐ.6aw -ćAHl-a;Ba>Oer[Vm7T8D0}lPi,j؊XSQQiֵZOmuatY R,XɴW5Yf")SoTe!B;6&hΕ+$ŚfU! 鈐-lJEYQwNf!|-!\FnU;=pZ(X0OXU>sS3c<bn0J2Ԫ1mcPsJbm,-ETb.8GGGGZ NN$~B-Fj)*;u)JVU~'DàHG{*YV#EUyS`oa>Ԫ yZ  `%m. [<.0y YjM:3FM!Ru6IFt"2G '^۳;dK&"8𒚉}fiYtlc!(L*100WV.;D(JC_EU\oTER}~E},#/';ex/ 'O2'ޘ@D6 ףxOj$^5K}F 'E$0ŒJO r%Fq!,í$,DJ#^;Yևs/р q VdpTWJn !SZ(;aFq6z;YԘfT٨a="bv"dS*G٦j^+2۩f,~+׳8 ,y^rbyԆTNyFv*D[ I!$ !ܗC \wItTۺ]hF GA斑YBoH?! C`>'Ƀ3u1 #-/ss.&S$FzLNܩߥ>\: =^߇B3q.xIpa/9P1k-YNJ("41RKE)A#F+(NVJwv=99afDz sc/AX#Zd$\{%@P vT޷;.U Φ[[vPZ}vո^COOYPm4MzKm\ ݬ:uCi`J:JjZ/pKRk$V1gnR>,D}͌;HWn573a@;O%/6ZN`uKP#ikF,b&bKxI0$YVϟ_8Kl;Es\Y.y0}c;WB8&f܋{;ݜҝE ,u@DzV{HJiHXJGVe( Kn82CmYKn~k7ڷEq/~|#u:Av .!3l,%zΕRg qb)<ͤb=-0*Z`rrrKǀg:YJK U=01Gq%ad-ӕHH$u`v~nw2a>|Hhtd>0# [z^҃r= “Gk`F(D0M;mZv8\8|2{@l1vWU8?g r]:j [Ѻ6AW",& hJ@"(H}T6d a>&̂" !z%B}oYi-۶Cvk]@.uo _}?އ~/~w>?\_>Ͼ|ٷ~_̟~R[^X^~63-J/ 6y(cgKCɇ@1%{g: d{sla5KU>0}(9L%YjaƤ.g263Bۤuu^ Rhl~b )X[Nfo)8.!.boRm94RrΡDa(f5~+P+pf{>cZeBɭ̛)-vBCGM*[E/}eW<'^Kt͍O~I7?ʿw{;>~W>ꝯx{neƪ)ɨgL%,KVSB (GYqZRmW%[xXDjΝ;RNpO{ګz%d-uü avml76(o_j΢89ja+c9[5#6KƵg#TY*U NH0Pܷ(9/o%F _aUm.nI|jr ;o(WkcQ}C_]xչ w?:;v^|w~lUhO\绵f†zcQrVttC=Y93iߍj,m|Wa3fZ& H]Kd˽smv>Q+m5lzt%2s~rBz3ڇT"O>Qn°n+V+M.7{u,V*DVt:mX"bZ'ܪ+ύz_i\滋%c.7E4,a=Vb#EBk%XkݰcZfC6y)!?j}ɳp·Tїy]o+7~jd6C/3w2Yߥpu!0]\< q#AmS.8= {S*"J@J5T"Xj lܚ+DVpv÷}L=o6 \hZq4MVaN걒=aHSJ.~UsVKFM2n*:L)/U9rUa T qIk*OZ1DD?yo_W1^|ʍbma^e9ZZ)>@nhDEstYWݗWfF=R&Z%ʮvnӴ *7'w2ũwƤ `(m~bUp4`&ՅF u@.]t||||| He9"PEcphmLJy=*?0D. (Eʢ倮֕]A!:2 dOjw%W.ZoC\G>?Ƨ]>|y/|yu'씷Tt,ՄZςs]IS2RkY|q\ @DpD[ć~/ vn.LvЌ._9O @1l:!E39μiWR\\+u =gi_2610 "O]KgʑP>RLH4.C05;۰kunUB(<vJVt& iʑ z5:B\={}Յooz=ӟ~ݟ>y_ ϼ`󘛾ٟ?Ohqt(neoBFS.U/#c&Jv\*F+W2N+ Us7`!U3w}qϪ"%z(CgRQ5^܍ &8z j=<,wNwzm  ZPZ6]+]χi# e}n뵑ywɵm?t ?U=ng??oxK|-??D/x^k/'?3oxזVO?_:G_Mϼ ׫Dݳz0?p;c! $V5QYo`` CgE,Z $c4$[fUVZ4e/@8@DlIaHnb"ug3+0/&Z*PKT7MaC_S|=hQgcN K"j9sP⒥sժBDqtՕSG9i|~#:9ʊVj>r9ڶ"mRKQK3*g]9BN`0l߯o̷*E`O# gK^ 'JhZWň/~u?p{^u'-?R%kkLR*YS}R oV۵KMt5j?$*2Z z ^Mm WEtP}l 2,Tch t,h!tZO"|YMGL4rZafb0r;g뉷#ٮ8#RR[qZ͞ne7;re+-(Mvx(wkMr ͚c p3m$Fr#ڸ`.ƩyndZ&/;W?XaLm"'Q"Etk1j,'QvWL$RBGgtu74c3!TI{V.m%a3©8е6LDk'+ ӨBmQ7)/,k)id.Z OљjX~ iݖꝒ^az~NQ!wV[࠰D.ZAv nHdAK}~[r'8Uz}F3GB *4,Lx|>ٹS.TVQY(]o˼)] fM:k=䄑p[[6/r_h^jYrOnp:q  w ش[T N}A-$d%Ifv>-QC8k^݉]O(82Ԉg*doҹ]Ewrkд\aj03PAM]VP; nkAsa.9]R6 4/?1v&N&ǺQ[5@@$65i"WhI$[~Л+tO8]dֽU/ᬕhK^=ժ:Z8gFؚ"DgDaۖ5uU~J'8 :y,">lll50D5U9N#$9!ϛ{r)JYa͞ޫjǓuDT^+؊_py4zrIVUѮ캚 W[δЅ,rg>^-ϡ" w2ΖȦ' p^ց값XNDd} 6Q'ZwE#еu^_h~.]"I:dY%k N<"TB+E@ty;%"Sof.p2ù,-w'#4!ajXsn`Hs\pf2jrU!&Z9XRՓznLWQ=ޯh:[ZOEXb}fLì0Mtrflv%6Yv'7,YIjPO3R%' 9=7fsn  oN^![։k6:1 *Y(]X!æ+$QT7--@"-"bL9 3Of2U!T9L4 2*lm|lj6 2MoZ`+)GsZPyd+/JEᡭTU!vwZh~zhU0+ψ.rM-6\ȝjLSn~ ) R',mE27Y٣ pIpsר7hh;Tmf8 g'vn\VJj[CH$Juܴ֟u [.""` @A}2a@i{?wAXD Ť&Hp s.E0LԆ" KܼogM,Iuc%O`?;fAPPg.[ QJ{x "C|ҍ,1O^g]"H0(h9[jwoݠ˩IveBi: Yt,-=Q>;+W@\BYf-3mvK[R{Sd28hJUuQN\RS:m6sEOPti'j:@𡫒鄲,sF W3cB֕\&ܴp+4IɝSpSHAk s '@O6kP$ڬ=Q4pZpU1C/쑦X*Ux89<;}?h/?UL9oݴ)gqC! sM'UW-$OTtEǑdVgeI˻yEfs…U!Q[m:p3 #"12E9d$$Q WlKR 2#v]v!`f~N-2g[Nwakצbh6L ϢI@1m\mtoNH @^H l hJ3;|jAG?bvi d@LO=lbV(IM2P=DSohu0mu*oӫ.ah rA`SL*if])I?)B7| tƵ(!U! 8D ,Ě}&@M0PÎ/G08s,SmA'4cKguG]bg&"J!c h%)aX!²$ V3b ШW]6(&i[rOٞ̂  *Ƀ,Taz +2䘰t Yg8"wl͛=Vuޖ2,RvU35Pg ӜU̩%T#cAլLDP)Sצk"uURi:J vSn/C{-cfee>'Ͷ1J], g[yRI S3rXs}g 3 #"7hCv܆2^@xjSsɩG q)%j4!:; (>Y;U*9HJ\832>6M6*IPڴ@ə 1[Zx2qPRB& r ![n=@!HNP~rR ~ـ SGi[XnTHVLj|$P WmR[7-w[9 ?3"B عUC0R4VW)(LM@)u` 2e.b5qqO[4UwhbX BE;̩^4MNp@-n͇p95's1mH¡e,"SgaJ's#0I^ 0m-ϥ;tH;iEG{?MOՍn7`N)ܫ+z"n$90{~&:gz""@5Ρ6YMObYrr .jQjrX j !›6H'0uj\ISTp]nKqNS뽼Nfu5a:&"W??|?M~I| '" $U+Z2MU6*$.1)L"Paб{"J;B<g iN@YI1qOoRr2FI9SC@Dժ kCDd@4s:8%znEQ7W!)\%ΗG,!^JCTw~/WBHlHQJ G.xG殌76&IP(sݪuna)s>ylCfhf5Uʈck\B^E?ܰ x]@@{דJn.,n\)\o}'$E Z ܲ3,9VjK1$ݼ{ɯA}_R=amEN2Ʀ˘CDZ)|sU|Nb=<3,̫Mlଚd~? zMU@s;D`<hjNcqf"i4{%*̍]gK {uAek-Ks Ȃ?dokh~\ ^/㙠`z7vmwqǭg?Ұ{6 6W\<:nun>:Z'/5 GiYA/YdqZdv DC\Nm/E֨Aέ7GfQqGҁj91&CDѶ2nh@t.ɱ~m6V 0룣J %x~(׬v~vSˉLN}T\(Pȡ&Q uIav'ۓKaUϽɥc`!@`q>a@tN1B2@a86GأD7ѹ::V(#͒4z2@ )ѨMC@avX5RQZ 65|M <#ȡL#$G3ӴZ}*py]}??++/| 7ly(ASp5\zYeAS@Mm:97 aXޝFR ,,RFmaWFrڮRQj`.ґW-?rWGz{-U}u]pHv٪-a,aoв-^UTQ2Ľ;3 ukS9Vqp\]^%ĵI¥tmzl{Mwcna쵕z{VgY<{kv^r-~~[o=sQC 8nCAv*Qʡdm{gVCKD2BCfHVΩVg*9U. ~хi=sϖAWܢD‹0(ߌ'\3d칕9CV_:ùǨ5 d۹R7;JmkN̑Au (>jֆV-GLI&vrạAlLZkNujLeJZ64׍) QEEnl+ 4 bɍ~lă;s;e{p~o|mwзKCDFYTS̊ͦl wl$ccaZaX)6I/`XDɮ"^>XVFO D;(bO)X"ȔFFCO){?Cv4Xuf#XN$X =1ciL-C*f8="{Nt\MZ  جu-8e%cąXSmJPڃ\`YѨz{F T YHVc/Jo"dD38<{R/<9|3{5y7FԉKfT%YQfO~ygy&5_T¨iE0^ȥy"s#U^ d"jKJ4>.: BkRu(0@ 7*,9]wx~}fw3s$|8-7%pA-{7+9yꆡm\F Lb-/w[s>Iecظ֊t:7ːONNp?MY2o &Te<)fl\_>~.n;U -A*.)tAW ੂx&xҥKpM7G~w~+^}Czի5puDbDa.I?qa{ [9ϴYF9^|Dy3;rJ^N3L8qZVXH#CBDfWG$OFG7Ɖ5.-z}كD}7=Br{Ec Fo2LZ6މsSrYn&c0e0JpxEC)O}+_9zGe /[? FGknRC,Dn D)3`v={ l# JoůycP֬0pX afg.dY:$i +*v @6[ȳHfoD.hRX# {,a&[(Έt]({@CӲ,KTܹQw\=JTVADSVJn*8J>Ћ / /j懛n馛n5)M\mTh6B𰤩F3 Id"*A2̴Il`fs$:M(Fe$F" H4e֜Jifj(%",ՙPs9"aH)tڝԤo' DHIiuz![Rh+h BJk N1$a;dvF[%3hE* ّ0hΘWig)̮Ҭ}#|!q&y_|=zQyd~Lh ,yg[txf͙_ f Qج}_LiY'~_!A`Y\D^hܤjrY/+e^:4?]fb4W D5'3 -W90ufe` 6_ѩYȬJ#2]+vY.hO4 Th$iᣃj4ÜÈHȨ$FI0&J!GR 1{R~0Dt:BYfB$ʚ,cDe5kf%6-BN{/.3UV6C}fyRV[l6A֙Cj4fZDkb>t(13X A@OW;sul^A.܅Lf#Pc^NlG6|6zLW,*$5)BN,ttDQ&!"TV(  tQcwiT/ pnhHl~IL,Ġ[gm6 PP8Oj4HD u*4\edi ,D% MhT:Shd$K#JYVebi挵:mklVdiZ @憿H$aOY(rjY:@ffÐkd"N2W0? ߣ{m6rZNŗݛ`!V*tlY\DIJi2ؒh܍+yV @p޲@pbkKJ2eNfA$`b"m/TRa<}}º(JҬMh2YY^p>N,4u |_5D3^|0w #I?P"~3ú6?tk#T}SFHTbhl@QEkfZS67,#cm\ 7ecPX!"Lj6f3i}}~#h&ѤFS5[@_l&݈H)j$*_?P )4Mj5fDvY{ddxxxhhxxhhxhNT5"u܈ IDAT2LtuΏ3<s+3f֜ gs/8Ykr=rs?hOâYҷ ~MQMKw@ma)SA{i FvoɗV,T4]F`%d A+pٟ03qzȌR=c6R.oU^Tz}"@N83$$1Y?XC;ʋGK?$袋ଳκ+ypc\ōCc\VeJ πXAnͦ|;M瀩d8fl=V6~U{ʔKEFtL("F#Iةi k2@ ZwQF48"Ṙ+D ΐefNy؏^i%@PxM#̢%gH(TD"}Dbȹɍq@F3 y'e9-Mȹ aUdp@ a6\l@. $([: GpO9!уP3KTf Nܟ" BYi]Q?v{v3N`){E" d]inUʞ&o^pXl2ح|*{]Y?"{@4M{챃>m'& U*;2W@w.9{:$&r]qkVorzjAsGYIl"N-Ӡni‰RJ-v D, ԉ뫤t:]"턞BD@d?wAz:PYkDM:ok92MՅV`?105>ѯ1Goau,;^ Vk \g\@fm!43Xk[C+l k@䧷LQͷb[~`SFYΤPN3'w0pG%dY.|`Sm bt؉=P䎑1X:hlꆹ"&7ވECzo/oZIS)` ^/R\~7<+)O.H`#"5#R2.%QS;la D3oaBt 2BsIIT;YvPkef%X "[-9[h""δsۺ mʲv5}lj%YՎsbM^j46gmoe% 6_i!#vG[|($ Dȴ6@L]5-34P]f 5|$ 0T@0Kƶ‚@zO댐̫HZȜapfH v> .ͳ([Kz01_5Hd&`&&$"C0%[j2:i༂IP JJBW աw-?vӦp,X먣 wlrNp "l_ =|;,0n™-!i3xͪ&AD twrF̗s|X먣+b& UfLgV0IuWG)BЂQL+2DR$!O)TV UQM7*9+`,Ѕѩ?Ԋ0azFy܋JI0!#>Bꢝc`]#htb*ZEP|Õ$T&DsI@'*XֵMlK ^JJ:e,U|"es-6d{P辅Uz>̻x iV3y? Yls;\^kiS̽ܞwS_7++`у_/6RJ:\!6|駀e.ynJYJ W$D`.RE\tWYcM'™k(> i!37ClYGut86f<>r' zz).EE?DRQq7txpCi4!V/a--yRhy !) IuQhu&P MwWΠ3>ǬSxC!U*,> #⇾ W 2p oaKrmLOf,lp J*}:u]2@1"hOs ɉE+檭bBWR70Knv5"6gMZn E.hH%ҹ@Yi~:$y=4/7?!xBׯ?xZf\@H@*9sשE~>2μٴsG>G"Xd~rO@-e%&Iu νr>ک_[=KXܞZp]Z씍j1SEr5ܳrl7\d';4֬s RzY:w=n?Ţ^ ?}㏾~~!Ǟ}S"/Kz_|'?tw[d/zAk>]PRsX4r3!UbKe L MZ˨Ó~i n$ DPqjPX}ݨ!j @.{1dXlՅZu'yNn~zvZ>z罯 Ϲy;o9xͩ^`n_;g'w3hrt+h@-V*Vr\ TKzۮD B(;,Eum׊9]$t1xGߵm]Ȉ{s-9]j>*L~Ǎsw^i]6럸;G_o6m5~{DXW")@*RmT֤!N9 `l7 φ+9ҋ/w|6/7P/r '֗q\9Ld4,щҷV`6c;ߟ[W^ɄVZy ДIݮw?w>m ?~ν/s,F4*ۘHuHLe P.WAzD!? (FN5wN}7G/Gp) y y2N8y67r2sǝ7j/yzn']_}z~=WD1&HHHXj|@ EolF(@!#0 (EW [#IZͦh4с! s7$DZ|g-25Et<`@q #bA,zTל✺y‚Wx8#acL!n;~Ïm;N᧮:[ νe\e=qŷ_|#;۩d#_xڋR<6~Ƹ gJYE ew QXeZR}Śru!)y AgYAnTyIpΏc+[R'ZY?+*}Vr5m;N<7n1-8a]Y2DkZ @mޏsǝ;k3~a,-0!)RDJ=:FX3F'z  J2I Ȅ@Kprk*ȶ6ЯaX  Vʹ}Q#SrQ$JRI(2);D\gBN U.izEBf&eeuQry"kZDtvhGis`pꕻ~8l΄[ݪ%;G87WA73xGM6hhhhxxxb߿&Gzy&njM/EWGhvd94fY1B Ȉ b6̋ ,3˶ޮq)97Cec2Mӳ}<`f޼/Ra2 ;l)'oVӱ@Z:Ia-<H~?i#hD9 u-6% \9$t^0#&3k $ɭϺv@ZM\QGȁ!u5- Xpe@ aŦga\v5(J }2M8$B7(BͭK7 BKq:dD%0W+O?A?{{@%UL)$":ꨣxTdv;S<&5>vbce'~FGͧ51{޷_m37]|Mo[W8[0/n3]z/u)9BX-"QDaS"u|W!P2$lgf$/3To{} 7w/Z$Iz@I5,KFy\>bm2Lߏ9eW4^o_׿ǾyNs?lM'>߲τ'x|`/~m3V\zC>3~zߖJ&okWK?uv\&.& 0S!! ZR)tPSDUG]n/_+k&=E&(90 ~/4_pkw9o޼nn ld˝:OvN}|okL8}&=]N8iI_woD~k Z~gGp͛IŅ'ni߹v3y+]0'^}_|:MQWY۱+w -1"HnӜly&{3D4v+pԸ+/PXA#7Dp ƬϬ 4x{ឋ$Fh$ZN͛;w7t]4BqdpF$oRm?{ۮ:Wn}oV-yM'"Zi=I<Hq r?7pq&|v+'N~?}g=ԪV &olv,$.T?L٢<gPbC R qQȷf ;b,%wT.YcG%˄\2~ p~m¡E/Cn6;nh7`8nӷ\jgd`IrE}pV[7-V w}" jĕ%;ܔ%.ՕW]^fH"h2\z,~ "gWOFhL~xKdf7q".^a^hɸYp@?Ȓq tḉ'Ь ̧~-{y̓ QA(l!*OPE:ÀZ v _vYхЅ0J*!2xA4M4I |;)ꩊ DeYe8c5|τc~ml3֗fmx"%C6fWnͻ{]/[,-dm&Uu7o?/ @⇧]X:~ gp\˧¢Mg*w30'9 `7Y$S1W*YE@h*ő*Puo{!i(  `` 4?N0CX񦐤 p=;g)"nwgwgZLmdp%x1ra+2~pEfl4 $I((HRm0:˲Tg W~j{jX R T.>{^ Cy=WYf t4齩P !\TֱpO]b Rf8GKxKK(Zy1k/r-Lw_I5EI椠: IDAT+Zv)fu5Âɝ \fH+ [ oJ#XjwDz \xEeQqu1j". F$(؝: p7RIfn <4!^FG',J$_.`,E4/VG=Rn|HʒADTD(9!BҬ) 9G.a.$aG\#I4ecujh@ao<{M1NgU9m0[Gu@٫8JU[gA 0V6vXj$*IiL (좩mH\}WRb Re>hoAF#u /+,h4SN1c5,Jf'R0UGf=y"nl\ N 8DFmux3W릱:eټy/̙\"U#nH z'P%jeqFy-n0+BQ;(Iu2ކ]Reՙ~BH<,:`iӦHt$IM`جDPgcűUe-Ɋ:_Ef]2u>xGrc.v%- U9 GL 0w3el Jd,BMa9*R/Qi`t"LLc$F"aFBaf "fW0K.^鷄DfNaCY~"ն~R!)!n"uFD,U[$!Lٜ0a|,d;NL]iɓ'A`_hpe>ث jk(4fFgDʴl'F_ˤe,_0Ʋ"e^hp8D =2ŹT0& -4L믴ThA+yHvnmhFjppN;;&YGut9T!$;?,Yl^)i. ?JQ+4^_tVqLK$Ufۇ HHq4Y4)Lbb+oN  "c6M#kf(\c) j9: qMl,\c2[fvDU|kUb,#K 6_VΗՕ)mYH* :E!H1Hab'ikcA{Rͮqu/;39HyC-8 D@a;-ﰥ]9Af BV{ŒLU+]kEW.`kΌ*AKrX<ڐK_OKpZDkZ2-Y&iYƬ9KqCH[NiaQou/·퓿!cM{jT&h)LOh f=\W.4Qj:!6*'k\#ڧf,qoeYf~Jc516qƏnW>c_ 5f~\qtβ!9J7a.*VnPD({{X1+IHa8O @L5{0"46mz߸N[gw`\lt:,:NI/;ȰaѓI& J(=ٳf}m6b/3|;<$I6d#Vcn{ɧ<ٳv^n'3O>g/f\OR%Ѩ+5z'θ)m}¾a+*hI믻fE$ "j&oƛ^I/>~ Yv,t`H42 ^,'SP9s;?7fkXE{p/C_?{'Gp:oZӇ&za8K|g_z\!˲?)o03_wM7_x/𕯟x|;U)zEϷ~Wt?}}[l~}q]˲ZNĵVm .m a9fdB\d [ b?ef]f;.PJa8h~H;$br3_?f}x3>'jh-lu06/0v(ˁ „|Yc[2N3iьLBJ+̨U-\峟?b]w>2yI^y՟{ 7!p9Og,nw˯wۗt*1$FjHmJ^fD4ު*v ):>B$F#fȲ,w,<0`%Wha`af`A̬5k-̒e53cٝ;{{??E><n`|$9^o>Ӈ3;ZK/'xsUpλ>Ӈ^paXy sJt3>{}_m,bEO>OuɅ7-B7hZ|cp5Np}lF̳sHE~ R&u6Ų"S4ᬠ|rh4ZV___ UL! 4*i)HzˆOl},/3%0m2La,"#-/iI Y[V^{MiLbppg{w=vr߸± .#ۨ.h4ưu .7x 7 p[sl׷;j`zg>Sgy歼T?EJw~HQ*7CIlD*mGł w4!$IѤolmDauj>fZIH0^(Jkahhn-{sϿ igV^+0u(hf(rAJb!G$%W)Ry0G3sOU oV3itRmXjdQ*WXC֬34K,M4M;C\ij!ZH-R +Ŵ 83l#9g?ϘJf<?.Ql?;~xZp,__zYqΙ#,䷗i߿[8?O~mnΡ}(< {Me]244{ϛp l-|XsrؑcPOz\cNS\{s000}λYc~4;_/Rn<_µ%Q|߭fL2u@(uyݯPe._Tݳ4M* ĸ ֙7uۛ@X)5aVB]I߯.7i n˱F]D2lM6N] _ׅod!I7o}nvmv3ǚ8a„vivSex߫vL8q^'%AQR`#ɬ$1ODRipZ%Hv,kԌ\ Z}$P7mLSY(3e48(3"(Z %Y$iZF!"F3!@*\Z^a2,~[|;mIZoA' 5E_c}1v1}oc7\o~礧~v){{>~w~~c ?#}>p +T(t&VrǾAi_pٚBh:?x1m""WSSD7>b`ITk&DqQt+;(FNTDfR*-<킙(NEzUg|6'YO:g}g͛m;<ܛ7]|9ѱ02pbb %RC 8$J"R&EkVL($])[x5DŜ"(%13vSI佀iK;XQc/aXT*zlL8-I(IHER;,k:}f%bBA%[lw-Ҽ(=ZԷ:A`U(9T(RFGD{3H )İc`t=zvּ ΧuⲪ9z'Zg GCE^zγ_z镁wl[mE_0 _.Z8qfGDe6엊aj6fb)WkukGBE=/T#ih3Ah> lAqًq{ GUn*&o f#=CDM";Nx_o ;F.^28<4<<e}UWdJCF:t HDemY'\¥Q, D2 )PVe8qZB֚T#1;U4oZkkw "6 R: ϙ YNֈbU3aRBqhkf<:Iˊ fIoB_P%,Ek:5)`E\Tli@ $I:5 MTKkXt&aگZM\-D8E0hoZkMEjM]iŋM[Yㆩ E`y彊bhdY6v"˲Y^`+S ALZ{""PDF2P­x [şF2fwj$"agv]@Z'Ej@ Ǎ?nӍ6l4E*[Gk4}$  KTH\e65S_5N:oӦM_eټy/O?ګ1"=x*ve6oh™!Rjro\ RhӼujT&aIZn0aV[l>0n@G}5s?dl̺Gxm(MtiJbƌk(m:u3fCv|!QqJgPai0:+Bf?i D@D{sEwfpebp%7"!x ,5dJ$$ Q^ NF}g7w <-I/zyK:03W9sd,գI` h?Qȉ( Z2@>z7{E[z2pTFX-E͓>u$¥T9Kt_"HZw==˽YxjlѪT$t[~ӥXd;aAgH!2[;0g*&VIhcVH*135Ctrhi^%.ND_(󋮟; UziFTAí*:z]a%/'"-V8F3 Xm+XBJ)$ &{sLZdMtP(vft:a䵎:_~J! b$)E*IIx! hg3 i&"H_OӐŰR ($D)ELP f֚R& lXKBS ̜ګqŭUӞDIi&GYʚ&-rk|i—kЂ'=,_Z*[{Zda^U,9SM"M+Xَ UܓG.^%>jDR[߼E7 R8BD)sjt/h7SJ!jx XKZg׼Zf `XI-5rr΁I^au (]>pˌ=ёڤ*Zs_R1/%4q%Z t҃33[ B<Ͳ lC 6Y"7꡸% *B`\|zd@@`3gW8]+j9I\jIuQ}6X7: qpT "T&b`7 %W$f,3DWf rB_(EM{g)A7 P F:y.AQAHQYr(^-G ThBAɀ}"Z)L2\a>HJb55AD("KUlbN\.a,mW,yo\XۆQYhNa^̃*%rz6i鯙"Q)$c"bn Vi ɣsI0tɊW׬~H$s*05 XI|5QG0tܨ+ Rp կV`ɮ[рdAe)Dz(;L/%'U! Ed|XCw:?n*|EAk`9CW!$!㊧NOWP l JC1sQJӚ3Rq(f(te_yJ2O0)B"̈H:ӷB- G@_l6[!7):~jUEIo=i K⮓4L LYs|RyѯV񓷟/]+O5sYim/ jajy~~`H'a;OىpVHW$ IDAT+•x!!,a5Y?@il6qxx$RD䴺|p_11tgVu>zWz{_ξfA^]j^ϸ:V\}Z4H7sB 7sRyFtPF BgYjL77tTJVL+ ¦ǟ\}Ddpp\80o p]X|ftԑ']e\=7Yc٫\sA~N7seC]]7\M묽[~;_ӕ ȇBT5Y(/ 1WA=xESnVt(5 6hnхQbHPtcɧow I*uĉgw|yҚGlZrW|GۏEZ˾|%}g/u3 Vs4u+>5>}۝'ۿ}EO:0q5@Uy>:"oU#vz:TJ"{&Ppi/f$J,qSNb jǏ;߁##Ù``` Fa7\]FN?]MQ3^z޷MXʫ{3߳2Nfm }ޛOv2mՕ^邋nxtߧ3ӰBı'OV=8{pZJW 2)_%,_2Qu2ӎ"PҖ;l͛$IpެRP1eɓ'_뭻I?[wǝ888eG>ԩ+1s̑tŶo[?vbOQ.YMn O6d㭶z "6m7a+amw|l]+xOѪ:ЦNlw@XDBhF++!-2 }n59{$ U;Ύ2LX-%niS?CZ}4MT%Q<`fΘJ߻W^Z,KM'hTnJ4<Ф_=gВL?v;z)@cu7YsBm-Qǘ%HȠIYPYfL$$cd!A}HQ@3 *`xCJ("xd MÞTx8fab ZkL@$1t#n6MC+il\qU7oi|W}1Zk$4V? W#V=n[`ƗN2Ʃu/k^s5<1L^u-$j R)E7(MHd"#div+Guk(0畁45{ LִFj0ov{W}vsO?ꫯ6}ڴVxp𩧟tMq`4M=wòeEg8rޗXYCݾqؾE;`;v8nǣyWX~,$fTb*09$Q-G"TZ]].\F2gt_W]uɓ'ZE-ZܰoAf[g}TQG{Xz N2-ܮQpQ}h4–ro.zs}00k?f ` :0R6be###os,G'ktPы:קMQ<6:Bk_Y%eу3Iʛ 0wяQݾ+eoaN ((XhQ*Q Q '->͉XJQT[h6f}Q!]hGl,d e=9fպdN\}:sEʴ9xހm2a,sϽ0g^ǏwԑϞ50]F\24tR`5f֚Y=윧y@Mt]KuQG܉w"U|%yG-:Fl4"70'WFQdP(2wJ=Jp  tLk4W`ɟ'!"ǧ D7k:x 8:3XzYF-Wb:$Q;μwP:_eWSu mC/g+&JO*gH\LuꫯN0g<_ K,tuVG sRwcP_h7m5iۙ=$.2e`<Oc!HOĽ.\? n}^σr Ҷhac*Yu:Ni=vshF]GFŪW@ whh|= 퍂r48M9皅4{oo9y x)o}v}+n]!,l|G>sFf o.{setYh&KKk { yѢE%+y蔥jFwY^ه<8җ,9}nڦnB  JjQ@AAW^{-ymW\DE\ @ ny̜wٳ! y8pvv9ݧ~oaaԟປ,~rK`t=O9s#_vq*:57m1W8t΃}*rԔ.9kFoy;ul tpe䬯i-ۮҎ>{M ~d$+:*o7^ g=~o[ڞ\1_ܻ~G+.Z:׶z_Sۗ~5}?=5W@Δ !wyK@w~^IV)}wk:>?k!ʸ:~-(=z ']rZ"D׷~{J}}tȤn}͇ao}M,IJ3=! qPz1N>ךrͅ,A3cI2ٳ^K3 \pŝxq3G+->R0|!~}L[n[߈gThkol54ilNg_KN "mhj{+\y>QHźOgҧʉ :DBwBT~!dj17=;"X=n\-GtX`n|j~8j\Ct?ã|:(\!ohAh2Qj `B Lv;TLIYdw )G!O~[O qSjiTYȆWxX}1|t"`PAb¹9rn% 2@db!P7"*N/U51L'' (%K\x٥UKށqnp5Z\uFL2K?"w-&,^4͎ ąe;9iio7]gyl7?zʙsb[\}ܣ[+v(@:>/7+RZ>ozrZ׃_8n^1n̙Sμ7߼!v31*D[%&(+W%hE TY( _~)?׌Xr|o L\|;|_j݋̛e?WeAtK<ğ/:gZg̓~}?<} 0~ >c92@=K?/uȸoܮ>uܜf?{,Xn۟bgMSV }gQ}VZTѽ6 Um}^:t:2kDQ<@Hsڰ=f;{\Kn=5Br*RUU)U(EPӽ; C@"]a0c3g9Q#2ϕ2>*BY_ " Z:mVO6B3Bs>4lH䈜q(q&_rhj#MBC u87)%&rN5hziSZ95ʽ)".5*.0ᣏ&q'aN,.9bin~xB= P"ƛ_٣ ހ7A]n];hu炰\{5D sabcOlN$l?B *tj~N%q(K)i+lwͣ*뫺x 5kUrs9DT*g5b$ <$s *]b2Շ~C mpTZ~Cp6y|(&ι0*>)*ש+lN<8mߧr\}ȤeVU7v hּX`I!-QDZ8M<$W !{$C"\W2UJw0NZMyZ9lP{/]ꨢJ7 r\h4 'vMy!:ig%𻙽&ęKX%'Nyq ,_=Q6wrsG<.]\pwق׀i^XZ7cZhv\9nj*b 8&0 O5 !DVF CTذ%TB)c  GNvP@=a%DP,DEQ*6Yda Y![H\ɵe}\RK|+F圇Zh#Eefz^3dn[nq0E@qM.S jðIwI@0V|@ׄk Kpx<5\l鳶҄Wb*IY@63h⋗NXw ~|}^ǡonuN_t\73x_u| |[nY^S\T3i뛾&o;!"߱|ە/0zs@9'P$8x)+Z$[=ʅ;^+ɳ6e\ 30~p0;S1M( s @ʿk:xdl-Ib}DGmŜCj<Ȧmq5"~o&qqKW)iO;0V"4z پx s괋l2WP|)]V,n39T902jOT^0=iO<#-` R=mV$|~g7tq-8Gw~7LnAصWM]rB-dǍYcN|5;.]ɺm6^ts~ Cg7R/1A |$v~cO+D"U}\1]5Ma-`XξGg EX\XVyNyaizT_xhlC]srZgΦO<:We|[J"duN#Rf&qRzʪM5f3we2+cteҖo,ws:pz~y)r1ߙ_%[SQ~.:yطvFr}O$$e?coMO |(v]kq/+?[&Jc]MoT|7ߪ$CZl!5,_stʅ/Z IDAT}ɏ-|Zlye>uxb7Ova0w <0=^gϪb?R X4֊7` Z#U[̻nFeYL(ź3b/J]ھr9cP NCKw!Uu` <3jH)Usi}*%X&CUH;!Кp G.*qd⦾_r0 fdl+oHV-~<{Ii{pI]{E :ٝwoxp\:{Lٕ-ߑd:.NEq5h+IK "$a"mU9.NZY 6klk1be|- m2Yz#M 0''1Ɛ< ! ~`d^$`ٕs>Vͮ37CV[e 얓t_}xEf&%voL?2h : ZwD!IE]7u'PPR'M囊V)t5i%<5}d +cx! Lkgz4 ђ=IA)T7]s$yՒo0}*B}?$\ē^iƏ,n=/W2Υ.w]F.˙sЁ֔{M;k;KV'挕G{1Jذ[βl3gVdxww^c(^xWTKU5*EUU#RMBMXyA2F"F' !DQEQdD(|: ;+YBfNw%AbG3rӫ<<"/f՗Ny.$/aLaain!\%@$#ka89/je8GUhVd!$G`jzFKЭDQ9uc\"2Źbsŏ9]EP܇7+T]62E5I*299 3mVmO+c،FGQhZp:#q1-[n_.tB mĠ$8q+}^ jSnVAsmRcQcx ݲdM댧oIiJ4Iu'm T:,u)7h=o12!̅8 j]c7;+t*NNʚ9-уQ?@ݿ]SU +9M=̗6s+ͭ)$VBRMwܳGZh>,VJ3g ]ΝA`5\,dw7t3.t;?@Zhlɮ!ʩNYd pFT%#򼛻k$C0|ʨຍ\|vq"}jǻ<6m޳tnu ̅ZhAfN5DD(|Gѿ/G9-_0MAWJ"^R5\w}khŎ=>V~S~+G|~|O^+ڱү* <~i__9 i|})7>Ы7̋W?=8Gmbu'/skwU[jC+]3"?=q$E+YVW)gQJMǜ@Ou{I]ߍs{W}y8Y3n;`0*ӯE|ڛg831N)ԇ7/y؃<psXplyb>?xbXx{c薋ER]0IWu'>yGt=uXE>Rn^oɩ/7Ξ>u38{z6c}LpM[ep̭'8๧~zqrG֜~pV4M[?-jrOyGL<}չQ>x9c0 $p(kd o/l'gśS#ҩ~tuUF\m}{"n݀J~bHS>a]ܲ㗜֔>ӧ`=tm?8)9+Zy-+>/mx^«w"H"[WtT>cW}}Y_l||VUh+>A2i0Qu~aGZ6Px?(.<.{$}v݆uo<C9-H!92`ah+8kl|Wek<ʞ+2\ݽcѬ2Jx㽷B:'8wӟ1yYq'kUk%O>scuMS0Ϗ?3Z2nNƧ{E vxvoߞ]=sI֓.9k ) 3?\7ټҟ>7ul{{{{{ē{}> _m[埻s.h~%_Z81 }{r! |vx!1s`F~ BRiX@8fbLKQ%/p#jb˥md\ϻi}H q3_zVn97=J[yq54Y/@yKÇ!m M5֦ ~o3^~⋇"Dcai3/8[veoXӹ8L6W3)p(j} m3F8|\t8pYpߙOX phQ9?#1ۇKJX-~#J8V@#=A2Ozgmcnyݝ HCK3`"wn哎ap @oV9.xw?Kh}k[=@!q=Ö&1g,9<wh?'_v?ઌң8GG$6s΢fFU"Y 1Wzn\{fq7ܯQK*DQKUUȷZ?LXh Mvro |c B 3H;o~h[13| 6=zܿ" o͏l7_G-zᕷeXF{]_=Sawn~zڧ_4o=ngVJ!<VAۯ2EZeyW-g'@o..B ̈ }]|ߜ>}6lu-(/>N?&y(d~{Wb&m;s AtK?cuO=okU_3-Ko:o-<p̈{P^z ?G=)-/:$}M!Aբ/' okU|t,ꐰlaP Q f(9ʈ-0H('Ho B F"i u],1S&\=_RQ02'6N冥`MP4 !JM(K9Y)8qt'Q릦 ?mjjt?p{5bם4Q^μkڇʼtI7z*EED*B "4g1HQT7r X"tcIVIٜ+TU㊢^*tͨ8z85qQ=(2bjEkhhꡇξszx 5H駟v``ŊUe<888< /}-"B3Y Dĥd)2X,&~;Tf-w%*(2YqH*.-XJMWĒ ^1w޷~zfvv.^UBȮ]>w700'P&F9&A?sҧefU NyyGLjK5Ց[چ=orR g'/8)f2|>oX&` fe0|&F# N1{zQ0҆Z 𹤇܎: z3dqP8rfheЙ3fp"^Ś@蹫^$ΔV;=ƸR(鴫9q&JNbO%B4q\)ZhC:;2>3oqyEd׳0{M9८t!W؉*O P|F_ЪW>c/^784 6lܴ_qgΘ1wGjTqLB f\RRS=U=ek?ڑ!Ðpd*ƪw#GrYbDEdh7TeVܻk\!\Q*j?Z*%JcG)*X.\W\rpx}m`(yJI"d2"6ZhiN+GV?hXel-NQ}rrAUD*mbw("KzLƒ3+'qAX_>"L/B5Mv?$JBJWu(U4xhE9/u ۰r5xss1v:P0 rt)'#Υ+cW$C -4 !Y@Pj!B*Pr_wyiH]yP{22/D캍 +NhTMq Ƿ47?g[eKox9EĉΌu?<z K4NE\C[gGd(&gݝySo'^8Fa+{O LZA=!BGb,bnn yoq%˕dP䚾;rAW]:=Lj-P9u̩,hoo[r_Θ)SH7Z\s cwѻm?=9r{ԙ"(@!'zɥGξAAXu&c'i\^CNAr8eҤ3O?ug/:ٳW\ _9 xpg 1?7=|Yc]+tu|NmZM_:G9(@5Aw۬.7Q8c\TA^Z}@wˎNExj wWLJ8=2nC/c-zࡥ=!l"}Ksz;ݽ~'>4۠|OL٧ծ'Ə>S=Pk# @YIБT[}#?KVgR5\'=6{D av fߜMLRE #0 N+E'oIE4[r˖=K/f]Our"95܇UtFxŇ?nƛܳ{'%"hzbS;'任3~'j|9oP]¼^o.H-ޡwW-o}PlJa7g/T襈HS8F`T^e2Amj*:cu}{7]ti17ixQJsw̾dЀ8"@xqz`L(t \CD7QizKGY +T@X,*>ud@ܻ3 '괫u]N]`6U=1݃ Pkf愨 !HHR8S$3;]H<)x=/wH;G*n M -ŚD/ZȆ"8Y;7yTJLa9ԥ'uREӸt/CIے.^7MwV#i/\np4("˚oL$ c&(€fнExby" K:^nG:ߩELm]>7p]7oٺu61 c:"Iw}6UMM`#=+\dr{[UEU9g*c[@+&$UG/"s@tǙB/0ld[X ^Blc]gsjy 2ޜ+/T)JB,!wcyrBY^Ε2^Kvg-}\ZmsT^uxK}Y\D.:=8 U(@ϰ*p_˕$#Jx9_v,ri bNwuJZhSK^M IDATW1&ԡ H}}+qVwl3 "(L[q.[UO/F+6]Ю,%WQ'r-8M1&zCE0ڥ/ͼA`γr_k* y~>s6rK}vGF{jڻ8ݜ^B=8+}_lN]lH.`؁l{Z8/^e!ĚE:L5oՊSA%>(]B 5Ƙ3+ *&wR pOɾ?_& 3ȗЭ"(l0=ypұBD -1(! A*֋k~@-UYJt/D:1l>Rje>o #E{򠪗V$LڨLD|9!R(ab Nf[r, hsXzK0Q@j"+$Q"#\1 18CG{?;PVx ~8 r `LӴ`B |xL8RK!LO ^p1< 8ݳq&b7DQ* l0+Ƙm2PEUU{J! vST?)V!$PBj^묔1ݧ(6}њIl3z@gl^)ܪrԛϪ]ܣA`g`q[2w E ݲTp;`o3_"Ukљeu] '\TAu]hy7gPeZ@Z}r.8:(-IމPNjrO[̷!3j+C45G@C`Z!qro%Eg|6A MD^j돭wX|W<ѷ,gM1a9kkݼ'J:4v߭u5D ?] pD~r'^=g04DL%[pJ.8Ԯ PG#S_Sq~"/ Xm P]x\W.WnݶcWշn/F&N ?UŠzA0;2#&W0\~WҴ-Gp%"e*1YGWn^QT""/١ZwK&,Zr9~4/c4,m_BsNؾeǞj…Mhs`B+&zEA٧pvdOt"ڕ B :2b2 +]pWv]U1m<#:NP& ]"/F3/8[veoXӹ8L a^{ 00huGӧr )S4.[dpǭf-NdL84먽"q{dc $ktߑP[`$#[fWwd϶_Dg¶Q ʼ1g,9<wZAnstD;=:Ga7J2c DR*4L1MUE$1J-r쌾XiZ4ڀ l2 F.s̞pלKKjȭ3u!+·!0 *%"˜ny2`]AL l+.\S>UQH24{sM.9~ò% M?9|7_?uf{o{噿r ^&끵=Rͅ,.ڭ~92{`S J۫I+1wrʉ}BZ7b6 2isHo?+%Mw9:;gsUʌ9k_m{yOq̒ 'dw ƃOXrdbƭxۿiӦN>eɳ8$v׾ri%w'z{W>7&Iߛ m}3-9GP]osw /4`V>Cn^ # T 8y fXtR9lwe?SOg>^Ӭ}e%\9}B!/3{߆Oo>yEgnw(НA4&(*D$Z©hzmlRH\*T)եS kGƽx'Av,\ Q+K.7` 9W7(;_tGwj'՞}u%ѐ >7?ވΚz]$;ҴdzKKS#8/m׵r`Ua98!K_Ml`L//~uEgޅ}$ |JÏVDƁB ut9$]RJ`"DMS2yQ1RH4o%nκ'6}q+ܫo7kyߎ{9MVqP6@xB 2Q$x.R7Č(R#H"+JFH$4M.ݽx\DRsi2mb5 IfUF\gvz+8(sN{H?hc A*eV(L֥wp3bP5HC -sh7BPD."TQ56 "AT!hV( 󈨪(Rpb>TjJTr>d,;]Ӭ@5G ehcssXss<֬liln45,s&9%nbv C Z)1ͣ(j* K%zS]tx`W!;H ȋ@!e|1JXDAnSx/H*WU\+댃,D(E_& CUh< `wH8@)P A 3}p*{׼?픙8AxG+P s7"ƛzB믽Qlo.NuUC mgsJZIgX,JR4888<<9#|oݻwvwo2FTJBT(fn9`zT0N.Aفs4Lj"xXKVLIU@j7_*g+׵Diyh熗3ol@61P2_W,hg!!ąZuQdc]\3Q5RgSD2UgeB>KZb3HĢRio8HÌ1%Bb״Q*dmJ&TBu ]JOM(¹aYθ/5af 0ܜoϠVfs9UMT_]6eVQBB!Fa,W( 7ӊ b>'h"B0LRP"$d" ˠ8TX,dsCCCnl\mƦ&*J4&mEQHv2lUh"؇P*Wk QaYNJNK蛡s,B{9|֗!];|i!DlkkSHcK55P4 B^*B"8EdCù\.-pv֮5iB.%t]td;WmR@sVM8jQ8衺ΟC4qV"xHͅ5Ȩys|P]c#%@uL.3L\*JbQT*DRT!۾}{__o=00F)hT"2nv&;ڵi@%C'r:t^ޣ1b;nCK~u{ \LV]GۯyKYAWsﭨm>H癔@(E瀑H]SӔֶ:`Q7tᬦ3d0-Ɩ1d 5JBP. !&0Qvv[nx8[.*b1Ji&abz߷nNA^p<9}txGwG69+a*>ER>ὧl{o?3X1'13a<D'Mv6HLֿb=]-Ifn7jPDuPfRDucZO4?vի eU%LsB, EM>cr}cرc |4!ǓdTk(CCCbQn j ?N s/Լm-$Ղ.\ޤ7 D#>EeljW~5!!c89ؿ |OV pa1HPK-5]bPjP0ҩVYIcT( 'cǍO54455U*f|0>hnJ$xX. ENu]/ˆbtT*10(j;qľm-5] jw˓-CY'_Jvtڵ E˗D"Q*Jycd2I)P,D"JeY1*9W'=u߼=q^Mf]'4(0D㚮uTjKG,L2u ӈ4B2 0Í1dL2٣|_k4o{YKwíd_j|-HMax) rN8*HTtkx!=m*Up.ꫯJD*d|lۘ40=RQU(IR1 B?L BU4RXNt8գm"8bEj Ϙt-IjP.m|=}'ڏߕzfY҆zzvyIy/M&P?W-z_RV~z568Ku:$;б5p܅>BәU7ZQB2dm\T%p h&!9Q(rn*8}r8)k֬Y͍ LzQu̘1]]]QUbx":"rƆAP(hU*j,d2BAR7%"UǶ'M0Ǽ#@nN*5yOz/j]q"`|A,ZˮmCљ>lf(H""HEA$3B ($"]k׮ #4էw9mZ˘V_RT瓑T"l4()ZsdnMvCp&;~X$JAomnb0QDOŇ.r҅`o(.RtT&@i$J,28ƁIC$ųыNZh̠UV7͠2 Y͘1㠃jmmFmmmT*-[VZ%dh0S*%-W.b M!34L֦RRU֊L@K&SpfdU+s{6+hQjDN8-_A^V́9JFzM8}} 6*Dpox,~4wәk: JR:`&u麺d2^.T8XtqT \p=ݻ8,73aL&Cph4@4FJZ^\8p,Q*,v{3 i|C -o^ ijpr9e#RTT(Κ5+UwQ.):!R)F %gww@(!L ֞Ȥmc6o<} 56665fr9TQ'BTQiq610/MAWwm<;~T$ ZnDE ׅkNː2Ђ]Gτ2#EUGȁ覰=W/a$54}rPT\.ke!L휞o,[ Et]TscC&3v^CÙ\v(0r ]Q!TRmn,;ro]dʙ bZfN3=+K. C9Zh5[pۂ9Ic&4U$ ! fJRk֬ճD"}GOPsQر'F#JX$`Qmlh;͍"6cOy3A,XNk+38ɈMa?|iTRFǍҚ/{vz1qFgP\nHemڷnj9&rCCÃ}۶mX$jزr%!HJ"VFz)j Khve^`" \/IKwiFf+0\*v\dg6%-gc;W\ߗJ_>Te]KV~&uQC8F x1mH8YWNR|!d.H)&R۷RdF1_~pvlW%_( %b1UU 24MT_NBE":ؚ #r7HZKx9OZA4Z: .ߍC4^^dONT(3*c33IZh8B HB)*^\9#&k$ i-2#mڰ1Mb5re:Udٲy֭&O>#mٲow?c,Hum>ytaw&J "hBmXCCC}D4}[9p9,%VSDJyP"tΧ1W#}u2X$G~=-44g@Q-ЂTXzTCFw&tzƌXkKΝ;k IDAT Bv8֚7)`|ǎtzuttiiټiK>۶l~z"H]]"R)D`4rh1"JLX %Eo笪j$6(q'fhxdԣ]X;P=3н?߽ޕ+b`f0D - Q]KVyۑGCP !:V97QֶvС!n\T* źtݜ#۸{ǎ[$'Fa0>h䩓'OhDQն6EQbI+X,ʮ8sQo'I:EQH%Ɲ^c)QȌ}~`T:R)^,m o~~!Z dTApGCDžV H+KtC,r\ކfJ$>sd2Gx2N 1f>DJ\fӆ +_{NRexx(7 ʅsvOWNUR J߄ΠD+Dx^U " J#m ]HbBRiRnW3ߏk7Qܓz>쳛zx;^7M\~j@TAPFTI8KssY'w9/v>Km2sk#7 ƣX5)dNj&K.ιZʵzS{ޝwy7+AX(^Yoluz~^{ݎ^JA\}ﯬ$I)^υfƘ$I$y?ӯRTɡưL!Ù>N/x~z/^h]w 7b&;Lb}.m{]>Ҿ/^A}C?=Gn‚7/‹?W_Ū4?%fk1,`B-5ˊu"  (j N"c_}ĥs;(~I}?Ir޳$?~ZAOO^ʗ-wpyj92d(`P,PUT)łJި8KD%سwE?lnaڨJ)7nwNݜkeff&cf_VrTu$az(r0ˤZ6M`hS"Ls %Hߠ$7 Hsϰ]x*.ӰHM\y^qG~m_ΦG^{o|WͿp~^R?5w~o]yS 揽YzʋX}4㝮$AD3MM}Ef ]`w o64ؒLڬTLq\Z4"Jx]- AUb V3vxQ+'?좋L?-bN$꬯G#{vaj'~ܜݱZ^_XG)÷bZ#Xx_;>^ M2&}5J\4qv9 ׾׾཯_Rcc]g"{#'On^_?~ 돾{+оo}{x$Iw[d?j6P} n/z\`n~}ʭ)4])VCs;p*b0Rd6Ɛ#$2E,E={T*r|Ea ݎ<nlo1us<:Jrر];wJQhE'OLbPHC;̮3JMm6P8z\|Q'c7lSI>؞Uf1/s^_}WkgWßk~eH߄Vef?gD8\{{ MةVQq4 Sշ履vZɿ}8#WP㺏lK_x4</Ϲoyv{ϼiw4]1mС2#҈ g=ёG~l3)*TFCvIV;jF @)EzAAyaqYo8p\\X@?y$h"O\X&H뵷jK`}?S:({wry$ߏ׳9r zѐO֧׉iZH?8r3yc ^'ӡ35]QJ)B$ȹI4-:>"ZniTdU*u7JKDl3Çv;w߿@~T.jzqk,3Jm6?????ߨPX\\T ` <_^$D}B=FpEI1^*.yx+z;h#i̭סp"@PHN}<]5]!: T 7!b|B~uk)c# ǎy$R,DtjZC*& nyҮr%"֚N$={3 8^_]jZ8lFQ.t Aut'xcgӕlo#7 RMtMש96$v(6>i8Dt]ȌEP13i7dU(M9xmE~ooEh+>G~;E[v4["tM׃Ƹ~1ĜtdxCedb0~5M(1-..۷ovv\pݻ]𵲲=n?O'w|TfTԒ]z ÝX7Nt 6Ħ  V!S<τ;Yp3KK51J)̎"RvZz56pI<ڭw=<ŷ隮}+pn_!?AS8:)h"p6l^ *9Pwݻ_%kŹcǎogZÙ_iN 'Fk$ IzQAg ws=J9rƏ,یs="^ݴ QYKȢ}OF0kᴮۮ`ꪫ9DQd8?|7֨J\nĉ7x֪j9rGS?쵷6{^B077W=GI'6#{nL_cD`n3{rms߈O;NtbQL8"0r4 XH2=8 "19Z)ݿ3cDffr$(5a 3SH =RF,y17gr7VpڭNlZ QR)"I (//cОyc}`'|pJ!RaCcR@l=Etyֺ>s_JWtOn9#qиm{;x_;~ox/(҅O|P=_m1nK9Kygذ䧑Q[29#Vy\S9zZٳyya1H֬ DQNZ NGՎ{ezX,hh't[tV5Wu%?gsg| ~ ~]/TP<5?zw_|s>|a6Ul)= 4E5f~p x n.|Py1** "fkwAIyi}weeru`XI`uuU?#h=Ꟁ7`3KjqU Z'._>e׽to}^̞uS_pY}?ª}o_пIҗS%׽?~=3Oy\D}~i^{uϽ}}Si驯~wg֞[?q<5/&򃏻+<_~mtV#/W1⸡bh~zmq8zeTG$e#CHCALGUر88c{ݎrݮhyy}̌vs3J XVzf`,[~ExY\ׇ}cacٿo>|>׾{~w]uݡC}ϯnI"wqwox]ޟxuŷ/˟̅;z~෿%?Io;kV~7}%:!k̗} /zF't}#x:f؈8a#h=\)<ݤ:Lk83333Ө[kϽ_ƙg-`QO{T֍7vxqUTZ7\=?@~K7ț3ܵxDTsQGG?W>gx^yuD.tzǏwNg&`]1hA5C#\[^{uZ&SV*rI.d !ZaְST_qXfЋ˪9R+O PE<=3ηc$qEI ^nEȑN =}iCp.#GLd\ϷݿZTк?)؄L#홹Yu[KFFL/m? xo7ğ5oEwCP&U,W"}3!9}5*LzN)#hw ٝ86YGH=)dfJ$I*J}޽nfggf*-v$}KFcccP---mmm>|xcm{䀹J{ &$$*{A! !wRܕo>g~wӗ^/j߫ެCk+5imya/]_j.c<zJfŝMh4Z K^ٯ#{ۼ(!5H?r^eHtN*f#Yi#`!a}W= <-iGD=#2 &i";v,fms 5NX[$>|&2P1mPM/|L о?_yW{_]5^o?rdOusK;:*5.71@ri g[ӫ?[Fl{OC/bOˏ|菿b}{~Z/ZZX @禿ݧ 2+Fi(p0Sտ 0OHWl~@D tp9%7Q8_L'Mr!`f֚R|fff66?p|9;לwrZ+ڛv{~~Mim5&`St>M4}8x.WO~;5_{uE>E߾7?2?o8#Ww/{cJ{~׾_}9w3{͇?7|cփK_^cX@ꣿs9x~cի~ɠG]CC=㗛{QqhB57LQN} FJƘ"T |aVWym60"zf~n޳.J.ioyyymmMk߿gf}_D(=:@K]r^/EY3ywNLt|rJ+"EDH(+[) 3m-T *5i9HC9p:I)e:#"kDAaСCGjiI-.XZ\tZ ya<~>-T>mC  隮:#kDwQ`8#Ep<(2D\ׄC63c8d4:dM9IBrlseu1ziݻkjժVsssIT+ tAZ6B381 yėZIA;]_ `ڌܽ>+`YB:LG)t[:a 'E%*dm1t_"Q1i\9;;AuZYݎ1ڙǹq3zO}!)CL8:q6N8H3]ӵ 8<9iЅ4@͍uX5FQ̾流6/Y5d"_)qD*B-:/3 Ν;{{v{keeZ$QVJcǎuZVVN8a(3+q3S隮m·b9!٘s{k\c߻?,Zi\U.1~?ܴ9U,vZ4cGUرcG67}drkm'I^Vqx}p~SWJ!Ai>{SmRQx隮ڎR :sL7 p7^Dx!ǔV<,}?5\Btw]H8!/e0KeW93e5SYl8 h,..~Z;vh6HkMIqOg6YZ"sV?c0U?}=|O!y:tMEDN$B2Q (Rnc~DV\~y9ef'!D@+Bz,eO$<+*ƺ|V&I=#bb "Vf=i4VdH& "۶aۏC[ZU j+[v;-_;q&܆(ܽo?*!X2ɈISv R {Z9> 1tM6G`9v (%ȑML  9V2D$I\V`Dz q'ndCw"-eW~M{]Er@as3"2??jZBxY1'O@E]66)7ʼnI%"TdL GՀY0򸇏TWPR56ɏ.gB ˛tM׃r3i 90 0t#GBvω 7$LCMĬ6ȗVVV" ;vca~y]3'bPv*XZ2j_<*̬}$I1N°&HJrX.V"i4h_Ts^Hf TIƊ#,3{JQDHfΔe`TS8>7=U 7`Qk;h< }V MaTbM=z4‡ߔp i u8z-et'(R0 )ױ (~ED 1 s_7); k=̂Jk%kv(:-l1TUDqDQ R*-Bs":v8Mb:ԇ"lL# 1qJn@AW<((\*\.x M>qP1t t;Ry*TL*6CB<&YV;R֒3x1*F;H4ȁ$ }EX?ti H; 'CO52}u;:ϽIQ޹sz:+5$L='ȶK!q5}{?zK[W0(f`3e2+JnKЊm}rWʎ"QAbQ#J1@;w0̝DSm@) 0J"BpQ0 )DD`-"X ܙˋȂn"%JbatE)"n:uxePH+ǡz(܊ժ1-WJHIf : )fc "h367e>>%)5hC 4gQһ< Ey!I1[1*KZdn^*Eb!f='р8'"L&F*b?6 \Pz)NF(BEW 8RB|!D@eUUDdaH t^uyA 8 Tr77@v1š@n !d5]iceBjAcD lbPA/G^Q050To>v`o#^0i,ՄᲙ6_ugCM F\)Mdr30}-feu q(@ mF\VN dh NSf.M2(#庠QJ$1EQHQek%k9IZJLDT*dLliذ"ԫu/@sڥ Bb.*@Iar;Jʑ{qu~(d.vȃ͌NBHAzݝ#0Pe|}.$q4;*깆W:声Lр@(U\ 5cA1MwpzX%tP}HS>.:—e_ ͷ: &1g3 F:d{l3ah%RBd-N"S]R冦2`>y`\硝yyx Ykz%!N9ebbg`/)ksfG"0yGX+1id\O%++.EҰ #-uYg9-JvZ/ )U.Y MZi&"yMwmmXfix:u#k$& #?J@k$Fyf8n6J)ӛ6y^V][[l+j"J bNMBf"l2 Dpq~fLvp#\rfakB:?3iYf+> h!+4}&E~|YLE69(Eu6G^M}'",LJI`1K܁R:LY=FTJ"8 pGb4nq&Yd-f̖mJ90s3B)DVJԤ@h4"$(JJqJZ__Xk$mscfV)]V\.3~_ 23W*(* 3+59 Tk]*.J̌L*#+JT  i!C ; "p$< %qxJ-رRE4m72_}f6 R1p9Ie(sEa^ u׎9ԧ1(}t1!fGEg7✲o4E3Œ8HG*ܠ0c-<IH)UNGjh\Ve _x|_JZీfDBA0Jݵ`qΝy@ :R0g(:]Q4*CbH\]مPB+?Y.1b6![ rʯHD̎RJkW BJ| )(9=Ǐ&cǏs\Z)RRblݹ#I0RjVUD~qR Ð=.urJA_3FmEB |6֐T3ؾ*wET9Q{-"R-P#̪CZBh .Gn4Y (9uXqHWJz}GQƎ,f6E}\WFtY߿~Cz`8/\7mI9TV~?b"0Ɩ}vt= =]p?g,DX-(5QAl!6t@\)KB)R)FYeQnk0Uep椢$hXb6@Vdu lO PHm#ṄnP7pRByC^hۘ?4р~VBP[(>z|gI;aVm@"J)\eai—/k$:."8 B)LhwD5`J(DDTE~DZILgcXc(XDYf$TQB6mA̘EiʖdcА1kًi޷8PRBQ, Xrj6O."b>./F*+Ѡ< nR)0*IJyX:ӀZXpFZ2 ; #$x)W i5ǟ|$")ݎk;)C'jaUVHHxS)OЯl|ҫ-"fAěVւnHy\Y wfչgTGy>4=ŀБ R8UU܈X @ Ф3n)EߊԯSe4IqgVaNigNе%Usb=g86V'˝NQU+͙ǎum w.(1hfM$ArRBq=Q*67zYl J%0RZH1H\#6+#d= "!r\ڂA) "4[ IDATH3ӥ T?dNvj&q)7@$Λ]٤8~q#*7 鎚( DTIJtM::m WuP<.;")O $S,.]^`ʏO4ъMQw2(1a*N+-zZNe2VWu@J@)"ҍVd,bs}7LOdb&L .RZ^331"MH "!+# |p=3=*s#d5qP49\æ%q ;I+j**}0 | RuQMIT*Z5Z$Izݮk;$ #D0F:vME"O{r g^Y c 9uN `E˝jt(';OxXGN䧅r$AZ%6/$5RAE)N@  aڳ!Ŧ+B1#w@[א1 )B@q-py"}f̋E(($Whq# ,\9BM5$b3ܴw s![m SCin4)rSJRtcf}*`Ǥvxom{YO)itS!B/6÷ƕ2y;d{a:)*B?;Y('^hH\lnXy-M'$*.{qfBO{ )Io#ifvh=KT*8i4̜ĉRѨ$̕JwbyP O{Jzta H%_Q#*#0{Oߏ h˥jz1"LLoAy-O&uhD9irۥ4Ρ sY 8|A#p(Wu HYXl2$W *@CI3 dLTr*M0W#gs:rCY±!|al"0tV q0c')e'hۅ.gNyZ=<;6j 7,8Mi&hI$Կ-_Ro6˳?ԟ֢ 3tB֎z-go"%Xl-Zk*3#HmLHd(h0%:s檚3AMOhD`ց<"|;wv;(JіR^1FaLYFl{$&V$)ʤT$;v,-,8euӞMRI:q3s9Vi[fR0 WW״Zfl$IlRjZ RDfU0%wm(oQ@DLÉՀPʅ8#ř!D w`@EHÑVA˰(f{i(3y>ă|hF ފ7&TvRsaqq_b `5q#Ն܅eiS0.j1{ j #YvuGIEʉēL&0 z'^kE߸Sq}S=pǡ9ԩ"q5bsQ&WX{^K;s3E*sD!uy^;8Ul98k?9.@~Ȍ zF@ ~l(#,8D*H盲JE)b(2IJ8O꺶4QJBr8u}'xbYvJ֦Rq׫*\HX\uUUB[Zu-WP޿`?>=94eYvOMXtoWJ)/4Zvm$Z#bE9RKڨ@PQN1BR`KJ!}z7Acxϥkۖt$ f^|'g |B$K5f Rt8$gRl$;D1aDYcKÅm<tmUS$_txU^?GtVto@GOVŠwRcnhҊ{M,cha[~9d._&X { pܭqt)D6eS.D#XKe aJY@v H03!q+<EcLp8$IBzs߿?>>U~^wo-`,///b8.""u6M[(>$f9bQUQ6Q[kjTJVJ2k!DeZPPb<2䔍TXb)1C~%FXt2 ԱZua#fx, (\f0 É0FŝωD' aW;GC'Y"­1]aCGx2OgK9 NB6ڹ.JWz*p1qI8''?|w]v/ɴ=tZ˭Nğ=kb#_tp\;NTйZK'lIWN$sr.noo驓kʲ,1RJ̻woY===)%Pqctz3]D\EQeɹ1FDZ$I}SM[)L>SN%$q2Ь3~ C5W9舽t,8D4ڎ> :Mz bM{<֌^:$h֞ ש %3dSLI4[Y̾M4>S#,n& Oc9×ow/Ϯ][x]$Z3yeT^">pH@G6 O@揊sr Z+Gu44͗ sjti]u]@:sDlڢKڭW{JVŧ~"ﯯ/۶ִm-rcTB)U1<^.Q),(hj6j)"6M#$qAQA+EFҜ6MbBzmj & :=FD`ףR-*ێS;3a;iڟ:e[6>@K2%f[`G0㜶A31S&ixS; >}uH0 sIB/Ҹ^D7\s[Ͼ!V_ſZ_-'a}Dt1f6}^ {p^"9z)\3+Z0|O绐2ry%,``1ֶB&PSӊ4M]UPg Ve4'|5EQpJ)gug/^\|MEm[[ !MSSLּ}*XǺ^5_.E$l6J)!Xdij].iʲVJ$IRUpFCW38JzXYtǬ¾8EB)TmH[-=Ӹ|:|^?Ϋ~Nw M{YKt4OulEfYiN˽*T8,޾ 3W#ШPA= F^}0ΚM%Xfq 'V!kH{'q^ʼn>c`C˾fv7E/)ȉ>`#6;i c4sԴ [pPɯޜpdrXm8:"kϸDi@G88+ 9CnKwZsګuQ;V[o&RFJfY6w1AzUU%IR5TYuız<"U( .DEi6N⤪:R}Ywy~qqqLӴ+ι6Z۶->q8,Gc9հBh\ R9yWU;'e; V<θ<@< è!G~z9G,)%zյ#SHJi9講[s浃fCUp:aS0L~i>Zd?1 8 8ͭqƫ"L.o6h{' Cgѐ}T;Pe?/n7yqᇢ;5dFL+ǐӮKe=/͔`u twO?+r)(&o''PxٵH-"`DvS|7U B8+E&Sses)kcnl6gM]iI)˸f-f֭뺪ʢ(...T$,3FJQb*˲nV/˛bq8.˒y+Q/Xk֚nnM$RuQ^C+(a~lg>[8һ^IrK)B@3`fa  BrSEq0>yOa23) - &O<ޥ@@R`# w9r`?Gd8b>HI SrG?L/yսO1"y#XF]9&Irv@d=I)4m&za p%ԝY/Zkwri(ceY EƶRJjE{vvV5wV;[UUUUY]ܔei-zXt8,6Xϖ!IV8ϗe$m&IRUiK._,<{zz!36(zē9~==;O6z>iC^Zg?}O$m8TP!. nRU 36s$! _Nxx ʎ})@y:QD1ഺ! T3Sy׉3D?cN#,1=02*x#Z0P#Ԙe$c,Aĺw͎k$b~_|EEʹf'IVrL8˓8KiIg]Bvk5Ѯ%MC4An ܚZ d9m(ZVmۦi*UGAJ~X)O(vv#)WIK ܔ0COI 'DRkmBXm<3U,gO;S( 'iȎwJDA9M1CcSQNF 09tLU˧TQ솇Dι7sç#I1ؽ#ˣ`CH}%sd uLp p~ag:2࠷o0˓74D#\7GK6[_/lXk?GdC Us nPr~V3c:"?# @ѷɁq Z,i$Is.777)E]Wh-\Fb]snm;p믭Fkb$nvK.˅`]qƘj2EQY !d{M.v+VUuLQFQ-ʲ֒aK0DbgFGZ=nE;0d ST, `#`ݙq m]œ>Qge  zqS b.9kmy qtk7;9*̖=8ŚL)D!I+8og\.-b@)s P:o}]:lوdqpZk$&3tXpES7PgtVRJT?;bĒ4 <"%a5CEkvpϋxYUUgu%狢`k*777 ֏Bj\.)=$ ":c˦\חf$bUe'lb~zβ:]Ӗ0OOO777mݽz%Pj=mQq7<COrSgak1F;B*[=J`F*Yϭim6f*B6gߍ+ֈ k:;q {l~̎">ߴ ԎgЅK+8_@!F -YDӻzޙnV7k4ѻR[XJ)$K=ƘbA맧 c:3AI8js,6RuE&psqy[1ƮYFf#Z,18;;j%!i#o޼huۦq|w=iuن@ =|;-3k ?PR2 +V}?`BD&;tfG>UfݶgܣRyLz5į)GY{O`H {Za}cðE> >%_E+p+p2g]aD9D1O""Z~ ݨC?̉ \\o8Hωi˂Pi;:Lpƭg\8'q΄ҡ8E`@p$M("5t=BkQ)EԽDf&B&ID~Z+Ƙ8QWk!ij-JXY./?RJI)ݮۺn8˲b!xU۶籊mc}90j06_,OGUeO3p,r:ھfph$i"zsYQs^Dij(˶B:T]Sz xEWνhAt-L@c8 KiXSze]‚žԬ1v@2qgSt^ < qiERJ`t9]_"ZrEU08Kn8݅_-vdtbic(a(+fC80`ֺc8 qS"O~4s&࢟4xu$D{z9oS<9:Ғ)F͑ KjcWJ u~/MW4MHEQH)I-ⳳ$IBŸPJI%^\JjzE#4pQzMqB[ rtei$ybd1ݛ7o޼*.Ӫl1,Kʗy9u@3'"Faz]갧3yXT{z>Ӫѧ`L08<ۭ6':LXv r`NJdShhxhwG"IU/1m1'ǡ##2K~\F>\0%8\ Υ$ZcR-" Z[4y^6_37p濦ʧعT%}X9j iϝQ+%! K%ZAmn6$Ei6FE }eeyr)D ycB-"ƑZ-Sz=qH)qgyN|ju{꺩J aֺ*T߿·YoJZ[{4mvR&:$~Wn=MN@)\. L*]])`@”~ʊ P:!;ʞ̝s9yÚ9ZG#|oj3&y ȡ_HA۶֢sqNIBp)4Ƭ3mt$h=mh{ MqTZ2Gjpn qEs 9gR T2+.9KΙƺԈXWy&̤/_q]={"Dʱ>cTP #9x5u;qms8EѶ-0EcTg VgxzzJO>v-iHP){xxJv(("yuu!%x[_xA(i'8@dZ+8'EmpЍ):MӒ/.5KDrcnCQ -a|Dp=*t Ƒq I?NMi=lEJ0b3W:vccA@ZmkF[{l0Int7F1pjl s߽@mzʛ n>u Q2v."(X,^}y%"mږU[mSI.8Z?/cߘuo8*JӖ ̋&$u c;GKp=3AiTp A9Hg3ƕCkxmG!\JXĄ@Ħmt],m۾Z.m~DT"S.۷B/"G?j:[Q$X "vH1QQMӴ9J.IR".˪eYg{ sbNa;FYT:e2(Nz#NzḑV[Cْhk1$4?l3g Y3́gd돉K2lP:=#NL.kt 3`B0!Rӗ\%R:m5WצtUM-"9\_}-<(̚G 6f©' 5CMJrBHH0vVtqfQ,tQ4sN9΅:ni$H!$"`z(u R$I"g}=dY$sNFJ[#inG}J ''ur!:qY7:sm&QX,(8=T%qXޝi )0 O?f0ڝ=58<ԢR*w<9[k=a26iđ:aFl-0FA3!:xd8'iagTn&zQW֣!>h"''y6expBXxs*c%,[ _1>F\0.q!QHd:e `#͛R~+`ۀ1Gs(%g"tp4 ֘O<6H8L{KS]r(HѲ#Jf~2&eRJ. Mke]|lZ狴mzfzomlEq3ޡa BLPyQMӒLqcCdLMD< Υv:J#! 1\Xz3YqT0#68!<2m8Od1VJdIcQ5?e ѼOh&(] 2B0Fii\K 2c7 8@>wN~OTRH!8~TUcLXk놆7W/8Brݴ)Յ4Kzi]׌,ʲRJRY1ƢX߾'YJkeK(nR_iڴrZ*tP t:w$yQxԋ ÍcFphk/S3 8mRÀ8uA/|SԏuBu0 d,S#T~d0y9 ֡*TD1_]] 7l޽#jӭmk[,*ųTwŐ  1n5İfy0S2wB{q'пJjOf =&${ئ#9w7|0RN a}@2^1 v&위➭ aM Fd'-faoX 2uV,Wt{se IDAT9O![#\ . hwqEn:h 8DkYƕ~|I?ZD݅jc~Va6f:gYᴀsYz{-,d3QLL2Py vDՋ6bżwU8tIY4m}qqZ-H%I*iq:@&Ul-\˥z')n[ X 1^)eČz]uݔ48g)Bv{z$Iʢ8Rʛ/^m*m 2,,˫Exv}xx(ݻ+G4Ҕ ~qqA^֯r x]a +c |BtƒI86J a'Imt8;` ?ZJX{³4tэAhvGpT%ԬF?>(IOoмdL &8sU*c bdEhRc)Y8gq` :E5m-/.U]hL ?'C`$4le5^ʊ>q2bϙ Vg|:zzHѳ3U6uFJ !bQTP*TdF$Ɛo9km@-M3HeYlŋel]^^\\;6x@l~ ܴڶmSu 塋bٵm8{zzsu[Ubey~vZۚ*D9@O(8Yix49$LñC^KXrvi!!/pԭ ](ʽ Hr MjxVx_SSo>5Ayu0:UF4N Q_R 1`sθ .z] hfYrD6m1Xqu89,h볳*q{ZpIa)G~; egyČ9gq~3[,3  M YUuS7Mlw[tN)E1۶kE@gq;Q9kֺ-p8-V˪h@sURƘVל4<=?nwf4MYTڶm߿ozXDTJ. Dah&l%񋫫,FvG5 vGJѴs`q#I lfaA#(rpj86gЈg2=OanG>-3gm01vc{dHʈ3).GСs,:ᎣVUܔ.?tqu%O8קoY$nU{vxlEF0gCЀH=1΄YYR*^UfTsXF|T͘>Xնr3~}yÃ>[}Xc ]?tJl>h1Hy[ vWQؑ2c挝m^RH* 9P8' 49)i&轖ӹrt/exR_w:Sc-ձe`6מJuvu:?2w?<;˒D2tHm#O\v}vVcYHznojyEa=8:sNd4޼Y̋5ӓșCrƸC@J:~N膕} 0A2 !>Gca3gP Ic\pbs@<릫MY7MI9R dUf!<Ϸۭm ̴1'k}YqbrbZ!%H:JҺ˻`^fySƘ'pzlŋׯ_׍nqŗ/6IBE!%>mڦƪ'77צQOO^sZ8#BOi~0wvCȁsn c3dku B@!uIq{gUI 3;-! fg a$}4R0 S}8K%d-A2aѕ[O}O5}{ufJ)>-CJ)wL;i1jj}u;u__ 泗-eD?xS2}FI$ :5HUF/0ȱ9{gX[IBXhaDTRu]um I, pJ.(:j"#$0wdU׵<.躮,,|"VidKtߓׯc8^VZt βBk]UEUUiVMSUUPJ4Ɲ6sQ*΋֌ ڦln%Lg:=YxEJQaMeC\c>bfmƺo٨=})ccٕS帴g];_W}cL✅/Y?"ԿoRm~=}_5}(%ߍQ;T ghGy)߻`r_وt; ޽xl0LJG1kMEO3Z]r!D^L̤~wmK.6T !n$$Z[ 4٪YuiuZTql4ZBu[UMC$((ʲjiJ)l3EQ|2RJH9:SqDOp8#e2VPy}Sp:.<5: Q/dz0X9}٤r7T"sǡy1:o*G>s->}W_goOB6oԍmӶVN(۪nqZ| E4G_8 @SY3c%_(Ϻ$1ιcGDL@"%JH }?O!Y2Ȇ^\eǦb,K@g,3MKA㊢t/cLUױ֊y4ݾyqqyyyN<۶RZH CvD41VvMY@]wwyw"㲮\I&:N_|ͻϜnvqAxT%<3<N_,NHxH:[qӧ\S}ltyk^85GbHȆz쳥oaPO~rq{vg zΡu,ZuC;;ͷ?˿"`iyH9GǧsCwi&2.9)Ѝpx#q;QCC]i<_*sFLh$ɲ.+$I$%Xm D2RkK 9Z7Z7L@ן~%IeY"^;WfwayՊ_ө*TnX,olBN,;WUnA(Qm[Vk":NUU W}04=|?i`*q*=Z3I(Ζ.b6F: R[]hp&}a|agUkqN`Hki1=ݻWx}D+ h dZ`KQUU,(bfIoTm3Q%*>, 4%xY &sA/Qlm>n'f`B" BK$ieYx*7o8u$Ƙ(R<+R)~nJV` ~Y eKeYV碕 OR*Inp`kj)Wx2'r%1 hzҙTcrn){]}If=f|@ڒz2˵znA܀Ngp#]:_d za0V:eNZ],Ѻ;pXX q umqıcE*T Jac*xo)% %QwC8G["1XL+AJBɞ{a)dJ)($Y2pBO/XEwWV>QߑQz;cYMJ!ꌻ,Xy !0Nn1!H!^^^XSILD,I)$i%vq_^,+ˢ,jl64M鷷Ƙ<@`EAaoB7nכMDEv^kȳ*%%Xn%% e!=?gD͖#>Fes"t P8 =Qޯ/PH!,,iDb7%28K#ВB,7v5'y!qJ)hmQ;j| Z29u|́ z`5}i̇imE\]]'?rq1$(RA(%-SbS$QZA:ymh_95"پ;½(Rt5쇰BmY8c)uIdwѫQ~cTGy%c1zZqi$y]yUrgr^q+)ֻݎ eH秧n0}:#w F t~ dIaȉ'"FDBJnޱ}* ~Hiy B7;X9i|t%ca#"Fa4L/,) C_BC:Jes+ g#qͥQެŬ2,r~]AjSL5P\h~{j_ons??//T6%m˪yᩴԧ;q;jJmfO!pfCwm ^LkYzA'ASQ( д32N;H8PܛMF} zIj !B?JJ'5E]d A\fYv{{z&ǧǏ?e4-윾ifgy84eWVNT* (*mJ)) vu]?a`R;N(xyu2EhL IcX,rbq@g@a: IC,uB# {7QE/`֎e(|6royXE!]952_ ;<~i7`w:O_{_7i&jțT>=Ͼ_>:ů~`~+iq^PXw'y-^1'Kȃ0& v<72W8G F9WI rŏ@zm8;aI.QE ,.[⢃grڈ4m[d<MӰ YUUE l6Z1LBD6R eYBJS`nTi$ X8nEJ)~ZӉ)hUUb(j߿c'Y/UG>XY&Mm]l8I?}G?l.d}L>su[Σ%@3e* oGdmj{:UumO/_9 J=?yh?J)[͟q,"KvdD9G(v#!~zl;c{7~Zk_@r+]\S^;3b2SGX8t-DakXS UJHJE1R%(DEڀ֦fC"ñRiV816V)FkQeY%xQKd/P%VRh-1Ŵ"8d#TCĄ~9=-Ns\X,&C` C*fikM4u˲ڈ@@ū:Yvvu]&YEQTe$e hYkmȴSrm C8I-Mc3S"T#A8AkFpWH:dXC!1,;@I%P3U@'vK<80lۙ6]* Nhy<"H*Iտ&t=5܉%].fa*s.- }^BOY زLG_.n(/m?4˛ө|~.Z{]]|RiɌScw";.;eSԿ$:L5q:v_-Uae]k!ZʁRZbi$y0 (0<YiSzt:mۛ޾f_RIhڛi)EDF7F1Fv*,~.9Lx/Tc ]8-ߍ[amin ' KTܩրJ^?/2 ~Sƿ9ix~9>~31~sд_?$J-pim,].aZpX9֘WR;fH\OTNhD†s+- uu(#m>U%K}ax>>T&>ck!K"R .A<>>&Iﭵ̐"0uy Q멮Z `1l6,_a>>?7MM}:LeYU{5Oj€ڞ]EkKbec.ͲhIOgv/6Û?)iKZ?4͍q6 # 'Tǵ9w$.8.[=T@UxS,x"?0Q00vmkfZB-zBjkoKӒ dmx0,"#R)Tw ԅt5 ,!f;N^!ֻĒ;S(@*aI0@K%`4MUUq}vHɆ4VBJAdik8NJl=n SXe<7('ND=KVM;b_;=\EW*<:FH^c= FD4"|KSllGvq0.M{>Zك$[(R(6X@ ;YVOO4=EQaCDj!(hH)TU +s!6fUR4MQXGd5MyGY;h_,YtDWe$4/իpuuD% v\)Bk)k}g=dir=Y bzsRqJ>44n>V@IDA TEN$4MGq4ҦB(":X糱M R*^'_~%wo0IX@oMIhҽ٩C1`"6ЇᣕsG<< &z׹DX޴!%n:S\Lb >n)vN[& yϡMmhűQ RyuΤ1 8|s_֍k e&BJW' ԍN9$PDDH›;SIӹ8PYPg5j= ܿnZB%m[cZ\8e'!niy^En-[4a4IӴUX]yA<=v9kŚTV)-(MZÇDvy"M~'j.vQeqe@BM6 TQUlY nC3Xg]OB>lF%p4:ݼ#5By= fMG2 LĬ]>(mv6̵RY5kF9`Ԝ[R5nZc@kURZ{,,޻U>s}8Rv(^ntB*B57E g U_~?F٠=W 6fm3j/v$0P nq-Td$Rxuuu:(z||$Ra~ 81X/l3Kkp9Z؈x:LӔU޾}J-*Bq{{[7eUjZ7RY~vtQuô0`6+"vO'>N𳏩4ywye빊k2=Gc.~e 8^&.04hj"^Vm އYkPNi7B ^u($-Aֶ3Y;KW  srE?ʸrXwV,VZ Xz_c}_->"T񁩵6d=ג֩0K٪گ$11B|hKx'6.2ccj*|9(Teu]&ɏ"u,NzβMm(2* BBYQY"K()=WEiyuݔuSD*%8,zmy^uÇ秧׷CAc%_~y:@ !ĝi,C˗st:; 9K;\ŸBǧF1{s:+ F_A[`4!VBA, ҆},-@s_lGB]9_4r_%pg]rm m9!Q GBcfyvGFcR'I9ʹ*du9M 6d12f :ʗcmEB(5Mp3=&$*\1W1 Cf|ݯ9t:>=ֵi9ZzB`l]IEH)x!sDd:s쫪*2FW8$RZ>㧧'a8>)_^WǧǢ(ޭY.iJ)Oӻ!-ZN1bO?OH E)2-8pp҅?ր#ڊ ub54/[4M٥sMг4loG75ӃG+B'wF(yz}lUӱʴMC+H½~*ֈ:cUL%CkɘQfY RDYH)uU>??o6(TNQRd,je4x82h]Yb(1 B^$jYeB1Z>cu]???׍p8(V/0B$IRuDd} @7#Givg5?fZXPdj)1ƞ@fKi{q\!ij^,0@G Q:]Sĉd Ĕ8Po`@Hϰ \9qIاu${\rZ7V7`ĈG7nB1Z&3!UY\7|B!bt2B[ ^h=lEP^,ܘ#0u]7UUEz:'qh,h[ZjUU5u}dl6JkEQA,{Xkz~~zzzIVupjkq=DQEQS5d*yijBqAJA2@NaNyE#ճ p瑽vbz״"]e: ǠKHq&N/d MFo7Nx?,yzpޕӨpo N"EYiʲ\J)a 0J `ǕTDֿ0n4;o6ix ^4g 8ֺ6"/BFYU2jmfS֕P5gDT #몪2˲v{us-c@^E;cRyp{Fەac0 &>vI0mqkyi_y FaW2;_/=~׃[C^kϊaWzhژR,`8aӄR $Dqs}9M(1Ɣe""fUUۮ0 Bɸ6kX ^MQfͦ(f/hf%0879|in(n߼)* 4GAYڿ/fQ(U-R*M$I\޷9q>bn=<޺ SY~x]&.A07^t<+B!,S% h;>Op%w7?#͆,Èvh25DeA o+Β#-Y@d-AKDIѶ Դy<iWA X2dGG!XakMӐ@ߞ1(قi6Tjjf$t-XOE!!3pB"Y sAJ VF IBdyY duQZWAJ * tnQ]VH*AZ]Cc޼2Uo|MNVA$,0 H֛~?tguU鬔q,pww֍%Y#P䛟Waahڨ@jj,BJ)96M0rm|h*r LV+i"7Я6}. :P y^Ub /g+;#YۧspS-Tk]X}] 5XXЬ nqXd&ZjvG1WiS(zi=.9)V(/X/! &e',.SC`᲼ŧ+F!ˆß,1~%]Zh=[jAt cfsuu7PiEQh(bRk6Uͦ/Y.|~|MM֦&O 0>d1%4- i)cFN5RJ3vqf&>>4;ь=;f5# .J9'?s-g?೭_~үW P[UAKY_pG4,z0WD1Xhm;lZ/ͶeOҚ5>{(4ݟٮ"Ĝ4,Y O柖E~t&\$pL#1>N8(0SDeMֶ 1K!5MDvoHl61˲ |,lVxs{c%ai&%`NBrT)yyya}s!!/KΫ9~d-8mcÌmnF 5?X?N'}Y'9)$/ /Onzɩ`qS'-؁W,o*mșaD3VIBBa:v*SԮ_՞ ?V({kΖG⚮r" .4H;$z()zh;M.~HF.Ɏ@V6hu)x<ā0Opj%B|׻΢%Ƙ9X뒛zr;'V)t:@QTBsS׺zHD q|8\k,v\Zkm6cL*Y$뚹wQuVÇ?|#QXJDR z4}2ZcVA;b +2MD `q9'K %h}OSEK P׭D YA>E!patz bYs_UYe8xf!A!=??qqݬFۜ(^} ŗ_~W_]__WU145'ܣ(bl777lA[]vnҬ "ʉ7` MJl\Unw|ܸ| % Ө4G]O镳)WgkuC7Վj!ۣU^B M/ i£hJ߈ٹwyHH*!S iDV1uqi)J鎥vj@RAub |癀/˲d<== 55Bmk!DQցZ80)X eY$wwwBE)u>kvÇw !QExc'v=Y7 C!J HSUUEqq 9-l^kt:qֱII0_ AKS,nvXN7ʄvdT.UoQ=%Ry y s4w~:}`oTq kbQ]LeM()W,|>O3hk@c˪:=ܿ/<~WzТ,L}YQ4M@Ѽl(RAZ>giM,]I~Z(͛7 pqbyy ҂Rh}:߷ x4Z(M 9zYQ5@eYJ)qo6ۛz]ߵz 6OLJsN'EadUJ1K3;98i~1\l +`Ng *@ "ۯ V`V44oV.IiWioZ&ۉv>y3iʆܧ}h4V?3f3GV)!nFqjc4MQކ6ض;ᨮ: w5 {9'M>, ktPx+wq*]'*K3kAke>K Ǒ@_]!bBH*lnoor>eyYeU,=N|,<#!DMcZ?>>yy<>@YkRZ9T EAdKYf]I !B+t)mcKˀ>on[36_g:٩hBf:fJhiZw߁@viwww)}Qpv#Éi 8֦D U=YDWeu$ Ȳ(DDY(DIJ, v-2,euJj(yRW#c81TE +3/+,h>iFYC7-ؑ7loV[K+^sQs:Y}Yٹ bnwRO\th%0>FknZg qG"=0SQ>bUr10 i|ՕަR3SMٱ;{öa>;=hرf(E8Nד2D,*/KVd P /I;rfc(*"R~eY^߽ZHd6qUDn:b@'J,"SOǺinooÇCUUR !j@]ZQbF8 פn(~ZW d\0%bR֓6A7W]X> 588q[ ٙqr[2-֟e =%%OlmйlyF1]$+Zq|Fm5f1w3Ç'0/[J~Cy -жG>𝈬5?'_bfȐ:MF8' @xzz2eenwVUPJk}8FXVmPXKDZvTPJ7B)cK\e)"ֵ> y'| Q~ou4Lc/cXDy>e$ >I r;ղDxhM6""RQ&b~:K)Ðm~%Ly$IU67}WI ~ƕlmx{5ań@0TpFm~JA`/zNp5OcZ,+q1ΰCצ9n< hd0 Yv$O`UcMLDH ɸ98OmO`$_uTMH%@"0N PJAX%2l7_h;rw=\R+Jh=dܑ9/Y,5F ~npc0֘:IBHW+hu>g'D d4Mi_v^[h(KMe*Ͼ]G4u]B+$z-\%AH4&Iw$R֦(4[xDĺj.(hAH-6.R`dQ E$fbBkx oB_|N>y, [6 [%-|a1! G;Ǽ110FH}G>c4ui QXkXxR)5_?h`b\5]B8V1Q`6ᅱqGV0 7&@FwMANg@4pQ?mvS*w+)OLCd\72gy&}}[d֋J$9[AqsИiPܷb?Ȣ(,B 8ʲyEQv{:E9W$f^ !KY0Z+],"(,?R\__[ 2 (כ%iu#wx( b@qQA̯0ZIn Nwc[qߗzt8ޙ/ħ/?. FQ)h.޾G-ˢ.}N#cYD #I]R*u'Ue.p.yh826MӺyhtK2`~||,Kkx]|/) Tu(֚U8qs棶RX K^_Kʥ,\f4dT})&gIo LG)@(My ܝ鴥`'{Z!\,Ƨ Alw满^vFo:Zݖcs.{ۺ4k~0sFf"iΪ|E9󢨚qV켙}3XkwݝR! Ov;+TJȮVV a0zUeuQfJ8x͈4MQ}[cc j 8ˊi mt-Df 3"`Nqw0"JFbNMdR7{ _0w9l:YmFO1vXR y?@;hHKWW:aj1'*nSj<..6)KsrB)N#$9Ԓ3xGK>b$p4L:*ۋgG,tBšsmX@xy<{t>G@HXK&(v1/,4fyyysUUqsJpv evhP!r8ԙ29]ne7ڂ;|DeI+OXae.jH9xrXՌ&GJG2o .8U|pI -o%oZǶf߳0kUty4=/ &~ϡcP^'+tEmE|ྑporDT8K)@u~ơwC+x]V///Dl,ˬY?f׷VȀ09As6|LzGG cX#l6k7}sRFQK]fXhKo G\z{sTVw=M3ΠOY`8=杍mjp8]7ħ%a $&m{Cխ|d6.>PM\hl]-CD- C`d+&݂Mabu#f2 wXkï̖ \0 W Z/~t,-fX,_~Z޽ZKƘ$ICaV4a8.DQDQdIuvn}VVJBȺMC 1BkmB A hN: ƂB8Q'ϱ`98xi.1I(#A P"FiX$G;mЬlj1G.Ƒ'= ]4"z5ݼ{)ltPnMpECRs%$'Kjډ= k{B5ST/% 㩖#Hkt??H!Dy>Ok)&p-ZJnn1S;vbۗ,˲,+bND&> +!5Ao?!"A^o_zs>V亵i  ٢(p;4MeR5v˲dC :z'I)`N֕^~d7.#%$WpP+l3 H4ԦF'P0/p6{;>x^1Go.D7Il5iSAHj*]71Ϊ*0ANĊgO!cX70Rz6e sϸ-° e^?5 B I?4-u?F,rF2 ᫽yKm YWVg>SKI^jw o&h:MZ)QsS)h*<Bm-R&f*pRD\\N*-}숔o4I)mQwc4M(2f>Ny^Jګ,swwwODIآȤ!uQ_^6McyI 2K&I QDvInJ"͛7Qa$k4)d]///2;qqKنuH 4"?0g%NNtڈ{ ^%cXk6ڥ.Ϛ"N׍pSn&2pE zdWb"!!@y=:rQ(`%$p/:DfS=?RBHes}1Ws]]>lDq g)(<) $I ZqƁ2cW QS:%S^g"p˔Oi[K~eU~ptd{w !][~m<$ttSf;N58;Q/?{|;SRđ`z'XnncsqGǦC#fP IDAT`8&|Xotci}Jh{xA]tĿ AkNCe*o=݄JI0J\m@eYA ܬ77E 777jJƘ*DY`$o޾^% 6\om_ɕ"VKcѰ 6'czHZduW/n-lAZlX/"~&ImIFʅlS7fs4m4MvOkmci`gQQ׏r?;Ѽ4Waf0BQ 6 8XxmwCβ|.ce"}|iVt"ImeYy$<ց inߠ,E\QUU+iȘ0F͞gye&MaawMho/"|"'Q-ø~+ oW?k4dNĥo37ڰn5_7w:cW ~q?XLa{SLɡnO]t \t?Ayc/s^1GwO!ѻE=EQU׶m˲RuX,Hi_5CXVvۭ4M˲BD4M(ϔOݭmIlm{www8֛FkEڀ &8"0NxlZq6T1kY!PA$bx[VUX,k4rM(4i\ôxbMsgy|j+҆G[6| ~6ӱXP^MY@[&90t1TcAt𯱆-!EV``(I$ƚQMsnqc!;.hzƐ!Z!() ^v̎6:*=/E XɈiE#)ctQ|.u4:D<Ɛ0hF9)pV7M#IUS2,ZPHUGy \Y+E_{(8J0YQqεQq`]m):8ʋۃTY.Kv8RtSׂ_ _KC`HuʹaikUj P<@9_ʼG-~f8x. /Jvߍ^ø1"_S% w| <ڷk2mW 3ٟ}HG T"]g7 ]ȋA`2bRۢZkPuSV-%'Q)%BJə~4( SZQHo߅Qvy:e!VnK4slJJ1eTۓ ڶ}xcQBd*c'mEPp6fU\I,c9vHn3yzO:( ],e̴N0ӐAxlMx@\\obja+;] 8xhio\vvf{pVO? ZDƽE,lGP`94I)lĮYFM"6RyO'Z0 A[7ՃR*ݻwa*ydJ)0ƥI7Uy xDeYB)3<ʓ$1FxK<ÇOOOu]Dž34ZJDQ4aj PHE9gu-oۚpiĤ OfAkkq6G 玥pC=8UdLpn%0 au0tn-gYxH#ɅPN>Ͼp7<إaOd7Advڌ :w͋`qňBfgsAwYviz3:%L%&z"jc6:۶0mnVM0!cio6 h[o4ZoRj.$!x48O3᪤x}히F䚙)}|>J),KDJ|~c]ק|>7MUUZz*@E F]6qkK/ρ2~@ew )1?v^+d8Y?NW^[]zW/zo>5Rߦ^P,z[GIg)#d\R2y0N`E{ KϏBv^N.C ΋ً̌a!\/5jAX pfu|h/|Cjr44?UCoZm8Ve ,_8lfHEX wߑ(\VZk!8ct:Zk*"2gKl$m[yN$&AAJÇr-xyy ǏJ)ư,KhVVk"H#w0$%XQX۶hJ \8lF4/x*pqڽZMŭpL+p#aD6s;qkL .ì1x`Bݡ*>l|u}>E=@F7Ms:(mi4>Mc[ҨRJa(%g%IZi)`ZH"!h*6mۚZQW)˲o&8eRJUUEȡt:EQ4"y(뻩0ΓCx Y+zdYv]hOiG\5v< "]<,JI_ dRLX+p|?ג0;=y!d e_o3-gSW}heuUqDԜ+?@높 c csE/z:7_m5u]?==E[yp_޿>==}1MSZjhp88"PpLUUvQꫯNS4R,˶-!X,z~~&Wb $xԭZyWuQUU$"x}O5/<}s.s9#Ds\Fж?sHJ~s43BrAGzzSpQ[+#(~8^Ii+!k6XB \fѧ['@]yLF+mƀ0H$tn 3p'Z~iHι5J٧OaI%HTzz~hm uBGje%TJ&#yq>QR JX.W .12Yښm>>}ڽeQ7*Ǐ"rrʦ᜷Mc&Ւsaʼrftџ. B77cMă)%Q cU7EUeA䎜i!{Iu/s#dzM7K$`E UNxGY#.=5䲤yC0I;󋀉aDwl0 -${ 铉INd>21*cЁgAW .Ii/IKV"R>>s mj8=x8fCy2t:6Zs~L$M7o޸L{Z5Miۓ2&v%_LDZkṘ8䜟g Zke6|>R*2:O-'"'U{8&M(]kkkԾ {[i6bjPT T: \:ڦP!lL)j's^֗6/ l9$gn@ [`Gm/GbIgdHM0}ӧ| i4ї*nź=ұZuH۶B`Et[䜿yfдOEQq(cqЈj#TU9? Xw(!; ͪT~?Uq+h$Y.D?DNҴ( ħLؿ#)!f]= ,+hI[ծ9\xӠl6ͬpt]* ( H%pgJ"db_~vVN̳ RȮ5=IUk.q`>3%]Kw!v"Z ^+nuBY+~dS7-fabq'*4`{0 lѶp~|||.1IEAחUhl$EQDqHqI:n]Wm$vmfݖe?Xk$iawjLD(!s0DZ'ay\*&OP[].XsY9 mr55!0Qb(צnzҹw,g7DA2<;!gx/N0}7k7 35729BvX$֢Ѯub7Qf,=wzӅf=*m,qQvȀh0]%ZC'Ĕ q SxK ƯPG@z# v 5 Hȳ0nk*"ݴ" 39m[2G3KJBv̔~=w]QD1G)N}(r` DQ$ k!d'Mӄah9ESu]+ܭ(jà~wwG<9X> ;˼̠u^.h:tR9}@Nh`(:'gɽujj?$LH0j]Wcz<7"?\Zz׆1 B0!t,kB Z;}~Nq C[ukldRC `<‹)[igQn7I\kMAN'fzyy.uYJ5@cg ҽ"=#k~/80$jEqyngtzzzZ,RWԬ։yqqlڶik8a$)!I4ay^f\0܏L"I7k ΩwB}$A?YAŽnQč8sy[uY^Gd_D"4{r)[1f E1DYA;ܖ1=CEB2H}̙שA}jSZH½১n=Sz_#88 8Z;K)kcfsi,˲ZK4ƴ8TÒR:gfC۷>Qm4C Ugkr <,yo6+2MJYQTT<ˢ( y$,7 ]TE$\tJ!]8{"hv[W's%Vl4j7_B)wcv_J⳺[K~3*~N8B`"^Ĝ#5aFAR *HV+R)%C{FT-Zc :Ag~#"N`Z8od}Uػ/ȗ*GQQsTІ1m ǏD|#.i Rk9SJ'Ibɲl\7m˲X(r@RdY9r\gdRme"B)b0$u] Pu04M $mtJ{:*p$Zk|:ԅ{{Qރ G "I5RvH(pvog8pC͆#˟Hy+@8|K%y?q_Rn+p 7:,AJ!82hYD~9d:]KFxwzݽ,m܌f%Ik/+u3%SI"ͧ> 3mkFnOb(i$IV6r0D~DQGUUkObJtLB|ZEy^-"#}6 kqx1F7MSEQMn6~?29nbȌ1,L Pk.W9FEQTy$ 9-c־Q{c{t[kЂR:mkQ@MUSkIwaҰBb~>3{ qwѬx/!i76lձ~JTVCX7j?I:ɞd>`-x23I a~Q:&r>Q:pbgM@逽s5㎼mr͘`_$m8C\ krC(C("\E]UUEZ?%a $I4M]rc9% ΊF_o!Z1*Z)`&l?}n)8ӧOAaHNZ+)Efu`zxd3nnfEDݦ N̖ 7f}JL1[:9X6d1kf-+PX6:ݎ'j@Nc(R$%h|>EZ etS &t 0~/|>7) xy}=}|qmz#x,KSuk1C ۨ9#ZU7Cqv+xA.>#CN@O;* Ŧ,-cZ)5ǞDOaZ hcGZXVHʊiavь5aOKe nƋDlQ4fck:_%u.^tG,c%[#B.f9t.eK|* 08egY VMQlUmU% 9VƘpOw=]Yu~􉔰m2 ÐN۶5)]]ז!޿OL!8ώƒq07%󦩈ҋɀ|늢?D,DմyY}|8a0P )juۤiJP5g'Y܌xn'|uVUbQmϜ/qN+fGt3֎3\ 0GԢAwA$n9}+};hp_`3W9W ӃN¼NviNcZ"eݶzNvf^,"lTȤeU7U׈qWu6RƘn}h~[7bPJ%I"BsN|B(CYTr$nc$Ib !tӾ}u]*8D{XQyKU xѶ RJ"r,P62[/BHwwwW_3vr#l,4jPRZI-,=ss` oF&7<;Lr{.^HC=-;)x*Amv ֚F+hMr)͎"1mRMha2k 4r9osK! lHЩ܆*'iG%1#qm\ !ޞq@f$/Qrtv? uc=h59qX19ÞMG(IӔ2\REQXkCcۦՈKR>gZ.a+e^^ q"t|!ueY0[w{Ûj(-pϲ,"S&Oqy޻N#8rk?gӯImOZ_wjӄ1 ` !PР hR#D TtΟ8M-y>8@"Ws L_0G>G$5xK7;sowt֥~g.i{sQ'>TrƘ@mGicg|:Ɓg.^w~з]O_'cҖ;IF?3fzoR0Kkvgc" K"d$Z[( |zzJނM9_.1,A1ƪ<BO//4M0`u]y^%M 5E\Y!^?E۶i0(c m72kRAXJ[IpΛF8EQ]({!V>d^qLZ)u>E/1fS-˲ mjz+w;ҢOfWgj7$5S:漚fva^s:̍wV@/OW$5`gs$NUN`16 t͠r!a =B.^=` hL `5doZ3ź1=}>.ZTs޶m4t] h[ն%AJ)RJ@JNg2?UPbJ1&^S۶I[ݻݚԝ///1QmO쥔wwwA{)T֭nV՜sڢ(t)x||ZbX/uVi!Z( ZZeY&QlS Ec an:/‘r,2cL^taXUp z>UQad>_p YchmC݂MWB?StQ]3&A.PC[ߛIٜv$C_*Y}x.oUN:nX[ pS`09RU{@r _ݬl+.WY׾ytZn>'-2`{;?s i=HV)[YGe蜜]J6F=־}x<4M򂈿$Gcv|>yGc kOGeMu]oFsG7HpΕnQr)+x͛7[ejbI}(P6rXy'ٯ?\&˲n]RbOȲ,qѥ֋aRDll&_5/(8v*z -9}fY&"W%ipɡnZՎEߎ)b>&ޗ}{aZ뻾pAQ@0{/""fF4P9ERDrbDm0ED`D!@=D@4 "\* *&Ah؝D]n@`?A @`/*l4CJ(vWawovdRw s,"^X$/v D,ǿvCfF0%?ޢƇv/E76" ~(|حqH@M17eɷ9b@y/,TffD A5AkPDΨ>a ] ETE$gXlgwb` _Q7& XF}Mbf ;-əU\T׃ە}9ig7GT6z3Ro꾞pՒtvn̫:1ru֥X=iX'H15®v-o b֚t"g +cY D@v#P]NO3N,1()@m><] & YZ63mH071I&cz'=;"!Y+Lq/ZgDIvDnVu5CYW؏1GubRG/Fw))wy'&"1,p̀H9󀈂ޢ](a ڥK`mzlyId"Da"bչuT]C1ؼBQYn?"vb9P0P5$\o\s_Ekz9kaj H\F"{A2Cv7r>;!szbW |l?XzEc}K UnBe/- ~JC/yge4z).buON@Ȅb6}PG.a3l'@T7L`b#d@!"H w+CFD0bb֊GXl7db6=i "m dGOC;sfn5$g`4 N) $hQ$O[4C#52f?ܗ7JֽqAy22ΈǻRGI,7Lh;ntpXucbf` BA ;]E3\C"63[_a!!1v<\{k-, `$:;M?ݒA,qRt'uOY?xƩ0ӏ=A*V>nb3DVDmSٿ1Y%L}v MT>g3f lH4,g &ǎ[I}k5-Qpz2BRǨ\C<%.Bĸ{΢HrruN Exy'Bί) ؅ ~3 V`0WC*#N%YE茡BLci3Yv6Pe>ˀ w*.B# f B6 6|$ XP_jQ; ^9ٞ@LsP'"0}\Ԟ8!*a#I͂"X]:J:"v 8T |E0H,aD`jƎE*݋l60=^Zv}X͒C{Mi?k^x$7,RY&ZWHnVF^t$,Fc(@rs~)d"UȽh${Gv͢ZV !ҬQ`E}u u.F{M#ߑaf xc)E\vs$kq4L|sH\\"a-~KYG\B<['ƷmzYl™8 #wad2s ڜ()XiѡE ֝b2 ‚EbnB19B@D?PM=~I!ImOWI̴֨Tǣsnmw$.XlKLTQ^|Xsw[lĩ0 E {K%kdA8rgF㔣~D|/n烨` FDŜL{?,8x݌4s؅(C1"FfR5V&AJQ N2YVOgODW0M4T?Zg"siC"atͲp;Bt)K8 T[NgA¶ VLj.%>M,y(&27ao5jV rf7zm H.  {&G0|u*y&n'3o$xCF6)@L_n F6FP<&`=QF;.H~Wc քbD0BJܨ'vF'lU-yrjQoc0N1BrC't)Th{zf#۬H". r904wQI&=E#KkP;\X9>[.uퟢ.[4ј{[z#;%8)\\9. _eM $2CHWvAi| `"IvbD"I sY=z03",شW  9BbúGm]U#\3%RJ(3lO>J*e;$ ! 宴QR$6lrЍGֶuavd> _q# L(-K1jF)1"A26d6 ZpB҄qLb]zg B.ƛI70} a3y|a'f ')柸{+"hjJ1#"&3d-]!Ŵ?D޷q$4WAl+U%y@;(jDAHfm!L !ҨX%/)(znmo'ýnH[ěI-H D 8dIu62-$cV/FqKGA]y;N8.kh ,软6mtb= 4{, 1  !Hk#j~ZR:^ HbYK!+dA0H)2" J˅ @G5r6פBHO+h2ľݚިwJ)K2iEK|9HG+z B!@z-OTͥvD ڞ|,Boh|s Ֆdm2Y$$""(F0LYV”c " ۀ CV?ľIDj8;r\0ZRGLΨFZ SN04eI6\APtBփsι: 1U<" ihz&"+DZ MnH;_3hҙ ʑOyED4kmo};d,bf/~vxE3'P3"1%@RBX,%sf,ʲf $DTQ'c f͒˒}Weom6ʲ䦹 6 lhAX"dKI%"Mغ9ipFk6K\u5˗l{DC86n<qC9NnkRXq$P}f4KgII%bxXB$1? QI.*nEIIx `o!qMA6c!7hE@1$/ATEj@u9LqIͻɕ\*Qo CAQ@֣&xgTW@)-,N7iE|wDp83d ?j @0 d".!QYWLPm ڷ;O׵LCEIW0 ջ Gd*mh\6˷]o{;x狮 Yv~wsXPrKv6T6aO0d)KCn9Hv5Gc\Agj$o|oEmڝ^q^!PK:2 L "9g)%uDvL*Yؔ5sYw켯tww'(_ٽmF S Igoj* EhDJ1kP\͝P;?\C!I[`GI MqMWw1lY-˻\(Ev>+V $T|w #58.=KX I Rzw ! N^#&2@JmMov8'Ѭws'gsgcfo{;~%3sSxIezzzȷMyy/,KF!"ɼ"3%  8 G̙n(IHY ,ZxL5pӏ8#q13mG _rc$_dK6D4 0v -Bsbw $5BhO&@O 'BLAxf1X\-DsԔ0NG%fPlC|JDʾe [` 5!aid'7`5B9H b~foQ'VP# !h;ݧe=g-d"|:J\jaDTf˷]+&Ͳ{PϠ뮿n^ |^DrmYzgr.-FbY6e,4'IxiŻI. m8~||2{ٚmflȐA2EA(o3XV)bkV%`Cf `xXf9% F e^W-O|SHEDI5mXDTfHHJR bfPe,R2rO(a? q>X=EӒc_D`ٻzq"tx_2 dp?Pp Uʨ9N_xT,T|~RϥE."n]SXuS`;,ի>a%?CG>/櫯jȐ!cmĈ6,˲,cwuXѥajīTH=)b ,Β4(-R^]b7Dew tx!} ojղ{HLy$+_8qRX0sLDټi;&,cfj XBab2 OM.E?bԙ`)ӏe˒" PA "rK|6i*<ԺG}+_?qҢ(/Z_x#@A',>~т\ 9&T 97ٚb`A0GADF0a,@@]e2Md}gաV>U5g{(--(1~8't5 -v6o)ojlV-[_碊^Vi; 9y4q],/r#b7i 3V:͎9Zu$ekD㐲 AA;[Q[3e%c{v3Dv #F #@AJ\|~W_7"Ѕ0Q5͇6u͟Ǐ?qJ.~E_~am"*S\ol+ƨI;yVO 4mPjAĆ KLeWy˭|@kn"?*FiĐlaQ)娡g,1DۮBgZ"wn'çSqvH}1CTL[# /qRc:6P Hš0)m#y_&DC]ӊkmD#"ౣx=[Z_:CH&SLʸPY롥{-Re59ӧ2zmTJ ?֮RF T4F5l谧=DgGw<ݔJn/Bl6?l6GY?ud.}Yuw5ضR*$d̘wzKzʲ;}Vx\j;V9]T3XfblID3z9uFE'$AXX|k4wj&yE,Ψx4_2lLɕ)v@ƋzʑjCE*q/U~슔Tj-KD<MՇZH̬0+ vw,ϕ7 F{AREMxg[vߡL 2c*n@f?"nOioѯ;e?wOT&dhM^pp1 ֭I&S۞+ʟ5T*yd6\Θl~Gl!PťP0ڇ:XwgoC$;/GNk4QE+y,2%Ddπ֊ y:Ws9=w)uf#1V8sG՛%˟ Ast\G_TW+CkwKCg]ˆTem$!' +d"Dc7Euhz%Mo@R|ϥ# 8DH\49C4g+̴= 4V$l2əПOrQK`T$Mffeְ_K@@MVO>^;WPm]$ ')R ~;t;@W yVKmdnHXŌYS{{ꢋc6uǎ{;g޽wu-Zx׮]?6jB sPL-8KYS_ɩ35}JhoJɚ ʢQt9DB$?˾k3ĥW]0"(VFTh]jZ)24xtIY>L~oPT@i0M֏OTXQvR1$[Ac(7[6XW:;N!,y+`(+'\i̤q^N,Pun%% ;m*0jkeϪefB"# qt܃r>\;$ELf OFB'ޏgy1SJdOc, 'DD~_&M7QA\|浈xݠ^zO9_IِVXP`UTD"Qa؂1mxͅliqܩM6HTsr%-U=$à`ZIS Y@ `.=]V@, X H~pU~<0oe˖-[vg>3wےIzP@_4d%Y`D'GU,QʟC,:n=E\ -`7LkM&, Ja De8h;h7t2U,w@BM5n+Qf%I J)ΒVgPWFE̴d3[ԴPf0 YgUW-8(ՁTꗭ1bv[<\ӡN7uedutiUCFyϿ_5ګ7}~4 ,)+ NށYXe\ZAXkhTQgEvN%eiK,ӕxU*U0bV1m]4S5UN[4K  Yݒd` [ ¥_7-zG>;9|FQ F^E3B[|*#!XIVӢ܅8ەNyf$(.}G,M5/lŧ+W ж4,KSQ bQP,$QnX8>_B$P@c *?D,(jUWnp!t3W ^IW4KdVDk;8);l.o>t%B|jmmȨzj`5ShS25r|k/P3d&U\mn~áf9ҩvɮ]tZ`8N)-"$!Wk4p0EAKP9Eӽ=vAgU Lge;@qàynd/֡67Ɇi ʽYғpqCN+X-!)!2>H `{x9!%΃0-@IDY|ŽVc7oK06UDĔUꚖ }C&׬q0KrYL "$QC̬b#C&ILj͓I !G-aAD6dOZBStiٱYYKGX[" H,l(vYE4F#WU0M!GG%!A,eT|~+2D_F<*h^ZQ*Hb25%#Vѭ( (–*ݿ;NkkEM}kqM!`L@}'#gSSDW/ޗPg\9?YU穋%`#k3uPR dnZ!\ /!U931Vo$4~K>;!XO1=lWPԀuzEP9#Wc0Hփ'KB%EԥLpQukḻAݡڄΥblm}d#=afQ$x}%$J7e0S_$ S'h~!G}rxA($=*(L^$ )O??C%h-QQ A"H'Lw4%\' R(5iٯpT`5rEAOU܋ !1*j4ˆj)k iOV*ѕI5;^]@9 xAyZ)%YiwL@$ы~J2 faΫcoag˕淡}aH`Vpxl/ .gu.Bh)&aOCpV@LAcPx(6% {lO吇M)5Fp} lesP]5#Hgp .|G&dL!MudMK1T W\J6Wʣ*aϺWjM %X#80.w@s}m(.&ejDл$wc]Ua+E/nCCP -в>I"Rd3[RD[jfPܠ= N+ahjnJe,c: yP)zP1XC TH-RTO0.T%R;6#c*3d%3#ڰ`fk'v01Լ X5S4RR2W>JN@`]ߚ/1^Fas}FW֑|aPL$Ϋ ZRٛ`-OIƄy`$\dqigt,*Wi09X&Ǒ\$ƷtI=>s-=02!jJO~j, )Z`N-rd*3ZydXvVPw_u-[_LNN'TC`C$>̑LqS"/onU2p*Q-V: =DI/ts *}=K04 "`b>*c(JA/[_%Fi^E.N⩤6[\n{hܵHFG^3V*mRiGTﭒfA~FT#_4Ih$!mxn%!4#)S#({Pc(Rt+5S&>ő1{:U %[:&^߽rZ'ZyR, Ő1+Br'zݶ)rJqtϤ.s:^AMBcg|& JN Ah#U_ Uʹ䅑^v"5M\dg0ȺY'[<-DR 1".x.iK;wּEAjFcȴ@e꺙5Ke-uL~=y$uޞv{.mNkL%tr4|Xղ2V9H,!9ںad>B!69e]vHQ6[[ jYL d1$8,aUi/ľ"^ZSک( 4U@ ?64nޢEeKXƣjo8N<|p0h q(6^V^'JH%9IO|i~ 4<@5O`FHM1'ˁ3f]:l%gmlx1Qio*d[ JmSR$ CZ؍&E+ZP(̱OP.0j|\\&Ni"]%(~osd1구]k 3u$Jy Xw"_@&&ҐgU#{_QZC/78+\&B'xꩧ~pMX~Ӻ6)9v(eEіqs9Dh%*LFeA>rzQҠ6xTlط0! $*SAHɚG' mI<ea^B\!h˥f}Hm-(r#yMFąJ_:tڂlڢz <\ѐ ˎ߅Zb=*hk |捠I԰QD:7ZqM0?8c tz W'Eezjjm!? ,'G:aa3ZZ!vkfx&ARzzHhu^SޅQbc Bws -=tIh4{ǝtOKLٚ8or^@"%_qithC*ιkQ,}Ag[f$Ù1qI0}+&GmĘaT ʨ5ֽm`r2ACJT5و7j^E+Iv A篬; bȁ.B& ((ff؏=}ҥK K.-r}G؂`,TƒQԓ }F]VteDRXʬ>Ыr<3ʲDB:3;dfyn;G}LA" =ir->U$-5"V @|×,Yl6z{~x[UF\\ߣʺdXUtڲk4h f(+02RWF555 v3VNJZP 3h4䞠VT˼;d |}] ]-J)UX5qeW4 bDUdU4@0>*{@7U=lWk.]:n85jБk}d]>k|aLE!y&*p"!`x/ԐbνCB\JRPQ4R!lۣ(boF'DX>Xo QB ))(LR_}{ z Za[L80F5UG$nZXGH-6bIDl!g3K Jmba_-<,a+-Y@ /T\~)#ۿxJٍݷm8 Hf"ys? ŌYUnl'NZǵ (&NPw:wu"=QHV/)|cK-B1'TffeOZCR0QXq|{" ]" MDXOiM:@ Mg(~TXv?8,PZ))pJe[AuAvBE B5`B*SJ?3v qPy8, :ǤM̭(F X"dAA1,\Kn:t.|ÂΞ=5jT7f}׫ܨB w?m$i@mRDPE=\C% "'o"7+kX.۾&x,aTy=9i]辪 Qs+p5K`B.Iy .IfbAW>8ʒYIQa!ӐX&^?#C(sIj ]6k|0'qC' U6V!Io鍓CnTQh3Ԃܤ5A<~P{wIɜk<ښVfꦩ\ zPtzb#,ծ1 ?8{PVI@P1X6zxU!Hs-ػB;]%NbR2WsP0C>jdH6"A@¢Q7/8aGɜ;v,Q1a줻]ĄĀ@|6o5`(+oBz&0-Wsw"Q}$$~k> IDAT-H`** *Gu4S"7ub GQŰ`t" ޙ+ǬyJ% 2.ߧD/x=R!J \"33tn-:׻bðړַTD@O[ _Ba1/۲LNxʀd8;LzI.ƹ:3QUG6ΪJhje2aZչTt)P Xjt.RJĎ*P)ƕ !m8D 9 XPz]cJ}k߾{&W\'! $¦(׿M NXxwo?^&t+S-Ll[S>/1b:!zlHiVŢ$ש:%$CONo "^>Kn':*zz1*` #IEMJPQIx D>HR:6Lg;aDu2*KN@A`aﻑMh(&u- tdzώW5 Hf/ZPpXD<$f\b`:PzƯjK+0l gH߂SQӹwٹ΄("DmM&#^ 16M-VRn.]*96j(h:%{t^?[` RvȄ ѣw=ڇP<"/!aqRYxĵ_rd UCkr1f0J{&V{Խ\ GwhQGXD(&s_oFOWlJN) a˵['ۆFwlg 2p)d֯oB͒PP /ޔz񩸠ą(,l*p{>ŰZ5atdȒ^C``A 3x.HqI@Bjf3#{zܹkhb[9 u".~C:p 1A=}賶5ruÆfQ`Da~qDrL (rYЪ|Fت759pѾSN}NWwC"oW?40==aÇ jG 6/s.@C|PI0yl6^~ȠAݠޓvuN$mUc--hvPgɈoU9DeJ(ɕa` ̑LfV-20e_c]9~V?]*{NF-lsw#ľ" lK%*"l AěbqHdZm^e/xJ)\e˖Ow>pPmVWf!+PӷRuqjը.SABy|Ԃ5@bZ ^WGjqlٲe*gxŲe˖-;w=f왳5p yo4;AXiG4|ab^Yj)# 8z߻{s^lJWmX˕nG4{kb?w -mCXA݌-?ΡW{o^򗷵jꇅ?ꄠ; AF ;xNxۥ%A d M "qK:_"A(ߡ>ᨔP9m GSY#{7gڍ62wɢY陹Rϰ#zo4'31iƱ =l=S7/]'1ѓB;w.$Fj )QfH0n\jiKλ~v9rԶʍW|yKg{+ye;N|l>׮ݣg\'~/G|Xw}3 kwIϸ_O!FcOmz?{Y]w=;^(;=~< {|۷{'}<ӟ#y.zA[}J>/_M=ys>{СFeK YQZ>&B!W=jmӯpGY{^{\|3Y˫om}C&N6up}m5~g>o|W1tç75{w62caqUѽ(ԩS:~ݒgD i(;iɈw^^Wrso-E[]u 7'S~_[vSN ۯlQxG'-ɪAÚǼ?~|䛏N|ދg%_sm`/^0lOWo>x˧?vgo/3Wo?#wعsOӿo߾? "ٵ=[Bf/db=I{K~߾uoNc˗ݻ;Xl35p}>23O莃nz`^h-oჰvו=7sO>f N? Hc>y9 !wްG~fl4wiOpqݘ[޷:wc0\n vc۞x9cT.}d\jyu0lng7ϙ-wSgn3wg#ݳgLZqOk%UQۊu1uق7~ݺ}d5?lFOOcz>錡%s~O䣿z&˷^~/{=G|yW]ܗO{m[?[Ð3^|%K+w=`1GNŲllL1!{65{8=n]M͏3l{tKZq?4&ߓy1iڄu+V/ 6 F9w=z/ܷ+?M}*yvlq`A8渹Shn`'Kn;x TT+5tO?^rz1qÖ;V =vdV]r} rg{J7%l(_߉sΚ׽ʻL;93H^ח!]K^7~z{dG C m4FLз{Woeڟ~ _|3>2; ޴ogu[`S=fڍMzg1 O:rdAOÏZ0wDh>v5bԱ 'ݵco\=sؼ׷kQ zizwc>z1uָM=v9DŽEg}:"Ο?Ȑ!GuTDܚПcw }Myũu`>v=y׮ ?t92z<> ,$ZTLkt5 LrӕGϿv^yǍ^?\y#9y^ٍtJu4V{#`-yfu7/eԐf>hۚ"ܻ "5t0={~"3bC<ޕnؾi̽w@)C }=4hOg/-0h Gغ}5}Fy }9Ո& gnZ裏ݻw\rڴiӧO?r L|SCV>m0<~Nz;KjKW;n΄F PWǟy,s߷~xȷ|=' }^ z5g( e_4ްu`7^u _w|Y3wm\e3@:O<SL?ꪛox:h;iӦ~SN=-ZzK.O>쳫j=2z.H;9cݞF^Q> EaQ_50>:;{M7޽{ȑ̼k׮oSN2d34;aFM[ٰgN}lmی<?}@`=Crp-kYcV޹GvFwe;3 };PwwQ4 KO5U!H5}su_#w5qhW?Juw8qa]5;z-8ux~7 ]=5w&Cٻءg.Sc}KL=Gܷz1TfvQGwժU8mڴoq߾}D4}~wb_Q~{?C}}}C ?~( V.!'n{o*i^mr/K?/:~]N{C?54^}}ҷ'˯ޟaݹc?ij?˥iQ/_NC:o[oZ7kG׬YO裏~_?׳[nKϟWrܸq7ؚA0QtM+bIF*UD-3 "q R'(… WXxn8WXpg-F})]Ȇ-vP1㧌(@{҂]_VC'Ν`P!_h_ݳG|z*=a8ܺEO=|* c{9ㄅ@E'Ͽ{[GNGݸwS/>XwuPw3c=boz'9;RsƲE 8xj"YS{_s`G9{h0oO=j腹VVZjYqVW}ng-Y@3~eyu]~H d?=CFG.\dĉ~Ε+WZs=s5rO>,^(,PkL"p/4'01ω%Sxт woXލ ]Gqq#ޏc9sڒo㤓3mމʥIk߿y+FQ /t}w5oI_k0P-.;ΦMşE; O Ǝ{'կɟqG 2w"!: *Ƃj2o꛷(XMθ5`ˢ;U G6>&;}:cՃ #Bkq ށ,zccn;^C?͛Cu^"L5 n,g^BǠ惍tElşo]fC s,o ebـqv ,oN-8pV-42Z߫61p9cO8=~qP0ҥK_򒗌3LxwUT;/// &\QPiAD XA,"R ,Y *@ RC(=?;J^%O>0of̼{Ϲ| M2ɉGNV,PO!j)S6mZM޽GݰaÚϴ;JC EyL._ Z̕d5[p8&ʶpCdW)ՁT^\4BXZu5}{ψŗ.zOb@Mg{eeenUt񪫮̴(gVѼ'~v aQwū=ү]܉Y&r IDATke'b;^79qb<"ѣGkѢE"OM5K.䬳n̚:ljju]ץK@s6̦H!# 4p"B T#!*(Hyq̖VQkv՘O91x˒g@Ĵ8n}~ [QeMyjlGc*ڼ W.N7? 7.~G ۈ._luOꂫf/z3S8g'/\}M6 /}{-n;9ǎN7M?J~{'3bl뼛?B)c:n?Z0+{s$3|{3/`I[rYpXK=.ogDozv+W͚w{gߩ4mRs;tC}_uaÆwgРArgML5&l␈#BrBp⊩Outڅ9'yw*%YΙ0f̈́g 3"Ӟ)^oy_Yֻ{Om_xR#-u zp {WN{ }X[AVL|lYW5}s[xw-;=$_CѷQ.4 Eg e Nk-e7l}}=N_H7oteUBGں %[_p_oݟ3Ŏ{yq>:N8}Q7꺫f 2dСo*J5׸8sPuDl sgWrTZ-2WH3̬@5kIj#  YL;׵g%[?6f舳S<]{ʃ}d'\2o/BTBrl Қ7Cd3QM^rubnvh^JZۿ Xir%M~]k>A~7Cw͢zДn-*Qm}N#%TܗTXj XB.qOKE "G||W^9s̬,M]CrML>!9}%^ۀ8'!57Bm-7˸'R p uK"r#i4z_{k,;+hMJv&dE=3?D%HTFzC;>;f/ޥ>-aQ bARz_Ʋ_H_*N\-:'Jr t0MT~y; Q?Rn4jeWs/i]тRcͼ⼬~9_K>yM&}rn;ޞIGq3&;܈4B B$ND~Ye⪋~Pu NefYͬRW&L"vII-[}*qͯv~~tG0SB-X{emk;'EQYwMGWoQELй]l'Mc9"ebqlZd}Dܐ$ ?.cī,\3u~zjbSɓo=URc}puUK0ܜҝm}yiq>/ܷQ AcQB6 eQ0!Iv`D²6kj!pMC Bcy=-I s˕) JcU8Z`%S?AM/Oow[I5ʚp7O:rG>ﻨ UMHACsA*pV_C@ƘG=#)֜P)'dfaNq[ҫ??w߷),ɁTtLc3ykr*T z؜yp27nh$'.= 3۴0G454]m5V!EjSAT$Pð .<  ܄l  [@ E4TH9P݊B]l~8*ɢp|iU*nln[x<ˬ+#S72 0nWmr+dhbb&V Z! >!"X\ނl XybyyywVKAe8V}5U?2T(do%ZgXY u=A:4k֬quI=/:c7W挬n"F Y&`eVŹ8mgy&''dܹz| qs_b}%7I(r<ɒCpGb[BDR21 g0m[DDDU[M)ņ-ׁ 8t;[o5kVIIIii/xKo߫}4ocUkHA@:lOI':zYrpOA-gX9q})ma[fpkG֭ҥK;v4UVz9s8{OB!ջvuj/&>qr(?amshN?Ofe&%5H#^~/ܱeA7IJ$Z9%DԓGg dGSۛ8{h4-o2Wn" 0P Y2s GsN@"T9o_8+++)+_@ݜSŬLEͯfņ ̙S^^t]~m"믿K/YTT7ߨP߶͛b%ԈpN\\///:rЗ:^ۿ/kw\ g?z_iQ8 )?z|(-sf `杢r n+Ŝ5Ϥ_ߋ}8i 4'dff^tEbyҥ%%%׻wor͚5gn۶\jժU~m۶V_o;=Md&`ªO'u$X|>yzJPthfӴ.Yku ER,Z]]BlP"]1j FAQ85 D`G+׵zHm}ank_n/++k ]n:/\5_ӏϩvaVVV֠{cm>WLϠ PQu56m4gΜMyyy?|AA\ve)))g,ݱcǑ#Goݺ~***j۶m~׶mۢ~i֭Uk=TPe6M/bP*sui ?'%Eդ 3G?[t( a"&2QD* &Z-,ִ#AkYqUGdW>v϶ǟ=~i{Wvh7ۇ?Ps!˻%jxO!7;f}}KTH`ͳgJ9su}]42C'4o޼QFҶl٢Q=feeu=%%EDt޽O>֭U ݭS@,hQyVs뺮9pyVW[5OӷO.CJn\3C[8Q"Čq,< 7ʃqjس"kpDy?/[S #?ֲ.i~+ŗ!e' Dtu%@ΦV(JjlNwO5Al2{.777''֬Y={Y+G:t nݪd} iOie[FEE۷`=үQPU/-]ֳK۠!u=#Etnt6`zAd)#BN#jYfhIyqox]z'>lzc U5h [^{iϯ >]9xG/>苍sGB)T*[evmk43fL$uرyw}݀^o۶m7l֭k޼0C-U=z}+ ݻwo4iveJҪNK oEGU 1>]W~)~=^ s,1C169B *5&:.῿obrw(HLOnP{~'$wiK?k xv6(uYbaӦMb]vbaΝb!--M1hc<,<O||+u5;j1*[Ku%Z$S5ӚzV$zd2z. >0F "D!'3Ҍ)UjQ|Ҷ\ %qEV.z^ >Mhո3˿I#¸ç2|{48V3q&o8Jc3 ={{^vv'B=~LL5_0׻vP_7G|8PrssE > Eت0('NDr3skys +_c",e%)Nz YSTAPhe*gT#;vXlgub;&*i C]|<)1###<1 ڵkwe ,yг>;[MJu֥$&&VЧMW[=Uq X@D$J܂sm$B-j 2W'a^ &27MS\@۶mov+օF@Ϟ=`բLNNVh6m-WHHHD$"DbO?%$$dddDEEUQY%7JxyYU8 6AWUZ~C& %UVh& nU91 Q8UTP{FhP4G@!YkoKKK{t2|pu?܃ݻw+6lآE7_>''GH[nƍ-ZҢ*[ze-HJee2s̡Eiޘ~getÎoodёؠձ0m3Sj!")8KWC&Ū IDAT WMzǪBu# Nz5HԾ}'%I&@ܥK:t 0VZ%Zn]^RTESRR2333336le˖-[%%%eff6o<%%jܓ5¶G(0͓\Gєp9ȻߎѦyc%tԝc]*Zt)]za]2j?ce.r] ~rsFt:v8q0~]vYÅ㷰bР Emذa͛7oޢEVZjժEbMÆ cjtGS=%UKIL=XLoa^XxdmܹgkժрFGGkV6hfGޏsv1,gr4?xCC$"Dw}g>MGG#@Hvqʬ?M1qTE*܈R9sn̼y^zG\dUjW^)gDEEՆ8I?:9o hyґcbY/|[g[q >jᔉ-<%?0}E}9;n;D-t(w.]DuTEV>힁mګ-Vm?\XTR_Z\#>׉S^x4'py.׽n=a=i]۴hҨQF>ϢVQ @fCDD\Z\Ȉ0.3T\Qd*s|綾1p[w}J~mgNx|ψ| ,JДen-d}d@h]ɻkY2C{s$"{\.1?w};WgK/ [4MSP9O.K3T,\]~iii^^^AAAiiHx<^7>>>>>>::Xs9x G<J.kv#L)`J+bΕ$w޹W޾9yoK o&mŝ1m$dwZtOp-k+-!:peiC;>;f/ޥ H;ySyrx<{֬Yƍk֬z` dzmT ^orrr<7@ 3qYA$"=57!uf ŵxMKpgfRi-I| oKFl fG0P [ 2>:<47Cx۬&BQ--C N\yql1h8YSB]fP@ Q1?k%0]É[,@9O.;' ĥVW?Q((e 1X6 Y؛+ЛB=,(((EvKeD %M֔`˃BPH̋N$>BQ.Mf*qlSkO:mV*((&^/6?- m=P*(C0jl̝@v`d>RY"\e(ԋ%$  kZr;i2 E[KDkqYV*M2MՔ)ȗ$*6 x,Ұ/l-?y3Ȟ0Bb#l٢nцQN IDIi<"SrZGa`!Z9+DE< uE"U!v©fcGmY2!ⱊȸ֘D#(hLJW͋3!Ek ݬ8:F^EN9w:jR#2PC+9C;HSAٞtGWEPR# 'm˶,гj6!'VzUOSV3׺(Si)@ P*($`ASUX ˬ$"` …*D'vU 'Ye3U2[ P$ Q+K&uqΫ~7kӧO?QtrB9{瞚̩ KFANsF$ [/p "" V;*EGy &L0ym۶]KOƌ}:IǼ WV+W=zڵk+\Y#x_~%W\\/w}ϯ3feeeee]ve+KKK,XpW(PPPp]wmڴI|ܲeKttt=^={5/3*&@A$UKrSF⬬?9k%1$ Ѵ' ޫ5E]s5-*++;9_hĉ+k4iҼy, ŋ^ >իW@V^y|0a„7|f-#G뮻9pƒ,,,뮻6n(>:th„ +V馛~G}Ԯ]~3̰PAP̭0g"tp5"W+V԰B~~yFW'F2Y(0aB sVV3fĈ 4C y333cjР#ƌc\}^zՋ/n߾)--;6lеk7|E5b^|6l֤4idɒ%mڴٮ] >bcC 'Enrv5wCŊD Q`G |o馕+W1WXq7AW6l0f̘+VTjؼy3 4|L2}:.ˆ&~Ș3g<0eʔ;vttQx-};v 1[ S r}PRxFsA [kEVgGQvDWSG4->& vjNtByy9tޅxNqN:=~',Df8%pX6L=D0-!C!DϘNhr\D87sMɅ,7n|=zc .<3*\Y{ܹk6hР WV`3fX:tcǎI&믧v̙3~I&޽?;vΝ;knڴM6@nnnrrr ˑcq9UcHF?:}&cUR'"!cА?"pN[ d-B`j}ҲP8~zK/RRRfϞ=~xfem1vϝ;I&֏ :kW_]xCʝ;w;V:thѢE"WR\}XoqÆ ]ty4M# 6\wu >V^'tZsС} 6۶m޽;m޼9##FgM!IU&J큊?ce.r] 6 W߭\H5gGNZ؂@Z*"ZdF'C)[4q.?q+Gg5u93Ϝ6mite "==<9] իרQx_|_/?{NdPosϭGv%\+.\pٲeٛ}>߬Y&O,RRR͛תU'x 2dӦM]C> E#$:Ow}g>MGGrB%Xwū=ү]:c;^79q~U8ԙĂ஌b|D}P#ʱ,dJ N{fZT؅^8~Nb}Y¯ 4讻WЕ52>;vl׮] ر#=ܚ:ș/^ѣG3W^-Z>}z˖-k$*>iӦ~Ҟyۯ_s'GIްc{ R*!4nFc [<=5 qS_4觷ߴŏ5勞[ݦws en3 ~ͥUa$gܺ p_Mia=ݴfj.gMy[|AUg.CعbZ_5k{>5<3Wu ǗSMž͠94 "dDr3FD'v=;;M66wLʁ6AWV‮teMzٻwoD>ӧNm6Auf@ PY@,C 2eg_{ /o̠dݮoÜy/v ώ;q |vBg_/.*о(՟+ Y͹P,D)#S@rDL&gqt]FD2dH;9Α45Kݺu=ztM4=׍ NP wcg#&=|S3t)G.=!IͽұoA8n CwȞ)jWˋ,g˱m6]mxSZz7=KiU ϝV_r*"p̛ʂFbYt/EZG7BYHjuD:ԣ׈N2儜8cO>SO=VEB\%eVwٓ{!w?$n#O&q%P 44LIDI/?Imn[-E(%p,:NDRB̨~v5jݼA0(?v ўzIvF[pt>MF7'KC"NhrDuˊ_u*HTA`$imxUȪ+[&3H`~Q3^*ep~JZ 8x܀hG?BL\vOPJKֱ+=#3Cd5.Qh hZ-d IDAT a*2@ }$5C* GցȌ`1Aku股$f1ͣ11$[U;-)x70,#nv $ Q$⪺B=t r COGmFq$4خ|Xv w0O`,'$)E1vĀh + X/Š؇&pNdBQ<|FF%ps.[b$fAP"0 *_@)<[0yk_VPd="KQA NT:B=!Nkn rc`oNoF*[\EY##N]h[!gt - ո!}<ɚ6$o:6YF8`MS."< N =|e *1'A)5fVHsFeȴ&cT\j<$:9F$8rz uðNdIW*M:0Eq'9ɛ_eU#1J֍ߏ̽jĂce)㋡A{F3pɪ(B8(xҘ1cJ O$w1_7UlYkwJϡjN@JFE1'A71CUyuyh%&eg!$aό%1zx+ T8?  CxU83%.C㕷L(wjl ] Ή{~SYѭVk,J&+?7c cs[?M9$ԉmqâ5~/ozsqv߿|=Sgz" qo{>j ],Ebz%xiVl,T 29P jw›bXL#%ZY eQ8 3i G+c( ~& 1'3J|l0(,'>\'$⢂qF1#*V R12 qM V2Pɉr(R7钶ibl͸ʀs5Eoڙ j͇ #~~LoxU4fSxm|S.7zzT3%\fA`t0n>[LA=++&JQ$R/1ה)zb.ۚw*DD"#_ @ иx754Ŝ?$Zn"# GI(V&fMHWzE5 wl9+Y;Nj$5!"CV 8y1d*'u1aT/zic2}lqÃiX #|i3ΎY>3AjÛ ,HE TϯPc{@ `ئf}_ ,k&a$ X602meۻ 7)'f~$5HȘi`%I3ӎF>#D@c^(Hb?C8/C. K sFLCfS8"F$Yyvja1D$A-@\ sHH25r2}M\&nqW*$hjW)@nGX1yo;~M'Tp&Kq^ A@(Yl1iW?^ugOslἷ4OlY<3bvbX2.ϾS5Fn|#H~_ R/С7*'Lu.&> 5FA([o5<"P (b>DDMafb0-i.C$yY`3Ns/*"rnXՠKs1xsVʊa:_ @$ۥ<߮Ff9fFU1L$3$g:MKڪ_s_߶ ~Vn3ӄ3o!Ov}co 5NtJ$K#0܋T"D<^y, *I̵{)OA}cayЦĖ]ZKuP"UÒR`c0Uu }=&QuʺH*. :@G, ;Ù&٣ `.%|,Xf ɤ.GޚӢ"]h1 QC`R0?.d ő?fV#V'Yp:"352 9?jӹŜLmO+/GbjG kiXᴿ-FG%>r&"61FCjQ1׋,pFaaequq%U>kЕ*g \;<;ĥIRȊa{uy%?2wFgPDL6`[w&#BUU&j 9}~H9 Q;$Q:X& U$f! T$ 81^U¹̞d^2 ݅7 0#"CN$^Hd*:pncKs@iRĬ NWʖJ!SJs=Pl7&T(AM0C~ HP;IhytE59ǘsԩ|^k1P@iU#QDNa3L]jLaZU9āTD\K)y H '/%E5d @.qM;}wiq8ݻgX:(0AX=цbVچ\(Hu?_X:-N5˲,5{ŹB߭cć4asj?jIv$H.h(GP`ME^F˔6;Bȭ1^pw뙆[lFX[0v5m{=HQ!2Zw"RmD0yy8fNLO?lg|צc:Y Jnv rYqK>nEr Uq7TAZI-dn3h+|!(˴F1+ԶTin'rI65yu1kj)`yDK/2$$4ipH.GPVRCZ)eh8pϵR  L# BJ7!F !wgRG:ɷ؟"Xӟa{T-h*0A9&6 ԱYJUϥ0M<{ujKJKJmwmjm-0UJ gدc{z/:, ⼭3uٗWG^)륔T?"о$h ~gϹ?%vsDvsA(duVz 6 RNI. ͐zر"zoŭb:/ 3N>2iq}n|u4*nhE6ogVw;ݻ(ijhY.j J % ji)i=wd^X݁pH0HrAVa0Tsn(IK(VŪƜs {Q QHgF cņ(z!~ >{d:)uQTgNָxܹ2z"씧uwnG0q7 )wsac>g?V*,la6afT #6(w9,TLEնmFe<; "[ifƊٍʋAm&Bԟ4#mjb)ƱCzJ||?.oĄ EA=hb=%8WiAYIN"x 3>_o߲5:?ᰣ_z8os?t⹿țM{/z]{'=㤷ri~6/Q;)-%Opq /6O7Af#3{x!n&%{Ij#L(dQ5 TM  fDo͊`{lπ\&t2br eJff5@1hq3LsKȄ$`xM4$kvHΖ!SI|-R l1kRyI]Y. Z ;MT}\Zh(+$RZa-D.1ωg_~W\q;\>?}e1W~֎>bg/Y._޺4hӼw=Gx˹M4|mρl4?Ώw7@vYJK%)K & jaftb?t3ш:'9T:r&Wn#U5p1Dj:%ݑ'"d*Q53lzMiֿzfx,YP+6II}M҄bPW]'nEORکGk/{ϼ3^r/UիV'~ىk]_~/~ t/7[sQ:ǟ+7%.IQO{nsp$n7'?'~;c/;LᓓhTaz?.;YhkwV^x <#6vs9QH5#@x* &j{!Zd^Uo(e٘!0"OI2 */U*iw}FrvPBJ)Y`QS_K \·4cJP 6dcU1,n$b_Yn2JBEٻ>O]|v_z7^_|w?q}S/ɯ~N9U[}w{k?6s.>t|{kqߏ;^^e۾޷\?^{>wߒI W͏8AzYvGԽx4ܬG^O:=*U܍Fѿ\:3 mjͷۜ;.] Q{;+ K4/w跴D,E.U!vCP,沪Ss,uj[ZdZ^]ڍV5Δ4s=ŪP-@6hrUZFT cUZQCDMƢ`4 d@-B3{EFdT1t(0!?9 AZUډ9T ͝N12p.s]3%I~88*Ll)i!?%\NR`Fq"|j%rw@2٤Ϟe'JmW,u@]QMC׌9O,7UT];ωgfGg>?~닇_X~{v^>{ܥq^+6\w[^h}9=gEj(!(<;\d6}ӵ%P݅)o8%,xit9L9ӎG=J|f@F4hܥ]D[L F$\&/ZB EcH 3Hr 6q$`@6L feEJ9 BjvB9MB6)Pr hiHVpN) Mbx*!6 RّSC+̎"]o }gwgzS Oz &УN{SּW~_Cn-Bz^>rg$\qkuy“yㅿf^~qޛ?7Lw*y7BQs!$Ѷt.~u.;}lBRb Npئ+Pr+ #ŌJt3Dk52B&ZʢH,{XN/ ) iR fh)J*ԝejf0!̉&5I4 ZfiFkF&-Y MHd~hV)Y MD:" |ģ[q2Y2B$6l׌=,;;~88m}gff6J7n40_ִ4µ"n)Y()zu0kToSePi7!t(t!eʼF)r4K_Nl])މpSBD=Ä%&vbH:A)" WbDVb]E\E (!p.Q|@La&&k.RJ:6,Ēlǫ"#yd: i4Gے%}Trv&+sZJF-Qd u3/4[ZԭBcwLz:c;4AlQceuq/[;QJ}d$ejr-KMMsRެU4P ]\:OivE{KT%+F?+wm?6s!Ry.RFhSAɊs,Al\LthY?gxnPAjXJ)gks2PU YFMcOfdm{:c:%r?ڬt,r(Gشwm͍4 EHE1#rbd`\F,U@U2y)";0BGp{ p]k-`f\U]je&Ќ L/ mXo؎%#[8aP7ݕm"EuXwtloatU`x[*{Ǔݥߋ ZƲsZNtXȆSd䀥( ^Z nJ]֛1*{$q#%Jt2ѻkueZN4 J%3vDdgH܂뙳{Vzf1xI!X1)-%Ru ܽi{аjy9O0?U 4fQ%c:-.0:_;9N4&Dz޷5T\bOތ$4="yidEɽ.`L$X@}&pS ҪR`q=tDr#[E 6"Bj*J #_, ,@c0>z.H@vKwhm..p5믓-PP+ww,l ~x9q\h^"YeMۢG5!zȟ+{qz< !X}XaIuZ ۡ `3۔{y! +Ubf Lm 4녙H50MrwH[H޺SJ,ʷ2s*`1-%-3rU@Jq9, +Q;"v:v]d'Uyl7qx0jT!m_tLVn!'twTuח+∪-Öz::u+%LZARytu3cI3i\#:c(JA X*riD'3*KE!,{LqݝDYj6)@vXI$C0ڢ8d0os"kW(51;uETaC,M a˜'F[$7ni\61sұuK:#H)KL* Z"+K]=B?ԉs;ϕqQaMDN [D }ۜ՘Z(#"Zy%2˓wQ091ddD8`ŘJF7t L,$TJz)$WNf&7 ht8ls dZ);S,1๵M hB^J&)-|+B薌r.ϳ@w"g4HL5lJܡXARZ(we<>S؞} ihA˭oNCwFKC/+įqRrDǿ*"{=SRΒ/0A5VVV2 Q ]#tRA+r#Q&di@{GɶuoE[$&p)Zr if64-qsٗ_qW\qŷ>w蟾o~s+Oy!hǟXK.~~~{fOc)htϭ7μgF vW鞕#3.f$Ѕr;>²:𑘕G2YS@CdR2gȱ'16պ<"&[ gF@2g\VRBb{Ԙ]rta h.F'v.֑ckq c#EQXi5E8G3iY;3\ e4^EgFaO,nx澏;jnّ>+y+k}tыV_;S|e{W]yK;j7zժW=/^;;qO{<e7}oN9UG{%%לya2lY#8IHCrq&6Vuy季և!&6hDÐR Ki@1Bdz@a8PАx|gB$ HkAh(O6mzh E(@%ɒb20ďXZ+))״sS>U+qYMCHѶGҳ/qۿ+~KVzO~#w|׭}Ga9cG׽GOm/}_|Gm|[.|}|ͻo4{?ǽm˦~Wߟ1>W _+>dƿy/KA6U*긪e_d fpTˍJ] Cz -DsnLF*~Q:uH(h1A(f; ZZG9#iր!@cRPLZ[/XH[0og7C2:<56;B˽m`kImU_flfA Cp\hUEߖ2 C1YPm-Kg=Cz//ÞUǞv#nwmxsO\~I{۸泗x ޳w'_Bg>^|cnGe[]؇L9vg}'L=7ۍoe1;67BI4cJn֪f#4<3O8a.'3Xܽ" )$$kj2mxd\0F;qsSQt"@g乥!L]#ޡ+7 ti3i@w “DʯbAYҵ 뷔Jt7micfS:Sƚ}4r+_q4~NRθu-/`>B^ +Z[\,30g}GpۚW^Y>w'=_qwL;Y0?:2{ݪ.ɞd:c:1: .Y}r/(J^CFJwr"{ ΆL /喊^tn+P"S4͒$9$wB6ʅmC0xvAV0zV$r݌я5iRj%GN-Ya$eUJ;ȴr9w|,v%xгZ \y1[s7:wރ}w, `w,?k7Ц.:jk-0=߽S~|.i tMwGӦ.9nq_d}kx;a:ե>f;|Vrx0QoI8s P.$m })W,Ү^u!EwGz3r #RIrY.{qSp?/ '~F-{^yI&s'֧Gaٮvġ{_ `R`{̛{e3 IDATχ>y%fe?ؒӟ[t,NAConG\00=" c<  HTJJ(6%F{ jZG x VJe8͌ʞCO0`9!ěi=V&ʩD 40Y0 41T΃Y(U!"AdD:jܳH& rwB/S0XdRV-eN%vae!V!߸q8;=7|mǿo'4njǐwLvtvsqͩ͂/]TiJͶ#- a(yj#B LPF`1D/&؁$TSr!C6$DgiLdBiOD#X=@⬇R9$,@#-lN`.[rO TݲghvnksvE-@]hsss]/P`r b,lhY!ݏ<;kݹ>riUcnģ"8Rej{EՏ`yգ($ENpdP`]KfQ/.$5YN{i3xFތROdVͨL^]KaZG| E⻑ TUw0ɫ7 TM@JnZ8R2Hl>DRXDlGQY0z'Dd'ET3rxo}MtL.=b4Xv(J3\WYK d2)WBP\: Je-Q(cR!4ZUZ=$34ΖbCwO0TK+S&]%+aRQJ(IRGܘ$er9hfF9rΡ!y%v,H2P9c-6*"#G"D@v?&RMa1ӱ̥skp-:B="driB5'A( drE3` 9jHR4 i$Agv`F*!ܝPd:"a)g[3K)1ܹLI 味L  5CpGVmRX9UVN NP=yn)h:c:FoGJˤOZgѹRC"ip *ZA;bz/٢DIiN\)YZ4J~tDj+PPxKl$~qrdZ h% s.cj6j:}VQcH"阎阎D[1pEXhE!N0&*D#E1"W]Hܺ ! a"Z)JffhI3fNX(ʊ]nDXAr!TIjc/ uB$"POd?Y$+J^Zuda?Ȍilƅ)!t:c:cKrH)_bS4tOQ'ڇK&ČabT@EOTzgg ,=@;0 9IPҸD6ha͂G{Dd1\iɢC+) ʳ~%\Wm$(-YFLC.mAy,(K!6>}eQ54阎阎{i]$a=&Qrg!Z):B,ᐙHGJӻDZ T%YM;-Ȍ?xZ|PfD%IX}4|Srd5^dSc` =`D0W6kB(H`h(,䵿DCQ)fQ 0Jw7F)#^‹փTX-x:F|Û-MD11 }RTa@D8C+ - <E]d N? e`3JjU¡63Y*&)*Q%=gB&WR*0QvVHz "-:@pRSh6_VB`aP?)m5kԖonUR9&I9ZcȠӟgg>< L1ӱdVqq< O/$ER;+AT#e(* T GfC=$Q%4-@ًBE﵄lv9$ ~ol(DJ"pX# A=n8HҬ t{9dp ?%#9w%ՊKU{n&> i go<11[eu]Y DߒK0KnF8³,ӁPb =Y6)U ?LRh %V\IN 5]> ϐQ&K/ %r ggkQ{Y:uU#2zP >i,UHwwr|pqh2!6ffk KSM_1qBi[$aue„ w0x=IjK#ZJ9!(n.gK ͶY"MD;qd0HudbJz[Q-Gh6Ee(`C+=^].Y&ђYz})slvGЍǯ~ӷoYs~сtp/=sn߹qO:_즽\ O:8?rOf阎{~Ib*4K-ܙD[gI%UJYd ,{DЖH8)wG ش0+]8Nʘs!0ӲA  M3cVzqjV,&G%L@ cVPD*b8,Ihl 6{vw9Aj2X2XY z+fKR}.MRȑ(Y ʌܶFfn?:E%#f9>3]LvS:~՟G}~ϼg.#>{ۋB¿z57~-'8ݽOt3q}g'}gh*vӚTS.E cC+T, BNs*;^DPȽj"W% ݣ8,֛a`D Vs qJMZu)9 FE6|SKpNKmq-lUjV4ʕL'JކK9Bsnm;Z#`^.(-/Wy%b0_Kpmˇs澏;jnѤm>/xs|.zӾ>kqʗ}o<+xqǿWmVZug|kg'u:~i׿|'~᯾^8[sQ:ǟ+7%.~ׯhG203}ccl~~Z9oO;lS?Ϧ)tLo:nu:"W&NԮgH]z `JxܽӒLHBJRLb%Ji 4Y5]X)XL'5MbJwihfRrג2RcD<|ZS6!%%ɲ'/E @RJV))Yg؍N/yşyٟ]-=ǭeÛuenF :x?/=/^t~|6qϨKp4_iIRxL6K25{h m]^>X?gwU(n\ȑB9;=31LEZ6GkhJC}Ik4=֑r΄s|g"'*9^y@6BCK]uYP} 9݄$rEyVM]ꄋ Kמu!=F?:'>qձ3==;pf'?~U뗴k>{Ǽ=p}~>-B>K/r1?`7ȣOn2zpL<&,vN_ܚ]#|;}l0c[|z ­M`\OwQ("0PcCqXEQ C/Yd2ܫKha imkR_9&byd4<i=#2ڐFކ =G3>]rm40< K6M/r'#`TOG#%__鷾xW걇<ެ~aA+c Ӟ+ehv-ϸl܏?\^&ަI O=r+?fC>pՏ.zKޮ}4MtYۉcChiֿ;ݢxWvG%G:/Q^u,HÚZ(6ޤGJ@M,6"rH6XcaeLA 0Eݒ4#Vd$9gK&p\ !B FhE " k m9J 3W1Cs"|٠{'螕 H};jPf!BRwo'o8̯1/s}vYWѦ[j4nVb/ni@{usךH+X2vkxˆM}#7k?1;.=xx;Leg|6)K/ )":SrJkJiRf\Ef w00$0<;%Z4bPOLyVj&);ܔfsiݕpI+%M$$N oSJ`C@^Z)A0n6C27:тkm' j= XC\O}r"*bq '03#|Ю0; iw]׮zCK+8akνF{ﮝ0ln__veug}tEg^SW=3qƲ7}|oŷ/=i ^u[}ܤ 8vpfß?vz`~G=i/$_/O\dzcacO?(4%}" \TdM &Ca^mdJM  Ƨ DZ/)iTʸTIIi01PK.0ڝXC&Z"Rv[&$,%# (UR419bcXQ`ƒ lh0*DQek% -hLDЌN4Z5 =muH<=G1{iוw}c~5ftX#?}{_o=i7jكOz 0i~$<ᴷ> o= v#j>^'q:fv{>%'` QoB3Vj`_O}˭sXկ7[m:.P*ckyv=(:>UcۦWuT,jW5䞇eQu8{YieUu.1Z9T!  *57@HQP1|6QԘ zZ &4("Tiksn7gSU4Us<@ޫsw}B+߄#5DgDQ6(h`=a.]t".)w'8?#wGkTe R>4_D%'Ӡ U\PE+tܑ*豏{TGٽDC BT>r=^JoXnң?liYvgYV4 lć׻#]l GQT6TwKf$"UWT 82J@{XDV(\zGFQ#qu-AT]W@L@Q$ ̤"@BRBC8k+]D 39V0BA "fqGO .@\b WnaAQ 1ޑ"PF.N/Akt$- ۋUb"S D%~]"hPk+p::<ķ)^gP>F׳SQ1;S % T"_ O $PMn$V})De` uQ̂Z%(:8OU`UԒ &_z-QfA> R!ä$8%w>OH8AdF:Yqrl bPCJ.`Kp\6 <B:4rS_U8 ~Z;G:ѢZ5Egpx\NRk9k+*PҚ8Up°b.PcF$Jb( %.P "0IES`Y Ȁ \X s 9j!e85P!Ӣ*j"|5UhRɉa3fql BaUB38eOA j*CɄ%vi"ˣ#.Hh#GŪ :Mĵrj鯺lh NmY'Dȡ!YU' |(Q (Y"U*)džRebO@"d:fbqOw XO 1aU* f'CșHC IDATlթ*-r 3P1D j epIXU~ hR@Ҳ'(h&#NѢE6. 1Dkت?"*fZu]4Z)w]ۆ\E40* D|[El ;vu1bS Ur 3Fl-<5aZCZs BJD%Ե0U +*Ժsaf0,S.6W¯`OЯHh9YRձDUѢ1hVDQhR% o*"^X~:A\R)}`&X!y5GYWT(Ȉc" CN]s:euL$LYCT[HK{H8$P^C_&6s; :P&9D׮{V1(+QMBW\ש c ֋e~j)ED0l1)zlۯ{\EV7Jz-umD/3Zuiz\(WZJR@4 #lUΧ#CrϺu(D\Q.9JQw>ǝg\ bu{p!dhꅙ,"ؐQ9"e[\sf )*Y۩"jE,kR8B_dbN"bχ9zX_P?wsV/tT>DVEG=3_%JGtxh^TR[o \GG-CP/" 6aQ_أJ "#ZLrP"&VJظx\br'@I/ T*HLއD=5!Y({VB$lؾ!"a`Ns5z'}2qVlϼvw\w=Pr37mzƻW~&糟Ύx?Phi2̮ZV2(xjz2!{9Qu͋A"Q*VԲZ4pz- '| ZÊ#BRnVl.ֹ"ddlVD<ϥ\EC2UQ WS)03!`0G*ЯZ+*rNXH 9C.:ȝќ;no_Oֿ/k_ҷ< `g.}mS ~t.½|4].ίr?^ҔDڰѓJJ Kmsw2UI4ۅ?-"bUU"+HU_c|@* QQ l]#%1op<%_EEd˕d\4FgZVu_'4ѻxuksN8K]l7.89󿽢K߻e6?%~~[^,`?O?9'kvVǻgrs>qGe?|obz?L*,^gᬗӞtoi'?w{?NX FU%;-9x\x@{$@U򐧄WVub!B$ޭA!6 v+r6LxER2 28$Qbwm 82 ;SS- Qڢ~Vd|u`*! p!)>x[(Ϡ%bI5ȩ:QQ éc"2k!\0"?M;/^;CՏ9׏<ԍ_{cwc^-y?~ه^y|/UW]~)?~'uW ]]~oϚ=8~>N ͏?顚F_k_=^x[o,>7GZ/ʫO(ڦ_SmʁT_9*@Urwz$ɭͭVsQX Ȃ@ψ+}3R,}ƒ` }k (C` xBD P19UD5Ż柖:j.{?صXùڎOjH*jcއб\**bO9 ;%,>BsKL76>_:G|kO9ač;vUGvH .hp C2w/^U̴(ⓀD`t xӻT 5B]aH@aX ""rž>(Yqa\Ū@I@UzQW u :VuGH"B| ﵫ?=A rS΋ &fD:~Q(21S+;̋?zΣԾU/U}{S皨5Ÿci'<|ۼWOG ?_}Fw9`v}O:1}w=a)=(jWepxeHDG==z18Y^f:|]Fu 4DAI>c/DJ B!G% gL' <an^ܳɥ82?6w_zRj0ٗgn< @_3/aS6G.@{;Z}0@مȱ ߾m9Cwk(3,1oƝ?aī<{2xq=zFGZh$P%KcA "\XH&hQ!^ӛkΕS"&qCU&hP\Ѡ$x!:T+omQQUbO8]0)X'b cef yLP=0u=)a`)p"J G"PZtA=;ۮOG*mruw <$۾\< ۖh?cڟmWl{C{#O鲯>}.@s|O9Oy`%$=ҩ~)G/|Wo) KwӥĽCohc#q6 >J~vF5ѨP]LURULdQ' bUXHaRfEEժ+bBGEѬ>#S]BDDZ},SjϢXNgD Y &##3Q㷼1Z8.W?zs|髞mwį<堛d;#twxZÞs_ ׸׿io~)-;9:S{䳏čy܈Z']ǿmoy_!83fɘQjUUr7kJ9[ԝюAýH\q«%$ UICXWd22 ZLlmN$B؟" vծ!# 됌>Jy[\QQ j7 &NgX)rU+UF z5o i]fu*Nڡӈ+41혋8z@mBrN%hrF I:3QRU&@|#lRzV ^s[T+xoYёbEp=gQUf\ ]m_5Y^;4g.c=5ѭ,4!=#X,Z ѼtqĢB $qNn"6h"NZ!C$N0`] j8u'YE *򪀢TS#/ $-*Pjh@|P]\5uh1ߩ7f!XsZHE @/ jbDtS;uC.!΁TIB~0uWC @dFQi2D aR"!"R#F|Κ츫 L @!Xv"']?1Wv u d⏧5RG y"Em5-Fi u#P4^4fe IB4Q̡1(\uk9#x.GR8 (VOi()Rq%P9,O 4 YuX&rBx$ 5!(Hu(V0L0-[BDh1v¤hXgX>tXJ7*^u`?W*jk. d?oV"j,)FA"JuhQfvl(| ⢸9V.\11sL.De~u8SUU$C |HK4ޏӔE60^lP@=.)W+#"Ƙ@SԓUNh8s7T ȫ-=}c?BDh,AD\INPw ZɾGjw] ffĢxJmDh ^.N"EQTY=h3cm5@aQ V*-"Hoqz_GK*Х"` ·"A0T U{"~RPvpY1Eʔdւy8d=T(YvI%gcX*en!BչJXYYFB]z*B5YF[t߽Kzi(t5(E?Dq)ࢳ'Xp ېFAXn]ʘK5A&Dۧn/Ɍ-XZ$Jҳ$p YϢtA*]4BTʾڨo#-/Q6z}n |@g )/ du%?J%}(bCࠂ@~_ ʕ,2U2r"*Z!=;WPEN2^ n?zѢE0h&4L=_UPZٗ| U4U@r%Iիa7WC Ui`E B+AȔ!6Z r@ P7 FaE%TQ4TCA+tOVY,lPS^+_܈1D}]9Z{c4ccFK^`Dneol% 0Yu$>)R,MBQ7VIBU.)zQb@U&~uʝ7V}/gRw)snj(hNɪ?8E3)5ZivUhȫ6ŬUrQѝYEE0p핔|P2eYx*Rܚ,9D S]8+)")qp _WoT$jR5=h_|}!хEI)qV(P#`#_| YĵW2ZS{JѤyQ*JQHgSe*':+:efRNUm(,8{P%ޣ"UUO*_dLJ>t <-Z8Golù4bN˦m%x |$̨; &e(P[W΁UPp[F̽tCIk4+i#lX`ԂnK Ћ8 |3_ R!TwKLJjxfhW IDATa<o[.xn O|ߥ/Gyح<[l +u_sN:M~(({ؙ[/>lڡq~ӎw٣Zߓ6WZyQG鞕~T\~eSClQ&5=A+ВDPZ .Kf!y;~eZV@1LiA"`S{n_m`BTWJg6GPXvؙGg~C9N?Cy? _@qW_y9G?|?C]Mgq hYQCGUv -.j<Ռ@Vb@Cp~Qkp>產F<_>5Ns6-Hǚ! W-BEmX j>ҐStwW[-}yƖ͛m}@[^%pշ^l7.89^>yY/y=oy/8Xwi.|n~ډ/.Kg?0,4oN~Q]䣍(Ǚm.Fr\WU\ˁTQ TH'-:=F]$l\?hZNўT!!:]=`x t2Z4rۦ|x<mOpd/篺 N{w?<׼iG_WY9k_=^x[o,M{-+_I?#]|UWl}'~H?G_+?c޴4ceˮ|Ѧ:'xG W+v}PDuS+бU1H13D`G'9Rpm K3;'\[V6;|uG =qn!oxCk?x3?nhSػn䂋 d0ߘ.ʲnv=hӿ5ƀbU^*[Wo/1!^TZ]%'hTP,Ԫ*ƒ[N>j%Ck`@>Ld :&& @2[Fdx]\k'bR2{\,xw?o߶yȜ;Q΅up'Ͻ|7]|/31Ϻ?O?zx髯u9-m7힛7+uFޢC➂FtjS@ZjKL@-7YK2;jgJabw GS߬BE~eͨ)KC χQ]*«V +)3iL2sN9vɋYvko>$kxHz7 } y@zw-n<&;y/𚙓6Gr>|tǭ?]m;=.k+/4mw,Te'jbv# : ]ZwY53W+UڑԋfBD^W qWQ'ϻخU* XD-T$eZ ׄ r@(r[Yi!^UERItw8Z i9UYAkG5);Iw՞Iy+]-v?WGfW=˯◝{rP {ةgyߗv18yk'wp']CG=Gy-{N{Q~+uGʐ^еL|~Otwٝ! Wg;ƤRN[^:򏂊*NKu.ZPCQn8`G+Ñ%J QP\^٣x4;HAeZmN՟FPn76SW^KO/,7li8ó,c31e6{4;AM9V ZdAK xp.ث.ƽbjܛƞ{ciZJUY;E 7v$%{#\λ-i`ӯRQ sxfN3J3J3mwҍvfC,;U[4>Łm7)sW36JU:4JepW&]ŤRQŃ"2 Zh"y&J dk qnff4 Kӡt~Yۑ8oD6Ewܴm"0..H4ƏvaDՏaC u_+!irSlq.ꮊWbA ?"6U^ʞ2\N gג܏](TAr0$jL4CQF4Zgh{i_^w*SS+`jV |u72-nRYR;dhtث/~)*n-lNc^O`$)ҌY:ӝIZA|EҍƊjdYfHZFEh^_Gr{\9h۩'h(5=:."Аܸ*L6rDZYW]_%&}e"fIBI,3nڝi-bfX-)PFw_8,&ׅY5 jUC h r!KR KDrBX DQUef)XslHA"F arrԥ&RM4k'I+3ifѸʎmm *tW~{)Y@TS:̳ZѠ,>A ¥TҰ_ !=:gq[zI+pyҡ /Of"6)%mJZ&I 䖬񅉶WbIJp_h.fiHXcig849T<-_PR_SQwؕj_]ꐋ|2D5,0đ#7ֿ! U*QH< %CcM[xfQ2}F;MLS^n6b V\zwDnee:]dwPW$ PbT4ϕ'؁FKle͕n%P <1BRQBkN۝6LBDB\8GkMvDE]xH%v[`v\dPxJ4WkCUŽJAԯ:V@@d %)-juMVۤmFjI҈{mop@idmG3Ezh!sclz%Ut@%hJE!?w^ICCEèެJ/]AEnSt3)d-^j"uD'h9PcZ]ɴ3L1u1hE%(%5h)Įq+*R?LGC\h!R.*,MDU Ì_ʁa s$udd(MfJ2tfl810(TaE7$^9Uw"sH MnTQGjwְWJf/TZ8x^N_Trr;(I81`N1-J2f$e2d Zɒ6g,IL$DB ؞~F7td݌j=xE? =\a7Uw_CRj6$*\E7 YdRռ%[}c^ QIA+`9 Хհ/)Mf5 $ڜ$1D fjqe3fd2`%gRJAe]$׸m w;|'KOؾJ<4#2qT3UUWÍQ !j5[cd*ZWXA03Yƭ;3\6; w6f=߃Vg$Y&Q@k^}+PKs4߮VJھ}y8uUxX]oP]+) JĿ?#:LV{TZEJ2PM_K aJ3cRԝMtN7mw$e2,DUL0bUynI-oߺ-5x.} /O>znccXs/7sam?ixv,a7o}w7{7muRW,m[ UkWi quȫ-GbW[DPCT:ƹNJ8!(ksglدj3v+tS(`0l.`be?.v=Y7T>7㏻Mx.%Y̑/~Ƈly3ٽs5~hYOw،0)Um(lXx MK{R,^YҘums1tï$a٤0)flҝ56fNI[VjC\mF'UVD-};xh2}v?xB@;=ٹou?Ǟt{ .y1g ~)ft-KMߺ;~?s/ΝgW˷lo~ǟwcv^6h:V bJ&t$!Vӳ>&JDb9A+jmʀK <ĸ͒Vfjsų\;fY7Iۆ[-oIVrYϯܽcyΕ;v//ٕݥn/ԝ?퉾[}ǟp.+>?x[/cu-{7GvtH|'m~3~0qA8{y6v"GX.7ye8U\D d\jʟХ ʃkCUt;8Wqة;IkOiRmI1jMw6vpf(apK5""~%~? ='V PSAk_]4>9=g_[V\qm?evLOLwO$q[v6-٘leHE'<zlm8=@;|"l[E{M}0nJeOve 8E㾈1VJ͡n|bGΕI:,IU%OLfs3vβRb|ە^qi9K ފU;֊G %C89xs;ͮlyס)p/Ci zE>f:$Bc\٬;$' gtht% sܴC.$wy߈mTTALmF]fݹNg;;;t[Y;iRD*!yeqi~~yaq~Bq[ȗ`i!DL$i0{C49 9i ~)G/|Wo) Kwӥ1\^y@zw-w_ߵEOQ9hцiZWu@jë́ o€-:T'$^<&f"!+.j6<&Ao`1-nϤݹ;fRnpn9胬8sGo|h3ot`lOZUX*yMǿ_<7N@ zE{QO}ѧoc~EMvG'o?5kpN"DעvO״De O+fht7 uG5_H: ?ًp<')Ķ3=՜o[vwWyJPN6b7&tv',k1swe 9bsE'ݾQU'Jpn!]GEjc0'Ҹ)nzUS-FԂzM`J2ȹ-goVhrmu䎗=lLwEC*e K)mHGI1 @Ƞ?XY\YWeeY=}}Q@o 0 K XN Cm-*VyDZ0mx=} 7#{u9nb4ކwmv WjFni? , /J5 J_YYeHCPbeÜiԐac(nh ۍ]OIzu=tPZC]xRi.ttCIbUl.^.Z{``V4JDDahr1IV+M Ij>hhQIa :l>HWָ3Bvt:TkwIn=xQ;+X+y_zAE*$ 3'Ybڝ$M$1Ye$TuaG/W%KJ&O[ dfa<8ow[TjQsPPynu}>)' B PH_Zz($KD\\>U! J΅%۷ߓH.2@>9Dt'`fNN2 gmvL 2IYib%di+ZFtƻ6ڮ{hӣgUmͿ7Σ67VAt헻05V daܢT-!N(kLƆ8ANmgY aw&t>Ů*Ln۽MPZ鶋I XlRuUG{/s pK$da2rA}ks @X+ &Rc$kqYu&夓3YfY2rBafM$h{qs1 SUhke6=06k+YoPleAtH7U} #ア HB.rS=@@,e:3Ig5cҔdiuq IDATRC Qv] ı4ھ:X=Y%4˔}) a]Fi'7N7x'6a!"QKu դ* $'Qu- pbLJi 34k3tgfڝ'MNfV4ZUV5UEE3azZ VhkM ok~W˛٪gS&5&)mˁ<ʂq9vxWA _볰:U.R$W %!f$$LwC:3lhmiu6YMdRcɘD1%M}!}vp6w~C{`UFd;SHV:9nCD5.%^ d$:ֆ sg;3n`2)II$<Xė!k؈tZuڊ^ˮu0=Mןĸ j[\Kz\Cͩoq3Җ1):̆ us$mgm2D:5~. +A[魬,7$:UBk¹&הDidU/{}4PWU^r!TC7LGK_=CV,!q.HZƤڙMsg6mjw;ٹD9zyK y>+++CPѢPs\{FMqf珎䂘P<SDMiU$I~%PPT Q/#*1PEv2_;יvlwff3qvZYKV&~O{ޒ~iyeayiaqa{RhVePD&>1!Bzx g 4eXr멠2F zx&?eC6?G|og]t'G=h:s]N=/:s50±Xfϛ x6h+sn4 [hO54oS3TxEEZ͈ؓ H 'HjI*y?=z`lN K>Wثs;w']Agy<>o5ϟOczw`vcy/65_ W.=6#޹gdٺY5mZU eurU=3N`U Xڕܱ0K"$$V'b@B_VKKKE;X+9 @GgyK/9Sxg ?.<$;~x O;ۿt叱7O{,t~y]mۗ~J%hkވOƼwfVɴ6*5e6uN!2ŷΉ>!BEl_BXK ;;旷/,./,\\\ܱ}iΝZ\X\ر[|@dcD|W?E?ixІC~ǣ]riRy ;_r~v; ['ejN}:[)6wlENsk:/Y.w)Bu(Zoi@,QkZk8JtKKsҁ&|D5lvѶUy7W!Bih5"A!J F+c$V0*āFD|XAр".{k{X{{{>{s~ʡŕʡ\<*(xeO>~=wxՕοOY|e Ǽ'BsO|-šDXj&!QmwHZ>+nN0]ISo[54ƂK' C0 peye!6ҁ=5//>ã>XCX;7>9~mƉ]y8u/=u}?=>' ;UǾ^|Os}3vSPHFXGiWӔsed)Z;\-s&%'QD$J!NmNwNsn6D4WYXZ CWVHbg9x|(?Ү]b_XHKy'7"Ls0oXa|[{E5xf:W"J{+ e/r2|wօ"Ie%)i: !jFXZ>I&}lf5|(,[Cniū1.#oзUjNJq)[S#郜=uK57gJ7Ԅu u+ZCeOqy["MTNa143ML/K0SRfYffLB --:m>{vlB#:2ǥ&,ϰqº:~]++K>* BLD".CfAW$i,#9(y*!"b͌ rf#bqiObmb`+@K|]VKhJ3 pΩ '=j >˚>i9YdB:85Dl9}7p U~a:o e[ƕť>Xh]^EeHD"A{jebm'{$oZHͽxOԊLER!(> 0$@OPIғcMOT3R\kL7d]/:ϘpA $gM8xOz`%> 0Y[+5"bzWt j\bٽ溦B  ڹ\ qh YFЁ"NJsciwØoDİGfU$:X iXZ*G4 fshQ&*&N4 M*D# F97>ұs Q&rSE[HlUoYՁ'y̲L ,!P#ءyK"0P\LQ6SibTتiv--R5Gu ..='Xt#p_8]1ZJ|)gf4: D-`*N{QHT[eX _B8@"{Zd9>cz0Ғy0ˍ!Z:-܃T܄tEũ5a`pND@'Ķk9t \#Ml(*mybf($@((b i>* kAU9xD"b?w6u~}9Kvd IxxCX@D !-x81]SmUWS՘(bkgibi1kq1;s3A#``0E#P 0>h1@Ж.Ujl؈2mXuX&"D/UrETσUO9nQ,X3щBXoLTs@j0j[4BU+m4K;hG@Vs=V5V$׊}:I6P_A|ɲfM+M`̼YpIBDDE2C3❗;79t[}\!bK4Ԉ-&Z(؀Ky0(<< yFߤogf9,%@5+|hbFPwP5u{+}ޥq^o#Vo7:9=knI6&#b·Bl(`*ͷ0pdaSGbfd+Gp @(*BPQ2DEDI!q%74#Կkn_wBZ{ڥ8 G=k.}7_B:Jb3 K 4qﱊ)S3[ sXR``f`^|ΐ[-o!fE~?܈DM$85rާspǟׯ}ٹ}Y|Yyֹ/芏}_w_2.gu֙g?}Wз׾'mtaIw Cw][?p=mw~_}g׾/]o~7^}gW䣷+^*zW{]7YK.]!]j>I]*MZNگ:^{5Z@([)$묭<;( `ʋK\jJQAz'mߺOy;<7}Kw7-< q|aG_εW}O|W?fW>\>מg7tWno>t5S˯ħnӏ{Rb"""69ǭ;1-,SkVg?9-&;iT .!1I%ITuNgw7^rƯ+}?)t):uNM~dX=;J=+v]ϞwtY8%/!4/?}߾}g@wW\=ʽ+.y x/=fsB@zOv?{`q51r~zfm;"[:mڮA8u9au$݄]8/?* & u4ĥ.IU,M$ I=_Kn+~^/qN#o7-{~!QÎݓuxpi}yt&=naw{ wI>qo>moz9xMbvS[!baWyrQDOm|XQ%c=+Ҿ/~V;טgoTxga]"IC]*\W~D6I->~)տt.}\z`Q `+-Urq1)ŝO8M8yOm~,3vU_87𩋏9T2Xʯ(j8P~M%w+s9G|ƹ7|[>?|ϥo;N<pI?7NxG>k?x -8gƽ]s 'kR:O{<[_=GGwX#0۞2p3+#Xpv?է)>@PDB3Iåj*K5]HSG UAaw~ "bs`iiN:iaadm}"U?UuyxЗaq0 2] g"fPSM\IFD$-(E{[y7bݯ22r=/z-:H_y{hBK$I%RMB0 Ӑfc3" >RL.֊+jqS%=BsX0Ma_E6?R( u$.uPMT52/nDDDDz 1d7WKÙ>rl-aI@Yڤ[[ɆZι&UuΩSQHDUHi,154SڞAe;TlBI""NUT@UBpt.2hDDDR͓|Z &[;wX"WS&gkO$H`'å,R6KX%Ԝht/mɚ%JCJgUEpNSJ y#|@wz3z=Q$RT (QHyܬ jPg&9%(Ю|Ug9k݁11HAs:$yD J07%A$9RCtU7K?ŴXg04Rffg)3hk(UEC_[E,@9,PLĂd9|`3c PՌf`&f0H2)Ji!?\ʎyt\uMhE2.8EaXGF 帗OA5h;An[XT6zq\Hsy\>($S M,M !7!H[QOYuTyP@@Z 6 *BZ$ͨIyV&RڛrFͦH`=c(g|lW=fwTnRjO֦f @E>@*%z3$i3X OV՝ؑR'ޞ4ЯOc@Y*D[|I :IXW#גz5*]z[=<6 m: nfbكcĉa7Z̛A%NI8AwVhA`!MB&PZ RBEA&SUY!jb`f_iY\ḱ#4 -ehTD,m?/wگ}Stt,J@Dv|p*v;ft3b.2(q>J&{v_I0k@ F ˟DiT[@AV{n,s)sbEX}iUmbDs/><(B^'~>3E؀Ruhaߏ9 l贾:{e[deDk6+֎TQQe[%EFs }tЏ|tsnȝ Zģ ="O#rhW Bw.mӖmNj{6MNN,u_Do@UGXvӈ:T )G d &- H(mS܀[a2j4n)eT/rR@Y{w\vsl ض6T; Gq:)f7MK%JD"ăP_>!)nMEƆ =j_+XOGCD0y׎Ru* nҐ.Ě:[5UbJrpn!HE&gP q=>0\iq/I4"nzՃ"8M7lS$RƲӕ][D⌈x>Sن5/BC!ɮN&cPYy|^(6rufjsnؚ jt68O'GmjT)u 8n#".]N,Fgy<``ϰNYvuA{UZ+ݛE,Yo(kq)ZzTw]ͯq w3:Ȉ%J[E z3X}h ߦu<jV++6o(; :RUX H>rpA~Y".ȬKyqTUU "kyy/ԗ?QU ·ql㊥_ea0XBAj Dž?|ElLŭRTWXT&IYq\B|X(ggQqQ֒+ w@nN˲fe%vc"lfo6v[ո؈,X HQS`vM1VMR[%+6Ya;>,*jvNm]؛o,**`wމ0`ОQXC AB\)- VQ9Ip(E=P^nwW uBU\A$?1ZM5+ p҂(1naM_j޽Nȩn]vʘ5soUUF 45ꪑT<&,BI&v<4i޼GeRU!ZmMAC>P*9#7< PJ*^y BHyC n]\~qw^=>"(.4p-#tŝs7_ jub=2ؽI#[ߘE5"L0K~(0٬j:>~pzWv|ߎU?>s(O;s; pAM >x#.hh4n,;o|+²l~~ p AiQ1nk{[nZLV>t$`mκg{ xt箱"{Hy?w'e^&:G##Q֦Wv>T^1ܼ|cooٷoo($$fcL9^|gݷ<==g?o*$gW|z1T*);M}˝7c^UCzֿIhmq{<~CMDŽlNI)T+yٛh0D]dzg'}q\IJiIiYlL,3_}dҟ~CAŏ?/f? d )yzʳg]J]:v l׶M-V]*qχ/OuScgDU5k"muϋ_c]@9@zJ)׍e2M22v݋U8أ{(\Qc+rN^jΤ+ F>4#>UAԔ+˿Qώ@>Ib}e%|u^S}3fΚ1Vqq}9|i7Af7F׫3Υ$%#@/ɷW;th7a䔳)Rl2 pȑIzMd-g/͜⮌X3gеK纗QJ-dd׷OJJrVVfn^n^="#"NGXXZwP밗Etrmh.K @RE|USJҦW[>yh'Ǫ?h>, B*pZbAњ86~Q%s{QYi9z,&0"Ac `'?>˗,[ ڧ4Б 4Υ*%x ,ٹSۧv݆s8m*9Ӳ*'Fپ}{iisSɝ7 oZ&2ZKAPW;N R?j\.޻i=Vn}9=4^\i2gx>f*,òEG(5kUUvLP"how=1 5t/?rΫ_ͭuUx^Xx6Icb ܜ<֭sOOA? 5VC͏|> yWh͒KvjIUBAMysCȊU#l ZFR )R*I"@) E ^5X{Ք˒Z~#"lgQg_ȯ{<1>@O<ᜏ8Vg<8}+G?p_}+n' K[տL~S%^:դb{NRɨjBAo0K(*%8E`UtPk#0 yI6M0>{u}z|Š.]:߼eg/<>3Ijp5JZ" -Ff]5r<),^rUaϺGܔDQxE ˘FAgҊV`SJ].7Y-5ڼ3dY}1,'R 1Bb(Z8}FTJeIhzpBA%i׬[Ox'_hUzX sg!i :]bԽlE,t{j3J)9{lO)BZV[λjԕPkӚcAV]vlJ^wF=Zfp\.P8R-;wh4 ew txOѦX EDfɲT̆vƟr8L:f< <܋,ǙMƆ9"#V8G \dowh@9󘧽{Lb32͍܋[ AKUU9$YMXG+  Gct}9G,f,+naGX4 rQu\qQh jUBA.:9>(iW7- +ΏQXHV@ArE) ݃\c XMXV@A.ƺS;ƋY|dCIc1\_Bi{:NႨ,m{/gQC$1ƴg?ٿ)X·Gl~mj,5T)P윐9#_ w;RSB5z|WhKj;Х4CYU/ h,C8+VZ[[GF*D!B")]66Ҧ^X=?̘Ta-Fq ܰhO{(Ԑ槱zMBqj/LZi"(k,J~ B:7"~!~b+I7Bmo GXU JcU ,R` 2%ʚ`њ H@ՠ',p甫* P B`B%{Oe/7؍ d}2o6k O*)8Q^.ixt΂s7V =vEψJѕ4US`1&_1sfATKxz ,,0ZQܾ3:E6+BU{|Tx&|ɃX棱Â0ҿ%2,+4QB) J)C~&-%")/GL{lL̉ RFAcԲ&~wBGFƪ$"9`@0;X!(( ϳ&!*9, +DVcт(s֛124&->ё<֗J &MTU KH4we73 #N4cDQ86"V]G zLBY+_ɷ+YuK@"h@ؓ^twLp̥Y|+HW~5%SrlK6VT@}~rر|=a=:Vِ 5rCRV0l#R/ $YVUatzA%>!P5cş,0<ñ0 a U(:bD#Cq%e[ R@Lj54'$wz.ڐUlu:MC ]._ԶmۤX6FDәe /2e"LzIB(FYmr|$E9^ Ef_~:0L71sGe T(Ba g噿b4& zeYUUJ)!a>61D= / e=!S|RTR SsW@cWȪQZ䓡m 6Im!ґY-DdH(ù ,_Hҡs5c!s!dڋ~,~,,X/LT}hGWD'{CO?i55R`5𥉉JC<^5jWHw{AOU`XVZ;E2GڸCjk,j\M3ssB^o ~ʜRaCJUϙM3ǂu~Oj̢WBU s x ⎽U]ƌ  i݃wKUrh`i&o8ck6Ph_hgx$=c $kuCb;=A^NhDEju ބ6Z7&lQy;gstɃZ[B`*#QE[T(jmfLhL  s˼QhV&g؋F!17: }vyq>PrVc1B'H.GHVVn"-&am{nc`}uPgAOQp<g6x/,.HQH#DSJbR{I5/} `91zd-//dF4)8Qʰ6AQTeaYe9Qc!ƺ5V($ )> \/7Y}.y1Ζ@G PPHj*. xתUQqqC nj\Q w}hxoK׶[*9QRS뺾"x[oXt!m蔏<.n]O:]$+Bpyw&wa??|⽽!]$yOvaL uwz<4(Xkv{(;&^7ʪ?,fa\g4Ca $Oѡ%~79mRmz,Քp<9l8*嫃R͓c`$gΞq- PIʎT5DլCKitf#.%UZv$8ϞL?{dmcrBb:oRN{yIYiAKtZ,p 7q/]iQG=/ ąOA_ /ɲRPI,J$I6 %ǩ@ AuJ| VH?"j48bҭϽ2q`T{>S S.UT͑*OL w?T Kkl1wg!+2' HsߋcNĶrʘ*/zWbT)Y}r@XUUmT&v̤D*B˩yنR: ؘc6CTVs`s6%8^$у^+٢Y(^AXR0(A5,;ʼnZB@M JR夕8`@JJJ8#56j]c"Ai|/{t t/ZSl;hwU?d,5|Jsko-Kh IDAT@po^.t}M޷(mQd~nލYAHS;)ͧ_7gS&(_6o=&}0u`Zh=\ X}6U;i ZQK<&!g[FS{_G׮n=%z5`ݛAkkx= {z*s}UJnɄ.yq扦`?mQ(=W BAuIizfcBܢlj}o <HDRr.Qf !~nuKt\+}`C ^Z Gj%l֪&S @A\C:ub"x~=1-Wl.iYtnm|Eç@\YcX5^0;{ J42z9/:J`3ܼ/0p.C+3vM&ȕae(=&&8t?,#߭1 2k?7Z#J]$)\߱FH .{97d ú +k\o9mȓV/=U)pAUǟkHSAJ4#@`ƬM [{c; c,z *Pȿ &w!6}gb^WF@_fJ 2nh4?:%x<^eMugJ= n=bbؚa@)=e!NQg 9ٳ9gSw#'\|( 7^%zy0V=ګ7^?fd݆M֯{/?V.~SkݬYyAܳiI3]nZ(ٴpʻoLw{QQҘ5JY\WAgA +W,ԙ듺ErGU bCr.~CA4 z@@%c!\tOM>EZ9i,~w=b)( 6goW ܝ 9.y. s%m PCƱ%%$ܽ:~,nF }ˏubo~~t, T?{ T?zg]{E5ņ75@ŁiW*m7h=cI6ղ}҆X71@? umO ;C# fןK~:R1Ds. b03~aą_֟gk‹gz=[268Z(ӲM'F*j^jWnP*o=<3Dљ "% Btw@̜b߳r`F{:]YQh`iqSeXeY@c4<^qQá>< >GXMLoKoPm2&@RX TBgvH RUdm>hUJ 7:R4B2&|uSκ 7>ILQ{~}ցQw﷫_:?^ *,4tJe]~a_;6d &0$*E6> k"E6ꫝw: ;8+NVYR9料)' =k-@EL)CҰE5xm@M#GWUGCRƝ5S j%V)<(QJHTǑ m4ŷrb7D1PeMspZs#"n\DFFtȑc%%h@||¸'tfZ98. ets -**{`fu+^ډ0QN'.`@V)0 cRVkZkËN[soD )ԠzI)zUXt}JN;2L چ%@urFHjP4ש(J FO* dSyJ[Ϸ&T@?(+DeZTUqBW֮__q(**>vF#׷(33 JnQ>C 1 u_n !>oӦMFSvdlz{kӦ-_jmmE6 xüTkmDTe2En[P.y p\Y3FQ2 ?z~_yM,eΫPZl:?o3ּf!ApRB NPc:);aרLNz6cUQ TP0d6nPu iͺ@N8w߁ҺĄZaKF^z{p`;^/2"b8o(44I+s !:`<%B[%aJuaYV$>_If=1 q=`6j,A -FcE  R $2 N[#9VBaBX|ɡ<ϋؐEy1H RBX D4j,A /]CC.:FdEQUJuȐ5",RYH*RCR`z&y^W wjBn7DE<(J&@sX8 i1 m )e֬kk2ڶIy*&:\.7,3Vvr4O;fm/O ].lbr |4>{YPT\BDDEl~kU\Ch |\EQ>=>էbv1&1++&:-z p!DUt۩ޱFݷ?3+;1!m~))؎: >AL&hώgf@(GiӦ 4NtLT0Qc5wm -aƍᐬ#BƍAXhA#j63^}퍷cY[ney˖пo"X c_כo~VvPaxEb1[-","d3/N)kN󛯿 9fCU*hn xڷoױc nd2DZ bA:^:RYk&R كcҮ(Ba;|pذadPJ[٨*UdEQ<}TViuj~,Ae6H2,ڬVN{aX-q9kӦM6mr}:ѫmf08VӭKؘ?\QN-A{RRZVV^N)m|iv0 %XnO]Ey5 2>Mc]ë?V`" keB.Uבm;-CƎHjDAKc!*zJrJ'Ii21w/k:z>" ȥ \gXM{\|YeN/V-g;6!MlS4xI[zC6wM8 dq1}+l j, J0ʾ]+3wgM} %r/U?| B*tV<$7v%(W?ظtm]/7I[g=!Ԝg uoc9/Ǻ +_Ys^Wq #8`g_;W9n~J#>_w8nW^Η1CbL}};rh\ǚ^~R5{ټ;E6n=-gAA.<uMK;8US; I%mdn;hj kf9h3Rw?9kqண ^۫d:VRkyqP=;{~z(Ffm$9 ogkB]z@Eo+EȪ'Og[ iG ؒJK|0INcݪkg>ַxc7?ڵ2*`-{[E:'> = ߁"[ٔUy6ٓX:OAW#?bLIlV05)$\֚[Y!<"%n  MQ3`S=\NA7pW}(J~F\ # @ݖ<6XgL=Xjqmvݘ:PjI]M8ǵko?#;聪FsW$V;ldm, '}f gYTgβ 6X=o 0BqQ%IAPU?v8fRcĊ)mt?  jmJlx;9P-VGjB8TF@tyv"SfKXbYWeᔏ%D0 Z633 uv'}BniV8@eYN=>pܢlŨc37i';\;,jr6' 7dY?zGN`eQ;|:T!MRv\A'(J'>x״l1#rz_Sq lNm)Κ;Oڵk\\/0 ť1wwKv-;;WK9 @HuS't kb1.R hlI:gyv=v#o!jCjSbFxw ߞ@a`&gTQժ*VUQԆE)7R`'еK9{]**n-ލmi1t3 ClA~eӫ/9ukX H ~\{j1ee儐HcD띡I!!$5 7VM s˲}{ts-l(Ʈ  -޾Dž_!h4 @-R"TU1DP;2,5VBdYv8\+"rf@iYj efBADEF4BJ5<4d\9eCpn$ad~ӱ,+IѨg?nF}si6ቓ`@  AANau%V$HR=;r魑+ !0!FPTEQlHv@8I~0zb1\nXm_m~uk E% iZaU;:.tctz^YKJKvȐ5",RYH*$ k2B^^Da?plJK 1!ȑyy'KN۶j&Tz`о{"aU:zuk|,\\;޹SǍ6|mS׮=vUU*(,*++Om&%%^?M(}b8rfsxe0 rAhKj|nUlefJاw~qQc5u_* Ԟod".}W~+ň#A0&lJ+{ʵk=k{ _(jYiylldr{7h3z au!NUЖl$IJ#c۶I9;'$7!L{ǯ|,I|~FE CVvίmzsrrON?Q0{K+WɭG:oOq' tg,^@7=U%kƍ ,^hZ!?-YRzw1 s<3s e~Xq$-q!/b^wTm&vl:얻o!c7sgDƘ}ػ?sﲯܙm\Q&+t/_"Ƅv6|V\|x[ˊdzn>>|ONw2\=:td4\ Sߚb~^|5\VZ\R}ْMii'}69xo$#.9 )c="}uFn({ GjXyȫt5kfXt"{'0ws蟤cGww<6a;iMAS:`ƫC&<0q̀V-]>1xov _8?;Aq<>S9 0 #ȩ?L`4ÓkɗouC>tZͩx< ;ˢ:k({W^32v|_~E/>Tcהf0Lvv?B=0>޻c5aO>_L}pIF^5OqV+gQE "GسhMiۉ3";ˮ2wFG̍9I [BR/} ~O60,٧s\ӦMvkadSM-`ߍ dfl- sTz0Wuպ3N}C~lb_;x"8SG $jSw*?V;b= ,ջ#[ IDAT|ߏk:wY˒`f=z jf ݀7s˺M] |%ȩA?Ev{:! @cA冱W}څslPc5gȮ[n7 93ݩ5K[TuuNc{J xmc Rж5/lD~`Z-Bur j恘en[G!Prp, rŠm˙΃{^vWהy~ãZ 2@`ߒW; {~WpFOQ~MᶫZgaѦƍNxqfn3^/fq^! jNPLf5߬%8y͝t@?%6<5Gd[zʏ Q>4s'c:Б埯sʀ^wT,)t[v{+%mTGMۦl݊[-0:0xAAзTt@xFҭŻٜŸ>,兏i)әg8n@`E mx+)mZuAA.AA?8V +D~qi+ĘwAwSX %] Ϗ0m...5:4G3f,K/ ƙƫ[oe CG^<ꚐZDD^C]|rR BfJS"0d$\=ws"yU-{w9l|>éoU=<1 1(?nIIshƇ5.޽;w޼dkLL;@/ ƉÒeyZ탗3wWQ*P3,˻w>NYS֭{]!$B9}EƥsCV;@.',xމ9%;{>t]gݟcl8pguxtj5 +:7;bxj0GaK2R<O;3 jo]诤Sظl psFv;w[צ kD koߺV&>ZA'<:D_|TN{79XO5QOXnE=Ƹth,l1:4,I;.'֎EM^x񅀇2*8VJ}j1(0^hܸQef[b\l*Ocٳ8zglf,KR{ssnۆ>|o? &++9sʤdžy6 }ɞ=z!0pqv~׮?t胔3Nme'qIbrdGմJ٥;/-B]hC!i4MѲW9ߝlBF#LBoY!Mz`u2^n٤w+ Q䝽ik؍cޚ!5>|?2(BOSĒzT*ħ)ڴ}jW^S^;eTbY8Op~C1GϦIPĝ_nuGZ{6 V]-[w/x7#|,BѣGϟ??`@}<_˲w 7Ɗk .L/*j'>ƲuN@HUӫaÆVkV=l3|ذWF_ޯ_$%ݡ/0 a}j52VʚKĤbUh1; V(ly8 p)+i9~O= c/L-7/ȆuP[܃(DwPeSi5HAtDi8:jQ%4[4':cÛ/([$޳3#5ss(pGM(ek,JנЅKi}} U~eSviM\xūn0֭[ř\%˗/ۓX?7mWij,ERk\z ''j/B ap|GkΕR5 Q{,QT0/'](g)>mС۸g>\pbjњ4!K=_pqcF?lʋyego~>eJ7y#^+p~ßذ|lɱKo jVA'&s]ntHb5!\1ٱǡ Zbcr Oi:[Ņ(J|W-n2:p@h6 "Py;CʼlQ5h[886l q)FR*U%)ϙ]Zl|hJq k&"e%ʜs7-5_V"@ч.49wlsvrҝk3ΥQ͝PBޭ/IN'7&?s[w2X (&D} oukz,X#O6K~o_xARUX(:erUcݏ<_}ջ[A<8Ϗ>R@ݺ #''YD. ?Œe-;;ՒmƝ/\}4ǯ5{aM[ u)D61n\6M76bKa-J6mړ}#  v,6=&nϩrc'͛ڤu8ږC]ǿ3KaғֽEDVa!AA 9TPPv51B#;J Y߳_f!AA~lq+j=Xn'6@ھe]s̈#Pn?VI I&7n@P` }R(9fQ brsP:fm=V,n@Tћ 3ҕd#[<|x˓2b|I뤹7}@7xhs{Ĕ|5N[dK3 ZbS\lkܤXvmײ [a ƛ|H7lHzq_99҅ ;M$g['F 88s :id6LTyɟ;szťS=rk͍~Ȕͫp w/olz1HW07*1PqbMJc =ZԦׯBZf۷cU_Q')S y=RfvaG3eV1#GHOψurrj׮-}Q(9~ zjf-Al -Ajً_@{_,u<݁t\'ҎE3Y^=WWWׄt|up镜VB0ƹn݊z{]zcsE%'L|3//^ӧwv, 3ر(O8|!>|ƛ>׵GRmb' XLwvv4il.tݽƍEFFЫBPe;?2P(J g̘agfS(5RG_<"BPR*15^~!*6BPƢ' nnnyZUf=$ƢP( OupP"YeYvv`LbA8!KFeYAdxNq0̲vQ, BP jY˜(dz!B m,0n!^[* zWSjh@<(B_XW5$ d]?zc}cItpPi%k9fQb9V!pfRK/jǢP( ^8Uh,cB$kӪwD7WW"NDL@P/"!TREP(ʿ BH&Q ADbE8;99qܫ=uhQ@Wڑe 06^0TcQ( bc p"`\VXӋ_&b1&<"DDIXca/OA=;wPJJXdqXAsQATT²ҔER( bMaXٳUD)Ç ֭uĔ\0A̩>X7"iHP+ElWֹq rwfcv.Yhoґ=~ۺCvsW ynu8]ZʓO% ByRj1+*q9"!uB\ T8Ȩ՚]jwINN)31-?t9ۍ@mj%c>sW56uE 9Zr;+BA@X? $GX5NێZ =%@ֺ]x#MBP³?JA7P T쭅g Y~˗W?mT?ii uue:5I7W#w^j =$ fKegVQ.NQ KA) XEK,İUt,>ӝʿܹcLP}۱cGpȲlX,T\Y8ls 'l۶shbjB 1!V"ਨ(Q{7b55EEH]w" wB`Ĉn׿?B`ɬjp,[+ yK>o/X X Xv D4 eL"<l]ں&P`Xm4`T4Wn PL0e "Vܳ Svu h-Nue"U,(2a =v5{-,1k[IE ) B eEY.\A6h`YaHs!0, E^UO COU90 b0*q M{ރX٩`L}YK )%D"vUi%-IZy{BP({@q' T4ZWB8VI@H2ī)#3ku+Pc{K.e9Lآ ECcYR(xT;MTcQ( b/,r1 eD,buVW]b!=9}$E%A+v>BP('h0,C$0MSqejU6L$²,ǢGiI„fBBkX BĻaf(Yq<:0`31fRi[+̢0RTW̢",T*jj, BPdA%`Fx8D Fl < "Ƹd`5b%ɂ1FQ*G)ƢP( 屃x^-l$Ɍ\,р(5ވEQ$,?ES g9NqOtTc>v׺R=(a8/;4P%= bU1qȲh"* ,[dJɱ,=]Ôvɸ|ȎHtWz&+$;j$S(c(XVRJ@o}/a FEvpPQUgU4tat4ؤ oa7;>YttmزûD~0{l5bºYӘhLC[w*ilq By`YVvzBTVd2M&8ZRQegЌ)݊ž A3\x1;i17Z0\OGq8XF.DX5 rSzkϏ ҨDI4&T ъb%LrP) gDŽZ(u7+" h6oѶS?{ݺXVm^=w.i诇D%0ea׍xe}?Lrp3^?""""_Jr'i__w+fwG/l>^QUٛ#=hrͷ7MҒ :LRG=J@A'GR<]Al2@$dM|Cnd4) ,j*,)İu$ Xbk.;Eyߺ9kGsRxxs`[5u S=oY==*͖uOv6}r[wЛ<}]DqOC17RsW56uE 9Zr;Xj<0iwFYflӮ.vHNpzBR;bC>H kR_l.cC*+GUmv-ʄ3kveqZzRT^2Gttʫj=nynkY {r%_/8Yozz[v,L(JLXeY+PU#<X*eXe~0SefOVŨ+aaNz+%sjzwdv]Kfu҄q ?\J, ! &G'ȧ>}/9c˭jKq%7@doxxCe2*f돟ƏsiN.WnuTѦ=mNdt7o_W*ru4S(qo$,'31QCgmDwcã5*o`er[<=Jĝ/04X?Z r3ۼ4$`ڙ 4)Ҏ9(ANm^ڏf0ckb{(DHH]X5D!@])ډiBՋT/~j\1ƢP(1=.fvIA1ecpM|nS/L70{7}ޘЯ)V &pjy36%K xz͇sZwƲΚ;Cf4ÇG rb̙1t:L*,qlaqĤ_|h3!{)>V;@]f J篺/wJ[-`{*|‚w띘["_O7Xunw^-`Jg͗rkxBL&!,ĐYHiŻb0.6z)gNX97)C2'vH3NZ :ĉEo"x+|ZۑYfK`m}aUӏunR@%$t 8+]O3~0%c]8&XWw,Yyv:Sn7GC~xG{Ѯ&wl!۬!pq`ˮ R鷀rØl@cKQ2j, beY)~JFrd1p S-4줎]s ӑ쏻Eۿ"dǗ#~&^hwīyNVZFM{~^*!7K*ak4bʄ.koD WQGvro%ZO:n/oSޭr&<[e"L "d=Ńk-c >|wv),ʅ`1A@:لi_g}̒3S?,b$?v!XdbFԛ R\&ES ~b1KβO ()D oT(0ƲEќAQJX_taJ/C4`[Ӧ9cċYkSblF L !}=*%vޛ`Lla0k>&w WkXqXF5By0 ]dhqKYthSr noh#ٴRF,ƵUp|i-uGkH&[% Ϲ n˝FSf'4%seۓ%in}hUyN?YѸM'cm]fʂopGų" &zx&xl p37h6SK3j)"v, ՊE`",ɇW)Y>UQB:cCfw[o8QB~`uusspw*#wy_\*61K3_:cd1q{v,b}3ː't2~V͂.}&yW}g<;/KI,k@fmЪ6INKڪW+.ˮJ5B}rM0])zWpr}jnfWQ{KiR ߼#q@wLioDgV3bB*?ݭn{>[9~GgGɀ1N[d ]:B9%J$F-@\!B>*~ϔ;8 `)>mС۸g>,d^h{9ِU"{WXM;R@^ R}Rz1!aD4ZL)DbEa>}ϔ;;/{1kLe!D, 6gqN2 oZCa- d6-AΟAr7I9`T_]l겼ѰG&kX~Nk:y8GqX6XBULbbtYcm7l&F`Tr$nQ]7C?L;5SتQ$NI ،xo+3&eU,ƪ1]x[Ѥ]}GtQ-z6}j,Jm4ZՄ_n۞-}tڐ#^sS+0PhdǥH'Nrtorm37ΰxѣ3#˿Lr* mړ6\J7E e; $>:s u0wO `Jw4SQ_OeEm9AH[_K7>ҕ2kbXijio_};:I$CPRcJL0SI. 5~qQa\y_X&QMH 7n,d8%bߒ8O:.g3 T^4$R7%/}qUvXw1r{fNhZz c\rGg=wYC)I)/!y(wrhw{ayK5*XSc..w4t? ftgx:Rn9+[Z<4=bI@bgqz=5H$;\y`Hɇ%unm_, }^);柄õLcYG KSݲ/Eq6٧)o IDAT?@HnxbUr*Xl֙^ B]h͓-xz8Cr/NПM'M[aC: d6\$`Mmo不'-Nrx^D.k4͐Fm9topֱo5[.䱅"'Fq.KJ, +慠 k'n⛍Dg9qޝlЩ}v{oB4cJg{~ҁ{+o ΘLڴWHOGqn;K[ӽrӔ)㯯<"%mϞs7k뱇WHT zOyF,2z./󶄔k\COK_7.j@[6 oJϼ=rp_i[&eȯ[Pvd7˷K5"ǀƞvUB,iLD޸KS&T"#חx /qq/W:fOP҇6!ŁP}ޟlmVTW{~1 ˲\dyE [J,fHt|€fo|}fv{\#?\]ÛSܪSV=eN?-|; Qr.ۮæOnz= _\:q̴7]]:7AL~%K [:2@o yp̵wQd# , V?Uec= &hMpҮ-Kz/𙛾ꬱ,dIvhՒعwR>8SkleM!%: J̊ hL#%nI}[ U۵P̚]Y#1:DA/ ݂ ,tg#<o{=8l$B@5Ӻc1:tkX^o.Y$%sjzwdvV횿{)ެn!cq ?\J, !4NJN[մxꥫ_NЃAjk<bY鲘BS^D By #caLB5ֿ7$Qi,:BqvuO7oKYIoD}FXdѾqnP&p?RN@9cX :R}V5y '`Eq.sצOyhD By$3fU*bmW;v& i!7!Ye 8 X"ov2"T X{Dx?F_NA+G ͛:ypU*3䁃s$7~̀>,>ӝmZ`Sn_K() pp%_e\ a'XݥjרD4m}ooT_V~*wj|w`σ+h5ȱj8U-۸Xݬ{߼˓-"/}eO<{ Q{mS { 7MkMT|YjG%!]xd8}65kbѠhO|!( B AJT JnN\ð 0Rj 1H) DxNTP,J-l*BP:~?iH{x]QEy1(xԏ ل.EPcQ(JciVzՂ_[Fi&{oli1nۭ [ Ygs*x7ow8{D|ݨ ŽΛf>;f_O>D j, BV^+8R^XT>l\=DFqcOܙVMh?т:j뫓Z-BB1.)c<ظl i=?գ wJoz F7&{vGU !/-vtތM§.^Ccz/@ꟳfΐMhᑭsf-7N3 a>;fXu1i7߹gG= |~H^ OkW*ڔaͼdm[w"U$/-z}({%?Q(TcQ('IJ,S%K$Sqd'ud-vi/f] 1/G!i4MѲW9ߝP up&~=?uO!US&t!緬X{&j`5b9z &dݚ3ML2^X/r Fo.HB5Byҩ3tᒡE-ygYMr']gJV}Dv!}y\RBҦ$HbHߜ}-ݒhu?New_) i䒹s޲\OӴm7zR/J4E5WJ+sSsxQ}rYۉDzUA oөST% ϟ=ks=B5ݵn}z4+_#s+"tSe:LppcJ#\g $7ګۅUs=Cc쥴Bo^L 8;r4=̚{zfDT~n`4s.R=3ahP_a %R+1 C9JX$ogPW@ X1]x[Ѥ]}G4nYճTcQELYo%DzrDAUbbB@.~ozH}d{ `9]b*(}"XE3b57%/58޳oǻ )UEwmWS1S8ЍBCZjyE,K@c~>USD !]#['63.gf;ai"ED@DQTDŎ[b5؂-V  J/K]P@vK>?>3{=XX]pِXo#͢l 9WDUQ<תu(~-dӏ,t s?}k^ $\㶝1j?±MVpPtimŰ9eq X ڭ8XRHH EQJmL9༶MVR(Rr\IiĘz|H*`ۙ@$G^6CUjd!0J`fIXT3NE?uԈ1\Ib0Eeˬ<~d8wG<=43?3HK-4~YBSay˲{MY˲vq-):Ш V" ->_nj;uŷleħtt'rj֨mg'}'^:RVשׁ54qyl_F-9۰Z18Ŕ}A'樂lN3݂Ccg%Rmڼ( ٶ< Ƴ7,0lcՅ M^{N޵vU?\4sǡv,qJ_37ʒs3d.W {K2%;Oz{-(7~DŽPBÑF3Ã( ۲Q;pḼ$_T'*UQ<3lQ>g3(ii΋Ta b4JX=u@Qr@TYM& 0;\4.>c{+zwaf#Qb4IYFNΜ;.֤(%Ǡ;skW͑x;[926hȬml,C;_X@QMp~^+e㠔IPO_t5C!ϾFsXzt+`,>- -6~jN~T s/M "9a_Xjh¨z{oDŽ3qE)B[o^V@[-Y_*vb v iVT͔ y05s-A+ /7.ZyM,WQmaMs) yn2e̶\L:gjۚYXk]?VKQT(Js"zJj{n]8Ytx };t'b'ӌjL=s+kkfͭ>6d&c@g{ͼo9{MSv8=v4[{J6LdYq;|o !LI6xڥ*Uo-yj@G;ftYvdL[ P¸Caymgͧ=Yv5`9 @ >54qGw׌!}d]W#qP1@oӤB ab@UBmaӧZ ͌I#:9:3J9Vp-f‚0}KT1MƎ9NrؾXj=q vŜ>1+B_;;k"4 LGZܷ?56t.7͙[R(H&ΝM߳O=͑*&oLڽK4m!Ό1FDDa_d*mۄVP Hc!OL!Q#!G7cO\e]5ZIP|m,ˤSataCZADžJYdrNR];^ V/  hBgyq$$gcN\v.xr?WJ O4鞽fګ7NYء͜:ȳv,:kR1kW?V:gxG7I/^&>Wg9ƷfFÁ7" 폖ةS ?ҴAv+6V5ӹ4ru ol+{O+z&ll< RI~4DKbyTkY3cDJ0F (JqRH0pq*A)l*?U.Jd6hHvoh{+fK1Ѝ qb_T ~{+fEDqq7Z]cQxzq/ZۄLPgG=Bc,J–-}^'c cL[a:w=nGDE>)V\Yu=嚨~& +sg()P@}!H"D޹wJW)3&Jr_gtN㜵x`R&УET>c\󤔢(%Gɱ`RϤfܸ\ k:ևi;gSG`degcfzKv{r#roK+6;NJPgѱZta5]Bb<ۑzw5j;ac g{ K,~M`H`[9wY)5qmo@oA\CgI0Lۺunb_*`Zcwbg7~=ތ,qm[kXЪ3 (=QZ!dVV!hkkk U $:t1!4ӘBp{kLmQy['[2$ngvΎ u{ Fx<%P"a\1{$ \L2,vىT)e!U?ۍM,WQǕF p;/=w6;kSYf>"n'A^ *0,wTz8m~jxv^3x|wMc_*7FsZw.Cw FF| c2 TPd'BO7nXD-%w/>2Yr'ӈ/a' g#{ N0 03}EDQ@Eo5- A1YLm-Ik}ot~ʕ+3?Mdʥ] @}>GܼhNh@|}ʺrrJj* bA$ `ZŽ~ ,X !"3=SyMe0eńn>MN@ @ XΈ= #9EԲ~@G@|iقݰ0m?-s @űDugӡBŁSZYBiv0ץfP831.F94짬릌\b̚po aޯ;`7ݺ{m4yi,xEy hkBIdpH\0yV]C>=ꍰd @g4@4DRRRWaeɩ%$$>IsirzcH0-x.YC{]ʭ`L7,rvpm=Ț`/W \n.+JvCRk9a6@}y㺳uet1}mC IDATXXQ535XhQ^÷57L9ǥAƵ]Xi11yqRçJ}v{/?͸ -Hu2l|sXAW4š|C[`4 DQ$I84=zrN~udy߁!/޷F(yy) |vж4A'/)?1ebתރaYa!24Zw2ܺ%{r <4۬!CELc޾˦5pP61ocXzyOHi_^-Pe3jc;~g^EC Hc}G*x4ص sJU;~ްw}C|\}6MqFuNh݋ b.^Nd'BJ^qZր1O^jb@TEFLv;|o_F(~-d97yhc~;*.`GN?hPgɊ-W ݽ-5v%)6*#BHa-zp"J^{5fʀzO"JZ)ahg$ jYTm58).yXClNT¤sJ ~H|6UjVO2\o 6tLwܚ}.yt5AgY8;P( U {oɄRRVKPt3׺$ nD (thX=ߑ[{^ *$@ 0-CZY?V:T.(+JMLM1_՝-ÍFo%Nu\Nac /Sx~18_QiR dԻݰQ%^eMGS截/},B4+}r:iuU hwr:po95k6OԳǓ^\z)wVLeLa/ mX}-8>cq)I;NI)#ئ7gSk,;Ǟ2Jl-&yM.mGygaoX`,Ǫ V\ },lϪ(^dIԎI)qSNmP(Kos:*Cfæ͐ z@7[AѥE|ߧ3b1h2`يʃRHuYcWǜ7 gkI$ XOVHe!lޙ[t9Ez UIC~o n6:,IUxz{%^OU􉔨 g:iG5Fņ4}لvll!3{OiܵG׎~ c^)6Z:ކ ,+nm* Qֆ\q$KƹdKKe$hwmܭkGK[OGj(aܡ3[Ӟ,]0}D,WXׂ$#*^ҹgׂyxLq(%+O)TPoGGN;l% ~43lF ;zPTTiG̰+Fx9/R515YԼO<~kJ&\%IMc5 -I~i,it}kc\snS:LayӜu]_ihc]?u7"_i2n{5=q3>SDpg׹{'D$]WipO?X?܍l-Uv}k"j/2T񃶿]+TX_8T 37\E7ieܙ~a̱1{o(=.`=\nRaȿl(%кC-0-3f}1'żiE |,,a¥ i@ صQπ3j/d?x썜A[3@țD4GKԩw7 !)Ո .g7͵7G.Frx*nIV{7F[f;Z|4@ |ՓrLlX6"ĉgoGܾ [tx}r iăFfG&HL1M59l෷b쨆id?ViARR.&=Lrv)ch2A""պ^1tEfТu-f,@  o8TE(`:5xͰѸ{?wqX(&Q+%[1OG,+91X9ہ%>LPmG5VX9l)>itW ";6w R L &=Vll- h,.B YJn+7\[o+3&Ҋ,}-`ui#b\hBW)a$!ȲسRqL=^Ȼgg8ҊN"YtgƪV~X8dͮ#kv]N|jÒ$m*m:{N]uV s m\ЛiVʀeVF.F#4Y@*$p٪~/0Ӵmuv ZuFw8i,/Q'|` +T*DzgwFܖ iqmxG&}`5|.%' 2W LF48*bG"(ޖk7p >tzAQIYib4 q47b(3 q fl۴r`cv狦'`Z]G{+qcU^Y[m8+=T<[zq UByd E x}ӡ!iqڋk>-}^1?7/)-Eb2kýKA @ _'7^b0F  XL"nZh،\`0Ր"ж[\lljտ22u7AE @ B! $I蚘j+MjQr5pɔyF{gɣWɀiE.=ӢG]YFi7R % ;q桠&02c( Lv:&5>n;|z 2;&h|t4ゴ гeգx- ;p U4@ I EyY̠TO l*_콒Zg"I*RXb=[J{C5]<-MnoOFz{Lq-՛zr,Ju!í z<|}o`Ayıpkw?3EmJ^^X@|($ %^&IKKUU45cC RU_)W0қ˔@bzᚩ#z= 8Stymhv%Mr6lFl̃S52 `ieVv5£uL)U)>Šʈe+cEQJmL9༶MVR*ٹҤu\LjR,w35PH͐B !ŷ>)8khOKL}2iyGNfkg_޸q4UIM9|5vֻwPɏsmyvҦS҂G_ νMO'#?H3&8hgZ'U&={Ͼ~cdYJŽ{ׂyxLq(%+O)i3öl`8./IWg<3lQ>g3(ii΋^3{Of4 }Zyy^Xp%dأr(Il;ϬtiIҭ;Oӣ#6g?|QJ(+X|6$[|y>dsBwѫ&DUl$Ϥm< x׺#ڳ[]7:JݤO|,oGKm֝ˏ L~o?֟Yiu=.~ ȤZ_ $jMJ>z+%|"r5wՋ4,ɉOUOլQ0chxt6..ܚg'wRFM; nDw 6 M2G^A&0@Q˅s6g 9`1`_=KDX^Jn`wj̔D,N&=}6a1[Wi KՙzvLI3;O%@%^pplk_D{k, }p>Wcr8(enËwp'?*tφ߀֔CVne[ q.rT< }lJ3ޫ-vb v iVT͔ y05s-A+ӊm9q}m2ݛV8xOmY#-ȬqV*UU EަYƿ{Q@Iomx+ZbEI6XxcX!{Xѫ4!@S*No"+gƠ><+>*+ Bc0dWg6lT)Ҝûڞ[_FCNfھlB;6t'4SںP Ĺ[׎c-67ާ-Lq#}BK3,;2\׭ (aܡ3[Ӟ,]0\òG_Kph-skCl=5Vm*b?žDG鰺pt)]Z[a1l}zY\;Cu2

("ϏOqۻ8"ʆ5[YECµ]83ֆeUlӆ ma\Bw]((;cܮ?վ4Gis-"h ~][_Je2<4v2=@5W"ID *mOTg+n/=:+Pіz\cD)|zhc|! "HcCpLlӺgՠ5l u o@|')$n vfDUuiَo"1{c~ہ,qIڜO>ёEK݁Yp%^IFTsgwIT&SNWTAuR{$BO7nXīTJIRNm(;ǓDE՝qoCZʂ!~퓴]qHe!Hc}V:̎ߒ'0Ɋq&AHi4AYze6蘁38L4Z?]^b՚ ;Ze 4Ql9a1 @\OaWDO?BU2y*k oH3k?*I;TǫC~5IjU"895?TJ/]U?;T]3S@kxVלD zDK&wg?+$jb[1OG,+9fDDma-?3ׄ\/8b9c.8Vwp,eS/;=zUv&DW݄"ӎmWFW@ n;ac g{ K,~M`H`Xmi}'X q,:3cU˥n?,fבKHg;rUFtӴ:Μoz׆0Ӵmuv ZuFw8_[^vU M= R!U$|wSL IDAT}@ ė3q(H`-!w/GA yюi3p: p0TEEP-טo _ :bhkXLW_+ėy>Gܼh$QVQ.@ q,{oCF=F _$@4_cqmyw*4llQgw0-Ǯپu3IXƤ7o8=||=5`UiBʿg̚poi]BJKbi,@ 90u=x8,0իR~E(o\wi'a;ev;"@ DE͙0m^ݿwbڏ0)L$W:٣Mٔ&?}$Hu]/(4)6b^ 4]m3<ǗF)m=;ݸ֌|g yii&)R EYUrA! 3Hc!qrȍPhhg$ jƯ#+S Z\Ӥ@Pz8VUb=1hcJtǭ7mQz`J㲥̬) ߼ߊ|\*i4y+,{qiZ! "ihcg*O&i9vT5;wvzuo?c@)c޺6INHx2o IR@kV>5$ëGGŨ&ܨ3(o:vbX޺0~]߲}yxG=M94:}_ oUkV_X2ϧm4c8O-Zsl vxkxF,Yl;V,HzVۈ&(|ɛ]Ey hkBIdpH\0yV]%ͻW<@4IUgfJm{@uЌ }]Z,@!uɭ_\L&~c" p3VMlUc،wE>%,X8R}&)^?h6X'pO\ ! Vbi$)%dV2U$ ǹ4qB8Z?_Od8o*OqWތ*U9q"󍻹ujIðd˨fVoآzP:'% NLPg'^ :s!Ea[6|}N\0+ԃBe`1~ @b i361z;HqT1vL?YҒ[wGGN;l% ~"ᒗYzM*r^Dڭ Ƌ) ,X5z(u%⬛qʱ;ƚIZ4b}|?W<-3sJHZt6y=կkҳZΞ*yUlzhGބ53h:߁L>|2Z.쾲JM_ËFER@B"'z;ҌiD'<%Ӭ յbrIs@Qg&t ZPMyLkm_cM*gEφ]>?Rw)NҨmcC}{q:;ǖQL$8^+"&N<Ӹ<RW)Ny$l}Y eBn9-V-Te׫UZBY=]{;@Q~H2r?ƞH9>!<.UxqU  啂O=SL֤Mh>z.XBtIx)(b<9fӯ/XS͆+;)L+N T2QIj8dM{tn=4:#h3's.i_JoXӛ8*ruQtsG5(I)VrDRgGVTFU*2I9{eƝ]tc_*SzͩBɅb0ҠJVZ.3m▷>ҠBoe7zl @Vؖѥ({ifj؏oqTWh|fzݼh*bog7xWq7*j g@Eͪq,:[1*$RXX*EH_Ov`sͶ"Cgedո+˪*ʊRv%F$fDE\() ~U!/[$x5nШ@.:vXE#B7uk~6\LM m:]0Ŗ;a%VOo.PIhwhzLm MY{8I5Fg$IQAPR$.Pxf?VEE5 zy+V4OΦtrnܫ(VNSi=|hu* V}:pL]i4`0tn#/Py'cT5;ŋ / ?pF L[At:=|CB\Oqޫև[\-&<-g㄂KTk -ȩ$:kРOuF0ikm)~߼Ws8>r Q^ k-ՕU$Ui׭SuQQkR()DbBAh4 ÐB|hk4V m]`;zM']/M:0IS h= _s5j} ^O[ǍQˎIoY&C)0msЇn?~MqUȉ9'AJ"]JjyNe׫2(/}xz{?ye?/!aX9 104NOűXVʹ1Lj"Z`R9l}+&߳և[ -^qw#T,aiJ$h6,t/ 27'IF4F8CCU0~Ui7!ߦF4\xڌfTgZ8T/٫ &}@z)˳UY_781 pÚ7K yjsPP$EE?idÆ"Pe 8gAEEP-טo _ :uB6p΢c4ñ:*뇀Xa4:Fc`t_Hc!ߧ;8VE 4@ OlC B Hފ(@#q#@ @ 4@ X@ @ B @3*qo>(#.Td4pH@Ri9vӜy(@||c_ZpǦa-xn[&C.2&8#`KLDUqfb\19c($T +)qA Hc}tmrBn ZC`)|vzg;)e۴Lt`c⇫&z84ێ ]kƘ=&Xa+ WEC~ gyF KOkNJJ!@g!+wTLi?ogg\תރa+@doI +$z2@ Q`{/Y?uo:fMm֚]qa;3EgQ r" zڰ+E;مeYPC!5LJӾZj}44̼R+5P LQNkXw~ʭQݙ~3w[F# n*-$ͨFU=H0}U&8MA^aKceǚ.fQdO{u?T/nD/y>;I"24vJn G*z:?,Ϝb7jR߻Vs^9mE4^OLJLJ/ 簴 YnCy ll)+Hd\JFf"㮑WƟݻfтC2x?,P F9ass#*_Bnv ];"n?zkS>t56~zLCDBsO3 xۨrSi``P@{%:C;q^%0feR7'-UUڈ)gƾw}|iLea#|sМuAsx!Y2?dԌ9")/i2u8/{0 ^c'"y<sZ EZ,hԪZtr+d9,01s<:\%6j^~Bɓ3 'aB0 Cf̽s߄x  Eӗ%Zju,?xW]k#f#DJޛ7ߑ^}Q4]o%1]GIen{Wm rK`VlƄ{ʬwrmYXoSǿaId:NO3}yaV_v/ 5c뗃m:B6 Wv{ܙA!3:5߹U>' caZ@u)m;|6ghgr}'̞6{K-;}߮d+e.%iL{H_|wM*ǡf2qI6ߤ*VWſm_o9y\Id0"Kg7;uezmFfbr5OS~<ܔ+膍zl ˮoo^&E_ߒɻ]m޲#m]h!x0Ks8yo+ItbNeJK[!)n0{^apؒt 7-oGٔݻj's:k77]ŏF>V1´Eʂ\sw}bh%Qrg+? iwbZ[xLK/^1[^!l;x4[pnxVuld9d@XzMYQv;>4NH^NDD<{zTEml?YDDN%gN}_gi|,Gt+D8IsU+#[_ tykeg_|ngx][ < n\XLDDuꮎY}z[m,Ft_6> Hg6VTuek@1ɣ6o-$Ԣ/p7|QcM!XХwxMAی,~Ӗ]^ݸ>iU%iXRW#K}#?U5\d,x $F/Hyeɉr=_Ulblۈ"5i DD%܉WV~s2aUy g>tID^:P^sLdqTaE),<(…ruS1F9X"u~B]bڪ['Ze3۟_w@{NfϿ8%А>Rğ޿p\oL=ʐ!]qJ̑VNjh[=- --cُZ<.|=ͅD_>|ZTx~U; H~m: Lҕ[|[U(&< }gჅr&SkBz=b0t&7h0lU.?RFg |ԥ"YmIsh񺋅|l]q_K@WŲ)el+ASe8mҺw/|;z[pwD"{_gkyZ"f#~3m'4䃰+J9x뙂21w]xTkR׬ne)Cv..IDAT%):/@u=ڇF@TgQ1IxNF D^턇n]HWt$#'葻uG״d堚skT JJeG^ƨךn0ͨFU=H0}U&8MA^aKcewu0am N~1H~xj,MWn7ITq w^mZ>ⱦ] uwk 2d4ri6vawi; Ko멞˜JNŤ8t6ݾ`;<R9jW(u.I -ts)d+麌Ƹ1b2ϠY9u03bmҺ;Q کzG*׺rLHdb1.:̥R ̈*43>z>[Z8κ'[f{ЏN/Xk4D$;42Dd6aV~a%+tiQW|`zkAsNkYj>k7ܽד[ܯ!"knb$^]jYw.lB_/f?MĈ|z9'5ytC|ۢզ_d,x{wjکh]V+vT;S$szj eŗGƫu\~):SSUvmU{.<_ooz@[$5R xVyNm,2V+2*pmD`J]Њ9סdqܵ#b־c>4E`n– K, ntNC"q!5mUihEX2Q rأh2;xۊؒt'}y~3Z}&F!Cn⺈/}r錞=:uk]qMmKSV{͖͊tYEhwoY`oo Ow^aq1_W(iϰ{{ytڧTd`'*Z#/۸mo1umON _RYCY<+\PNu|f鸑O"UE.jx"ˮ!ei6_YϺ4Q'X]9v9^׫JF=jZ_H)O8zZ^L5utEWrKko:mO1ryYqD_[~N&*/lχ.Tkڲ/j""o۟Yg2dHWs}‹[=>)u/g"Yt+9s-n4wj,)^Hљvx٠<-󇷮]u[δ642 c?Pyݗ.dv'7l wکv[iX{/Kj- "m/il5츋q IɉN?Lbt\@ ¡ is"ʂ)S2BK7"1Nx+麌Ƹ1b2JD%hmqЦ_1:)T1i"%A?< }ѥۢ=h:OojǗGƫtu7_goz6YrᲭG3вl|i4]k]&/)S+m+ܯH]}lIۧ/9[3Y""7[ʊ8~7Hd,- M+4lFxd''_Td`'xwhi0IGvFx:-|JUHHwg e?8p̨iCV\yQu|.CFx#"8r -cVh!ɇxxsk !eBb! r^:s\!XXXtSa+59ir?;ai@xKҡY"9^nW5n߹ơ Q g+sSo^8rJo,;TDEY<0B̐^9b_U5K*XԘQ1bvˎAED[~N&*/lχ.T5“i`Ǣ5_UVWz?{6""uѕ_P L=2'݇ uߘILD :?!^!r|G25D$fd' *Ґz 2@]zeX,bHW[Ǝo3-£2#~%t{9ھ%g}d|㷝kSB'͵.bخζzI;ߎv'_DbQa G̚l}АJ8/ww,8e{J5pdД)E+hM@arp zĦQѫZq(EfewYqǠVyGn-SCDe]' o[pi*"O-\2yX+U!5Sޝ҆ _3 co(()K_""un?TDlcIO)KڱEW2 zshtA>!Z:)T1i"HhΎU܉4HH'<צ+麌Ƹ1b2J"+7lx/=[\g.vz쒭c>Yj-娬lY9u03bmʧj P!7b=&MI)qv ߿uO\#ሪ1;C:URY<]lLî,Tbh6TmU{ҕɼւ:bXyD1z\@a \i){=ܪgN,ظRVđȸE3,ͯa1Kw𶭼7)+u%yrZR=sT=QKlY応b ߴbȴq2P@(̖= .tjrS>0(N*/u脬UfNZϢ[O77ujwа ᑙO1wGɇ#{w߭UCKoGM{MͣS>}}-ըbqd+ӥU{Mݺ5fM3O^h1p.>ve2~ȨsDS_.¿YV2bT>#"7\N _RYCY<+\}58n )8"Ҕ71)~zU葽GM) S^klT#fߴY:ndӃxUu:61p\nW@ Zxxsk Z6rL!IX Ɓf̽ss22@s1-H$D#ˏ^ojjZRRԤUe2whw@,`D{)\:=Zz24*"bacDRo]!cl] ^C<(f # XRY,/f=Ώ cY<:u Abp; yt ^                     QWIENDB`goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/index.rst000066400000000000000000000053461337147241200233440ustar00rootroot00000000000000.. title:: Goodvibes - A Lightweight Radio Player for GNU/Linux .. toctree:: :maxdepth: 1 :hidden: :titlesonly: screenshots downloads user-guide changelog credits .. image:: images/goodvibes.png :align: center **Goodvibes** is an internet radio player for GNU/Linux. It aims to be light, simple, straightforward. Features -------- The main interface is quite stripped-down, but there's a couple settings and features waiting for those who dare to open the *Preferences* window! * **multimedia keys** support - ie. the ````, ```` and ```` keys that are present on most keyboards. * **notifications** - ie. information that pops up when the song change. * **do not suspend** - ie. prevent the system from suspending while a radio is playing. * **autoplay** - ie. start playing the last radio when you start the application. * **MPRIS2** support - ie. integration with modern desktops. And for those of you who live mainly in a terminal and avoid graphical user interfaces, you should know that you can build Goodvibes without GUI, and control it via the command-line client provided. Under the hood -------------- Goodvibes is an open-source software, released under the `GPLv3 `_. It relies on various open-source libraries. The core building blocks are provided by `GLib `_, the HTTP bits are handled by `LibSoup `_, the audio part is left to `GStreamer `_, and the graphical user interface is written with `GTK+ `_. **Development** takes place on `GitLab `_, and this is where you should head if you want to get in touch. El Boulangero is the main author, and is the one writing these lines right now. **Translation** happens on `Weblate `_. If you want to contribute to the translation effort, don't be shy, `get involved `_! **Documentation** is hosted by `Read the Docs `_ on this very page (you're already there). **Artwork** is made by Hector Lahminèwskï. Feel free to visit his homepage `Lahminewski Lab `_ and get in touch. Similar Projects ---------------- * `GRadio `_ is a GTK3 app for finding and listening to internet radio stations. * `Pithos `_ is a native Pandora Radio client for Linux. * `RadioTray `_ (unmaintained) is an online radio streaming player that runs on a Linux system tray. goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/screenshots.rst000066400000000000000000000031521337147241200245660ustar00rootroot00000000000000Screenshots =========== Modern Desktops --------------- Nowadays, desktop environments usually provide a media player applet, integrated somewhere in the desktop user interface. This applet provides basic control over media players that implement the `MPRIS2 `_ specification. These screenshots show both Goodvibes and the media player applet from different desktop environments. .. figure:: images/screenshot-2017-01-gnome.png :scale: 100% :align: center **GNOME** (Ubuntu 16.04 LTS) with the Media Player Indicator extension .. figure:: images/screenshot-2017-01-ubuntu.png :scale: 100% :align: center **Unity** (Ubuntu 16.04 LTS) .. figure:: images/screenshot-2017-01-pantheon.png :scale: 100% :align: center **Pantheon** (Elementary OS 0.4) Old-style Desktops ------------------ It's possible to launch Goodvibes with the ``--status-icon`` option. In this mode, Goodvibes will not display a main window, but will instead add an icon to your notification area (also called system tray). This is a kind of legacy feature, and it will be removed from Goodvibes at some point in the future. This is not supported by the toolkit (GTK+) anymore, and this is also disappearing from various desktop environments (GNOME and Ubuntu at least). Anyway, here's how Goodvibes looks like in *Status Icon* mode. .. figure:: images/screenshot-2017-01-mate.png :scale: 100% :align: center **MATE** (Linux Mint 18.1) .. figure:: images/screenshot-2017-01-openbox-tint2.png :scale: 100% :align: center **Openbox/Tint2** (Debian Stretch) goodvibes-v0.4.2/docs/goodvibes.readthedocs.io/user-guide.rst000066400000000000000000000144661337147241200243110ustar00rootroot00000000000000User Guide ========== .. highlight:: none This user guide is still a work in progress, and for now it's just about documenting advanced tips and tricks. If you're interested in helping with the documentation, please get in touch. Keyboard Shortcuts ------------------ Here are the shortcuts that you can use when the main menu has the focus: * ```` to start/stop the playback. * ```` to minimize the mindow. Additionally, if you click on the menus, you will see that the more common actions are associated with a shortcut. At last, don't forget that you can enable the multimedia keys. The setting can be found under *Preferences -> Controls -> Keyboard*, and it is disabled by default. You should know that multimedia keys are a global shortcut, and can be claimed by only one application at a time: first come, first served. GStreamer Output Pipeline ------------------------- This setting can be configured under *Preferences -> Misc -> Playback*. It's quite a powerful thing, but not for the faint of heart. If you want to experiment with that, you should install the `GStreamer Tools `_, run a quick ``man gst-launch-1.0`` to warm you up, and then start experimenting with ``gst-launch-1.0``. Here are a few examples of what you can achieve with that. Use ALSA for the audio output, and use the second soundcard ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Command-line:: gst-launch-1.0 \ souphttpsrc location='http://direct.fipradio.fr/live/fip-midfi.mp3' ! \ decodebin ! \ alsasink device=hw:2 Goodvibes setting:: alsasink device=hw:2 See `GstAlsaSink `_ for details. Delay the audio output ^^^^^^^^^^^^^^^^^^^^^^ Command-line:: gst-launch-1.0 \ souphttpsrc location='http://direct.fipradio.fr/live/fip-midfi.mp3' ! \ decodebin ! \ autoaudiosink ts-offset=5000000000 Goodvibes setting:: autoaudiosink ts-offset=5000000000 See `GstAutoAudioSink ts-offset `_ for details. Command-Line And Scripting -------------------------- Goodvibes can be used and abused with the command-line. The command ``goodvibes-client`` gets you started, and provides basic control over Goodvibes. The main purpose of this client is actually to ease development and debug, and that's why it's a bit rough on the edge, and doesn't offer everything that is provided by the user interface. But it's still good enough for people who prefer the command-line over a graphical ui. However, if you want to write some scripts that interact with Goodvibes, I recommend **NOT TO** use the client, which might be modified anytime, and doesn't promise any backward compatibility. Instead, you should make good use of the D-Bus interfaces implemented by Goodvibes. Goodvibes implements and exposes two interfaces on D-Bus (assuming you didn't disable it in the preferences): * the native D-Bus interface is actually the one used by ``goodvibes-client`` to communicate with Goodvibes. This interface might be modified anytime a new version is released, there's no plan to maintain any compatibility over different versions. * the MPRIS2 D-Bus interface is an interface implemented by many media players on GNU/Linux. To get you started with D-Bus, you should know that there are different dbus command-line tools around: * ``dbus-send`` is the most basic tool, provided with the dbus daemon. * ``gdbus`` is provided by GLib. * ``qdbus`` is provided by Qt. * ``busctl`` is provided by Systemd. If you plan to write scripts that interact with Goodvibes, I recommend to use the MPRIS2 interface as much as possible, mainly for two reasons: * It's well defined, stable over time, therefore your script will not break when a new version of Goodvibes is released. * You will be able to re-use your work with any other media player that support the MPRIS2 interface. And there's a lot of them. You should refer to the `MPRIS2 Specification `_ for more details. A quick example with ``qdbus``:: qdbus org.mpris.MediaPlayer2.Goodvibes \ /org/mpris/MediaPlayer2 \ org.mpris.MediaPlayer2.Player.Metadata Conky Example ------------- Here's a snippet to integrate Goodvibes in `Conky `_:: ${if_match "${execi 10 qdbus org.mpris.MediaPlayer2.Goodvibes /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlaybackStatus}" == "Playing"}\ Goodvibes Info Station:${execpi 10 qdbus org.mpris.MediaPlayer2.Goodvibes /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Metadata | grep "^goodvibes:station:" | cut -d':' -f3-} Artist - Track:${execpi 10 qdbus org.mpris.MediaPlayer2.Goodvibes /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Metadata | grep "^xesam:title:" | cut -d':' -f3-}\ ${else}\ Goodvibes is not playing\ ${endif} There are a few things to notice here: * **Don't use the native interface** ``io.gitlab.Goodvibes``, use the MPRIS2 interface instead, ie. ``org.mpris.MediaPlayer2.Goodvibes``. The reason being that when you query Goodvibes on its native interface, it will automatically be launched by D-Bus in case it's not running. * If for some reason you really need to use the native interface, then use a D-Bus command-line tool that allows you to disable auto-starting the service. As far as I know, you can only do that with ``busctl --user --auto-start=no``. One last word: I'm not a Conky guru, don't quote me on that snippet above ;) Custom User-Agent ----------------- If for some reasons you need to use a different user-agent for a specific radio station, it's possible. You just need to edit the file where the stations are stored (usually ``~/.config/goodvibes/stations``), and add a field ```` to the station you want to customize. For example:: http://example.com/radio Example Radio Custom/1.0 goodvibes-v0.4.2/docs/notes/000077500000000000000000000000001337147241200157505ustar00rootroot00000000000000goodvibes-v0.4.2/docs/notes/bookmarks.md000066400000000000000000000015641337147241200202700ustar00rootroot00000000000000Bookmarks ========= GLib ---- - [First steps with GSettings](https://blog.gtk.org/2017/05/01/first-steps-with-gsettings/) GTK+ ---- - [GTK+ Development Blog](https://blog.gtk.org/) - [Those “realize” & “map” widget signals](https://blogs.gnome.org/jnelson/2010/10/13/those-realize-map-widget-signals/) - [Another Container for Gtk+ 3.12 | GtkActionBar](http://worldofgnome.org/another-container-for-gtk-3-12-gtkactionbar/) - Composite Widgets - [Announcing Composite Widget Templates](https://blogs.gnome.org/tvb/2013/04/09/announcing-composite-widget-templates/) - [Embeding GtkBuilder UI definitions into GObject classes!](https://blogs.gnome.org/xjuan/2012/07/13/embeding-gtkbuilder-ui-definitions-into-gobject-classes/) - [GTK+: Aligning / Justification in text widgets](https://www.murrayc.com/permalink/2015/03/02/gtk-aligning-justification-in-text-widgets/) goodvibes-v0.4.2/docs/notes/gobject-lessons-learned.md000066400000000000000000000260341337147241200230100ustar00rootroot00000000000000GObject lessons learnt ====================== Just sharing the long journey of learning GObject. How Do I Learn GObject? ----------------------- First, you need to bookmark the official documentation. It's the best place to start, but also a good place to go back afterward. Things that don't make sense at the first reading will become clear as you go further. - [GObject Reference Manual](https://developer.gnome.org/gobject/stable/) Second, there's one essential article that nicely sums up the GObject construction process, and raise some important points around it. This is the most important blog post you'll come accross, read it again and again. - [A gentle introduction to gobject construction](https://blogs.gnome.org/desrt/2012/02/26/a-gentle-introduction-to-gobject-construction/) At last, you need some good examples. You will find it first within the GObject library itself, but you will need more. I recommend the main GObject based libraries, and applications where development is very active. You will find sometimes that the code is very up-to-date with the latest best practices, so it's good to look in different codes. Warm up your git, sharpen your grep, and there you go! - [GLib](https://git.gnome.org/glib) - [GTK+](https://git.gnome.org/gtk+) - [GStreamer](https://github.com/GStreamer) - [Rhythmbox](https://git.gnome.org/rhythmbox) - [Totem](https://git.gnome.org/totem) Good Practices -------------- #### Follow The Conventions The first place to learn about good practices is the GObject conventions page. It's more than good practice actually. GObject expects you to follow a few conventions, so that code generation tools (`glib-mkenum`) and other stuff can actually work. Don't try to avoid that. - [GObject Conventions](https://developer.gnome.org/gobject/stable/gtype-conventions.html) #### Use `G_DECLARE_*` And `G_DEFINE_*` Do not clutter the header file with too much boilerplate, as it's often seen in some old GObject-based code. - [G_DECLARE_FINAL_DERIVABLE_TYPE](https://blogs.gnome.org/desrt/2015/01/27/g_declare_finalderivable_type/) #### Do The Minimum In `_init()` There should be almost nothing in the `init()` function. Most of the time, it just boils down to setting up the private pointer, if any. For all the rest, it's better to implement it in the `constructed()` method. - [A gentle introduction to gobject construction](https://blogs.gnome.org/desrt/2012/02/26/a-gentle-introduction-to-gobject-construction/) #### Always Use A Private Pointer You will notice that in here, I never put anything in the object structure. I ALWAYS use a private structure. Using a private structure is particularly useful for derivable objects, since for these objects, the object structure *must be* defined in the header (public) to allow inheritance. And of course, you want to keep your private data private, so it makes sense to define it within the implementation, therefore using a private structure. But when the object is not derivable, and therefore the object structure *should be* defined in the implementation, what's the point of having a private struct? 1. Consistency - you want all your objects to look the same. 2. Code Refactoring - when your objects change from final to derivable, or the other way around, you'll be happy that you don't have to constantly move your data from the object struct to the priv struct. - [Changing quickly between a final and derivable GObject class](https://blogs.gnome.org/swilmet/2015/10/10/changing-quickly-between-a-final-and-derivable-gobject-class/) Also, I *always* use a pointer named `priv` to access it. It's just a convention, but a very common one, and doing it everywhere makes the whole code consistent. HOWEVER, I've read somewhere that I shouldn't! But I didn't have time to dig into that yet. #### Best Way To Set Properties The correct way to set an object pointer. It's not as trivial as it seems. - [Use g_set_object() to simplify (and safetyify) GObject property setters](https://tecnocode.co.uk/2014/12/19/use-g_set_object-to-simplify-and-safetyify-gobject-property-setters/) Quick FAQ --------- #### How To Change A Construct-Only Property Via Inheritance This questions is asked on StackOverflow: - Unfortunately, the answer given by Emmanuele Bassi is wrong: setting the property in the `constructed()` virtual method won't work. Now that I've been dealing with GObject for a while, I see 3 ways to do that: - hack around with the `constructor()` method, as suggested in . - don't use construct-only properties, use construct properties. Then you can modify it in `constructed()`. - set the desired values within a wrapper, ie. `my_inherated_object_new()`. If you only create your objects with these wrappers, it's fine. However that is not binding friendly. I personally settled for the third solution within Goodvibes. #### How To Implement A Singleton This is discussed here and there on the Net. - [GObject API Reference](https://developer.gnome.org/gobject/unstable/gobject-The-Base-Object-Type.html#GObjectClass) - [How to make a GObject singleton](https://blogs.gnome.org/xclaesse/2010/02/11/how-to-make-a-gobject-singleton/) #### How To Have Protected Fields GObject doesn't provide any solution to have protected fields. I managed to do without up to now, but just in case, here's a workaround. Basically, it's just about defining the private structure in the header (therefore making it public), and naming it 'protected' instead of 'private'. - [GObject and protected fields – simple hack](http://codica.pl/2008/12/21/gobject-and-protected-fields-simple-hack/) #### Any Design Patterns Out There? For design pattern examples, the best is to hunt for it directly in the source code of some big projects. Additionally, here are some discussions about it. - - [GObject design pattern: attached class extension](https://blogs.gnome.org/swilmet/2017/08/09/gobject-design-pattern-attached-class-extension/) Been there, done that --------------------- #### Should I Set A Property Asynchronously? Sometimes, setting a property is just about setting a variable. But at other times, it might trigger quite a bit of code execution. For example, at some point I had an `enable` property, and when I set it to true, it really enables the whole object, and possibly a lot of things happens. For some technical reasons, I had to actually delay the execution of the enable/disable code, with `g_idle_add()`. And since I wanted the value of the `enable` property to really represent the state (enabled or disabled), I decided to change the value of this property only *after* this asynchronous code was executed. Ie, only when the `g_idle_add` callback runs. Only then the `notify` signal was sent. This seems OK at a first glance. But in practice, there's a problem. The code that sets the property may not want to receive the notify signal, and therefore use `g_signal_handler_block()` before setting the property, and `g_signal_handler_unblock()` after setting it. If I set the property asynchronously, and therefore send the notify signal afterwards in a callback, the code that sets the property will receive the notify signal, even if it tried to block it. So it creates a bit of an unexpected behavior for the code setting the property. So... Should I set a property asynchronously? The answer is NO. For this particular problem of the `enable` property, I solved it by having two properties: - `enabled`, readwrite, which reflects how the object is *configured*. Therefore this property is set synchronously. - `state`, readable only, which gives the current state of the object, and go from *disabled* to *enabling* to *enabled*. This property is tied to `enabled`, but its value changes aynchronously. With two properties, one can connect to the `enabled` property to know how the object is configured, and one can connect to the `state` property to know the real object state. #### Should I Define A Base Class For My Object Hierarchy? For example, GStreamer has its own `GstObject`. And at some point, I thought it was cool, and furthermore it seemed that it would solve some of the problems I faced. So I defined my own `GvObject` for a while. I then removed it quickly enough. Here's my feedback from that experience. First questions that arises: should all my objects inherit from GvObject? Or only those who have a solid reason for that? At first, I went for the latter, because I thought that if an object has no reason to inherit GvObject, then it should remain as lightweight and simple as possible, and inherit GObject. So I ended up with a mix of objects that were either GObject derived, either GvObject derived. It's a bit of a mess suddenly, because the separation between both, and the reasons for choosing the right parent, was all very unclear. Then, serialization kicked in, I wanted to be able to serialize objects to save it as configuration (I didn't use GSettings by the time). And suddenly I had another dilemna. If the serialization API targets GvObject, then GObject derived objects were left out, and couldn't be serialized. On the other hand, if the API targeted GObject, I couldn't take advantage of some of the specific features GvObject, that were actually very helpful for serializing. Which made me change my mind: if I define a GvObject base-class, then I should embrace it completely, and have every object inherit it. It looked nice and consistent now, but I just added an additional layer, I just made things a bit more complicated, and I was wondering if it was worth it. But still, I was happy because now the serialization was easier to implement, since I embedded some convenient stuff for it within my base class GvObject. But then, another problem kicked in. The UI part is implemented by inheriting GTK widgets. How will I serialize this GObject-derived objects, now that the serialization API targets the GvObject type? My only solution here was to rewrite the UI, use composition instead of inheritance, so that my UI objects could be derived from GvObject and be serializable. Which promised to be quite some work of moving code around... I didn't do that though. Instead, I came back to GObject for the base class. For all the problems that I solved with GvObject, I thought again and solved them differently, and in a better way. After all that, I draw some conclusions for myself: - Think twice before defining a base class for your object hierarchy. It has its good points, for sure, but also its drawbacks. - If you work with GObject-based libraries, be aware that the base class you define can't help for objects from those libraries, or that you inherit from those libraries. Your lower denominator for the whole set of classes you deal with will always be GObject. So, does you base type really helps, or does it get in the way? - Ask yourself if you can't solve your problem with an interface instead. Interface are more flexible. Even if you use objects from other libraries, you can still inherit them, and implement an interface in the child. goodvibes-v0.4.2/docs/notes/implementation-details-core.md000066400000000000000000000251661337147241200237020ustar00rootroot00000000000000Implementation Decisions - Core =============================== Some decisions I made early on regarding the implementation. I write it down here just for my own remembering. Why Implementing The MPRIS2 D-Bus API First? -------------------------------------------- Implementing the MPRIS2 D-Bus API was one of the first thing I did. It was not the easiest thing to start with though, but it forced me to implement all the common features expected from a music player. Once MPRIS2 was implemented, I knew I had the right design for the core, and indeed, I never had to rework it afterward. So I really recommend that to anyone writing a music player: implement the MPRIS2 D-Bus API early, it will help you to get the design right, you won't regret the pain. Why Not Choosing Python? ------------------------ I definitely wanted to implement the MPRIS2 D-Bus API. Which means implementing a D-Bus server. Implementing a fully featured D-Bus server in Python is a bit of a frustrating experience. Properties are not fully handled. See: - The quick answer, as I understood it, is that the D-Bus python binding is broken by design, and there will be no attempt to fix it. This bug will stay opened forever. If you have a look at QuodLibet code, you will see that these guys had to write quite a bit of code to add support for properties. But they did it well, and the code is easy to take out of QuodLibet. So my guess is that the best way to fully implement the MPRIS2 D-Bus API is to copy/paste some code from QL. It's suggested here and there to use another Python D-Bus implementation, like GLib's implementation GDBus, through PyGObject. This is perfect if you're a D-Bus client, however if you want to implement a D-Bus daemon? Apparently, at the time of this writing, this is yet to be implemented. See progress here: - So, with this situation, I decided to write in C instead, as I was sure that I would be able to implement the MPRIS2 D-Bus API properly, without having to hack my way around broken things. Note that it's probably not an exhaustive view of the situation, and I might have overlooked other solutions. Another reason is that Goodvibes is a minimalistic app doing very little, and it's pleasant to see it start in a fingersnap. With Python you don't quite get that, especially if you're the first Python app being launched and there's nothing cached yet. On the other hand, there was a BIG reason to choose Python rather than C: development time. Writing in Python is so much faster, you have no idea until you have the chance to maintain both a C and a Python desktop app. Why Choosing GObject? --------------------- GObject has its cons, that I was very aware of when I started to learn it. As often when I face a new technology, I was a bit reluctant and only saw the downside: - Boilerplate: there so much copy/paste to do that I ended up writing a script to automate C/H file creation. - Magic: some of the boilerplate is hidden by macros. It's great since it saves code typing, but it's bad since it makes some definitions implicit. So you end up calling functions or macros that are defined nowhere, and it's a bit unsettling at first. GObject code looks like dark magic for the newcomer. - Steep learning curve: yep, learning GObject by yourself can be a bit tedious. On the other hand, GObject brings in everything I needed to implement cleanly this MPRIS2 D-Bus API: - signals: from the moment I have signals internally, there's no need to hack around to send signals on D-Bus. It's trivial. - properties: same thing. From the moment the code is GObject-oriented, getting and setting properties through D-Bus is trivial. Plus, GObject allows you to write quite clean object-oriented code in C. The memory management through reference counting is nice and clean. And the more you go on working with GObject libraries (GIO, GTK+, GStreamer), the more you realise that your code benefits from being GObject oriented. You can use high-level functions, bind your objects here and there, and a lot of problems are solved by the libraries you use. But these libraries expect GObjects, so if your code is not written as such, you might be left out and bitter, rewriting stuff that is already implemented in libraries. So that's maybe the most important: if your application lives in a GObject ecosystem, make it GObject, and it will fit in nicely, good surprises will abound all along. But be aware that GObject is much more than a library. It really changes the architecture of your application, which basically becomes event-driven. Which is exactly what you want when you're writting a GUI application. Why Not Using Gdbus-Codegen? ---------------------------- First, I'm not a big fan of code generation. Second, as far as I remember, the generated code produces some warnings at compile time, and I usually compile with the flag `-WError`. It's annoying. Third, there's a bug about `Property.EmitsChangedSignal`, that has been pending forever, and in all likelihood will never be fixed. - This bug affects the MPRIS2 D-Bus API, so I prefer to stay away from that `gdbus-codegen` and remain in control of the code. Why Using Explicit Notify Only? ------------------------------- This is just an convention in use in the code, I just find it more logical when I started, and I never had to come back on this choice. For this reason, every property is created with the `G_PARAM_EXPLICIT_NOTIFY` flag, and emitted only if the value of the property changed. It's a bit more job on the object where the properties are changed, but it's less job on the object listening for signals, since they can be sure that they have to do something when the signal handler is invoked. Why Having This Feature Thing? ------------------------------ You will see in the code that some features are quite isolated from the rest of the code. First, there's a `GvFeature` object, that is the parent of all features. Then, features just need to inherit this object, connect to some signals from the core, and that's it. Isolating features is simply a way to keeps things cleanly separated, and to allow to disable some code at compile time, without having to cripple the code with `#ifdef`. It's possible thanks to the fact that features just react to signals. The core of the code itself is blissfully ignorant of the existence of these features, and never explictly invokes any function from there. Features are created once at init, destroyed once at cleanup, and that's the only place where you will see `#ifdef`. They live in a world of their own. Such a strong isolation has some benefits: easier debug and maintenance, mainly. And it's nice to be able to disable it at compile-time, if ever people want to integrate the application in an environment where some of these features don't make sense (like on embedded devices). It also forces a better overall design. However, it can be a pain in the ass when a new feature shows up, and doesn't quite fit in, and the whole thing needs to be reworked, and all the existing features need to be updated to match the changes... Ultimately, with some improvements, these features could be loaded dynamically, and from this point they could be called plugins. And then, it's very common to have plugins in GNU/Linux media player, right? So, these features can be seen as a poor's man plugin system. Station List: Shuffle Implementation ------------------------------------ Things I've tried and didn't work. One idea was that the player would have two StationList objects. One would be ordered, the other would be shuffled. With that, the 'shuffle problem' is taken out of the StationList object (which is therefore esier to implement) and is then solved outside of it. Mainly in the player, I thought. In practice, it adds quite a lot of complexity, because these two lists must be kept in sync (from a more genereal point of view, from the moment you duplicate data, you need to keep it in sync, and that's why duplicating data is always something to avoid). Furthermore, the station list is a global variable, that can be accessed by anyone. How to handle duplication then? We could have both list (ordered and shuffled) global. But it would be a mess for the rest of the code. Always wondering which one you must deal with, and what if you want to add a new station? So the code is responsible for adding it to both list? And therefore, the code is responsible for keeping lists in sync? Of course not, the sync between both lists must be automatic, but if both are global, it means that they both must watch each other's signals to keep in sync. Or another object must watch both of them and keep them in sync. This doesn't sound sweet to my ears. Another possibility is to have only the ordered station list public, and the shuffled one private to the player, and then the player would be left with the responsability to keep the shuffled list in sync, somehow. Hmmm, I don't like that too much either. OK, so let's forget about that. If the shuffle problem is not to be solved outside of the list, then let's solve it within the list. We could have a 'shuffle' property added to the station list, and that's all. The code set it to true or false, and doesn't bother anymore. It kind of works, but the StationList API becomes confusing. Because if you look at it as it is, you will notice that some functions are expected to support the shuffle property (obviously, `next` and `prev` will return a different value depending on the shuffle property), while other actually don't care about the shuffle property (`append`, `prepend` only deal with the ordered list). So, completely hiding the 'shuffle' from the outside world is also not a very good solutions. At some point, when you use the API, you start to find it confusing. Even if you set the shuffle property, some functions keep on dealing with an ordered list internally. It's magic, and we don't like magic. Actually, from the user point of view, if you enable shuffle, the list still appears ordered in the ui. It just affects the previous and next actions. And the best is to see this behavior appearing in the API. So in the end, the best implementation of the shuffle I came with is a 50/50: both the Player object and the StationList object know about it, and both do their job handling it. It appears explicitely in the StationList API, and only makes sense for `next` and `prev`. The Player is the 'owner' of the shuffle setting, and feeds it to StationList when calling methods that require it. In the end, this implementation is the most simple, and leaves little place for magic. I've been happy with it so far. goodvibes-v0.4.2/docs/notes/implementation-details-ui.md000066400000000000000000000102341337147241200233550ustar00rootroot00000000000000Implementation Decisions - UI ============================= Some decisions I made early on regarding the implementation. I write it down here just for my own remembering. How much margin and spacing for the UI? --------------------------------------- When designing the UI in Glade, there's no margins by default, so the whole thing looks ugly at this point. Margins, spacings, paddings and such must be set, and it's up to me to decide how much. So I wonder: how much? I have no idea, and don't especially care, so I let someone else decide for me. I just followed the guidelines provided by GNOME. - [GNOME HUman Interface Guidelines](https://developer.gnome.org/hig/stable/) Why not using GtkDialog to handle preferences? ---------------------------------------------- Mainly, it's because a dialog requires a transient parent, aka another window that must be the parent. Ever had this warning in your console? Gtk-Message: GtkDialog mapped without a transient parent. This is discouraged. But Goodvibes might be running in `--status-icon` mode, and in such case it doesn't really have a main window to speak of. Loosely speaking, we could use the popup window as the transient parent, since it always exists. It's hidden most of the time, but it exists. But it looks a bit hacky to me. I don't really know why GTK+ needs a transient parent, and what it expects from this parent, but I'm almost sure the popup window is not up to the expectation. So in the end, I find it cleaner to use a window rather than a dialog. Should I use the GObject property details in the UI? ---------------------------------------------------- Short answer: No! It might be tempting though, simply to avoid duplicating strings. Indeed, in the preferences dialog, often a setting is mapped to an object's property. Given that a property already has a nick (that could be mapped to the setting label) and a blurb (ie. a description that could be mapped to the setting tooltip), why should we define these strings again in the glade file? Why don't we just set labels and tooltips from the property nick and blurb in the C code? It's tempting. Even more after reading this piece of the doc: - However, I have good reasons not to do so, both for the nicks and blurbs (although in the following, I'll only talk about nicks, for clarity). First, there's the context. Let's suppose you have a property that decides what action should be associated with a middle-click. In your code, an ideal nick for the property would be "Mouse Middle Click". However, assuming that in the UI you have a "Mouse" section for the settings related to the mouse, then you don't want to repeat the word "Mouse". The ideal name for the setting would then be "Middle Click". So, because of a different context, the best name for a property in the code, and the best name for the corresponding setting in the UI, might be different. Second, there's more problems when translation kicks in. Because then, the property nicks must be translatable, and if I want to be consistent, then **ALL** the properties should have a translatable nick. It means that those properties which don't appear in the UI will be translated for nothing. Useless work for translators, isnt't it? Or should I only translates the nicks that appear in the UI? But then how do I maintain this mess? What will happen, in the end, is that I'll have nicks marked as translatable, but don't appear in the UI, while others appear in the UI but are not marked as translatable... The REAL problem, here, is that there will be no warnings for these missing translations. So there will always be missing translations in each new release. So, here are my two main issues: context, and translations. In the end, my choice is to use nicks and blurbs only as hints for developpers passing by. For the UI, I'll define everything in Glade, or in the related C file. It solves the context issue, and also the translation issue, since Glade automatically mark strings as translatable, so I don't have to bother. More interesting reading: - goodvibes-v0.4.2/docs/notes/under-the-hood.md000066400000000000000000000037021337147241200211160ustar00rootroot00000000000000Under the hood ============== Features -------- #### Notifications Notification support is provided by GApplication. Under the hood, two backends exist out there: `org.freedesktop.Notifications` and `org.gtk.Notifications`. Glib gives preference to the Gtk backend in case both are available. As of 2018-11, it seems that notifications in Flatpak don't work, using the Freedesktop runtime 18.08. #### Multimedia Keybinding Currently implemented with libkeybinder, which works at the X11 level to take global keybindings. Obviously this won't do with Wayland. Another possibility is to use D-Bus: `org.gnome.SettingsDaemon.MediaKeys`, but this is GNOME-specific as you can see (I think there's also a Mate-specific `org.mate.SettingsDaemon` D-Bus service). There's a discussion at But the truth is: keybinding is actually not needed if the desktop already has some kind of MPRIS2 support, and also grabs the multimedia keys by default. Which is the case for GNOME and most modern desktops I suspect. In this case, when a multimedia key is pressed, it's intercepted by GNOME, which forwards that to the current MPRIS2 player active. So the multimedia keybinding feature in Goodvibes is mostly useful for stripped down desktop environments with no MPRIS2 support. And in this case it's useless to try the GNOME daemon, so we're just fine implementing X11 keybinding only. #### Sleep inhibition Gtk+ provides everything through GtkApplication, however when I came accross it, I had already implemented my stuff through an internal lib "libcaphe". So, well... Looking into details, I saw that Gtk+ implementation doesn't try to inhibit with `org.freedesktop.PowerManagement` (I think it's been deprecated for a while). However at the time I was using a fairly old Openbox system, which had only this service running to provide sleep inhibition. So I kept my own implementation in libcaphe, and I don't use Gtk+ sleep inhibition. goodvibes-v0.4.2/goodvibes-launcher.sh000077500000000000000000000017441337147241200200150ustar00rootroot00000000000000#!/bin/bash # The launcher is for people who want to run Goodvibes from the source tree, # without installing it. GLib and GTK+ need to access some shared resources, # so we need to tell them to look for it in-tree. fail() { echo >&2 "$@"; exit 1; } BUILDDIR=${BUILDDIR:-build} GOODVIBES=$BUILDDIR/src/goodvibes [ -d $BUILDDIR ] || fail "Build directory '$BUILDDIR' does not exit" [ -x $GOODVIBES ] || fail "Executable '$GOODVIBES' not found" # XDG_DATA_DIRS is needed for: # - GTK+ to load local icons from '$BUILDDIR/data/icons' [ "$XDG_DATA_DIRS" ] \ && XDG_DATA_DIRS="$BUILDDIR/data:$XDG_DATA_DIRS" \ || XDG_DATA_DIRS="$BUILDDIR/data:/usr/local/share:/usr/share" export XDG_DATA_DIRS # GSCHEMA_SETTINGS_DIR is needed for: # - GLib to load local settings schemas from '$BUILDDIR/data' # If you don't define it, you'll be hit by something like that: # [GLib-GIO] Settings schema '...Goodvibes.Core' is not installed export GSETTINGS_SCHEMA_DIR="$BUILDDIR/data/" $GOODVIBES $@ goodvibes-v0.4.2/meson.build000066400000000000000000000064431337147241200160410ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later project('goodvibes', 'c', version: '0.4.2', license: 'GPLv3+', default_options: [ 'buildtype=debugoptimized', 'c_args=-Wshadow', 'c_std=gnu99', 'warning_level=2', # -Wextra ] ) # Paths prefix = get_option('prefix') assert(prefix.startswith('/'), 'Prefix is not absolute: "@0@"'.format(prefix)) bindir = join_paths(prefix, get_option('bindir')) datadir = join_paths(prefix, get_option('datadir')) localedir = join_paths(prefix, get_option('localedir')) mandir = join_paths(prefix, get_option('mandir')) # Modules gnome = import('gnome') i18n = import('i18n') # Project settings gv_binary = meson.project_name() gv_version = meson.project_version() gv_name_lowercase = 'goodvibes' gv_name_camelcase = 'Goodvibes' gv_name_uppercase = 'GOODVIBES' gv_application_id = 'io.gitlab.Goodvibes' gv_application_path = '/io/gitlab/Goodvibes' gv_icon_name = gv_application_id gv_homepage = 'https://gitlab.com/goodvibes/goodvibes' gv_copyright = 'Copyright (C) 2015-2018' gv_author_name = 'Arnaud Rebillout' gv_author_email = 'elboulangero@gmail.com' # Goodvibes Core cc = meson.get_compiler('c') math_dep = cc.find_library('m') glib_req = '>= 2.44' glib_dep = dependency('glib-2.0', version: glib_req) gobject_dep = dependency('gobject-2.0', version: glib_req) gio_dep = dependency('gio-2.0', version: glib_req) gio_unix_dep = dependency('gio-unix-2.0', version: glib_req) gst_req = '>= 1.4.4' gst_dep = dependency('gstreamer-1.0', version: gst_req) gst_base_dep = dependency('gstreamer-base-1.0', version: gst_req) gst_audio_dep = dependency('gstreamer-audio-1.0', version: gst_req) libsoup_req = '>= 2.42' libsoup_dep = dependency('libsoup-2.4', version: libsoup_req) gv_feat_console_output = get_option('feat-console-output') gv_feat_dbus_server = get_option('feat-dbus-server') gv_feat_inhibitor = get_option('feat-inhibitor') # Goodvibes UI gv_ui_enabled = get_option('ui-enabled') if gv_ui_enabled gv_feat_hotkeys = get_option('feat-hotkeys') gv_feat_notifications = get_option('feat-notifications') else gv_feat_hotkeys = false gv_feat_notifications = false endif if gv_ui_enabled gtk_req = '>= 3.12.0' gtk_dep = dependency('gtk+-3.0', version: gtk_req) endif if gv_feat_hotkeys keybinder_req = '>= 0.3' keybinder_dep = dependency('keybinder-3.0', version: keybinder_req) endif # Subdirectories subdir('data') subdir('src') subdir('po') # Post-install meson.add_install_script('scripts/meson/post-install', datadir) # Summary summary = [ '', '--------------------------------', '', ' @0@ - @1@'.format(meson.project_name(), meson.project_version()), '', ' Core', ' Console output: @0@'.format(gv_feat_console_output), ' D-Bus server : @0@'.format(gv_feat_dbus_server), ' Inhibitor : @0@'.format(gv_feat_inhibitor), '', ' UI', ' Enabled : @0@'.format(gv_ui_enabled), ' Hotkeys : @0@'.format(gv_feat_hotkeys), ' Notifications : @0@'.format(gv_feat_notifications), '', '--------------------------------', '', ] message('\n'.join(summary)) # Install the git pre-commit hook git_hook = run_command(join_paths(meson.source_root(), 'scripts/install-git-hook.sh')) if git_hook.returncode() == 0 message(git_hook.stdout().strip()) endif goodvibes-v0.4.2/meson_options.txt000066400000000000000000000014141337147241200173250ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later option('ui-enabled', type: 'boolean', value: true, description: 'Enable the graphical user interface (depends on GTK)') option('feat-console-output', type: 'boolean', value: true, description: 'Enable the console-output feature') option('feat-dbus-server', type: 'boolean', value: true, description: 'Enable the dbus-server feature') option('feat-inhibitor', type: 'boolean', value: true, description: 'Enable the inhibitor feature') option('feat-hotkeys', type: 'boolean', value: true, description: 'Enable the hotkeys feature (requires ui, depends on keybinder)') option('feat-notifications', type: 'boolean', value: true, description: 'Enable the notifications feature (requires ui)') goodvibes-v0.4.2/po/000077500000000000000000000000001337147241200143065ustar00rootroot00000000000000goodvibes-v0.4.2/po/LINGUAS000066400000000000000000000000331337147241200153270ustar00rootroot00000000000000cs nl fr nb_NO de ru hu es goodvibes-v0.4.2/po/POTFILES000066400000000000000000000011651337147241200154610ustar00rootroot00000000000000# Integration files data/io.gitlab.Goodvibes.appdata.xml.in data/io.gitlab.Goodvibes.desktop.in # Glade ui files src/ui/resources/app-menu.glade src/ui/resources/main-window.glade src/ui/resources/menubar.glade src/ui/resources/prefs-window.glade src/ui/resources/station-dialog.glade src/ui/resources/status-icon-menu.glade # Source files src/main.c src/core/gv-engine.c src/core/gv-player.c src/core/gv-station-list.c src/ui/gv-main-window.c src/ui/gv-prefs-window.c src/ui/gv-station-context-menu.c src/ui/gv-station-dialog.c src/ui/gv-status-icon.c src/feat/gv-hotkeys.c src/feat/gv-inhibitor.c src/feat/gv-notifications.c goodvibes-v0.4.2/po/cs.po000066400000000000000000000254171337147241200152640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-04-02 10:36+0000\n" "Last-Translator: Lukáš Linhart \n" "Language-Team: Czech \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 2.20-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Přehrát webové rádio" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Přehrávač rádia" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Audio;Radio;Přehrávač;Zvuk;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Přidat stanici" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Nastavení" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Online nápověda" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "O programu" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Zavřít rozhraní" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Ukončit" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Nebyla zvolena žádná stanice" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Zastaveno" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Menu" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Nápověda" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Automaticky přehrávat při spuštění" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Potvrdit" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Přehrávání" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Zabránit uspání při přehrávání" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "Nastavení systému" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Nativní D-Bus Server" #: src/ui/resources/prefs-window.glade:209 #, fuzzy msgid "MPRIS2 D-Bus Server" msgstr "MPRIS2 D-Bus Server" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Různé" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "Automatická výška okna" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "varianta motivu" #: src/ui/resources/prefs-window.glade:291 #, fuzzy msgid "System Default" msgstr "Nastavení systému" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "Upřednostnit tmavý motiv" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "Upřednostnit světlý motiv" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "Okno" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "Poslat oznámení" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "oznámení" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Konzolový výstup" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "konzole" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Displej" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "" #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "" #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "" #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "" #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "" #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "" #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "" #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "" #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "" #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "" #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Přehrávač rádia Goodvibes" #~ msgid "File" #~ msgstr "Soubor" #~ msgid "Player" #~ msgstr "Přehrávač" goodvibes-v0.4.2/po/de.po000066400000000000000000000343771337147241200152540ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-08-10 12:38+0000\n" "Last-Translator: vinz \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 3.2-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Webradios abspielen" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" "Goodvibes ist ein einfaches Internetradio-Abspielprogramm für GNU-Linux." #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" "Es enthält alle Funktionen, die man von einem Internetradio-Player erwartet, " "wie: Multimediatastenunterstützung, Benachrichtigungen, " "Systemruhestandsunterdrückung und MPRI2-Unterstützung." #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Radio-AbspielprogrammRadio-Player" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Audio;Radio;Player;Webradio;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Radiosender hinzufügen" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Einstellungen" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Online-Hilfe" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "Über" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Benutzeroberfläche schließen" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Beenden" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Keinen Radiosender ausgewählt" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Angehalten" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Menü" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Hilfe" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Automatisch beim Start abspielen" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "Benutzerdefinierte Ausgangspipeline" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Übernehmen" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Wiedergabe" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Ruhemodus währen der Wiedergabe verhindern" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "System" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Nativer D-Bus-Server" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "MPRIS2 D-Bus-Server" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Sonstiges" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "Fensterhöhe automatisch festlegen" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "Thema Variante" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "Systemstandard" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "Lieber Dunkel" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "Lieber Licht" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "Fenster" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "Benachrichtigungen senden" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "Benachrichtigungen" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Konsolenausgabe" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "Konsole" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Anzeige" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "Multimedia-Tasten" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "Tastatur" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "Mittelklick" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "Wiedergabe/Pause umschalten" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "Stummschaltung" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "Blättern" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "Radiosender wechseln" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "Lautstärke ändern" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "Maus (Statussymbol-Modus)" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "Bedienelemente" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "Schließen" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "Name" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "Adresse" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "%s:%s" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "Pipeline-Beschreibung konnte nicht analysiert werden" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "'%s' ist weder eine bekannte Station noch eine gültige Internetadresse" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "Senderliste konnte nicht gespeichert werden" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "Senderinformationen" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "Benutzer-Kennung" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "Bitrate" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "Metadaten" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "Künstler" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "Titel" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "Album" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "Genre" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "Jahr" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "Kommentar" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "Wiedergabe" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "Verbindung wird hergestellt…" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "Wird zwischengespeichert…" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "Funktion zur Kompilierungszeit deaktiviert." #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "" "Gibt an, ob die Wiedergabe beim Start automatisch gestartet werden soll." #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" "Gibt an, ob eine benutzerdefinierte Ausgabepipeline verwendet werden soll." #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" "Die für die Wiedergabe verwendete GStreamer-Ausgabepipeline. Beispiele " "finden Sie in der Online-Dokumentation." #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "Verhindern, dass das System bei der Wiedergabe in den Ruhemodus geht." #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" "Aktivieren Sie den nativen D-Bus-Server (für die Befehlszeilenschnittstelle " "erforderlich)." #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "Den MPRIS2 D-Bus Server aktivieren" #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "Bevorzugen Sie eine andere Variante des Themas (falls verfügbar)." #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" "Fensterhöhe automatisch anpassen, wenn eine Station hinzugefügt oder " "entfernt wird." #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "Einstellung im Statussymbolmodus nicht verfügbar." #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "Benachrichtigung anzeigen, wenn sich der Status ändert." #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "Informationen zur Standardausgabe anzeigen." #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" "Multimedia-Tasten verbinden (Wiedergabe/Pause/Stopp/Vorherige/Nächste)." #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" "Aktion, die durch einen mittleren Klick auf das Statussymbol ausgelöst wird." #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" "Aktion, die durch Blättern mit der Maus auf dem Statussymbol ausgelöst wird." #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "Einstellung nur im Statussymbolmodus verfügbar." #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "Radiosender entfernen" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "Radiosender bearbeiten" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "Abbrechen" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "Speichern" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "Angehalten" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "Verbindung wird hergestellt" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "Wird zwischengespeichert" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "Wird wiedergegeben" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "Unbekannter Status" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "Stummgeschaltet" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "Laut." #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "Kein Radiosender" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "Keine Metadaten" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" "%s:\n" "%s" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "Folgende Tasten konnten nicht gebunden werden" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "Fehler beim Verhindern des Systemschlafs" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "Spiele %s" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "Spiele <%s>" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "Radiosender wiedergeben" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "(Unbekannter Titel)" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "Neuer Titel" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "Fehler" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Goodvibes Radio-Player" #~ msgid "Window Behavior" #~ msgstr "Fensterverhalten" #~ msgid "" #~ "The GStreamer output pipeline used for playback. For example, use " #~ "'alsasink device=hw:2' to force audio output to ALSA, 2nd soundcard.\n" #~ "For more details, please refer to the gst-launch manual page." #~ msgstr "" #~ "Die GStreamer-Ausgabepipeline, die für die Wiedergabe verwendet wird. " #~ "Verwenden Sie z.B. „alsasink device=hw:2”, um die Audioausgabe auf ALSA, " #~ "2. Soundkarte, zu erzwingen.\n" #~ "Weitere Einzelheiten entnehmen Sie bitte der gst-launch-Handbuchseite." #~ msgid "Emit Notifications" #~ msgstr "Benachrichtigungen ausgeben" #~ msgid "End of stream" #~ msgstr "Ende des Streams" #~ msgid "Connecting..." #~ msgstr "Verbinden …" #~ msgid "Buffering..." #~ msgstr "Zwischenspeicherung …" #~ msgid "Seeting available only in status icon mode." #~ msgstr "Einstellung nur im Statussymbolmodus verfügbar." #~ msgid "Add new station" #~ msgstr "Neuen Radiosender hinzufügen" #~ msgid "Edit station" #~ msgstr "Radiosender bearbeiten" #~ msgid "Add New Station" #~ msgstr "Neuen Radiosender hinzufügen" #~ msgid "File" #~ msgstr "Datei" #~ msgid "Player" #~ msgstr "Player" #~ msgid "Set Timeout" #~ msgstr "Zeitüberschreitung einstellen" #~ msgid "How long the notifications should be displayed (seconds)." #~ msgstr "" #~ "Wie lange die Benachrichtigungen angezeigt werden sollen (Sekunden)." goodvibes-v0.4.2/po/es.po000066400000000000000000000247721337147241200152710ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-09-27 18:38+0000\n" "Last-Translator: benages \n" "Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 3.2-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Escuchar radios online" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" "Goodvibes es un simple reproductor de radios de internet para GNU/Linux." #: data/io.gitlab.Goodvibes.appdata.xml.in:13 #, fuzzy msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" "Viene con todas las funciones que puede esperar de un reproductor de audio, " "cómo teclas multimedia, notificaciones, desactivación del autoapagado del " "ordenador y soporte MPRIS2." #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Reproductor de Radio" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Audio;Radio;Reproductor;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Añadir estación" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Preferencias" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Ayuda Online" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "Acerca de" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Cerrar GUI" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Salir" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "No hay ninguna estación seleccionada" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Parado" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Menu" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Ayuda" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Reproducir automáticamente al arrancar" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Aplicar" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Reproducción" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "Sistema" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "" #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "" #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "" #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "" #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "" #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "" #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "" #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "" #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "" #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "" #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "" goodvibes-v0.4.2/po/fr.po000066400000000000000000000336271337147241200152700ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-05-22 07:26+0000\n" "Last-Translator: Arnaud Rebillout \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 3.0-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Écouter des radios web" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Lecteur radio" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Audio;Radio;Player;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Ajouter une station" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Préférences" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Aide en ligne" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "À propos" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Fermer l'interface" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Quitter" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Aucune station sélectionnée" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Arrêté" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Menu" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Aide" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Lancer l'écoute au démarrage" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "Chaîne de traitement de sortie personnalisée" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Appliquer" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Lecture" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Empêcher la mise en veille lors de l'écoute" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "Système" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Serveur D-Bus natif" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "Serveur D-Bus MPRIS2" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Divers" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "Fixer automatiquement la hauteur de la fenêtre" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "Variante de thème" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "Valeur par défaut du système" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "Plutôt sombre" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "Plutôt clair" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "Fenêtre" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "Afficher des notifications" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "Notifications" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Sortie de la console" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "Console" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Affichage" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "Touches Multimédia" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "Clavier" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "Clic milieu" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "Lecture/Pause" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "Silence" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "Défilement" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "Changer de station" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "Changer le volume" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "Souris (en mode icône de statut)" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "Contrôles" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "Fermer" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "Nom" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "Adresse (URI)" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "%s: %s" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "Impossible d'obtenir la description de la chaîne de traitements" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "'%s' n'est ni une station connue, ni une URI valide" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "Échec de l'enregistrement de la liste des stations" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "Information sur la station" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "Agent utilisateur" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "Débit" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "Métadonnée" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "Artiste" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "Titre" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "Album" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "Genre" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "Année" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "Commentaire" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "Lecture" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "Connexion…" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "Mise en mémoire tampon…" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "Fonctionnalité désactivée lors de la compilation." #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "Automatiquement reprendre la lecture au démarrage." #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" "Nécessité d'utiliser une chaîne de traitements de sortie personnalisée." #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" "La chaîne de traitement GStreamer utilisée pour la lecture. Vous pouvez vous " "référer à la documentation en ligne pour avoir des exemples." #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "Empêcher le système de se mettre en veille lors de la lecture." #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" "Activer le serveur natif D-Bus (requis par l'interface en ligne de commande)." #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "Activer le serveur D-BUS MPRIS2." #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "Choisir une variante du thème actuel (si disponible)." #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" "Ajuster automatiquement la hauteur de la fenêtre lorsqu'une station est " "ajoutée ou supprimée." #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "Paramètre non disponible en mode icône de statut." #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "Afficher une notification lorsque le statut change." #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "Afficher les informations sur la sortie standard." #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "Associer les touches multimédia (play/pause/stop/previous/next)." #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "Action effectuée lors d'un clic milieu sur l'icône de statut." #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" "Action effectuée après utilisation de la molette sur l'icône de statut." #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "Paramètre disponible seulement en mode icône de statut." #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "Supprimer la station" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "Éditer la station" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "Annuler" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "Enregistrer" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "Stoppé" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "connexion" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "mise en mémoire tampon" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "lecture" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "état inconnu" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "coupé" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "vol." #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "Aucune station" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "Aucune métadonnée" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" "%s:\n" "%s" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "Échec lors de l'association des touches suivantes" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "Impossible d'empêcher la mise en veille du système" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "Lecture de %s" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "Lecture de <%s>" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "Lecture de la station" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "(Titre inconnu)" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "Nouvelle piste" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "Erreur" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Lecteur de radio Goodvibes" #~ msgid "Window Behavior" #~ msgstr "Comportement de la fenêtre" #~ msgid "" #~ "The GStreamer output pipeline used for playback. For example, use " #~ "'alsasink device=hw:2' to force audio output to ALSA, 2nd soundcard.\n" #~ "For more details, please refer to the gst-launch manual page." #~ msgstr "" #~ "Chaîne de traitements de sortie utilisée par GStreamer. Par exemple, " #~ "entrer 'alsasink device=hw:2' pour forcer l'utilisation de la 2nd carte " #~ "son détectée par ALSA.\n" #~ "Pour plus de détails, se référer à la page de manuel de gst-launch." #~ msgid "Emit Notifications" #~ msgstr "Émettre des notifications" #~ msgid "End of stream" #~ msgstr "Flux terminé" #~ msgid "Connecting..." #~ msgstr "Connexion…" #~ msgid "Buffering..." #~ msgstr "Mise en mémoire tampon…" #~ msgid "Seeting available only in status icon mode." #~ msgstr "Paramètre disponible uniquement en mode icône de statut." #~ msgid "Add new station" #~ msgstr "Ajouter une nouvelle station" #~ msgid "Edit station" #~ msgstr "Éditer la station" #~ msgid "Add New Station" #~ msgstr "Ajouter une nouvelle station" #~ msgid "File" #~ msgstr "Fichier" #~ msgid "Player" #~ msgstr "Lecteur" #~ msgid "Set Timeout" #~ msgstr "Durée d'affichage" #~ msgid "How long the notifications should be displayed (seconds)." #~ msgstr "Durée d'affichage des notifications (en secondes)." goodvibes-v0.4.2/po/goodvibes.pot000066400000000000000000000235361337147241200170240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: goodvibes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "" #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "" #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "" #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "" #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "" #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "" #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "" #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "" #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "" #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "" #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "" goodvibes-v0.4.2/po/hu.po000066400000000000000000000235301337147241200152650ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "" #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "" #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "" #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "" #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "" #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "" #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "" #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "" #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "" #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "" #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "" #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "" goodvibes-v0.4.2/po/meson.build000066400000000000000000000001401337147241200164430ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later i18n.gettext(meson.project_name(), preset: 'glib') goodvibes-v0.4.2/po/nb_NO.po000066400000000000000000000330111337147241200156370ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-08-29 16:40+0000\n" "Last-Translator: Allan Nordhøy \n" "Language-Team: Norwegian Bokmål \n" "Language: nb_NO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 3.2-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Spill av nettradio" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "Goodvibes er en enkel internettradiospiller for GNU/Linux." #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" "Den kommer med grunnleggende enkle funksjoner du kan forvente av en " "lydspiller, som multimediataster, merknader, systemsøvnunngåelse, og MPRIS2-" "støtte." #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Radioavspiller" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Lyd;Radio;Avspiller;Nettradio;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Legg til stasjon" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Innstillinger" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Nettbasert hjelp" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "Om" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Lukk grensesnitt" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Avslutt" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Ingen stasjon valgt" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Stoppet" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Meny" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Hjelp" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Spill av automatisk ved oppstart" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "Egendefinert utdatakanal" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Bruk" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Avspilling" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Forhindre dvalgang under avspilling" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "System" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Lokal D-Bus-tjener" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "MPRIS2 D-Bus-tjener" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Ymse" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "Sett vindushøyde automatisk" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "Tema Variant" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "Systemforvalg" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "Foretrukket Mørk" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "Foretrekker Lys" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "Vindu" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "Send merknader" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "Merknader" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Konsollutdata" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "Konsoll" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Vis" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "Multimediataster" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "Tastatur" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "Midtklikk" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "Veksle Spill/Pause" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "Demp" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "Rulling" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "Endre stasjon" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "Endre lydstyrke" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "Mus (statusikonmodus)" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "Kontroller" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "Lukk" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "Navn" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "URI" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "%s: %s" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "Klarte ikke å tolke kanalbeskrivelse" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "'%s' er hverken en kjent stasjon eller en gyldig URI" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "Klarte ikke å lasgre stasjonsliste" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "Stasjonsinformasjon" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "Brukeragent" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "Bitrate" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "Metadata" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "Artist" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "Tittel" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "Album" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "Sjanger" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "År" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "Kommentar" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "Spiller" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "Kobler til…" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "Mellomlagrer…" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "Funksjon avskrudd ved kompilering." #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "Hvorvidt avspilling skal startes automatisk ved oppstart." #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "Hvorvidt en egendefinert utdatakanal skal brukes." #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" "GStreamer utgangsrørledningen brukes til avspilling. Se referanse " "dokumentasjon for eksempler." #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "Forhindre systemet fra dvalgang under avspilling." #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" "Skru på den lokale D-Bus-tjeneren (trengs for kommandolinjegresesnitt)." #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "Skru på MPRIS2 D-Bus-tjeneren." #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "Foretrekker en annen variant av temaet (hvis tilgjengelig)." #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" "Juster vindushøyden automatisk når en stasjon legges til eller fjernes." #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "Innstillingen er ikke tilgjengelig i statusikonsmodus." #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "Vis merknad når statusen endres." #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "Vis informasjon i standardutdata." #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "Tilknytt multimediataster (spill/pause/stopp/forrige/neste)." #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "Handling utløst av midtklikk på statusikonet." #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "Handling utløst av muserulling på statusikonet." #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "Innstillingen er kun tilgjengelig i statusikonsmodus." #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "Fjern stasjon" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "Rediger stasjon" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "Avbryt" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "Lagre" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "stoppet" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "kobler til" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "mellomlagrer" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "spiller" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "ukjent tilstand" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "dempet" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "lydst." #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "Ingen stasjon" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "Ingen metadata" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" "%s:\n" "%s" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "Klarte ikke å tilknytte følgende taster" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "Klarte ikke å forhindre systemdvalgang" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "Spiller %s" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "Spiller <%s>" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "Spiller stasjon" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "(Ukjent tittel)" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "Nytt spor" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "Feil" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Goodvibes radioavspiller" #~ msgid "Window Behavior" #~ msgstr "Vindusoppførsel" #~ msgid "" #~ "The GStreamer output pipeline used for playback. For example, use " #~ "'alsasink device=hw:2' to force audio output to ALSA, 2nd soundcard.\n" #~ "For more details, please refer to the gst-launch manual page." #~ msgstr "" #~ "GStreamer-utdatakanal brukt for avspilling. Bruk for eksempel \"alsasink " #~ "device=hw:2\" for å tvinge lydutdata til ALSA, lydkort nummer to.\n" #~ "For flere detaljer, sjekk gst-launch -manualsiden." #~ msgid "Emit Notifications" #~ msgstr "Vis merknader" #~ msgid "End of stream" #~ msgstr "Dataflytens slutt" #~ msgid "Connecting..." #~ msgstr "Kobler til…" #~ msgid "Buffering..." #~ msgstr "Mellomlagrer…" #~ msgid "Seeting available only in status icon mode." #~ msgstr "Innstillingen er ikke tilgjengelig i statusikonmodus." #~ msgid "Add new station" #~ msgstr "Legg til ny stasjon" #~ msgid "Edit station" #~ msgstr "Rediger stasjon" #~ msgid "Add New Station" #~ msgstr "Legg til ny stasjon" #~ msgid "File" #~ msgstr "Fil" #~ msgid "Player" #~ msgstr "Avspiller" #~ msgid "Set Timeout" #~ msgstr "Sett tidsavbrudd" #~ msgid "How long the notifications should be displayed (seconds)." #~ msgstr "Hvor lenge merknader skal viser (sekunder)." goodvibes-v0.4.2/po/nl.po000066400000000000000000000340301337147241200152570ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2018-11-10 04:18+0000\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 3.3-dev\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Internetradiostations afspelen" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" "Goodvibes is een eenvoudige speler voor GNU/Linux voor het afspelen van " "internetradiostations." #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" "Het bevat elke basisfunctie die je mag verwachten van een audiospeler, zoals " "multimediatoetsen, meldingen, slaapstandonderdrukking en MPRIS2-" "ondersteuning." #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Radiospeler" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Audio;Radio;Speler;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 msgid "Add Station" msgstr "Station toevoegen" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Voorkeuren" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Online hulp" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "Over" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "UI sluiten" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Afsluiten" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Geen station geselecteerd" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Gestopt" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "Menu" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Hulp" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Automatisch afspelen na opstarten" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "Aangepaste uitvoer-pijpleiding" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "Toepassen" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "Afspelen" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Slaapstand onderdrukken tijdens afspelen" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "Systeem" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Systeem-D-Bus-server" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "MPRIS D-Bus-server" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Diversen" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "Vensterhoogte automatisch instellen" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "Themavariant" #: src/ui/resources/prefs-window.glade:291 msgid "System Default" msgstr "Systeemstandaard" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "Donker prefereren" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "Licht prefereren" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "Venster" #: src/ui/resources/prefs-window.glade:336 msgid "Send Notifications" msgstr "Meldingen versturen" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "Meldingen" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Terminaluitvoer" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "Terminal" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Weergave" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "Multimedia-sneltoetsen" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "Toetsenbord" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "Middelklik" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "Schakelen tussen afspelen/pauzeren" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "Dempen" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "Scrollen" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "Station wijzigen" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "Volume wijzigen" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "Muis (systeemvakpictogram-modus)" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "Bediening" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "Sluiten" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "Naam" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "URI" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "%s:%s" #: src/core/gv-engine.c:268 msgid "Failed to parse pipeline description" msgstr "Het verwerken van de pijpleiding-constructie is mislukt" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "'%s' is een onbekend station of ongeldige URI" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "Het opslaan van de stationslijst is mislukt" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "Stationsinformatie" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "Gebruikersagent" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "Bitsnelheid" #: src/ui/gv-main-window.c:290 msgid "Metadata" msgstr "Metagegevens" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "Artiest" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "Titel" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "Album" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "Genre" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "Jaar" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "Opmerking" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "Bezig met afspelen" #: src/ui/gv-main-window.c:366 msgid "Connecting…" msgstr "Bezig met verbinden…" #: src/ui/gv-main-window.c:369 msgid "Buffering…" msgstr "Bezig met bufferen…" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "Mogelijkheid is uitgeschakeld tijdens het compileren." #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "Of de applicatie automatisch moet afspelen tijdens het opstarten." #: src/ui/gv-prefs-window.c:430 msgid "Whether to use a custom output pipeline." msgstr "Of een aangepaste uitvoer-pijpleiding moet worden gebruikt." #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" "De voor afspelen gebruikte GStreamer-uitvoerpijplijn. Bekijk de online " "documentatie voor voorbeelden." #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "Voorkom dat het systeem in de slaapstand gaat tijdens het afspelen." #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "" "Schakel de system-D-Bus-server in (benodigd voor de command-line interface)." #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "Schakel de MPRIS2 D-Bus-server in." #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "Andere themavariant prefereren (indien beschikbaar)." #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" "Vensterhoogte automatisch aanpassen als een station wordt toegevoegd of " "verwijderd." #: src/ui/gv-prefs-window.c:483 msgid "Setting not available in status icon mode." msgstr "De instelling is niet beschikbaar in systeemvakpictogram-modus." #: src/ui/gv-prefs-window.c:487 msgid "Show notification when the status changes." msgstr "Meldingen weergeven bij statuswijzigingen." #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "Informatie weergeven over de standaard uitvoer." #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" "Multimedia-toetsen gebruiken (afspelen/pauzeren/stoppen/vorige/volgende)." #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "" "Actie die wordt getriggerd door een middelklik op het systeemvakpictogram." #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "" "Actie die wordt getriggerd door met de muis te scrollen over het " "systeemvakpictogram." #: src/ui/gv-prefs-window.c:516 msgid "Setting only available in status icon mode." msgstr "De instelling is alleen beschikbaar in systeemvakpictogram-modus." #: src/ui/gv-station-context-menu.c:36 msgid "Remove Station" msgstr "Station verwijderen" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 msgid "Edit Station" msgstr "Station bewerken" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "Annuleren" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "Opslaan" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "gestopt" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "bezig met verbinden" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "bezig met bufferen" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "bezig met afspelen" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "onbekende status" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "gedempt" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "vol." #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "Geen station" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "Geen metagegevens" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" "%s:\n" "%s" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "Het koppelen van de volgende toetsen is mislukt" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "Voorkomen van slaapmodus is mislukt" #: src/feat/gv-notifications.c:59 #, c-format msgid "Playing %s" msgstr "Bezig met afspelen van %s" #: src/feat/gv-notifications.c:62 #, c-format msgid "Playing <%s>" msgstr "Bezig met afspelen van <%s>" #: src/feat/gv-notifications.c:65 msgid "Playing Station" msgstr "Afspelend station" #: src/feat/gv-notifications.c:103 msgid "(Unknown title)" msgstr "(onbekende titel)" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "Nieuw nummer" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "Fout" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Goodvibes Radiospeler" #~ msgid "Window Behavior" #~ msgstr "Venstergedrag" #~ msgid "" #~ "The GStreamer output pipeline used for playback. For example, use " #~ "'alsasink device=hw:2' to force audio output to ALSA, 2nd soundcard.\n" #~ "For more details, please refer to the gst-launch manual page." #~ msgstr "" #~ "De voor het afspelen gebruikte GStreamer-uitvoerpijpleiding. Gebruik " #~ "bijv. 'alsasink device=hw:2' om audio-uitvoer te forceren naar ALSA op de " #~ "2e geluidskaart.\n" #~ "Voor meer informatie, lees de gst-launch man-pagina." #~ msgid "Emit Notifications" #~ msgstr "Meldingen weergeven" #~ msgid "End of stream" #~ msgstr "Einde van stream" #~ msgid "Connecting..." #~ msgstr "Bezig met verbinden..." #~ msgid "Buffering..." #~ msgstr "Bezig met bufferen..." #~ msgid "Seeting available only in status icon mode." #~ msgstr "De instelling is alleen beschikbaar in systeemvakpictogram-modus." #~ msgid "Add new station" #~ msgstr "Nieuw station toevoegen" #~ msgid "Edit station" #~ msgstr "Station bewerken" #~ msgid "Add New Station" #~ msgstr "Nieuw station toevoegen" #~ msgid "File" #~ msgstr "Bestand" #~ msgid "Player" #~ msgstr "Speler" #~ msgid "Set Timeout" #~ msgstr "Timer instellen" #~ msgid "How long the notifications should be displayed (seconds)." #~ msgstr "Hoe lang de meldingen moet worden weergegeven (seconden)." goodvibes-v0.4.2/po/ru.po000066400000000000000000000343161337147241200153030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR # This file is distributed under the same license as the goodvibes package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: goodvibes 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-09 19:25+0700\n" "PO-Revision-Date: 2017-10-31 10:49+0000\n" "Last-Translator: Алексей Выскубов \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 2.17\n" #: data/io.gitlab.Goodvibes.appdata.xml.in:4 #: data/io.gitlab.Goodvibes.desktop.in:3 src/main.c:149 msgid "Goodvibes" msgstr "Goodvibes" #: data/io.gitlab.Goodvibes.appdata.xml.in:5 #: data/io.gitlab.Goodvibes.desktop.in:5 msgid "Play web radios" msgstr "Воспроизведение веб-радиостанций" #: data/io.gitlab.Goodvibes.appdata.xml.in:10 msgid "Goodvibes is a simple internet radio player for GNU/Linux." msgstr "" #: data/io.gitlab.Goodvibes.appdata.xml.in:13 msgid "" "It comes with every basic features you can expect from an audio player, such " "as multimedia keys, notifications, system sleep inhibition, and MPRIS2 " "support." msgstr "" #: data/io.gitlab.Goodvibes.desktop.in:4 msgid "Radio Player" msgstr "Радио-проигрыватель" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.gitlab.Goodvibes.desktop.in:7 msgid "Audio;Radio;Player;" msgstr "Аудио;Радио;Проигрыватель;" #. TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! #: data/io.gitlab.Goodvibes.desktop.in:11 msgid "io.gitlab.Goodvibes" msgstr "io.gitlab.Goodvibes" #: src/ui/resources/app-menu.glade:6 src/ui/resources/menubar.glade:8 #: src/ui/resources/status-icon-menu.glade:6 #: src/ui/gv-station-context-menu.c:35 src/ui/gv-station-dialog.c:424 #, fuzzy msgid "Add Station" msgstr "Добавить новую станцию" #: src/ui/resources/app-menu.glade:13 src/ui/resources/menubar.glade:15 #: src/ui/resources/status-icon-menu.glade:12 src/ui/gv-prefs-window.c:629 msgid "Preferences" msgstr "Параметры" #: src/ui/resources/app-menu.glade:19 src/ui/resources/menubar.glade:36 #: src/ui/resources/status-icon-menu.glade:18 msgid "Online Help" msgstr "Онлайн-справка" #: src/ui/resources/app-menu.glade:24 src/ui/resources/menubar.glade:43 #: src/ui/resources/status-icon-menu.glade:22 msgid "About" msgstr "О программе" #: src/ui/resources/app-menu.glade:28 src/ui/resources/menubar.glade:21 msgid "Close UI" msgstr "Закрыть окно" #: src/ui/resources/app-menu.glade:33 src/ui/resources/menubar.glade:26 #: src/ui/resources/status-icon-menu.glade:26 msgid "Quit" msgstr "Выход" #: src/ui/resources/main-window.glade:47 src/ui/gv-main-window.c:350 msgid "No station selected" msgstr "Не выбрана радиостанция" #: src/ui/resources/main-window.glade:63 src/ui/gv-main-window.c:373 msgid "Stopped" msgstr "Остановлено" #: src/ui/resources/menubar.glade:5 msgid "Menu" msgstr "" #: src/ui/resources/menubar.glade:33 msgid "Help" msgstr "Справка" #: src/ui/resources/prefs-window.glade:35 msgid "Autoplay on Startup" msgstr "Воспроизведение при Запуске" #: src/ui/resources/prefs-window.glade:48 msgid "Custom Output Pipeline" msgstr "" #: src/ui/resources/prefs-window.glade:79 msgid "Apply" msgstr "" #: src/ui/resources/prefs-window.glade:102 msgid "Playback" msgstr "" #: src/ui/resources/prefs-window.glade:138 msgid "Prevent sleep while playing" msgstr "Отключить спящий режим" #: src/ui/resources/prefs-window.glade:151 msgid "System" msgstr "Система" #: src/ui/resources/prefs-window.glade:197 msgid "Native D-Bus Server" msgstr "Родной сервер D-Bus" #: src/ui/resources/prefs-window.glade:209 msgid "MPRIS2 D-Bus Server" msgstr "MPRIS2 сервер D-Bus" #: src/ui/resources/prefs-window.glade:222 msgid "D-Bus" msgstr "Шина D-Bus" #: src/ui/resources/prefs-window.glade:238 msgid "Misc" msgstr "Разное" #: src/ui/resources/prefs-window.glade:262 msgid "Autoset Window Height" msgstr "" #: src/ui/resources/prefs-window.glade:278 msgid "Theme Variant" msgstr "" #: src/ui/resources/prefs-window.glade:291 #, fuzzy msgid "System Default" msgstr "Система" #: src/ui/resources/prefs-window.glade:292 msgid "Prefer Dark" msgstr "" #: src/ui/resources/prefs-window.glade:293 msgid "Prefer Light" msgstr "" #: src/ui/resources/prefs-window.glade:310 msgid "Window" msgstr "" #: src/ui/resources/prefs-window.glade:336 #, fuzzy msgid "Send Notifications" msgstr "Оповещения" #: src/ui/resources/prefs-window.glade:359 msgid "Notifications" msgstr "Оповещения" #: src/ui/resources/prefs-window.glade:394 msgid "Console Output" msgstr "Вывод консоли" #: src/ui/resources/prefs-window.glade:407 msgid "Console" msgstr "Консоль" #: src/ui/resources/prefs-window.glade:426 msgid "Display" msgstr "Интерфейс" #: src/ui/resources/prefs-window.glade:464 msgid "Multimedia Hotkeys" msgstr "Мультимедиа-клавиши" #: src/ui/resources/prefs-window.glade:477 msgid "Keyboard" msgstr "Клавиатура" #: src/ui/resources/prefs-window.glade:503 msgid "Middle Click" msgstr "Средняя кнопка" #: src/ui/resources/prefs-window.glade:516 msgid "Toggle Play/Pause" msgstr "Воспр./Пауза" #: src/ui/resources/prefs-window.glade:517 msgid "Mute" msgstr "Приглушить" #: src/ui/resources/prefs-window.glade:530 msgid "Scrolling" msgstr "Скроллинг" #: src/ui/resources/prefs-window.glade:543 msgid "Change Station" msgstr "Сменить станцию" #: src/ui/resources/prefs-window.glade:544 msgid "Change Volume" msgstr "Регулировка громкости" #: src/ui/resources/prefs-window.glade:558 msgid "Mouse (Status Icon Mode)" msgstr "Мышь (режим Status Icon)" #: src/ui/resources/prefs-window.glade:577 msgid "Controls" msgstr "Управление" #: src/ui/resources/prefs-window.glade:598 msgid "Close" msgstr "Закрыть" #: src/ui/resources/station-dialog.glade:12 src/ui/gv-main-window.c:261 msgid "Name" msgstr "Название" #: src/ui/resources/station-dialog.glade:34 src/ui/gv-main-window.c:262 msgid "URI" msgstr "URI" #: src/core/gv-engine.c:267 src/core/gv-station-list.c:1134 #, c-format msgid "%s: %s" msgstr "" #: src/core/gv-engine.c:268 #, fuzzy msgid "Failed to parse pipeline description" msgstr "Невозможно сохранить настройки радиостанции" #: src/core/gv-player.c:819 #, c-format msgid "'%s' is neither a known station or a valid URI" msgstr "" #: src/core/gv-station-list.c:1135 msgid "Failed to save station list" msgstr "Невозможно сохранить список радиостанций" #: src/ui/gv-main-window.c:259 msgid "Station Information" msgstr "" #: src/ui/gv-main-window.c:273 msgid "User-agent" msgstr "" #: src/ui/gv-main-window.c:277 msgid "Bitrate" msgstr "" #: src/ui/gv-main-window.c:290 #, fuzzy msgid "Metadata" msgstr "Нет метаданных" #: src/ui/gv-main-window.c:292 msgid "Artist" msgstr "" #: src/ui/gv-main-window.c:293 msgid "Title" msgstr "" #: src/ui/gv-main-window.c:294 msgid "Album" msgstr "" #: src/ui/gv-main-window.c:295 msgid "Genre" msgstr "" #: src/ui/gv-main-window.c:296 msgid "Year" msgstr "" #: src/ui/gv-main-window.c:297 msgid "Comment" msgstr "" #: src/ui/gv-main-window.c:363 src/ui/gv-main-window.c:391 msgid "Playing" msgstr "Воспроизведение" #: src/ui/gv-main-window.c:366 #, fuzzy msgid "Connecting…" msgstr "соединение" #: src/ui/gv-main-window.c:369 #, fuzzy msgid "Buffering…" msgstr "буферизация" #: src/ui/gv-prefs-window.c:281 msgid "Feature disabled at compile-time." msgstr "Отключено при компиляции." #. #. * Setup settings and features. #. * These function calls create a binding between a gtk widget and #. * an internal object, initializes the widget value, and set the #. * widgets tooltips (label + setting). #. #. Misc #: src/ui/gv-prefs-window.c:424 msgid "Whether to start playback automatically on startup." msgstr "Начинать ли воспроизведение автоматически при запуске." #: src/ui/gv-prefs-window.c:430 #, fuzzy msgid "Whether to use a custom output pipeline." msgstr "Использовать ли настраиваемый таймаут для оповещений." #: src/ui/gv-prefs-window.c:436 msgid "" "The GStreamer output pipeline used for playback. Refer to theonline " "documentation for examples." msgstr "" #: src/ui/gv-prefs-window.c:452 msgid "Prevent the system from going to sleep while playing." msgstr "Предотвратить переход системы в спящий режим во время воспроизведения." #: src/ui/gv-prefs-window.c:457 msgid "Enable the native D-Bus server (needed for the command-line interface)." msgstr "Включить родной сервер D-Bus (необходимо для работы в консоли)." #: src/ui/gv-prefs-window.c:463 msgid "Enable the MPRIS2 D-Bus server." msgstr "Включить MPRIS2 сервер D-Bus." #. Display #: src/ui/gv-prefs-window.c:469 msgid "Prefer a different variant of the theme (if available)." msgstr "" #: src/ui/gv-prefs-window.c:476 msgid "" "Automatically adjust the window height when a station is added or removed." msgstr "" #: src/ui/gv-prefs-window.c:483 #, fuzzy msgid "Setting not available in status icon mode." msgstr "Приложение не было запущено в режиме значка статуса." #: src/ui/gv-prefs-window.c:487 #, fuzzy msgid "Show notification when the status changes." msgstr "Выводить оповещения при изменении статуса." #: src/ui/gv-prefs-window.c:492 msgid "Display information on the standard output." msgstr "Выводить информацию в консоль." #. Controls #: src/ui/gv-prefs-window.c:498 msgid "Bind mutimedia keys (play/pause/stop/previous/next)." msgstr "" "Назначить мультимедиа-клавиши (воспроизведение/пауза/стоп/назад/вперёд)." #: src/ui/gv-prefs-window.c:504 msgid "Action triggered by a middle click on the status icon." msgstr "Действие, выполняемое средней кнопкой мыши по значку статуса." #: src/ui/gv-prefs-window.c:510 msgid "Action triggered by mouse-scrolling on the status icon." msgstr "Действие, выполняемое при проматывании мышью по значку статуса." #: src/ui/gv-prefs-window.c:516 #, fuzzy msgid "Setting only available in status icon mode." msgstr "Приложение не было запущено в режиме значка статуса." #: src/ui/gv-station-context-menu.c:36 #, fuzzy msgid "Remove Station" msgstr "Удалить станцию" #: src/ui/gv-station-context-menu.c:37 src/ui/gv-station-dialog.c:424 #, fuzzy msgid "Edit Station" msgstr "Изменить станцию" #: src/ui/gv-station-dialog.c:281 msgid "Cancel" msgstr "Отмена" #: src/ui/gv-station-dialog.c:282 msgid "Save" msgstr "Сохранить" #: src/ui/gv-status-icon.c:131 msgid "stopped" msgstr "остановлено" #: src/ui/gv-status-icon.c:134 msgid "connecting" msgstr "соединение" #: src/ui/gv-status-icon.c:137 msgid "buffering" msgstr "буферизация" #: src/ui/gv-status-icon.c:140 msgid "playing" msgstr "воспроизведение" #: src/ui/gv-status-icon.c:143 msgid "unknown state" msgstr "неизвестное состояние" #: src/ui/gv-status-icon.c:150 msgid "muted" msgstr "приглушено" #: src/ui/gv-status-icon.c:154 msgid "vol." msgstr "гром." #: src/ui/gv-status-icon.c:161 msgid "No station" msgstr "Нет станции" #: src/ui/gv-status-icon.c:168 msgid "No metadata" msgstr "Нет метаданных" #: src/feat/gv-hotkeys.c:145 #, c-format msgid "" "%s:\n" "%s" msgstr "" #: src/feat/gv-hotkeys.c:146 msgid "Failed to bind the following keys" msgstr "Не удалось назначить следующие клавиши" #: src/feat/gv-inhibitor.c:111 msgid "Failed to inhibit system sleep" msgstr "" #: src/feat/gv-notifications.c:59 #, fuzzy, c-format msgid "Playing %s" msgstr "Воспроизведение" #: src/feat/gv-notifications.c:62 #, fuzzy, c-format msgid "Playing <%s>" msgstr "Воспроизведение" #: src/feat/gv-notifications.c:65 #, fuzzy msgid "Playing Station" msgstr "Сменить станцию" #: src/feat/gv-notifications.c:103 #, fuzzy msgid "(Unknown title)" msgstr "неизвестное состояние" #: src/feat/gv-notifications.c:110 msgid "New Track" msgstr "" #: src/feat/gv-notifications.c:122 msgid "Error" msgstr "" #~ msgid "goodvibes" #~ msgstr "goodvibes" #~ msgid "Goodvibes Radio Player" #~ msgstr "Радио-проигрыватель Goodvibes" #~ msgid "Emit Notifications" #~ msgstr "Показывать оповещения" #~ msgid "Connecting..." #~ msgstr "Соединение..." #~ msgid "Buffering..." #~ msgstr "Буферизация..." #, fuzzy #~ msgid "Seeting available only in status icon mode." #~ msgstr "Приложение не было запущено в режиме значка статуса." #~ msgid "Add new station" #~ msgstr "Добавить новую станцию" #~ msgid "Edit station" #~ msgstr "Изменить станцию" #, fuzzy #~ msgid "Add New Station" #~ msgstr "Добавить новую станцию" #~ msgid "File" #~ msgstr "Файл" #~ msgid "Player" #~ msgstr "Проигрыватель" #~ msgid "Set Timeout" #~ msgstr "Установить таймаут" #~ msgid "How long the notifications should be displayed (seconds)." #~ msgstr "Насколько долго должны отображаться оповещения (секунды)." goodvibes-v0.4.2/scripts/000077500000000000000000000000001337147241200153575ustar00rootroot00000000000000goodvibes-v0.4.2/scripts/code/000077500000000000000000000000001337147241200162715ustar00rootroot00000000000000goodvibes-v0.4.2/scripts/code/gv-object-make.sh000077500000000000000000000114321337147241200214240ustar00rootroot00000000000000#!/bin/bash -e type="$1" objname="$2" parentname="$3" print_usage() { echo "Usage: $(basename $0) []" echo echo "Create a new goodvibes object in './src'," echo "that's to say a .c and a .h skeleton file." echo echo "Parameters:" echo " might be 'core', 'framework', 'ui', 'feat'." echo " should contain only lowercase, digits and dashes, and start with 'gv-'." echo " should contain only lowercase, digits and dashes." echo echo "Example:" echo " $0 ui gv-about-dialog gtk-about-dialog" } name_get_invalid_chars() { # Keep only invalid chars tr -d 'a-z0-9-' <<< "$1" } name_to_lower() { # To lowercase, '-' to '_' sed -e 's/-/_/g' <<< "$1" } name_to_upper() { # To uppercase, '-' to '_' sed -e 's/-/_/g' -e 's/\([a-z]\)/\U\1/g' <<< "$1" } name_to_camel() { # To camelcase, '-' removed sed -e 's/-\([a-z]\)/\U\1/g' -e 's/^\([a-z]\)/\U\1/' <<< "$1" } # -------------------------------------------------------- # # Check and make everything ready # # -------------------------------------------------------- # # Check for help arguments [ "$1" == "-h" -o "$1" == "--help" ] && \ { print_usage; exit 0; } # Ensure we're in the right directory [ -d "src" ] || \ { echo >&2 "Please run from project root directory"; exit 1; } # Sanity check on [ -z "$objname" ] && \ { print_usage; exit 1; } grep -q '^gv-' <<< "$objname" || \ { echo >&2 "'$objname' should start with 'gv-' prefix"; exit 1; } [ -z "$(name_get_invalid_chars "$objname")" ] || \ { echo >&2 "'$objname' contains invalid characters"; exit 1; } # Notice that has no 'gv-' prefix from now on. # We explicitely add it when needed. objname="$(sed 's/^gv-//' <<< $objname)" # -------------------------------------------------------- # # Copy skeleton files to source directory # # -------------------------------------------------------- # # Select source depending on the type srcdir="scripts/code/gv-object-templates" srcfile="" case "$type" in core|framework|ui) srcfile=gv-dummy;; feat) srcfile=gv-feature-dummy;; esac [ -z "$srcfile" ] && \ { print_usage; exit 1; } # Ensure that destination files don't exist dstdir="" case "$type" in core) dstdir="src/core";; framework) dstdir="src/framework";; ui) dstdir="src/ui";; feat) dstdir="src/feat";; esac [ -z "$dstdir" ] && \ { print_usage; exit 1; } dstfile=gv-$objname [ -e $dstdir/$dstfile.c ] && { echo >&2 "$dstdir/$dstfile.c already exists"; exit 1; } [ -e $dstdir/$dstfile.h ] && { echo >&2 "$dstdir/$dstfile.h already exists"; exit 1; } # Copy files cp "$srcdir/$srcfile.c" "$dstdir/$dstfile.c" cp "$srcdir/$srcfile.h" "$dstdir/$dstfile.h" # -------------------------------------------------------- # # String substitutions # # -------------------------------------------------------- # # Customization for ui files if [ "$type" == "ui" ]; then # Replace 'core' by 'ui' in the include path sed -i \ -e "s|core/gv-dummy|ui/gv-dummy|" \ $dstdir/$dstfile.c fi # Customization for framework files if [ "$type" == "framework" ]; then # Replace 'core' by 'framework' in the include path sed -i \ -e "s|core/gv-dummy|framework/gv-dummy|" \ $dstdir/$dstfile.c fi # Special customization for ui files if [ "$type" == "ui" ]; then # Add gtk include sed -i \ -e "//a #include " \ $dstdir/$dstfile.c $dstdir/$dstfile.h # Return a GtkWidget sed -i \ -e "s/^GvDummy \*/GtkWidget */"\ $dstdir/$dstfile.c $dstdir/$dstfile.h fi # Replace 'gobject' and variants by another parent if [ -n "$parentname" ]; then upper="$(name_to_upper $parentname)" upper_pfx="$(cut -d_ -f1 <<< $upper)" upper_end="$(cut -d_ -f2- <<< $upper)" camel="$(name_to_camel $parentname)" sed -i \ -e "s/GObject parent_instance/$camel parent_instance/" \ -e "/^G_DEFINE/s/G_TYPE_OBJECT/${upper_pfx}_TYPE_${upper_end}/" \ $dstdir/$dstfile.c sed -i \ -e "/^G_DECLARE/s/GObject/$camel/" \ $dstdir/$dstfile.h fi # Replace 'dummy' and variants by lower="$(name_to_lower $objname)" upper="$(name_to_upper $objname)" camel="$(name_to_camel $objname)" sed -i \ -e "s/gv-dummy/$dstfile/g" \ -e "s/gv_dummy/gv_$lower/g" \ -e "s/DUMMY/$upper/g" \ -e "s/Dummy/$camel/g" \ -e "s/dummy/$lower/g" \ $dstdir/$dstfile.c $dstdir/$dstfile.h # Fix things ./scripts/code/standard-header.sh add $dstdir/$dstfile.c $dstdir/$dstfile.h ./scripts/code/header-namespace.sh fix $dstdir/$dstfile.h # Done ! echo "Skeleton files '$dstdir/$dstfile.[ch]' created." echo "Don't forget to add these new files to the meson build file'." goodvibes-v0.4.2/scripts/code/gv-object-templates/000077500000000000000000000000001337147241200221455ustar00rootroot00000000000000goodvibes-v0.4.2/scripts/code/gv-object-templates/gv-dummy.c000066400000000000000000000056641337147241200240710ustar00rootroot00000000000000#include #include #include "framework/glib-object-additions.h" #include "framework/log.h" #include "core/gv-dummy.h" /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ // TODO fill with your properties /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * Signals */ /* * GObject definitions */ struct _GvDummyPrivate { // TODO fill with your data }; typedef struct _GvDummyPrivate GvDummyPrivate; struct _GvDummy { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvDummyPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvDummy, gv_dummy, G_TYPE_OBJECT) /* * Helpers */ /* * Signal handlers & callbacks */ /* * Property accessors */ static void gv_dummy_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvDummy *self = GV_DUMMY(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); // TODO handle properties (void) self; (void) value; switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_dummy_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvDummy *self = GV_DUMMY(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); // TODO handle properties (void) self; (void) value; switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ GvDummy * gv_dummy_new(void) { return g_object_new(GV_TYPE_DUMMY, NULL); } /* * GObject methods */ static void gv_dummy_finalize(GObject *object) { GvDummy *self = GV_DUMMY(object); GvDummyPrivate *priv = self->priv; TRACE("%p", object); // TODO job to be done (void) priv; /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_dummy, object); } static void gv_dummy_constructed(GObject *object) { GvDummy *self = GV_DUMMY(object); GvDummyPrivate *priv = self->priv; TRACE("%p", object); /* Initialize properties */ // TODO (void) priv; /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_dummy, object); } static void gv_dummy_init(GvDummy *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_dummy_get_instance_private(self); } static void gv_dummy_class_init(GvDummyClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_dummy_finalize; object_class->constructed = gv_dummy_constructed; /* Properties */ object_class->get_property = gv_dummy_get_property; object_class->set_property = gv_dummy_set_property; // TODO define your properties here // use GV_PARAM_DEFAULT_FLAGS g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/scripts/code/gv-object-templates/gv-dummy.h000066400000000000000000000005021337147241200240600ustar00rootroot00000000000000#ifndef __GV_DUMMY_H__ #define __GV_DUMMY_H__ #include /* GObject declarations */ #define GV_TYPE_DUMMY gv_dummy_get_type() G_DECLARE_FINAL_TYPE(GvDummy, gv_dummy, GV, DUMMY, GObject) /* Data types */ /* Methods */ GvDummy *gv_dummy_new(void); /* Property accessors */ #endif /* __GV_DUMMY_H__ */ goodvibes-v0.4.2/scripts/code/gv-object-templates/gv-feature-dummy.c000066400000000000000000000022041337147241200255050ustar00rootroot00000000000000#include #include #include "framework/log.h" #include "framework/gv-feature.h" #include "core/gv-core.h" #include "feat/gv-dummy.h" /* * GObject definitions */ struct _GvDummy { /* Parent instance structure */ GvFeature parent_instance; }; G_DEFINE_TYPE(GvDummy, gv_dummy, GV_TYPE_FEATURE) /* * Helpers */ /* * Feature methods */ static void gv_dummy_disable(GvFeature *feature) { GvPlayer *player = gv_core_player; // FILL THAT /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_dummy, feature); } static void gv_dummy_enable(GvFeature *feature) { GvPlayer *player = gv_core_player; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_dummy, feature); // FILL THAT /* Signal handlers */ // g_signal_connect_object(player, "blabla", on_blabla, feature, 0); } /* * GObject methods */ static void gv_dummy_init(GvDummy *self) { TRACE("%p", self); } static void gv_dummy_class_init(GvDummyClass *class) { GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GvFeature methods */ feature_class->enable = gv_dummy_enable; feature_class->disable = gv_dummy_disable; } goodvibes-v0.4.2/scripts/code/gv-object-templates/gv-feature-dummy.h000066400000000000000000000004161337147241200255150ustar00rootroot00000000000000#ifndef __GV_DUMMY_H__ #define __GV_DUMMY_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_DUMMY gv_dummy_get_type() G_DECLARE_FINAL_TYPE(GvDummy, gv_dummy, GV, DUMMY, GvFeature) #endif /* __GV_DUMMY_H__ */ goodvibes-v0.4.2/scripts/code/header-namespace.sh000077500000000000000000000044341337147241200220170ustar00rootroot00000000000000#!/bin/bash -e source $(dirname $0)/lib-git.sh print_usage() { echo "Usage: $0 [...]" echo echo "Commands:" echo " check Check if the headers have proper ifdef directives" echo " fix Fix the namespace in the ifdef directives." echo " Assume that ifdef directives are already there," echo " and that only the namespace needs to be fixed." } make_namespace() { local file="$1" local bn=$(basename "$file" | tr '\-.' _ | tr '[:lower:]' '[:upper:]') # Take out the first directory in the name local first_dir=$(echo $file | cut -d/ -f1) # Goodvibes sources if [ $first_dir == src ]; then local next_dirs=$(echo $file | cut -d/ -f2- | xargs dirname) if [ $next_dirs == '.' ]; then echo __GOODVIBES_${bn}__ else local dn=$(echo $next_dirs | tr '/' '_' | tr '[:lower:]' '[:upper:]') echo __GOODVIBES_${dn}_${bn}__ fi # Anything else else local first_dir_up=$(echo $first_dir | tr '\-.' _ | tr '[:lower:]' '[:upper:]') echo __${first_dir_up}_${bn}__ fi } do_check() { local file="$1" local ns=$(make_namespace "$file") #echo ">> $file" #echo ">> $ns" if ! git_is_tracked $file; then return 0 fi if [ $(grep $ns "$file" -c) -eq 3 ]; then return 0 else return 1 fi } do_fix() { local file="$1" local ns=$(make_namespace "$file") # Assume the ifdef directives are already there sed -i \ -e "s:#ifndef __.*:#ifndef $ns:" \ -e "s:#define __.*:#define $ns:" \ -e "s:#endif /\* __.*:#endif /* $ns */:" \ $file } # Check for help arguments [ "$1" == "-h" -o "$1" == "--help" ] && \ { print_usage; exit 0; } # Check for proper usage [ $# -lt 1 ] && \ { print_usage; exit 1; } # Ensure we're in the right directory [ -d "src" ] || \ { echo >&2 "Please run from root directory"; exit 1; } # File list if [ $# -eq 1 ]; then files=$(find libcaphe src -name \*.h) else files="${@:2}" fi # Do the job case $1 in check) for f in $files; do if ! do_check $f; then echo "'$f': invalid namespace." fi done ;; fix) for f in $files; do if do_check $f; then continue; fi echo "'$f': fixing namespace." do_fix $f done ;; *) print_usage exit 1 ;; esac goodvibes-v0.4.2/scripts/code/indent.sh000077500000000000000000000053051337147241200201140ustar00rootroot00000000000000#!/bin/bash MODE="$1" FILES="" C_FILES="" H_FILES="" source $(dirname $0)/lib-git.sh print_usage() { echo "Usage: $0 [] []" echo echo "Modes:" echo " all Indent the whole source tree" echo " files Indent files given in argument" echo " staged Indent git staged files" } do_remove_untracked_files() { FILES_ORIG="$FILES" FILES="" for file in $FILES_ORIG; do if git_is_tracked $file; then FILES="$FILES $file" fi done } do_remove_nonexisting_files() { FILES_ORIG="$FILES" FILES="" for file in $FILES_ORIG; do if [ -f $file ]; then FILES="$FILES $file" fi done } do_split_files() { for file in $FILES; do if [[ $file = *.c ]]; then C_FILES="$C_FILES $file" elif [[ $file = *.h ]]; then H_FILES="$H_FILES $file" fi done } do_indent() { if [ -z "$FILES" ]; then echo "No input files" return fi do_split_files echo ">>> Removing trailing whitespaces..." sed -i 's/[ \t]*$//' $FILES # A few words about options... # # - 'pad-oper' can't be used, since it breaks GQuark definition. # For example: # G_DEFINE_QUARK(gszn-error-quark, gszn_error) # becomes: # G_DEFINE_QUARK(gszn - error - quark, gszn_error) # # - 'indent-preproc-define' broke one file lately, so good-bye. if [ -n "$C_FILES" ]; then echo ">>> Indenting code..." astyle --suffix=none \ --formatted \ --style=linux \ --indent=tab=8 \ --indent-labels \ --pad-header \ --pad-comma \ --align-pointer=name \ --convert-tabs \ --max-code-length=100 \ $C_FILES fi if [ -n "$H_FILES" ]; then echo ">>> Indenting headers..." astyle --suffix=none \ --formatted \ --style=linux \ --indent=tab=8 \ --pad-comma \ --align-pointer=name \ --convert-tabs \ --max-code-length=100 \ --max-instatement-indent=100 \ $H_FILES fi } # Check for proper usage [ $# -eq 0 ] && \ { print_usage; exit 0; } # Check for commands command -v astyle >/dev/null 2>&1 || \ { echo >&2 "Please install astyle."; exit 1; } # Do the job case $MODE in all) [ $# -eq 1 ] || { print_usage; exit 1; } FILES="$(find -name '*.[ch]' | tr '\n' ' ')" do_remove_untracked_files do_indent ;; files) FILES="${@:2}" do_remove_nonexisting_files do_indent ;; staged) [ $# -eq 1 ] || { print_usage; exit 1; } FILES="$(git_list_staged)" do_indent ;; *) print_usage exit 1 ;; esac goodvibes-v0.4.2/scripts/code/lib-git.sh000066400000000000000000000011561337147241200201570ustar00rootroot00000000000000# Git utility functions GIT_TRACKED="" GIT_TRACKED_FILLED=0 git_is_tracked() { local file="$1" # Fill list of tracked files only once if [ $GIT_TRACKED_FILLED -eq 0 ]; then GIT_TRACKED="$(git ls-files)" GIT_TRACKED_FILLED=1 #echo "--------" #echo "$GIT_TRACKED" #echo "--------" fi # Remove leading './' from filename if any file="$(echo $file | sed 's:^\.\/::')" echo "$GIT_TRACKED" | grep -q "$file" } git_has_changes() { local file="$1" ! git diff-files --quiet "$file" } git_list_staged() { git diff --cached --name-only --diff-filter=d | \ tr '\n' ' ' } goodvibes-v0.4.2/scripts/code/standard-header.sh000077500000000000000000000044001337147241200216540ustar00rootroot00000000000000#!/bin/bash -e SPDX_LICENSE_ID=GPL-3.0-or-later source $(dirname $0)/lib-git.sh print_usage() { cat << EOF Usage: $(basename $0) [...] Tool to check/add standard headers in C files. Commands: check Check if source files have a standard header. add If standard header is missing, add it. EOF } unit_name() { local file="$1" if echo $file | grep -q libcaphe; then echo "Libcaphe" else echo "Goodvibes Radio Player" fi } do_check() { local file="$1" local unit="$(unit_name "$file")" if ! git_is_tracked $file; then return 0 fi head -n 4 "$file" | tr -d '\n' | \ grep -q -F "/* * $unit * * Copyright (C)" } do_add() { local file="$1" local unit="$(unit_name "$file")" cat << EOF > "$file" /* * $unit * * Copyright (C) $(date +%Y) $(git config user.name) * * SPDX-License-Identifier: ${SPDX_LICENSE_ID:?} * * 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 . */ $(cat $file) EOF } # Check for help arguments [ "$1" == "-h" -o "$1" == "--help" ] && \ { print_usage; exit 0; } # Check for proper usage [ $# -lt 1 ] && \ { print_usage; exit 1; } # Ensure we're in the right directory [ -d "src" ] || \ { echo >&2 "Please run from root directory"; exit 1; } # File list if [ $# -eq 1 ]; then files=$(find libcaphe src -name \*.[ch]) else files="${@:2}" fi # Do the job case $1 in check) for f in $files; do if ! do_check $f; then echo "'$f': no standard header found." fi done ;; add) for f in $files; do if do_check $f; then continue; fi echo "'$f': adding standard header." do_add $f done ;; *) print_usage exit 1 ;; esac goodvibes-v0.4.2/scripts/install-git-hook.sh000077500000000000000000000003641337147241200211060ustar00rootroot00000000000000#!/bin/bash set -e set -u cd "$MESON_SOURCE_ROOT" [ -f .git/hooks/pre-commit.sample ] || exit 0 [ ! -f .git/hooks/pre-commit ] || exit 0 install -m0755 .git/hooks/pre-commit.sample .git/hooks/pre-commit echo 'Git pre-commit hook installed' goodvibes-v0.4.2/scripts/meson/000077500000000000000000000000001337147241200165005ustar00rootroot00000000000000goodvibes-v0.4.2/scripts/meson/post-install000077500000000000000000000010701337147241200210550ustar00rootroot00000000000000#!/bin/sh set -e DATADIR="$1" # Package managers set this so we don't need to run if [ "$DESTDIR" ]; then exit 0; fi cmdcheck() { command -v "$1" >/dev/null 2>&1; } if cmdcheck glib-compile-schemas; then echo "Compiling GSettings schemas..." glib-compile-schemas "$DATADIR/glib-2.0/schemas" fi if cmdcheck gtk-update-icon-cache; then echo "Updating icon cache..." gtk-update-icon-cache -t -f "$DATADIR/icons/hicolor" fi if cmdcheck update-desktop-database; then echo "Updating desktop database..." update-desktop-database "$DATADIR/applications" fi goodvibes-v0.4.2/scripts/meson/validate-ui-file000077500000000000000000000005221337147241200215460ustar00rootroot00000000000000#!/bin/sh set -e UIFILE="$1" SKIP=77 # the Meson test harness recognises exit code 77 as skip cmdcheck() { command -v "$1" >/dev/null 2>&1; } # Skip test if there's no graphics [ "$DISPLAY" ] || exit $SKIP # Skip test if gtk-builder-tool is not installed cmdcheck gtk-builder-tool || exit $SKIP gtk-builder-tool validate "$UIFILE" goodvibes-v0.4.2/scripts/rebuild-images.sh000077500000000000000000000040021337147241200206030ustar00rootroot00000000000000#!/bin/bash set -e set -u CMD="${1:-}" APPID=io.gitlab.Goodvibes SVGDIR=data/icons/src ICONDIR=data/icons/hicolor SITEDIR=docs/goodvibes.readthedocs.io usage() { echo "Usage: $(basename $0) " echo echo "This script is used to re-build various images out of the svg sources" echo exit 0 } fail() { echo >&2 "$@" exit 1 } checkcmd() { command -v $1 >/dev/null 2>&1 || fail "Command '$1' is not installed, aborting" } checkdir() { [ -d "$1" ] || fail "Directory '$1' does not exist, aborting" } do_icons() { checkdir $ICONDIR echo '--- Building small icons ---' for size in 16 22 24 32 48; do inkscape \ --export-area-page \ --export-width $size \ --export-png $ICONDIR/${size}x${size}/apps/$APPID.png \ $SVGDIR/goodvibes-small.svg done echo '--- Building large icons ---' for size in 256 512; do inkscape \ --export-area-page \ --export-width $size \ --export-png $ICONDIR/${size}x${size}/apps/$APPID.png \ $SVGDIR/goodvibes-large.svg done echo '--- Copying symbolic icons ---' cp $SVGDIR/goodvibes-symbolic.svg $ICONDIR/symbolic/apps/$APPID-symbolic.svg } do_site() { checkdir $SITEDIR echo '--- Building favicon ---' tmpdir=$(mktemp --directory --tmpdir=$(pwd) favicon.XXXXXX) trap "rm -fr $tmpdir" EXIT for size in 16 24 32 48 64; do inkscape \ --export-area-page \ --export-width $size \ --export-png $tmpdir/$size.png \ $SVGDIR/goodvibes-favicon.svg done convert $tmpdir/*.png $SITEDIR/images/favicon.ico identify $SITEDIR/images/favicon.ico echo '--- Building logo ---' inkscape \ --export-area-page \ --export-width 192 \ --export-png $SITEDIR/images/goodvibes.png \ $SVGDIR/goodvibes-large.svg } checkcmd convert checkcmd identify checkcmd inkscape checkdir $SVGDIR case $CMD in icons) do_icons ;; site) do_site ;; *) usage ;; esac goodvibes-v0.4.2/scripts/test/000077500000000000000000000000001337147241200163365ustar00rootroot00000000000000goodvibes-v0.4.2/scripts/test/hotkey.sh000077500000000000000000000005701337147241200202020ustar00rootroot00000000000000#!/bin/sh command -v xdotool >/dev/null 2>&1 || \ { echo >&2 "Please install xdotool."; exit 1; } case $1 in play) key=XF86AudioPlay ;; pause) key=XF86AudioPause ;; stop) key=XF86AudioStop ;; prev|previous) key=XF86AudioPrev ;; next) key=XF86AudioNext ;; *) echo >&2 "Usage: $0 " exit 1 ;; esac xdotool key $key goodvibes-v0.4.2/scripts/test/mpris2.sh000077500000000000000000000042501337147241200201120ustar00rootroot00000000000000#!/bin/bash MPRIS_OBJ='org.mpris.MediaPlayer2.Goodvibes' MPRIS_PATH='/org/mpris/MediaPlayer2' MPRIS_IFACE='org.mpris.MediaPlayer2' print_usage() { echo "Usage: $0 [options]" echo "" echo "Commands:" echo " introspect" echo " call [type:arg] [type:arg] ..." echo " get " echo " set " echo " watch-signals" echo "" echo "Examples:" echo " $0 call Player Play" echo " $0 call TrackList GoTo objpath:\"/io/gitlab/Goodvibes/TrackList/0x55be33e73a60\"" echo " $0 call TrackList GetTracksMetadata array:objpath:\"/io/gitlab/Goodvibes/TrackList/0x56502c272620\",\"/io/gitlab/Goodvibes/TrackList/0x56502c272660\"" } iface_real() { SFX="" case $1 in Root) SFX="";; Player|TrackList) SFX=".$1";; *) echo >&2 "Wrong mpris iface" exit 1 esac echo $SFX } case $1 in introspect) dbus-send --print-reply=literal \ --dest=$MPRIS_OBJ \ $MPRIS_PATH \ org.freedesktop.DBus.Introspectable.Introspect | tr \" \' ;; call) IFACE=$2 METHOD=$3 dbus-send --print-reply=literal \ --type=method_call \ --dest=$MPRIS_OBJ \ $MPRIS_PATH \ $MPRIS_IFACE$(iface_real $IFACE).$METHOD \ ${@:4} ;; get) IFACE=$2 PROP=$3 dbus-send --print-reply=literal \ --dest=$MPRIS_OBJ \ $MPRIS_PATH \ org.freedesktop.DBus.Properties.Get \ string:"$MPRIS_IFACE$(iface_real $IFACE)" \ string:"$PROP" ;; set) IFACE=$2 PROP=$3 TYPEVALUE=$4 dbus-send --print-reply=literal \ --dest=$MPRIS_OBJ \ $MPRIS_PATH \ org.freedesktop.DBus.Properties.Set \ string:"$MPRIS_IFACE$(iface_real $IFACE)" \ string:"$PROP" variant:$TYPEVALUE ;; watch-signals) dbus-monitor "type='signal',sender='$MPRIS_OBJ'" ;; *) print_usage exit 1 ;; esac goodvibes-v0.4.2/scripts/translators.sh000077500000000000000000000031741337147241200202770ustar00rootroot00000000000000#!/bin/bash -e # Print the list of translators LCS=() LANGS=() NAMES=() EMAILS=() fail() { echo >&2 "$@" exit 1 } usage() { echo "$0 " exit 1 } get_translators() { for file in po/*.po; do lc=$(basename $file | cut -d'.' -f1) line=$(grep '^"Language-Team' $file) value=$(echo $line | cut -d':' -f2- | sed 's/^ *//') lang=$(echo $value | cut -d'<' -f1 | sed 's/ *$//') line=$(grep '^"Last-Translator' $file) value=$(echo $line | cut -d':' -f2- | sed 's/^ *//') name=$(echo $value | cut -d'<' -f1 | sed 's/ *$//') email=$(echo $value | cut -d'<' -f2- | cut -d'>' -f1) LCS+=("${lc:?}") LANGS+=("${lang:?}") NAMES+=("${name:?}") EMAILS+=("${email:?}") #echo "$name <$email> ($lc) ($lang)" done } output_code() { # C style output, to be pasted directly in the code local len=${#NAMES[@]} for ((i=0; i<$len; ++i)); do local lc=${LCS[$i]} local lang=${LANGS[$i]} local name=${NAMES[$i]} local email=${EMAILS[$i]} [ $i -lt $(expr $len - 1) ] && \ echo -e "\t\"$name <$email> - $lang ($lc)\\\n\" \\" || \ echo -e "\t\"$name <$email> - $lang ($lc)\";" done } output_doc() { # rst output local len=${#NAMES[@]} for ((i=0; i<$len; ++i)); do local lc=${LCS[$i]} local lang=${LANGS[$i]} local name=${NAMES[$i]} local email=${EMAILS[$i]} echo " * $name - $lang ($lc)" done } [ -d "po" ] || fail "'po' directory not found." [ $# -eq 1 ] || usage get_translators case $1 in code) echo "-------- 8< --------" output_code echo "-------- >8 --------" ;; doc) echo "-------- 8< --------" output_doc echo "-------- >8 --------" ;; *) usage ;; esac goodvibes-v0.4.2/src/000077500000000000000000000000001337147241200144575ustar00rootroot00000000000000goodvibes-v0.4.2/src/client.c000066400000000000000000000457071337147241200161160ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include #include "framework/config.h" /* http://misc.flogisoft.com/bash/tip_colors_and_formatting */ #define ESC "\033" #define BOLD_CODE "[1m" #define RESET_CODE "[0m" #define BOLD(str) ESC BOLD_CODE str ESC RESET_CODE #define print(fmt, ...) fprintf(stdout, fmt"\n", ##__VA_ARGS__) #define print_err(fmt, ...) fprintf(stderr, fmt"\n", ##__VA_ARGS__) /* * Help */ const char *app_name; void help_init(const char *str) { app_name = str; } void help_and_exit(int exit_code) { #define REVISION(name) print("%s (version " PACKAGE_VERSION ")", name); #define USAGE(name) print("Usage: %s []", name); #define TITLE(str) print(BOLD(str ":")) #define COMMAND(cmd, desc) print(BOLD(" %-32s") "%s", cmd, desc) #define DESC(desc) print(" %-32s%s", "", desc) #define NL() print(""); REVISION(app_name); USAGE(app_name); NL(); TITLE ("Base commands"); COMMAND("launch", "Launch " GV_NAME_CAPITAL); COMMAND("quit", "Quit " GV_NAME_CAPITAL); COMMAND("is-running", "Check whether " GV_NAME_CAPITAL " is running"); COMMAND("help", "Print this help message"); NL(); TITLE ("Control"); print (". can be the station name or uri"); COMMAND("play []", "Without argument, play the current station"); DESC ("Otherwise, play the station given in argument"); COMMAND("stop", "Stop playback"); COMMAND("play-stop", "Toggle play/stop mode"); COMMAND("next", "Play next station"); COMMAND("prev(ious)", "Play previous station"); COMMAND("volume []", "Get/set volume (in %)"); COMMAND("mute [true/false]", "Get/set mute state"); COMMAND("repeat [true/false]", "Get/set repeat"); COMMAND("shuffle [true/false]", "Get/set shuffle"); COMMAND("current", "Get info on current station"); COMMAND("playing", "Get playback status"); NL(); TITLE ("Station list"); print (". can be the station name or uri"); COMMAND("list", "Display the list of stations"); COMMAND("add [] [[first/last] [before/after ]]", ""); DESC ("Add a station to the list"); COMMAND("remove ", "Remove a station from the list"); COMMAND("rename ", "Rename a station"); COMMAND("move [[first/last] [before/after ]]", ""); DESC ("Move a station in the list"); NL(); TITLE ("Configuration"); print (". sections: core, ui, feat."); COMMAND("conf get

", "Get a config value"); COMMAND("conf set
", "Set a config value"); COMMAND("conf list-keys
", "List config keys"); COMMAND("conf describe
", "Describe a config key"); exit(exit_code); } /* * DBus */ #define DBUS_NAME GV_APPLICATION_ID #define DBUS_PATH GV_APPLICATION_PATH #define DBUS_ROOT_IFACE GV_APPLICATION_ID #define DBUS_PLAYER_IFACE DBUS_ROOT_IFACE ".Player" #define DBUS_STATIONS_IFACE DBUS_ROOT_IFACE ".Stations" int dbus_call(const char *bus_name, const char *object_path, const char *iface_name, const char *method_name, GVariant *args, GVariant **output) { GDBusConnection *c; GVariant *result; GError *error = NULL; if (output) *output = NULL; c = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); if (c == NULL) { print_err("DBus connection error: %s", error->message); g_error_free(error); return -1; } result = g_dbus_connection_call_sync(c, bus_name, object_path, iface_name, method_name, args, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error) { if (error->domain == G_DBUS_ERROR && error->code == G_DBUS_ERROR_NAME_HAS_NO_OWNER) { /* Goodvibes is not running */ print_err(GV_NAME_CAPITAL " is not running !"); } else { /* Other error, just dump the GError */ print_err("DBus call error: %s", error->message); } g_error_free(error); return -1; } g_dbus_connection_close(c, NULL, NULL, NULL); if (output) *output = result; else if (result) g_variant_unref(result); return 0; } /* * Supported DBus commands */ enum cmd_type { METHOD, PROPERTY }; struct cmd { enum cmd_type type; const char *cmdline_name; const char *dbus_name; int (*parse_args) (int, char *[], GVariantBuilder *); void (*print_result) (GVariant *); }; struct interface { const char *name; const struct cmd *cmds; }; int parse_play_args(int argc, char *argv[], GVariantBuilder *b) { const char *station; if (argc == 0) station = ""; else if (argc == 1) station = argv[0]; else return -1; g_variant_builder_add(b, "s", station); return 0; } int parse_add_args(int argc, char *argv[], GVariantBuilder *b) { const char *station_uri; const char *station_name; const char *around_keyword; const char *around_station; if (argc == 0) return -1; station_uri = argv[0]; argc--; argv++; station_name = ""; if (argc > 0) { if (strcmp(argv[0], "first") && strcmp(argv[0], "last") && strcmp(argv[0], "before") && strcmp(argv[0], "after")) { station_name = argv[0]; argc--; argv++; } } around_keyword = ""; around_station = ""; if (argc == 0) { /* Nothing to do */ } else if (argc == 1) { if (!strcmp(argv[0], "first") || !strcmp(argv[0], "last")) around_keyword = argv[0]; else return -1; } else if (argc == 2) { if (!strcmp(argv[0], "before") || !strcmp(argv[0], "after")) { around_keyword = argv[0]; around_station = argv[1]; } else { return -1; } } else { return -1; } g_variant_builder_add(b, "s", station_uri); g_variant_builder_add(b, "s", station_name); g_variant_builder_add(b, "s", around_keyword); g_variant_builder_add(b, "s", around_station); return 0; } int parse_remove_args(int argc, char *argv[], GVariantBuilder *b) { const char *station; if (argc != 1) return -1; station = argv[0]; g_variant_builder_add(b, "s", station); return 0; } int parse_rename_args(int argc, char *argv[], GVariantBuilder *b) { const char *station; const char *new_name; if (argc != 2) return -1; station = argv[0]; new_name = argv[1]; g_variant_builder_add(b, "s", station); g_variant_builder_add(b, "s", new_name); return 0; } int parse_move_args(int argc, char *argv[], GVariantBuilder *b) { const char *station; const char *where; const char *around_station; if (argc < 2) return -1; station = argv[0]; where = argv[1]; argc -= 2; argv += 2; around_station = ""; if (argc == 0) { if (strcmp(where, "first") && strcmp(where, "last")) return -1; } else if (argc == 1) { if (!strcmp(where, "before") || !strcmp(where, "after")) around_station = argv[0]; else return -1; } else { return -1; } g_variant_builder_add(b, "s", station); g_variant_builder_add(b, "s", where); g_variant_builder_add(b, "s", around_station); return 0; } int parse_boolean(int argc, char *argv[], GVariantBuilder *b) { gboolean value; if (argc != 1) return -1; if (!g_strcmp0(argv[0], "true")) value = TRUE; else if (!g_strcmp0(argv[0], "false")) value = FALSE; else return -1; g_variant_builder_add(b, "v", g_variant_new_boolean(value)); return 0; } int parse_volume(int argc, char *argv[], GVariantBuilder *b) { long int value; char *endptr; if (argc != 1) return -1; value = strtol(argv[0], &endptr, 10); if (*endptr != '\0') return -1; g_variant_builder_add(b, "v", g_variant_new_uint32(value)); return 0; } void print_boolean(GVariant *result) { gboolean bool; bool = g_variant_get_boolean(result); if (bool) print("true"); else print("false"); } void print_volume(GVariant *result) { guint volume; volume = g_variant_get_uint32(result); print("%u%%", volume); } void print_current(GVariant *result) { GVariantIter *iter; GVariant *value; gchar *key; gchar *uri = NULL; gchar *name = NULL; gchar *artist = NULL; gchar *title = NULL; gchar *album = NULL; gchar *genre = NULL; gchar *year = NULL; gchar *comment = NULL; g_variant_get(result, "a{sv}", &iter); while (g_variant_iter_loop(iter, "{sv}", &key, &value)) { if (!g_strcmp0(key, "uri")) g_variant_get(value, "s", &uri); else if (!g_strcmp0(key, "name")) g_variant_get(value, "s", &name); else if (!g_strcmp0(key, "artist")) g_variant_get(value, "s", &artist); else if (!g_strcmp0(key, "title")) g_variant_get(value, "s", &title); else if (!g_strcmp0(key, "album")) g_variant_get(value, "s", &album); else if (!g_strcmp0(key, "genre")) g_variant_get(value, "s", &genre); else if (!g_strcmp0(key, "year")) g_variant_get(value, "s", &year); else if (!g_strcmp0(key, "comment")) g_variant_get(value, "s", &comment); } g_variant_iter_free(iter); /* Radio name and URI */ print(BOLD("%s")"%s(%s)", name, name ? "\t" : "", uri); /* Artist and title */ if (artist || title) print(BOLD("> %s%s%s"), title ? title : "", title && artist ? " - " : "", artist ? artist : ""); /* Album, year and genre */ if (genre) { gchar *tmp = genre; genre = g_strdup_printf("(%s)", tmp); g_free(tmp); } if (album || year || genre) print("%s%s%s%s%s", album ? album : "", album && year ? " " : "", year ? year : "", (album && genre) || (year && genre) ? " - " : "", genre ? genre : ""); /* Freedom for the braves */ g_free(uri); g_free(name); g_free(artist); g_free(title); g_free(album); g_free(genre); g_free(year); g_free(comment); } void print_list_result(GVariant *result) { GVariantIter *iter1; GVariantIter *iter2; GVariant *value; gchar *key; g_variant_get(result, "(aa{sv})", &iter1); while (g_variant_iter_loop(iter1, "a{sv}", &iter2)) { gchar *uri = NULL; gchar *name = NULL; while (g_variant_iter_loop(iter2, "{sv}", &key, &value)) { if (!g_strcmp0(key, "uri")) g_variant_get(value, "s", &uri); else if (!g_strcmp0(key, "name")) g_variant_get(value, "s", &name); } print(BOLD("%-20s") "%s", name ? name : "", uri); g_free(uri); g_free(name); } g_variant_iter_free(iter1); } struct cmd root_cmds[] = { { METHOD, "quit", "Quit", NULL, NULL }, { METHOD, NULL, NULL, NULL, NULL } }; struct cmd player_cmds[] = { { METHOD, "play", "Play", parse_play_args, NULL }, { METHOD, "stop", "Stop", NULL, NULL }, { METHOD, "play-stop", "PlayStop", NULL, NULL }, { METHOD, "next", "Next", NULL, NULL }, { METHOD, "prev", "Previous", NULL, NULL }, { METHOD, "previous", "Previous", NULL, NULL }, { PROPERTY, "current", "Current", NULL, print_current }, { PROPERTY, "playing", "Playing", NULL, print_boolean }, { PROPERTY, "repeat", "Repeat", parse_boolean, print_boolean }, { PROPERTY, "shuffle", "Shuffle", parse_boolean, print_boolean }, { PROPERTY, "volume", "Volume", parse_volume, print_volume }, { PROPERTY, "mute", "Mute", parse_boolean, print_boolean }, { PROPERTY, NULL, NULL, NULL, NULL } }; struct cmd stations_cmds[] = { { METHOD, "list", "List", NULL, print_list_result }, { METHOD, "add", "Add", parse_add_args, NULL }, { METHOD, "remove", "Remove", parse_remove_args, NULL }, { METHOD, "rename", "Rename", parse_rename_args, NULL }, { METHOD, "move", "Move", parse_move_args, NULL }, { METHOD, NULL, NULL, NULL, NULL } }; struct interface interfaces[] = { { DBUS_ROOT_IFACE, root_cmds }, { DBUS_PLAYER_IFACE, player_cmds }, { DBUS_STATIONS_IFACE, stations_cmds }, { NULL, NULL } }; /* * DBus related commands */ static int handle_launch(int argc, char *argv[] G_GNUC_UNUSED) { GVariantBuilder b; GVariant *args; int err; if (argc != 0) help_and_exit(EXIT_FAILURE); g_variant_builder_init(&b, G_VARIANT_TYPE_TUPLE); g_variant_builder_add(&b, "s", DBUS_NAME); g_variant_builder_add(&b, "u", 0); args = g_variant_builder_end(&b); err = dbus_call("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "StartServiceByName", args, NULL); return err; } static int handle_is_running(int argc, char *argv[] G_GNUC_UNUSED) { GVariantBuilder b; GVariant *args, *result; int err; if (argc != 0) help_and_exit(EXIT_FAILURE); g_variant_builder_init(&b, G_VARIANT_TYPE_TUPLE); g_variant_builder_add(&b, "s", DBUS_NAME); args = g_variant_builder_end(&b); result = NULL; err = dbus_call("org.freedesktop.DBus", "/", "org.freedesktop.DBus", "NameHasOwner", args, &result); if (result) { gboolean is_running; g_variant_get(result, "(b)", &is_running); g_variant_unref(result); print("%s", is_running ? "true" : "false"); } return err; } static int handle_dbus_command(int argc, char *argv[]) { struct interface *iface; const struct cmd *cmd; GVariant *args, *result; int err = 0; /* Find command in lists */ for (iface = interfaces; iface->name; iface++) { for (cmd = iface->cmds; cmd->cmdline_name; cmd++) { if (!strcmp(argv[0], cmd->cmdline_name)) break; } if (cmd->cmdline_name) break; } if (iface->name == NULL) help_and_exit(EXIT_FAILURE); /* Discard arguments that has been processed */ argc -= 1; argv += 1; /* Process arguments left */ args = NULL; switch (cmd->type) { case METHOD: /* For methods, if there's a parse function provided, we run it, * no matter the number of arguments left. */ if (cmd->parse_args) { GVariantBuilder b; g_variant_builder_init(&b, G_VARIANT_TYPE_TUPLE); err = cmd->parse_args(argc, argv, &b); args = g_variant_builder_end(&b); } else if (argc > 0) { help_and_exit(EXIT_FAILURE); } break; case PROPERTY: { /* For properties, it's the number of remaining argument which * determines if it's a get or a set. Zero argument means get. */ GVariantBuilder b; g_variant_builder_init(&b, G_VARIANT_TYPE_TUPLE); g_variant_builder_add(&b, "s", iface->name); g_variant_builder_add(&b, "s", cmd->dbus_name); if (argc > 0) { if (cmd->parse_args) err = cmd->parse_args(argc, argv, &b); else help_and_exit(EXIT_FAILURE); } args = g_variant_builder_end(&b); break; } } if (err) help_and_exit(EXIT_FAILURE); /* DBus action (method call, property get/set) */ result = NULL; switch (cmd->type) { case METHOD: err = dbus_call(DBUS_NAME, DBUS_PATH, iface->name, cmd->dbus_name, args, &result); break; case PROPERTY: if (argc == 0) /* Get command */ err = dbus_call(DBUS_NAME, DBUS_PATH, "org.freedesktop.DBus.Properties", "Get", args, &result); else /* Set command */ err = dbus_call(DBUS_NAME, DBUS_PATH, "org.freedesktop.DBus.Properties", "Set", args, NULL); break; } if (err) exit(EXIT_FAILURE); /* Print result */ if (result && cmd->print_result) { // print("%s", g_variant_print(result, FALSE)); if (cmd->type == METHOD) { cmd->print_result(result); } else { /* cmd->type == PROPERTY */ /* Result is always a GVariant, encapsulated in a tuple */ GVariant *tmp; g_variant_get(result, "(v)", &tmp); cmd->print_result(tmp); g_variant_unref(tmp); } } if (result) g_variant_unref(result); return 0; } /* * Configuration related commands * * Here I sse GSettings through a subshell for convenience. * It's a bit ugly, but maybe it's a strong hint that I should rewrite * this client in a script language... */ static void capitalize_first_letters(gchar *str) { char *ptr; if (!str) return; *str = toupper(*str); str++; for (ptr = str; *ptr != '\0'; ptr++) { if (*ptr == '.') { ptr++; if (*ptr != '\0') *ptr = toupper(*ptr); } } } static int handle_conf_command(int argc, char *argv[]) { const gchar *cmd; const gchar *section; const gchar *key; const gchar *value_str; gchar *schema_id; gchar *gsettings_cmd; int success; if (argc < 1) help_and_exit(EXIT_FAILURE); /* Command */ cmd = argv[0]; /* Section */ capitalize_first_letters(argv[1]); section = argv[1]; schema_id = g_strjoin(".", GV_APPLICATION_ID, section, NULL); argc -= 2; argv += 2; /* Process the other arguments */ gsettings_cmd = NULL; if (!strcmp(cmd, "get")) { if (argc != 1) help_and_exit(EXIT_FAILURE); key = argv[0]; gsettings_cmd = g_strdup_printf("gsettings get %s %s", schema_id, key); } else if (!strcmp(cmd, "set")) { if (argc != 2) help_and_exit(EXIT_FAILURE); key = argv[0]; value_str = argv[1]; gsettings_cmd = g_strdup_printf("gsettings set %s %s %s", schema_id, key, value_str); } else if (!strcmp(cmd, "list-keys")) { if (argc != 0) help_and_exit(EXIT_FAILURE); gsettings_cmd = g_strdup_printf("gsettings list-keys %s", schema_id); } else if (!strcmp(cmd, "describe")) { if (argc != 1) help_and_exit(EXIT_FAILURE); key = argv[0]; gsettings_cmd = g_strdup_printf("gsettings describe %s %s", schema_id, key); } else { help_and_exit(EXIT_FAILURE); } success = system(gsettings_cmd); g_free(gsettings_cmd); g_free(schema_id); return success; } int main(int argc, char *argv[]) { int err; err = 0; help_init(argv[0]); if (argc < 2) help_and_exit(EXIT_FAILURE); if (!strcmp(argv[1], "help")) { /* Help command */ help_and_exit(EXIT_SUCCESS); } else if (!strcmp(argv[1], "launch")) { /* Launch commmand */ argc -= 2; argv += 2; err = handle_launch(argc, argv); } else if (!strcmp(argv[1], "is-running")) { /* Launch commmand */ argc -= 2; argv += 2; err = handle_is_running(argc, argv); } else if (!strcmp(argv[1], "conf")) { /* Configuration related commands */ argc -= 2; argv += 2; err = handle_conf_command(argc, argv); } else { /* DBus related command */ argc -= 1; argv += 1; err = handle_dbus_command(argc, argv); } return err ? EXIT_FAILURE : EXIT_SUCCESS; } goodvibes-v0.4.2/src/core/000077500000000000000000000000001337147241200154075ustar00rootroot00000000000000goodvibes-v0.4.2/src/core/gst-additions.c000066400000000000000000000031351337147241200203260ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include /* * Version Information */ const gchar * gst_get_runtime_version_string(void) { static gchar *version_string; if (version_string == NULL) { guint major; guint minor; guint micro; guint nano; gst_version(&major, &minor, µ, &nano); version_string = g_strdup_printf("GStreamer %u.%u.%u.%u", major, minor, micro, nano); } return version_string; } const gchar * gst_get_compile_version_string(void) { static gchar *version_string; if (version_string == NULL) { version_string = g_strdup_printf("GStreamer %u.%u.%u.%u", GST_VERSION_MAJOR, GST_VERSION_MINOR, GST_VERSION_MICRO, GST_VERSION_NANO); } return version_string; } goodvibes-v0.4.2/src/core/gst-additions.h000066400000000000000000000020251337147241200203300ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GST_ADDITIONS_H__ #define __GOODVIBES_FRAMEWORK_GST_ADDITIONS_H__ /* * Gst */ const gchar *gst_get_runtime_version_string(void); const gchar *gst_get_compile_version_string(void); #endif /* __GOODVIBES_FRAMEWORK_GST_ADDITIONS_H__ */ goodvibes-v0.4.2/src/core/gv-core-internal.h000066400000000000000000000021471337147241200207400ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This header contains definitions to be used internally by core files */ #ifndef __GOODVIBES_CORE_GV_CORE_INTERNAL_H__ #define __GOODVIBES_CORE_GV_CORE_INTERNAL_H__ #include /* Global variables */ extern GSettings *gv_core_settings; extern const gchar *gv_core_user_agent; #endif /* __GOODVIBES_CORE_GV_CORE_INTERNAL_H__ */ goodvibes-v0.4.2/src/core/gv-core.c000066400000000000000000000076451337147241200171310ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include "framework/gv-framework.h" #include "core/gv-engine.h" #include "core/gv-player.h" #include "core/gv-station-list.h" #define CORE_SCHEMA_ID_SUFFIX "Core" /* * Public variables */ GApplication *gv_core_application; GSettings *gv_core_settings; GvStationList *gv_core_station_list; GvPlayer *gv_core_player; gchar *gv_core_user_agent; /* * Private variables */ static GvEngine *gv_core_engine; static GList *core_objects; /* * Underlying audio backend */ #include #include "core/gst-additions.h" void gv_core_audio_backend_cleanup(void) { if (gst_is_initialized()) gst_deinit(); } GOptionGroup * gv_core_audio_backend_init_get_option_group(void) { return gst_init_get_option_group(); } const gchar * gv_core_audio_backend_runtime_version_string(void) { return gst_get_runtime_version_string(); } const gchar * gv_core_audio_backend_compile_version_string(void) { return gst_get_compile_version_string(); } /* * Helpers */ static gchar * make_user_agent(void) { const gchar *os = NULL; gchar *agent; /* https://sourceforge.net/p/predef/wiki/OperatingSystems */ #ifdef __gnu_linux__ os = "GNU/Linux"; #elif __linux__ os = "Linux"; #endif if (os) agent = g_strdup_printf("%s/%s (%s)", GV_NAME_CAPITAL, PACKAGE_VERSION, os); else agent = g_strdup_printf("%s/%s", GV_NAME_CAPITAL, PACKAGE_VERSION); return agent; } /* * Core public functions */ void gv_core_quit(void) { g_application_quit(gv_core_application); } void gv_core_configure(void) { GList *item; /* The station list must be loaded before any configuration is done. * Otherwise, configure the player current station will fail. */ gv_station_list_load(gv_core_station_list); /* Configure each object that is configurable */ for (item = core_objects; item; item = item->next) { GObject *object = item->data; if (!GV_IS_CONFIGURABLE(object)) continue; gv_configurable_configure(GV_CONFIGURABLE(object)); } } void gv_core_cleanup(void) { /* Destroy core objects */ core_objects = g_list_reverse(core_objects); g_list_free_full(core_objects, (GDestroyNotify) g_object_unref); /* Clear application pointer */ gv_core_application = NULL; /* Free strings */ g_free(gv_core_user_agent); } void gv_core_init(GApplication *application) { GList *item; /* Create strings */ gv_core_user_agent = make_user_agent(); DEBUG("User agent: %s", gv_core_user_agent); /* Keep a pointer toward application */ gv_core_application = application; /* Create core objects */ gv_core_settings = gv_get_settings(CORE_SCHEMA_ID_SUFFIX); core_objects = g_list_append(core_objects, gv_core_settings); gv_core_station_list = gv_station_list_new(); core_objects = g_list_append(core_objects, gv_core_station_list); gv_core_engine = gv_engine_new(); core_objects = g_list_append(core_objects, gv_core_engine); gv_core_player = gv_player_new(gv_core_engine, gv_core_station_list); core_objects = g_list_append(core_objects, gv_core_player); /* Register objects in the framework */ for (item = core_objects; item; item = item->next) { GObject *object = G_OBJECT(item->data); gv_framework_register_object(object); } } goodvibes-v0.4.2/src/core/gv-core.h000066400000000000000000000033011337147241200171170ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This header contains definitions to be used by core users */ #ifndef __GOODVIBES_CORE_GV_CORE_H__ #define __GOODVIBES_CORE_GV_CORE_H__ #include #include #include "core/gv-metadata.h" #include "core/gv-player.h" #include "core/gv-station.h" #include "core/gv-station-list.h" /* Global variables */ extern GApplication *gv_core_application; extern GvPlayer *gv_core_player; extern GvStationList *gv_core_station_list; /* Functions */ void gv_core_init (GApplication *app); void gv_core_cleanup (void); void gv_core_configure(void); void gv_core_quit (void); /* * Underlying audio backend */ GOptionGroup *gv_core_audio_backend_init_get_option_group (void); void gv_core_audio_backend_cleanup (void); const gchar *gv_core_audio_backend_runtime_version_string(void); const gchar *gv_core_audio_backend_compile_version_string(void); #endif /* __GOODVIBES_CORE_GV_CORE_H__ */ goodvibes-v0.4.2/src/core/gv-engine.c000066400000000000000000000760461337147241200174470ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gst-additions.h" #include "core/gv-core-enum-types.h" #include "core/gv-core-internal.h" #include "core/gv-metadata.h" #include "core/gv-station.h" #include "core/gv-engine.h" /* Uncomment to dump more stuff from GStreamer */ //#define DEBUG_GST_TAGS //#define DEBUG_GST_STATE_CHANGES /* * Properties */ #define DEFAULT_VOLUME 100 #define DEFAULT_MUTE FALSE enum { /* Reserved */ PROP_0, /* Properties - refer to class_init() for more details */ PROP_STATE, PROP_BITRATE, PROP_STATION, PROP_METADATA, PROP_VOLUME, PROP_MUTE, PROP_PIPELINE_ENABLED, PROP_PIPELINE_STRING, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvEnginePrivate { /* GStreamer stuff */ GstElement *playbin; GstBus *bus; /* Properties */ GvEngineState state; guint bitrate; GvStation *station; GvMetadata *metadata; guint volume; gboolean mute; gboolean pipeline_enabled; gchar *pipeline_string; /* Retry on error with a delay */ guint error_count; guint when_timeout_start_playback_id; }; typedef struct _GvEnginePrivate GvEnginePrivate; struct _GvEngine { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvEnginePrivate *priv; }; G_DEFINE_TYPE_WITH_CODE(GvEngine, gv_engine, G_TYPE_OBJECT, G_ADD_PRIVATE(GvEngine) G_IMPLEMENT_INTERFACE(GV_TYPE_ERRORABLE, NULL)) /* * GStreamer helpers */ static void set_gst_state(GstElement *playbin, GstState state) { const gchar *state_name = gst_element_state_get_name(state); GstStateChangeReturn change_return; change_return = gst_element_set_state(playbin, state); switch (change_return) { case GST_STATE_CHANGE_SUCCESS: DEBUG("Setting gst state '%s'... success", state_name); break; case GST_STATE_CHANGE_ASYNC: DEBUG("Setting gst state '%s'... will change async", state_name); break; case GST_STATE_CHANGE_FAILURE: /* This might happen if the uri is invalid */ DEBUG("Setting gst state '%s'... failed !", state_name); break; case GST_STATE_CHANGE_NO_PREROLL: DEBUG("Setting gst state '%s'... no preroll", state_name); break; default: WARNING("Unhandled state change: %d", change_return); break; } } #if 0 static GstState get_gst_state(GstElement *playbin) { GstStateChangeReturn change_return; GstState state, pending; change_return = gst_element_get_state(playbin, &state, &pending, 100 * GST_MSECOND); switch (change_return) { case GST_STATE_CHANGE_SUCCESS: DEBUG("Getting gst state... success"); break; case GST_STATE_CHANGE_ASYNC: DEBUG("Getting gst state... will change async"); break; case GST_STATE_CHANGE_FAILURE: DEBUG("Getting gst state... failed !"); break; case GST_STATE_CHANGE_NO_PREROLL: DEBUG("Getting gst state... no preroll"); break; default: WARNING("Unhandled state change: %d", change_return); break; } DEBUG("Gst state '%s', pending '%s'", gst_element_state_get_name(state), gst_element_state_get_name(pending)); return state; } #endif static guint taglist_get_nominal_bitrate(GstTagList *taglist) { guint bitrate = 0; gst_tag_list_get_uint_index(taglist, GST_TAG_NOMINAL_BITRATE, 0, &bitrate); /* Should I divide by 1000 or 1024 ? */ bitrate /= 1000; return bitrate; } static guint taglist_get_bitrate(GstTagList *taglist) { guint bitrate = 0; gst_tag_list_get_uint_index(taglist, GST_TAG_BITRATE, 0, &bitrate); /* Should I divide by 1000 or 1024 ? */ bitrate /= 1000; return bitrate; } static GvMetadata * taglist_to_metadata(GstTagList *taglist) { GvMetadata *metadata; const gchar *artist = NULL; const gchar *title = NULL; const gchar *album = NULL; const gchar *genre = NULL; const gchar *comment = NULL; GDate *date = NULL; gchar *year = NULL; /* Get info from taglist */ gst_tag_list_peek_string_index(taglist, GST_TAG_ARTIST, 0, &artist); gst_tag_list_peek_string_index(taglist, GST_TAG_TITLE, 0, &title); gst_tag_list_peek_string_index(taglist, GST_TAG_ALBUM, 0, &album); gst_tag_list_peek_string_index(taglist, GST_TAG_GENRE, 0, &genre); gst_tag_list_peek_string_index(taglist, GST_TAG_COMMENT, 0, &comment); gst_tag_list_get_date_index (taglist, GST_TAG_DATE, 0, &date); if (date && g_date_valid(date)) year = g_strdup_printf("%d", g_date_get_year(date)); /* Create new metadata object */ metadata = g_object_new(GV_TYPE_METADATA, "artist", artist, "title", title, "album", album, "genre", genre, "year", year, "comment", comment, NULL); /* Freedom for the braves */ g_free(year); if (date) g_date_free(date); return metadata; } /* * Private methods */ static void gv_engine_reload_pipeline(GvEngine *self) { GvEnginePrivate *priv = self->priv; GstElement *playbin = priv->playbin; gboolean pipeline_enabled = priv->pipeline_enabled; const gchar *pipeline_string = priv->pipeline_string; GstElement *cur_audio_sink = NULL; GstElement *new_audio_sink = NULL; g_return_if_fail(playbin != NULL); /* Get current audio sink */ g_object_get(playbin, "audio-sink", &cur_audio_sink, NULL); DEBUG("Current audio sink: %s", cur_audio_sink ? GST_ELEMENT_NAME(cur_audio_sink) : "null (default)"); /* Create a new audio sink */ if (pipeline_enabled == FALSE || pipeline_string == NULL) { new_audio_sink = NULL; } else { GError *err = NULL; new_audio_sink = gst_parse_launch(pipeline_string, &err); if (err) { WARNING("Failed to parse pipeline description: %s", err->message); gv_errorable_emit_error(GV_ERRORABLE(self), _("%s: %s"), _("Failed to parse pipeline description"), err->message); g_error_free(err); } } DEBUG("New audio sink: %s", new_audio_sink ? GST_ELEMENT_NAME(new_audio_sink) : "null (default)"); /* True when one of them is NULL */ if (cur_audio_sink != new_audio_sink) { gv_engine_stop(self); if (new_audio_sink == NULL) INFO("Setting gst audio sink to default"); else INFO("Setting gst audio sink from pipeline '%s'", pipeline_string); g_object_set(playbin, "audio-sink", new_audio_sink, NULL); } if (cur_audio_sink) gst_object_unref(cur_audio_sink); } /* * Property accessors */ GvEngineState gv_engine_get_state(GvEngine *self) { return self->priv->state; } static void gv_engine_set_state(GvEngine *self, GvEngineState state) { GvEnginePrivate *priv = self->priv; if (priv->state == state) return; priv->state = state; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_STATE]); } guint gv_engine_get_bitrate(GvEngine *self) { return self->priv->bitrate; } static void gv_engine_set_bitrate(GvEngine *self, guint bitrate) { GvEnginePrivate *priv = self->priv; if (priv->bitrate == bitrate) return; priv->bitrate = bitrate; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_BITRATE]); } GvStation * gv_engine_get_station(GvEngine *self) { return self->priv->station; } static void gv_engine_set_station(GvEngine *self, GvStation *station) { GvEnginePrivate *priv = self->priv; if (g_set_object(&priv->station, station)) g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_STATION]); } GvMetadata * gv_engine_get_metadata(GvEngine *self) { return self->priv->metadata; } static void gv_engine_set_metadata(GvEngine *self, GvMetadata *metadata) { GvEnginePrivate *priv = self->priv; /* Compare content */ if (priv->metadata && metadata && gv_metadata_is_equal(priv->metadata, metadata)) { DEBUG("Metadata identical, ignoring..."); return; } /* Assign */ if (g_set_object(&priv->metadata, metadata)) g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_METADATA]); } guint gv_engine_get_volume(GvEngine *self) { return self->priv->volume; } void gv_engine_set_volume(GvEngine *self, guint volume) { GvEnginePrivate *priv = self->priv; gdouble gst_volume; if (volume > 100) volume = 100; if (priv->volume == volume) return; priv->volume = volume; gst_volume = (gdouble) volume / 100.0; gst_stream_volume_set_volume(GST_STREAM_VOLUME(priv->playbin), GST_STREAM_VOLUME_FORMAT_CUBIC, gst_volume); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_VOLUME]); } gboolean gv_engine_get_mute(GvEngine *self) { return self->priv->mute; } void gv_engine_set_mute(GvEngine *self, gboolean mute) { GvEnginePrivate *priv = self->priv; if (priv->mute == mute) return; priv->mute = mute; gst_stream_volume_set_mute(GST_STREAM_VOLUME(priv->playbin), mute); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_MUTE]); } gboolean gv_engine_get_pipeline_enabled(GvEngine *self) { return self->priv->pipeline_enabled; } void gv_engine_set_pipeline_enabled(GvEngine *self, gboolean enabled) { GvEnginePrivate *priv = self->priv; if (priv->pipeline_enabled == enabled) return; priv->pipeline_enabled = enabled; gv_engine_reload_pipeline(self); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_PIPELINE_ENABLED]); } const gchar * gv_engine_get_pipeline_string(GvEngine *self) { return self->priv->pipeline_string; } void gv_engine_set_pipeline_string(GvEngine *self, const gchar *pipeline_string) { GvEnginePrivate *priv = self->priv; if (!g_strcmp0(pipeline_string, "")) pipeline_string = NULL; if (!g_strcmp0(priv->pipeline_string, pipeline_string)) return; g_free(priv->pipeline_string); priv->pipeline_string = g_strdup(pipeline_string); gv_engine_reload_pipeline(self); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_PIPELINE_STRING]); } static void gv_engine_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvEngine *self = GV_ENGINE(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_STATE: g_value_set_enum(value, gv_engine_get_state(self)); break; case PROP_BITRATE: g_value_set_uint(value, gv_engine_get_bitrate(self)); break; case PROP_STATION: g_value_set_object(value, gv_engine_get_station(self)); break; case PROP_METADATA: g_value_set_object(value, gv_engine_get_metadata(self)); break; case PROP_VOLUME: g_value_set_uint(value, gv_engine_get_volume(self)); break; case PROP_MUTE: g_value_set_boolean(value, gv_engine_get_mute(self)); break; case PROP_PIPELINE_ENABLED: g_value_set_boolean(value, gv_engine_get_pipeline_enabled(self)); break; case PROP_PIPELINE_STRING: g_value_set_string(value, gv_engine_get_pipeline_string(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_engine_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvEngine *self = GV_ENGINE(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_VOLUME: gv_engine_set_volume(self, g_value_get_uint(value)); break; case PROP_MUTE: gv_engine_set_mute(self, g_value_get_boolean(value)); break; case PROP_PIPELINE_ENABLED: gv_engine_set_pipeline_enabled(self, g_value_get_boolean(value)); break; case PROP_PIPELINE_STRING: gv_engine_set_pipeline_string(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ void gv_engine_play(GvEngine *self, GvStation *station) { GvEnginePrivate *priv = self->priv; const gchar *station_stream_uri; g_return_if_fail(station != NULL); /* Station must have a stream uri */ station_stream_uri = gv_station_get_first_stream_uri(station); if (station_stream_uri == NULL) { WARNING("Station '%s' has no stream uri", gv_station_get_name_or_uri(station)); return; } /* Cleanup error handling */ priv->error_count = 0; if (priv->when_timeout_start_playback_id) { g_source_remove(priv->when_timeout_start_playback_id); priv->when_timeout_start_playback_id = 0; } /* Set station */ gv_engine_set_station(self, station); /* Clear metadata */ gv_engine_set_metadata(self, NULL); /* According to the doc: * * > State changes to GST_STATE_READY or GST_STATE_NULL never return * > GST_STATE_CHANGE_ASYNC. * * https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/ * GstElement.html#gst-element-set-state */ /* Ensure playback is stopped */ set_gst_state(priv->playbin, GST_STATE_NULL); /* Set the stream uri */ g_object_set(priv->playbin, "uri", station_stream_uri, NULL); /* Go to the ready stop (not sure it's needed) */ set_gst_state(priv->playbin, GST_STATE_READY); /* Set gst state to PAUSE, so that the playbin starts buffering data. * Playback will start as soon as buffering is finished. */ set_gst_state(priv->playbin, GST_STATE_PAUSED); gv_engine_set_state(self, GV_ENGINE_STATE_CONNECTING); } void gv_engine_stop(GvEngine *self) { GvEnginePrivate *priv = self->priv; /* Cleanup error handling */ priv->error_count = 0; if (priv->when_timeout_start_playback_id) { g_source_remove(priv->when_timeout_start_playback_id); priv->when_timeout_start_playback_id = 0; } /* Radical way to stop: set state to NULL */ set_gst_state(priv->playbin, GST_STATE_NULL); gv_engine_set_state(self, GV_ENGINE_STATE_STOPPED); } GvEngine * gv_engine_new(void) { return g_object_new(GV_TYPE_ENGINE, NULL); } /* * GStreamer playbin signal handlers */ static void on_playbin_source_setup(GstElement *playbin G_GNUC_UNUSED, GstElement *source, GvEngine *self) { GvEnginePrivate *priv = self->priv; GvStation *station = priv->station; static gchar *default_user_agent; const gchar *user_agent; if (default_user_agent == NULL) { gchar *gst_version; gst_version = gst_version_string(); default_user_agent = g_strdup_printf("%s %s", gv_core_user_agent, gst_version); g_free(gst_version); } user_agent = default_user_agent; if (station) { const gchar *station_user_agent; station_user_agent = gv_station_get_user_agent(station); if (station_user_agent) user_agent = station_user_agent; } g_object_set(source, "user-agent", user_agent, NULL); DEBUG("Source setup with user-agent '%s'", user_agent); } /* * GStreamer bus signal handlers */ static gboolean when_timeout_start_playback(gpointer data) { GvEngine *self = GV_ENGINE(data); GvEnginePrivate *priv = self->priv; if (self->priv->state != GV_ENGINE_STATE_STOPPED) { set_gst_state(priv->playbin, GST_STATE_NULL); set_gst_state(priv->playbin, GST_STATE_READY); set_gst_state(priv->playbin, GST_STATE_PAUSED); gv_engine_set_state(self, GV_ENGINE_STATE_CONNECTING); } priv->when_timeout_start_playback_id = 0; return G_SOURCE_REMOVE; } static void retry_playback(GvEngine *self) { GvEnginePrivate *priv = self->priv; guint delay; /* We retry playback after there's been a failure of some sort. * We don't know what kind of failure, and maybe the network is down, * in such case we don't want to keep retrying in a wild loop. So the * strategy here is to start retrying with a minimal delay, and increment * the delay with the failure count. */ if (priv->when_timeout_start_playback_id) return; g_assert(priv->error_count > 0); delay = priv->error_count - 1; if (delay > 10) delay = 10; INFO("Restarting playback in %u seconds", delay); priv->when_timeout_start_playback_id = g_timeout_add_seconds(delay, when_timeout_start_playback, self); } static gboolean on_bus_message_eos(GstBus *bus G_GNUC_UNUSED, GstMessage *msg G_GNUC_UNUSED, GvEngine *self) { GvEnginePrivate *priv = self->priv; priv->error_count++; WARNING("Gst eos message"); /* Stop immediately otherwise gst keeps on spitting errors */ set_gst_state(priv->playbin, GST_STATE_NULL); /* Restart playback if needed */ if (self->priv->state != GV_ENGINE_STATE_STOPPED) retry_playback(self); /* Emit an error */ //gv_errorable_emit_error(GV_ERRORABLE(self), "%s", _("End of stream")); return TRUE; } static gboolean on_bus_message_error(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self) { GvEnginePrivate *priv = self->priv; GError *error; gchar *debug; priv->error_count++; /* Parse message */ gst_message_parse_error(msg, &error, &debug); /* Display error */ WARNING("Gst bus error msg: %s:%d: %s", g_quark_to_string(error->domain), error->code, error->message); WARNING("Gst bus error debug: %s", debug); /* Cleanup */ g_error_free(error); g_free(debug); /* Stop immediately otherwise gst keeps on spitting errors */ set_gst_state(priv->playbin, GST_STATE_NULL); /* Restart playback on error */ if (self->priv->state != GV_ENGINE_STATE_STOPPED) retry_playback(self); /* Emit an error signal */ //gv_errorable_emit_error(GV_ERRORABLE(self), "GStreamer error: %s", error->message); return TRUE; /* Here are some gst error messages that I've dealt with so far. * * GST_RESOURCE_ERROR: GST_RESOURCE_ERROR_NOT_FOUND * * Could not resolve server name. * * This might happen for different reasons: * - wrong url, the protocol (http, likely) is correct, but the name of * the server is wrong. * - name resolution is down. * - network is down. * * To reproduce: * - in the url, set a wrong server name. * - just disconnect from the network. * * File Not Found * * It means that the protocol (http, lilely) is correct, the server * name as well, but the file is wrong or couldn't be found. I guess * the server could be reached and replied something like "file not found". * * To reproduce: at the end of the url, set a wrong filename * * Not Available * * Don't remember about this one... * * GST_CORE_ERROR: GST_CORE_ERROR_MISSING_PLUGIN * * No URI handler implemented for ... * * It means that the protocol is wrong or unsupported. * * To reproduce: in the url, replace 'http' by some random stuff * * Your GStreamer installation is missing a plug-in. * * It means that gstreamer doesn't know what to do with what it received * from the server. So it's likely that the server didn't send the stream * expected. And it's even more likely that the server returned an html * page, because that's what servers usually do. * Some cases where it can happen: * - wrong url, that exists but is not a stream. * - outgoing requests are blocked, and return an html page. For example, * you're connected to a public Wi-Fi that expects you to authenticate * or agree the terms of use, before allowing outgoing traffic. * * To reproduce: in the url, remove anything after the server name */ } static gboolean on_bus_message_warning(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self G_GNUC_UNUSED) { GError *warning; gchar *debug; /* Parse message */ gst_message_parse_warning(msg, &warning, &debug); /* Display warning */ DEBUG("Gst bus warning msg: %s:%d: %s", g_quark_to_string(warning->domain), warning->code, warning->message); DEBUG("Gst bus warning debug : %s", debug); /* Cleanup */ g_error_free(warning); g_free(debug); return TRUE; } static gboolean on_bus_message_info(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self G_GNUC_UNUSED) { GError *info; gchar *debug; /* Parse message */ gst_message_parse_info(msg, &info, &debug); /* Display info */ DEBUG("Gst bus info msg: %s:%d: %s", g_quark_to_string(info->domain), info->code, info->message); DEBUG("Gst bus info debug : %s", debug); /* Cleanup */ g_error_free(info); g_free(debug); return TRUE; } #ifdef DEBUG_GST_TAGS static void tag_list_foreach_dump(const GstTagList *list, const gchar *tag, gpointer data G_GNUC_UNUSED) { gchar *str = NULL; switch (gst_tag_get_type(tag)) { case G_TYPE_STRING: gst_tag_list_get_string_index(list, tag, 0, &str); break; case G_TYPE_INT: { gint val; gst_tag_list_get_int_index(list, tag, 0, &val); str = g_strdup_printf("%d", val); break; } case G_TYPE_UINT: { guint val; gst_tag_list_get_uint_index(list, tag, 0, &val); str = g_strdup_printf("%u", val); break; } case G_TYPE_DOUBLE: { gdouble val; gst_tag_list_get_double_index(list, tag, 0, &val); str = g_strdup_printf("%lg", val); break; } case G_TYPE_BOOLEAN: { gboolean val; gst_tag_list_get_boolean_index(list, tag, 0, &val); str = g_strdup_printf("%s", val ? "true" : "false"); break; } } DEBUG("%10s '%20s' (%d elem): %s", g_type_name(gst_tag_get_type(tag)), tag, gst_tag_list_get_tag_size(list, tag), str); g_free(str); } #endif /* DEBUG_GST_TAGS */ static gboolean on_bus_message_tag(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self) { GvEnginePrivate *priv = self->priv; GvMetadata *metadata; GstTagList *taglist = NULL; const gchar *tag_title = NULL; TRACE("... %s, %p", GST_OBJECT_NAME(msg->src), self); /* Parse message */ gst_message_parse_tag(msg, &taglist); #ifdef DEBUG_GST_TAGS /* Dumping may be needed to debug */ DEBUG("-- Dumping taglist..."); gst_tag_list_foreach(taglist, (GstTagForeachFunc) tag_list_foreach_dump, NULL); DEBUG("-- Done --"); #endif /* DEBUG_GST_TAGS */ /* Get nominal bitrate */ guint nominal_bitrate = taglist_get_nominal_bitrate(taglist); if (nominal_bitrate > 0) { GvStation *station = priv->station; if (station) gv_station_set_nominal_bitrate(station, nominal_bitrate); } /* Get bitrate */ gv_engine_set_bitrate(self, taglist_get_bitrate(taglist)); /* Tags can be quite noisy, so let's cut it short. * From my experience, 'title' is the most important field, * and it's likely that it's the only one filled, containing * everything (title, artist and more). * So, we require this field to be filled. If it's not, this * metadata is considered to be noise, and discarded. */ gst_tag_list_peek_string_index(taglist, GST_TAG_TITLE, 0, &tag_title); if (tag_title == NULL) { DEBUG("No 'title' field in the tag list, discarding"); goto taglist_unref; } /* Turn taglist into metadata and assign it */ metadata = taglist_to_metadata(taglist); gv_engine_set_metadata(self, metadata); g_object_unref(metadata); taglist_unref: /* Unref taglist */ gst_tag_list_unref(taglist); return TRUE; } static gboolean on_bus_message_buffering(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self) { GvEnginePrivate *priv = self->priv; static gint prev_percent = 0; gint percent = 0; /* Handle the buffering message. Some documentation: * https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer/html/ * GstMessage.html#gst-message-new-buffering */ /* Parse message */ gst_message_parse_buffering(msg, &percent); /* Display buffering steps 20 by 20 */ if (ABS(percent - prev_percent) > 20) { prev_percent = percent; DEBUG("Buffering (%3u %%)", percent); } /* Now, let's react according to our current state */ switch (priv->state) { case GV_ENGINE_STATE_STOPPED: /* This shouldn't happen */ WARNING("Received 'bus buffering' while stopped"); break; case GV_ENGINE_STATE_CONNECTING: /* We successfully connected ! */ gv_engine_set_state(self, GV_ENGINE_STATE_BUFFERING); /* NO BREAK HERE ! * This is to handle the (very special) case where the first * buffering message received is already 100%. I'm not sure this * can happen, but it doesn't hurt to be ready. * * To avoid warnings, the next line could be: * __attribute__((fallthrough)); * * However this is gcc-7 only, and I don't know about clang. * So we use a more portable (and more magic) marker comment. */ // fall through case GV_ENGINE_STATE_BUFFERING: /* When buffering complete, start playing */ if (percent >= 100) { DEBUG("Buffering complete, starting playback"); set_gst_state(priv->playbin, GST_STATE_PLAYING); gv_engine_set_state(self, GV_ENGINE_STATE_PLAYING); priv->error_count = 0; } break; case GV_ENGINE_STATE_PLAYING: /* In case buffering is < 100%, according to the documentation, * we should pause. However, more than often, I constantly * receive 'buffering < 100%' messages. In such cases, following * the doc and pausing/playing makes constantly cuts the sound. * While ignoring the messages works just fine, do let's ignore * for now. */ if (percent < 100) { DEBUG("Buffering < 100%%, ignoring instead of setting to pause"); //set_gst_state(priv->playbin, GST_STATE_PAUSED); //gv_engine_set_state(self, GV_ENGINE_STATE_BUFFERING); } break; default: WARNING("Unhandled engine state %d", priv->state); } return TRUE; } static gboolean on_bus_message_state_changed(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, GvEngine *self G_GNUC_UNUSED) { #ifdef DEBUG_GST_STATE_CHANGES GstState old, new, pending; /* Parse message */ gst_message_parse_state_changed(msg, &old, &new, &pending); /* Just used for debug */ DEBUG("Gst state changed: old: %s, new: %s, pending: %s", gst_element_state_get_name(old), gst_element_state_get_name(new), gst_element_state_get_name(pending)); #else (void) msg; #endif return TRUE; } /* * GObject methods */ static void gv_engine_finalize(GObject *object) { GvEngine *self = GV_ENGINE(object); GvEnginePrivate *priv = self->priv; TRACE("%p", object); /* Remove pending operations */ if (priv->when_timeout_start_playback_id) g_source_remove(priv->when_timeout_start_playback_id); /* Stop playback */ set_gst_state(priv->playbin, GST_STATE_NULL); /* Unref the bus */ gst_bus_remove_signal_watch(priv->bus); gst_object_unref(priv->bus); /* Unref the playbin */ gst_object_unref(priv->playbin); /* Unref metadata */ g_clear_object(&priv->station); g_clear_object(&priv->metadata); /* Free resources */ g_free(priv->pipeline_string); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_engine, object); } static void gv_engine_constructed(GObject *object) { GvEngine *self = GV_ENGINE(object); GvEnginePrivate *priv = self->priv; GstElement *playbin; GstElement *fakesink; GstBus *bus; /* Initialize properties */ priv->volume = DEFAULT_VOLUME; priv->mute = DEFAULT_MUTE; priv->pipeline_enabled = FALSE; priv->pipeline_string = NULL; /* GStreamer must be initialized, let's check that */ g_assert(gst_is_initialized()); /* Make the playbin - returns floating ref */ playbin = gst_element_factory_make("playbin", "playbin"); g_assert_nonnull(playbin); priv->playbin = g_object_ref_sink(playbin); /* Connect playbin signal handlers */ g_signal_connect_object(playbin, "source-setup", G_CALLBACK(on_playbin_source_setup), self, 0); /* Disable video - returns floating ref */ fakesink = gst_element_factory_make("fakesink", "fakesink"); g_assert_nonnull(fakesink); g_object_set(playbin, "video-sink", fakesink, NULL); /* Get a reference to the message bus - returns full ref */ bus = gst_element_get_bus(playbin); g_assert_nonnull(bus); priv->bus = bus; /* Add a bus signal watch (so that 'message' signals are emitted) */ gst_bus_add_signal_watch(bus); /* Connect bus signal handlers */ g_signal_connect_object(bus, "message::eos", G_CALLBACK(on_bus_message_eos), self, 0); g_signal_connect_object(bus, "message::error", G_CALLBACK(on_bus_message_error), self, 0); g_signal_connect_object(bus, "message::warning", G_CALLBACK(on_bus_message_warning), self, 0); g_signal_connect_object(bus, "message::info", G_CALLBACK(on_bus_message_info), self, 0); g_signal_connect_object(bus, "message::tag", G_CALLBACK(on_bus_message_tag), self, 0); g_signal_connect_object(bus, "message::buffering", G_CALLBACK(on_bus_message_buffering), self, 0); g_signal_connect_object(bus, "message::state-changed", G_CALLBACK(on_bus_message_state_changed), self, 0); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_engine, object); } static void gv_engine_init(GvEngine *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_engine_get_instance_private(self); } static void gv_engine_class_init(GvEngineClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_engine_finalize; object_class->constructed = gv_engine_constructed; /* Properties */ object_class->get_property = gv_engine_get_property; object_class->set_property = gv_engine_set_property; properties[PROP_STATE] = g_param_spec_enum("state", "Playback state", NULL, GV_TYPE_ENGINE_STATE, GV_ENGINE_STATE_STOPPED, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_BITRATE] = g_param_spec_uint("bitrate", "Bitrate", NULL, 0, G_MAXUINT, 0, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_STATION] = g_param_spec_object("station", "Current station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_METADATA] = g_param_spec_object("metadata", "Current metadata", NULL, GV_TYPE_METADATA, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_VOLUME] = g_param_spec_uint("volume", "Volume in percent", NULL, 0, 100, DEFAULT_VOLUME, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_MUTE] = g_param_spec_boolean("mute", "Mute", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PIPELINE_ENABLED] = g_param_spec_boolean("pipeline-enabled", "Enable custom pipeline", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PIPELINE_STRING] = g_param_spec_string("pipeline-string", "Custom pipeline string", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/core/gv-engine.h000066400000000000000000000043061337147241200174420ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_ENGINE_H__ #define __GOODVIBES_CORE_GV_ENGINE_H__ #include #include #include "core/gv-station.h" #include "core/gv-metadata.h" /* GObject declarations */ #define GV_TYPE_ENGINE gv_engine_get_type() G_DECLARE_FINAL_TYPE(GvEngine, gv_engine, GV, ENGINE, GObject) /* Data types */ typedef enum { GV_ENGINE_STATE_STOPPED = 0, GV_ENGINE_STATE_CONNECTING, GV_ENGINE_STATE_BUFFERING, GV_ENGINE_STATE_PLAYING } GvEngineState; /* Methods */ GvEngine *gv_engine_new (void); void gv_engine_play(GvEngine *self, GvStation *station); void gv_engine_stop(GvEngine *self); /* Property accessors */ GvEngineState gv_engine_get_state (GvEngine *self); guint gv_engine_get_bitrate (GvEngine *self); GvMetadata *gv_engine_get_metadata (GvEngine *self); guint gv_engine_get_volume (GvEngine *self); void gv_engine_set_volume (GvEngine *self, guint volume); gboolean gv_engine_get_mute (GvEngine *self); void gv_engine_set_mute (GvEngine *self, gboolean mute); gboolean gv_engine_get_pipeline_enabled(GvEngine *self); void gv_engine_set_pipeline_enabled(GvEngine *self, gboolean enabled); const gchar *gv_engine_get_pipeline_string (GvEngine *self); void gv_engine_set_pipeline_string (GvEngine *self, const gchar *pipeline); #endif /* __GOODVIBES_CORE_GV_ENGINE_H__ */ goodvibes-v0.4.2/src/core/gv-metadata.c000066400000000000000000000223001337147241200177420ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-metadata.h" // WISHED I think this doesn't need to be a GObject. A simple boxed type would do. /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_TITLE, PROP_ARTIST, PROP_ALBUM, PROP_GENRE, PROP_YEAR, PROP_COMMENT, // WISHED Label ? // WISHED Cover ? /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvMetadataPrivate { gchar *title; gchar *artist; gchar *album; gchar *genre; gchar *year; gchar *comment; }; typedef struct _GvMetadataPrivate GvMetadataPrivate; struct _GvMetadata { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvMetadataPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvMetadata, gv_metadata, G_TYPE_OBJECT) /* * Property accessors */ const gchar * gv_metadata_get_title(GvMetadata *self) { return self->priv->title; } void gv_metadata_set_title(GvMetadata *self, const gchar *title) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->title, title)) return; g_free(priv->title); priv->title = g_strdup(title); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_TITLE]); } const gchar * gv_metadata_get_artist(GvMetadata *self) { return self->priv->artist; } void gv_metadata_set_artist(GvMetadata *self, const gchar *artist) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->artist, artist)) return; g_free(priv->artist); priv->artist = g_strdup(artist); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_ARTIST]); } const gchar * gv_metadata_get_album(GvMetadata *self) { return self->priv->album; } void gv_metadata_set_album(GvMetadata *self, const gchar *album) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->album, album)) return; g_free(priv->album); priv->album = g_strdup(album); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_ALBUM]); } const gchar * gv_metadata_get_genre(GvMetadata *self) { return self->priv->genre; } void gv_metadata_set_genre(GvMetadata *self, const gchar *genre) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->genre, genre)) return; g_free(priv->genre); priv->genre = g_strdup(genre); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_GENRE]); } const gchar * gv_metadata_get_year(GvMetadata *self) { return self->priv->year; } void gv_metadata_set_year(GvMetadata *self, const gchar *year) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->year, year)) return; g_free(priv->year); priv->year = g_strdup(year); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_YEAR]); } const gchar * gv_metadata_get_comment(GvMetadata *self) { return self->priv->comment; } void gv_metadata_set_comment(GvMetadata *self, const gchar *comment) { GvMetadataPrivate *priv = self->priv; if (!g_strcmp0(priv->comment, comment)) return; g_free(priv->comment); priv->comment = g_strdup(comment); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_COMMENT]); } static void gv_metadata_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvMetadata *self = GV_METADATA(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_TITLE: g_value_set_string(value, gv_metadata_get_title(self)); break; case PROP_ARTIST: g_value_set_string(value, gv_metadata_get_artist(self)); break; case PROP_ALBUM: g_value_set_string(value, gv_metadata_get_album(self)); break; case PROP_GENRE: g_value_set_string(value, gv_metadata_get_genre(self)); break; case PROP_YEAR: g_value_set_string(value, gv_metadata_get_year(self)); break; case PROP_COMMENT: g_value_set_string(value, gv_metadata_get_comment(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_metadata_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvMetadata *self = GV_METADATA(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_TITLE: gv_metadata_set_title(self, g_value_get_string(value)); break; case PROP_ARTIST: gv_metadata_set_artist(self, g_value_get_string(value)); break; case PROP_ALBUM: gv_metadata_set_album(self, g_value_get_string(value)); break; case PROP_GENRE: gv_metadata_set_genre(self, g_value_get_string(value)); break; case PROP_YEAR: gv_metadata_set_year(self, g_value_get_string(value)); break; case PROP_COMMENT: gv_metadata_set_comment(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ gboolean gv_metadata_is_equal(GvMetadata *self, GvMetadata *against) { GvMetadataPrivate *priv1 = self->priv; GvMetadataPrivate *priv2 = against->priv; if (g_strcmp0(priv1->title, priv2->title)) return FALSE; if (g_strcmp0(priv1->artist, priv2->artist)) return FALSE; if (g_strcmp0(priv1->album, priv2->album)) return FALSE; if (g_strcmp0(priv1->genre, priv2->genre)) return FALSE; if (g_strcmp0(priv1->year, priv2->year)) return FALSE; if (g_strcmp0(priv1->comment, priv2->comment)) return FALSE; return TRUE; } gchar * gv_metadata_make_title_artist(GvMetadata *self, gboolean escape) { GvMetadataPrivate *priv = self->priv; gchar *str; if (priv->artist && priv->title) str = g_strdup_printf("%s - %s", priv->title, priv->artist); else if (priv->title) str = g_strdup_printf("%s", priv->title); else if (priv->artist) str = g_strdup_printf("%s", priv->artist); else str = NULL; if (str && escape == TRUE) { gchar *str2 = g_markup_escape_text(str, -1); g_free(str); str = str2; } return str; } gchar * gv_metadata_make_album_year(GvMetadata *self, gboolean escape) { GvMetadataPrivate *priv = self->priv; gchar *str; if (priv->album && priv->year) str = g_strdup_printf("%s (%s)", priv->album, priv->year); else if (priv->album) str = g_strdup_printf("%s", priv->album); else if (priv->year) str = g_strdup_printf("(%s)", priv->year); else str = NULL; if (str && escape == TRUE) { gchar *str2 = g_markup_escape_text(str, -1); g_free(str); str = str2; } return str; } GvMetadata * gv_metadata_new(void) { return g_object_new(GV_TYPE_METADATA, NULL); } /* * GObject methods */ static void gv_metadata_finalize(GObject *object) { GvMetadataPrivate *priv = GV_METADATA(object)->priv; TRACE("%p", object); /* Free any allocated resources */ g_free(priv->title); g_free(priv->artist); g_free(priv->album); g_free(priv->genre); g_free(priv->year); g_free(priv->comment); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_metadata, object); } static void gv_metadata_init(GvMetadata *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_metadata_get_instance_private(self); } static void gv_metadata_class_init(GvMetadataClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_metadata_finalize; /* Properties */ object_class->get_property = gv_metadata_get_property; object_class->set_property = gv_metadata_set_property; properties[PROP_TITLE] = g_param_spec_string("title", "Title", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_ARTIST] = g_param_spec_string("artist", "Artist", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_ALBUM] = g_param_spec_string("album", "Album", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_GENRE] = g_param_spec_string("genre", "Genre", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_YEAR] = g_param_spec_string("year", "Year", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_COMMENT] = g_param_spec_string("comment", "Comment", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/core/gv-metadata.h000066400000000000000000000042361337147241200177570ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_METADATA_H__ #define __GOODVIBES_CORE_GV_METADATA_H__ #include /* GObject declarations */ #define GV_TYPE_METADATA gv_metadata_get_type() G_DECLARE_FINAL_TYPE(GvMetadata, gv_metadata, GV, METADATA, GObject) /* Methods */ GvMetadata *gv_metadata_new (void); gchar *gv_metadata_make_title_artist(GvMetadata *self, gboolean escape); gchar *gv_metadata_make_album_year (GvMetadata *self, gboolean escape); gboolean gv_metadata_is_equal (GvMetadata *self, GvMetadata *against); /* Property accessors */ const gchar *gv_metadata_get_title (GvMetadata *self); void gv_metadata_set_title (GvMetadata *self, const gchar *title); const gchar *gv_metadata_get_artist (GvMetadata *self); void gv_metadata_set_artist (GvMetadata *self, const gchar *artist); const gchar *gv_metadata_get_album (GvMetadata *self); void gv_metadata_set_album (GvMetadata *self, const gchar *album); const gchar *gv_metadata_get_genre (GvMetadata *self); void gv_metadata_set_genre (GvMetadata *self, const gchar *genre); const gchar *gv_metadata_get_year (GvMetadata *self); void gv_metadata_set_year (GvMetadata *self, const gchar *year); const gchar *gv_metadata_get_comment(GvMetadata *self); void gv_metadata_set_comment(GvMetadata *self, const gchar *comment); #endif /* __GOODVIBES_CORE_GV_METADATA_H__ */ goodvibes-v0.4.2/src/core/gv-player.c000066400000000000000000000627231337147241200174730ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-engine.h" #include "core/gv-core-enum-types.h" #include "core/gv-core-internal.h" #include "core/gv-metadata.h" #include "core/gv-station.h" #include "core/gv-station-list.h" #include "core/gv-player.h" /* * Properties */ #define DEFAULT_VOLUME 100 #define DEFAULT_MUTE FALSE #define DEFAULT_REPEAT FALSE #define DEFAULT_SHUFFLE FALSE #define DEFAULT_AUTOPLAY FALSE enum { /* Reserved */ PROP_0, /* Construct-only properties */ PROP_ENGINE, PROP_STATION_LIST, /* Engine mirrored properties */ PROP_BITRATE, PROP_METADATA, PROP_VOLUME, PROP_MUTE, PROP_PIPELINE_ENABLED, PROP_PIPELINE_STRING, /* Properties */ PROP_STATE, PROP_REPEAT, PROP_SHUFFLE, PROP_AUTOPLAY, PROP_STATION, PROP_STATION_URI, PROP_PREV_STATION, PROP_NEXT_STATION, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ typedef enum { GV_PLAYER_WISH_TO_STOP, GV_PLAYER_WISH_TO_PLAY, } GvPlayerWish; struct _GvPlayerPrivate { /* Construct-only properties */ GvEngine *engine; GvStationList *station_list; /* Properties */ GvPlayerState state; gboolean repeat; gboolean shuffle; gboolean autoplay; /* Current station */ GvStation *station; /* Wished state */ GvPlayerWish wish; }; typedef struct _GvPlayerPrivate GvPlayerPrivate; struct _GvPlayer { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvPlayerPrivate *priv; }; static void gv_player_configurable_interface_init(GvConfigurableInterface *iface); G_DEFINE_TYPE_WITH_CODE(GvPlayer, gv_player, G_TYPE_OBJECT, G_ADD_PRIVATE(GvPlayer) G_IMPLEMENT_INTERFACE(GV_TYPE_CONFIGURABLE, gv_player_configurable_interface_init) G_IMPLEMENT_INTERFACE(GV_TYPE_ERRORABLE, NULL)) /* * Signal handlers */ static void gv_player_set_state(GvPlayer *self, GvPlayerState value); static void on_station_notify(GvStation *station, GParamSpec *pspec, GvPlayer *self) { GvPlayerPrivate *priv = self->priv; const gchar *property_name = g_param_spec_get_name(pspec); TRACE("%p, %s, %p", station, property_name, self); g_assert(station == priv->station); if (!g_strcmp0(property_name, "stream-uris")) { DEBUG("Station %p: stream URIs have changed", station); /* Check if there are some streams, and start playing if needed */ if (gv_station_get_stream_uris(station)) if (priv->wish == GV_PLAYER_WISH_TO_PLAY) gv_player_play(self); } /* In any case, we notify if something was changed in the station */ g_object_notify(G_OBJECT(self), "station"); } static void on_engine_notify(GvEngine *engine, GParamSpec *pspec, GvPlayer *self) { GvPlayerPrivate *priv = self->priv; const gchar *property_name = g_param_spec_get_name(pspec); TRACE("%p, %s, %p", engine, property_name, self); if (!g_strcmp0(property_name, "bitrate")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_BITRATE]); } else if (!g_strcmp0(property_name, "metadata")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_METADATA]); } else if (!g_strcmp0(property_name, "volume")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_VOLUME]); } else if (!g_strcmp0(property_name, "mute")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_MUTE]); } else if (!g_strcmp0(property_name, "pipeline-enabled")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_PIPELINE_ENABLED]); } else if (!g_strcmp0(property_name, "pipeline-string")) { g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_PIPELINE_STRING]); } else if (!g_strcmp0(property_name, "state")) { GvEngineState engine_state; GvPlayerState player_state; engine_state = gv_engine_get_state(priv->engine); /* Map engine state to player state - trivial */ switch (engine_state) { case GV_ENGINE_STATE_STOPPED: player_state = GV_PLAYER_STATE_STOPPED; break; case GV_ENGINE_STATE_CONNECTING: player_state = GV_PLAYER_STATE_CONNECTING; break; case GV_ENGINE_STATE_BUFFERING: player_state = GV_PLAYER_STATE_BUFFERING; break; case GV_ENGINE_STATE_PLAYING: player_state = GV_PLAYER_STATE_PLAYING; break; default: ERROR("Unhandled engine state: %d", engine_state); /* Program execution stops here */ break; } /* Set state */ gv_player_set_state(self, player_state); } } static void on_engine_error(GvEngine *engine G_GNUC_UNUSED, const gchar *error_string G_GNUC_UNUSED, GvPlayer *self) { /* Whatever the error, just stop */ gv_player_stop(self); } /* * Property accessors - construct-only properties */ static void gv_player_set_engine(GvPlayer *self, GvEngine *engine) { GvPlayerPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_null(priv->engine); g_assert_nonnull(engine); priv->engine = g_object_ref(engine); /* Some signal handlers */ g_signal_connect_object(engine, "notify", G_CALLBACK(on_engine_notify), self, 0); g_signal_connect_object(engine, "error", G_CALLBACK(on_engine_error), self, 0); } static void gv_player_set_station_list(GvPlayer *self, GvStationList *station_list) { GvPlayerPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_null(priv->station_list); g_assert_nonnull(station_list); priv->station_list = g_object_ref(station_list); } /* * Property accessors - engine mirrored properties * We don't notify here. It's done in the engine notify handler instead. */ guint gv_player_get_bitrate(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_bitrate(engine); } GvMetadata * gv_player_get_metadata(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_metadata(engine); } guint gv_player_get_volume(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_volume(engine); } void gv_player_set_volume(GvPlayer *self, guint volume) { GvEngine *engine = self->priv->engine; gv_engine_set_volume(engine, volume); } void gv_player_lower_volume(GvPlayer *self) { guint volume; guint step = 5; volume = gv_player_get_volume(self); volume = volume > step ? volume - step : 0; gv_player_set_volume(self, volume); } void gv_player_raise_volume(GvPlayer *self) { guint volume; guint step = 5; volume = gv_player_get_volume(self); volume = volume < 100 - step ? volume + step : 100; gv_player_set_volume(self, volume); } gboolean gv_player_get_mute(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_mute(engine); } void gv_player_set_mute(GvPlayer *self, gboolean mute) { GvEngine *engine = self->priv->engine; gv_engine_set_mute(engine, mute); } void gv_player_toggle_mute(GvPlayer *self) { gboolean mute; mute = gv_player_get_mute(self); gv_player_set_mute(self, !mute); } gboolean gv_player_get_pipeline_enabled(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_pipeline_enabled(engine); } void gv_player_set_pipeline_enabled(GvPlayer *self, gboolean enabled) { GvEngine *engine = self->priv->engine; gv_engine_set_pipeline_enabled(engine, enabled); } const gchar * gv_player_get_pipeline_string(GvPlayer *self) { GvEngine *engine = self->priv->engine; return gv_engine_get_pipeline_string(engine); } void gv_player_set_pipeline_string(GvPlayer *self, const gchar *pipeline_string) { GvEngine *engine = self->priv->engine; gv_engine_set_pipeline_string(engine, pipeline_string); } /* * Property accessors - player properties */ GvPlayerState gv_player_get_state(GvPlayer *self) { return self->priv->state; } static void gv_player_set_state(GvPlayer *self, GvPlayerState state) { GvPlayerPrivate *priv = self->priv; if (priv->state == state) return; priv->state = state; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_STATE]); } gboolean gv_player_get_repeat(GvPlayer *self) { return self->priv->repeat; } void gv_player_set_repeat(GvPlayer *self, gboolean repeat) { GvPlayerPrivate *priv = self->priv; if (priv->repeat == repeat) return; priv->repeat = repeat; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_REPEAT]); } gboolean gv_player_get_shuffle(GvPlayer *self) { return self->priv->shuffle; } void gv_player_set_shuffle(GvPlayer *self, gboolean shuffle) { GvPlayerPrivate *priv = self->priv; if (priv->shuffle == shuffle) return; priv->shuffle = shuffle; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_SHUFFLE]); } gboolean gv_player_get_autoplay(GvPlayer *self) { return self->priv->autoplay; } void gv_player_set_autoplay(GvPlayer *self, gboolean autoplay) { GvPlayerPrivate *priv = self->priv; if (priv->autoplay == autoplay) return; priv->autoplay = autoplay; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_AUTOPLAY]); } GvStation * gv_player_get_station(GvPlayer *self) { return self->priv->station; } GvStation * gv_player_get_prev_station(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; return gv_station_list_prev(priv->station_list, priv->station, priv->repeat, priv->shuffle); } GvStation * gv_player_get_next_station(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; return gv_station_list_next(priv->station_list, priv->station, priv->repeat, priv->shuffle); } static const gchar * gv_player_get_station_uri(GvPlayer *self) { GvStation *station; const gchar *station_uri; station = gv_player_get_station(self); if (station == NULL) return NULL; station_uri = gv_station_get_uri(station); return station_uri; } void gv_player_set_station(GvPlayer *self, GvStation *station) { GvPlayerPrivate *priv = self->priv; if (station == priv->station) return; if (priv->station) { g_signal_handlers_disconnect_by_data(priv->station, self); g_object_unref(priv->station); priv->station = NULL; } if (station) { priv->station = g_object_ref_sink(station); g_signal_connect_object(priv->station, "notify", G_CALLBACK(on_station_notify), self, 0); } g_object_notify(G_OBJECT(self), "station"); g_object_notify(G_OBJECT(self), "station-uri"); INFO("Station set to '%s'", station ? gv_station_get_name_or_uri(station) : NULL); } gboolean gv_player_set_station_by_name(GvPlayer *self, const gchar *name) { GvPlayerPrivate *priv = self->priv; GvStation *station = NULL; station = gv_station_list_find_by_name(priv->station_list, name); if (station == NULL) { DEBUG("Station name '%s' not found in station list", name); return FALSE; } gv_player_set_station(self, station); return TRUE; } gboolean gv_player_set_station_by_uri(GvPlayer *self, const gchar *uri) { GvPlayerPrivate *priv = self->priv; GvStation *station = NULL; /* Look for the station in the station list */ station = gv_station_list_find_by_uri(priv->station_list, uri); if (station == NULL) { DEBUG("Station URI '%s' not found in station list", uri); return FALSE; } gv_player_set_station(self, station); return TRUE; } gboolean gv_player_set_station_by_guessing(GvPlayer *self, const gchar *string) { GvPlayerPrivate *priv = self->priv; GvStation *station = NULL; station = gv_station_list_find_by_guessing(priv->station_list, string); if (station == NULL) { DEBUG("'%s' not found in station list", string); return FALSE; } gv_player_set_station(self, station); return TRUE; } static void gv_player_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvPlayer *self = GV_PLAYER(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_BITRATE: g_value_set_uint(value, gv_player_get_bitrate(self)); break; case PROP_METADATA: g_value_set_object(value, gv_player_get_metadata(self)); break; case PROP_VOLUME: g_value_set_uint(value, gv_player_get_volume(self)); break; case PROP_MUTE: g_value_set_boolean(value, gv_player_get_mute(self)); break; case PROP_PIPELINE_ENABLED: g_value_set_boolean(value, gv_player_get_pipeline_enabled(self)); break; case PROP_PIPELINE_STRING: g_value_set_string(value, gv_player_get_pipeline_string(self)); break; case PROP_STATE: g_value_set_enum(value, gv_player_get_state(self)); break; case PROP_REPEAT: g_value_set_boolean(value, gv_player_get_repeat(self)); break; case PROP_SHUFFLE: g_value_set_boolean(value, gv_player_get_shuffle(self)); break; case PROP_AUTOPLAY: g_value_set_boolean(value, gv_player_get_autoplay(self)); break; case PROP_STATION: g_value_set_object(value, gv_player_get_station(self)); break; case PROP_STATION_URI: g_value_set_string(value, gv_player_get_station_uri(self)); break; case PROP_PREV_STATION: g_value_set_object(value, gv_player_get_prev_station(self)); break; case PROP_NEXT_STATION: g_value_set_object(value, gv_player_get_next_station(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_player_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvPlayer *self = GV_PLAYER(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_ENGINE: gv_player_set_engine(self, g_value_get_object(value)); break; case PROP_STATION_LIST: gv_player_set_station_list(self, g_value_get_object(value)); break; case PROP_VOLUME: gv_player_set_volume(self, g_value_get_uint(value)); break; case PROP_MUTE: gv_player_set_mute(self, g_value_get_boolean(value)); break; case PROP_PIPELINE_ENABLED: gv_player_set_pipeline_enabled(self, g_value_get_boolean(value)); break; case PROP_PIPELINE_STRING: gv_player_set_pipeline_string(self, g_value_get_string(value)); break; case PROP_REPEAT: gv_player_set_repeat(self, g_value_get_boolean(value)); break; case PROP_SHUFFLE: gv_player_set_shuffle(self, g_value_get_boolean(value)); break; case PROP_AUTOPLAY: gv_player_set_autoplay(self, g_value_get_boolean(value)); break; case PROP_STATION: gv_player_set_station(self, g_value_get_object(value)); break; case PROP_STATION_URI: gv_player_set_station_by_uri(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ void gv_player_stop(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; /* To remember what we're doing */ priv->wish = GV_PLAYER_WISH_TO_STOP; /* Stop playing */ gv_engine_stop(priv->engine); } void gv_player_play(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; GvStation *station; GSList *uris; /* If no station is set yet, take the first from the station list */ if (priv->station == NULL) { GvStation *first_station; first_station = gv_station_list_first(priv->station_list); gv_player_set_station(self, first_station); } /* If there's still no station, return */ station = priv->station; if (station == NULL) return; /* To remember what we're doing */ priv->wish = GV_PLAYER_WISH_TO_PLAY; /* Stop playing */ gv_engine_stop(priv->engine); /* Get station data */ uris = gv_station_get_stream_uris(station); /* If there's no URIs, that probably means that the station URI * points to a playlist, and we need to download it. */ if (uris == NULL) { /* Download the playlist that contains the stream URIs */ if (!gv_station_download_playlist(station)) WARNING("Can't download playlist"); /* Downloading a playlist is an asynchronous operation. * We have nothing left to do here. */ return; } else { /* Play the station */ gv_engine_play(priv->engine, station); } } gboolean gv_player_next(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; GvStation *station; station = gv_player_get_next_station(self); if (station == NULL) return FALSE; gv_player_set_station(self, station); if (priv->wish == GV_PLAYER_WISH_TO_PLAY) gv_player_play(self); return TRUE; } gboolean gv_player_prev(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; GvStation *station; station = gv_player_get_prev_station(self); if (station == NULL) return FALSE; gv_player_set_station(self, station); if (priv->wish == GV_PLAYER_WISH_TO_PLAY) gv_player_play(self); return TRUE; } void gv_player_toggle(GvPlayer *self) { GvPlayerPrivate *priv = self->priv; switch (priv->wish) { case GV_PLAYER_WISH_TO_STOP: gv_player_play(self); break; case GV_PLAYER_WISH_TO_PLAY: gv_player_stop(self); break; default: ERROR("Invalid wish: %d", priv->wish); /* Program execution stops here */ break; } } void gv_player_go(GvPlayer *self, const gchar *string_to_play) { GvPlayerPrivate *priv = self->priv; /* If we have no argument, we rely on 'autoplay' */ if (string_to_play == NULL) { if (priv->autoplay) { INFO("Autoplay is enabled, let's play"); gv_player_play(self); } return; } /* If we have an argument, it might be anything. * At first, check if we find it in the station list. */ if (gv_player_set_station_by_guessing(self, string_to_play)) { INFO("'%s' found in station list, let's play", string_to_play); gv_player_play(self); return; } /* Otherwise, if it's a valid URI, try to play it */ if (is_uri_scheme_supported(string_to_play)) { GvStation *station; station = gv_station_new(NULL, string_to_play); gv_player_set_station(self, station); INFO("'%s' is a valid URI, let's play", string_to_play); gv_player_play(self); return; } /* That looks like an invalid string then */ WARNING("'%s' is neither a known station or a valid URI", string_to_play); gv_errorable_emit_error(GV_ERRORABLE(self), _("'%s' is neither a known station or a valid URI"), string_to_play); } GvPlayer * gv_player_new(GvEngine *engine, GvStationList *station_list) { return g_object_new(GV_TYPE_PLAYER, "engine", engine, "station-list", station_list, NULL); } /* * GvConfigurable interface */ static void gv_player_configure(GvConfigurable *configurable) { GvPlayer *self = GV_PLAYER(configurable); TRACE("%p", self); g_assert(gv_core_settings); g_settings_bind(gv_core_settings, "pipeline-enabled", self, "pipeline-enabled", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "pipeline-string", self, "pipeline-string", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "volume", self, "volume", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "mute", self, "mute", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "repeat", self, "repeat", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "shuffle", self, "shuffle", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "autoplay", self, "autoplay", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_core_settings, "station-uri", self, "station-uri", G_SETTINGS_BIND_DEFAULT); } static void gv_player_configurable_interface_init(GvConfigurableInterface *iface) { iface->configure = gv_player_configure; } /* * GObject methods */ static void gv_player_finalize(GObject *object) { GvPlayer *self = GV_PLAYER(object); GvPlayerPrivate *priv = self->priv; TRACE("%p", object); /* Unref the current station */ if (priv->station) g_object_unref(priv->station); /* Unref the station list */ g_object_unref(priv->station_list); /* Unref the engine */ g_object_unref(priv->engine); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_player, object); } static void gv_player_constructed(GObject *object) { GvPlayer *self = GV_PLAYER(object); GvPlayerPrivate *priv = self->priv; TRACE("%p", object); /* Ensure construct-only properties have been set */ g_assert_nonnull(priv->engine); g_assert_nonnull(priv->station_list); /* Initialize properties */ priv->repeat = DEFAULT_REPEAT; priv->shuffle = DEFAULT_SHUFFLE; priv->autoplay = DEFAULT_AUTOPLAY; priv->station = NULL; /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_player, object); } static void gv_player_init(GvPlayer *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_player_get_instance_private(self); } static void gv_player_class_init(GvPlayerClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_player_finalize; object_class->constructed = gv_player_constructed; /* Properties */ object_class->get_property = gv_player_get_property; object_class->set_property = gv_player_set_property; /* Construct-only properties */ properties[PROP_ENGINE] = g_param_spec_object("engine", "Engine", NULL, GV_TYPE_ENGINE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_STATION_LIST] = g_param_spec_object("station-list", "Station list", NULL, GV_TYPE_STATION_LIST, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); /* Engine mirrored properties */ properties[PROP_BITRATE] = g_param_spec_uint("bitrate", "Bitrate", NULL, 0, G_MAXUINT, 0, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_VOLUME] = g_param_spec_uint("volume", "Volume in percent", NULL, 0, 100, DEFAULT_VOLUME, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_MUTE] = g_param_spec_boolean("mute", "Mute", NULL, DEFAULT_MUTE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PIPELINE_ENABLED] = g_param_spec_boolean("pipeline-enabled", "Enable custom pipeline", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PIPELINE_STRING] = g_param_spec_string("pipeline-string", "Custom pipeline string", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PIPELINE_STRING] = g_param_spec_string("pipeline-string", "Custom pipeline string", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); /* Player properties */ properties[PROP_STATE] = g_param_spec_enum("state", "Playback state", NULL, GV_TYPE_PLAYER_STATE, GV_PLAYER_STATE_STOPPED, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_REPEAT] = g_param_spec_boolean("repeat", "Repeat", NULL, DEFAULT_REPEAT, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_SHUFFLE] = g_param_spec_boolean("shuffle", "Shuffle", NULL, DEFAULT_SHUFFLE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_AUTOPLAY] = g_param_spec_boolean("autoplay", "Autoplay on startup", NULL, DEFAULT_AUTOPLAY, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_METADATA] = g_param_spec_object("metadata", "Current metadata", NULL, GV_TYPE_METADATA, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_STATION] = g_param_spec_object("station", "Current station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_STATION_URI] = g_param_spec_string("station-uri", "Current station URI", "This is used only to save the current station in conf", NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_PREV_STATION] = g_param_spec_object("prev", "Previous station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_NEXT_STATION] = g_param_spec_object("next", "Next station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/core/gv-player.h000066400000000000000000000070631337147241200174740ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_PLAYER_H__ #define __GOODVIBES_CORE_GV_PLAYER_H__ #include #include "core/gv-engine.h" #include "core/gv-metadata.h" #include "core/gv-station.h" #include "core/gv-station-list.h" /* GObject declarations */ #define GV_TYPE_PLAYER gv_player_get_type() G_DECLARE_FINAL_TYPE(GvPlayer, gv_player, GV, PLAYER, GObject) /* Data types */ typedef enum { GV_PLAYER_STATE_STOPPED, GV_PLAYER_STATE_CONNECTING, GV_PLAYER_STATE_BUFFERING, GV_PLAYER_STATE_PLAYING } GvPlayerState; /* Methods */ GvPlayer *gv_player_new (GvEngine *engine, GvStationList *station_list); void gv_player_go (GvPlayer *self, const gchar *string_to_play); void gv_player_play (GvPlayer *self); void gv_player_stop (GvPlayer *self); void gv_player_toggle(GvPlayer *self); gboolean gv_player_prev (GvPlayer *self); gboolean gv_player_next (GvPlayer *self); /* Property accessors */ GvPlayerState gv_player_get_state (GvPlayer *self); guint gv_player_get_bitrate (GvPlayer *self); GvMetadata *gv_player_get_metadata (GvPlayer *self); GvStation *gv_player_get_station (GvPlayer *self); GvStation *gv_player_get_prev_station (GvPlayer *self); GvStation *gv_player_get_next_station (GvPlayer *self); const gchar *gv_player_get_stream_uri (GvPlayer *self); void gv_player_set_station (GvPlayer *self, GvStation *station); gboolean gv_player_set_station_by_name (GvPlayer *self, const gchar *name); gboolean gv_player_set_station_by_uri (GvPlayer *self, const gchar *uri); gboolean gv_player_set_station_by_guessing(GvPlayer *self, const gchar *string); gboolean gv_player_get_repeat (GvPlayer *self); void gv_player_set_repeat (GvPlayer *self, gboolean repeat); gboolean gv_player_get_shuffle (GvPlayer *self); void gv_player_set_shuffle (GvPlayer *self, gboolean shuffle); gboolean gv_player_get_autoplay (GvPlayer *self); void gv_player_set_autoplay (GvPlayer *self, gboolean autoplay); guint gv_player_get_volume (GvPlayer *self); void gv_player_set_volume (GvPlayer *self, guint volume); void gv_player_lower_volume (GvPlayer *self); void gv_player_raise_volume (GvPlayer *self); gboolean gv_player_get_mute (GvPlayer *self); void gv_player_set_mute (GvPlayer *self, gboolean mute); void gv_player_toggle_mute (GvPlayer *self); gboolean gv_player_get_pipeline_enabled(GvPlayer *self); void gv_player_set_pipeline_enabled(GvPlayer *self, gboolean enabled); const gchar *gv_player_get_pipeline_string (GvPlayer *self); void gv_player_set_pipeline_string (GvPlayer *self, const gchar *pipeline); #endif /* __GOODVIBES_CORE_GV_PLAYER_H__ */ goodvibes-v0.4.2/src/core/gv-playlist.c000066400000000000000000000356501337147241200200370ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This code is based on / inspired by the following: * - Parole * - Totem (Playlist Parser) * Big thanks to them. * * A definitive guide to playlist formats (a must-read, though I didn't): * http://gonze.com/playlists/playlist-format-survey.html */ #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-playlist.h" // WISHED Test with a lot, really a lot of different stations. // I'm pretty sure this playlist implementation is not robust enough. // TODO Validate URI, send an error message if it's invalid ? // But then, shouldn't that be done in GvStation instead ? Or not ? /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_URI, PROP_STREAM_LIST, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * Signals */ enum { SIGNAL_DOWNLOADED, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * GObject definitions */ struct _GvPlaylistPrivate { gchar *uri; GvPlaylistFormat format; GSList *streams; }; typedef struct _GvPlaylistPrivate GvPlaylistPrivate; struct _GvPlaylist { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvPlaylistPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvPlaylist, gv_playlist, G_TYPE_OBJECT) /* * Helpers */ typedef GSList *(*PlaylistParser) (const gchar *, gsize); /* Parse a M3U playlist, which is a simple text file, * each line being an uri. * https://en.wikipedia.org/wiki/M3U */ static GSList * parse_playlist_m3u(const gchar *text, gsize text_size G_GNUC_UNUSED) { GSList *list = NULL; const gchar *eol; gchar **lines; guint i; /* Guess which delimiter is used to separate each lines. * Traditionnaly, UNIX would only use `\n`, Windows would use `\r\n`. */ if (strstr(text, "\r\n")) eol = "\r\n"; else eol = "\n"; /* Split into different lines */ lines = g_strsplit(text, eol, -1); if (lines == NULL) { WARNING("Empty m3u playlist"); return NULL; } /* Iterate on lines */ for (i = 0; lines[i] != NULL; i++) { gchar *line = lines[i]; /* Remove leading & trailing whitespaces */ line = g_strstrip(line); /* Ignore emtpy lines and comments */ if (line[0] == '\0' || line[0] == '#') continue; /* If it's not an URI, we discard it */ if (!strstr(line, "://")) continue; /* Add to stream list */ list = g_slist_append(list, g_strdup(line)); } g_strfreev(lines); return list; } /* Parse a PLS playlist, which is a "Desktop Entry File" in the Unix world, * or an "INI File" in the windows realm. * https://en.wikipedia.org/wiki/PLS_(file_format) */ static guint pls_get_n_items(GKeyFile *keyfile) { const gchar *keys[] = { "NumberOfEntries", "numberofentries", "NumberOfEvents", "numberofevents", NULL }; const gchar **ptr; const gchar *key = NULL; GError *err = NULL; gint n_items; for (ptr = keys; *ptr; ptr++) { if (g_key_file_has_key(keyfile, "playlist", *ptr, NULL)) { key = *ptr; break; } } if (key == NULL) { WARNING("Failed to get the number of items in pls playlist"); return 0; } n_items = g_key_file_get_integer(keyfile, "playlist", key, &err); if (err) { WARNING("Failed to get key '%s': %s", key, err->message); g_error_free(err); return 0; } return n_items; } static GSList * parse_playlist_pls(const gchar *text, gsize text_size) { GSList *list = NULL; GKeyFile *keyfile; GError *err = NULL; guint i, n_items; keyfile = g_key_file_new(); /* Parse the file */ g_key_file_load_from_data(keyfile, text, text_size, 0, &err); if (err) { WARNING("Failed to parse pls playlist: %s", err->message); g_error_free(err); goto end; } /* Get the number of items */ n_items = pls_get_n_items(keyfile); if (n_items == 0) goto end; /* Get all stream uris */ for (i = 0; i < n_items; i++) { gchar key[8]; gchar *str; g_snprintf(key, sizeof key, "File%u", i + 1); str = g_key_file_get_string(keyfile, "playlist", key, &err); if (err) { WARNING("Failed to get '%s': %s", key, err->message); g_error_free(err); err = NULL; continue; } /* Add to stream list. * No need to duplicate str, it's already an allocated string. */ list = g_slist_append(list, str); } end: g_key_file_free(keyfile); return list; } /* Parse an ASX (Advanced Stream Redirector) playlist. * https://en.wikipedia.org/wiki/Advanced_Stream_Redirector */ static void asx_parse_element_cb(GMarkupParseContext *context G_GNUC_UNUSED, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error G_GNUC_UNUSED) { GSList **llink = (GSList **) user_data; const gchar *href; guint i; /* We're only interested in the 'ref' element */ if (g_ascii_strcasecmp(element_name, "ref")) return; /* Get 'href' attribute */ href = NULL; for (i = 0; attribute_names[i]; i++) { if (!g_ascii_strcasecmp(attribute_names[i], "href")) { href = attribute_values[i]; break; } } /* Add to stream list */ if (href) *llink = g_slist_append(*llink, g_strdup(href)); } static void asx_error_cb(GMarkupParseContext *context G_GNUC_UNUSED, GError *error G_GNUC_UNUSED, gpointer user_data) { GSList **llink = (GSList **) user_data; g_slist_free_full(*llink, g_free); *llink = NULL; } static GSList * parse_playlist_asx(const gchar *text, gsize text_size) { GMarkupParseContext *context; GMarkupParser parser = { asx_parse_element_cb, NULL, NULL, NULL, asx_error_cb, }; GError *err = NULL; GSList *list = NULL; context = g_markup_parse_context_new(&parser, 0, &list, NULL); if (!g_markup_parse_context_parse(context, text, text_size, &err)) { WARNING("Failed to parse context: %s", err->message); g_error_free(err); } g_markup_parse_context_free(context); return list; } /* Parse an XSPF (XML Shareable Playlist Format) playlist. * https://en.wikipedia.org/wiki/XML_Shareable_Playlist_Format */ static void xspf_text_cb(GMarkupParseContext *context, const gchar *text, gsize text_len G_GNUC_UNUSED, gpointer user_data, GError **error G_GNUC_UNUSED) { GSList **llink = (GSList **) user_data; const gchar *element_name; element_name = g_markup_parse_context_get_element(context); /* We're only interested in the 'location' element */ if (g_ascii_strcasecmp(element_name, "location")) return; /* Add to stream list */ *llink = g_slist_append(*llink, g_strdup(text)); } static void xspf_error_cb(GMarkupParseContext *context G_GNUC_UNUSED, GError *error G_GNUC_UNUSED, gpointer user_data) { GSList **llink = (GSList **) user_data; g_slist_free_full(*llink, g_free); *llink = NULL; } static GSList * parse_playlist_xspf(const gchar *text, gsize text_size) { GMarkupParseContext *context; GMarkupParser parser = { NULL, NULL, xspf_text_cb, NULL, xspf_error_cb, }; GError *err = NULL; GSList *list = NULL; context = g_markup_parse_context_new(&parser, 0, &list, NULL); if (!g_markup_parse_context_parse(context, text, text_size, &err)) { WARNING("Failed to parse context: %s", err->message); g_error_free(err); } g_markup_parse_context_free(context); return list; } /* * Signal handlers & callbacks */ static void on_message_completed(SoupSession *session, SoupMessage *msg, GvPlaylist *self) { GvPlaylistPrivate *priv = self->priv; PlaylistParser parser; GSList *item; TRACE("%p, %p, %p", session, msg, self); /* Check the response */ if (!SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) { WARNING("Failed to download playlist: %s", msg->reason_phrase); goto end; } if (msg->response_body->length == 0) { WARNING("Empty playlist"); goto end; } //PRINT("%s", msg->response_body->data); /* Get the right parser */ switch (priv->format) { case GV_PLAYLIST_FORMAT_M3U: parser = parse_playlist_m3u; break; case GV_PLAYLIST_FORMAT_PLS: parser = parse_playlist_pls; break; case GV_PLAYLIST_FORMAT_ASX: parser = parse_playlist_asx; break; case GV_PLAYLIST_FORMAT_XSPF: parser = parse_playlist_xspf; break; default: WARNING("No parser for playlist format: %d", priv->format); goto end; } /* Parse */ if (priv->streams) g_slist_free_full(priv->streams, g_free); priv->streams = parser(msg->response_body->data, msg->response_body->length); /* Was it parsed successfully ? */ if (priv->streams == NULL) { WARNING("Failed to parse playlist"); goto end; } DEBUG("Playlist parsed, %d streams found", g_slist_length(priv->streams)); for (item = priv->streams; item; item = item->next) { DEBUG(". %s", item->data); } end: // TODO Is it ok to unref that here ? g_object_unref(session); /* msg needs not to be unreferenced. According to the doc, * it's consumed when using the queue() API. */ /* Emit completion signal */ g_signal_emit(self, signals[SIGNAL_DOWNLOADED], 0); } /* * Property accessors */ const gchar * gv_playlist_get_uri(GvPlaylist *self) { return self->priv->uri; } static void gv_playlist_set_uri(GvPlaylist *self, const gchar *uri) { GvPlaylistPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_null(priv->uri); g_assert_nonnull(uri); priv->uri = g_strdup(uri); /* Set format */ priv->format = gv_playlist_get_format(uri); } GSList * gv_playlist_get_stream_list(GvPlaylist *self) { return self->priv->streams; } static void gv_playlist_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvPlaylist *self = GV_PLAYLIST(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_URI: g_value_set_string(value, gv_playlist_get_uri(self)); break; case PROP_STREAM_LIST: g_value_set_pointer(value, gv_playlist_get_stream_list(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_playlist_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvPlaylist *self = GV_PLAYLIST(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_URI: gv_playlist_set_uri(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ void gv_playlist_download(GvPlaylist *self, const gchar *user_agent) { GvPlaylistPrivate *priv = self->priv; SoupSession *session; SoupMessage *msg; DEBUG("Downloading playlist '%s' (user-agent: '%s')", priv->uri, user_agent); session = soup_session_new_with_options(SOUP_SESSION_USER_AGENT, user_agent, NULL); msg = soup_message_new("GET", priv->uri); soup_session_queue_message(session, msg, (SoupSessionCallback) on_message_completed, self); } GvPlaylist * gv_playlist_new(const gchar *uri) { return g_object_new(GV_TYPE_PLAYLIST, "uri", uri, NULL); } /* * GObject methods */ static void gv_playlist_finalize(GObject *object) { GvPlaylistPrivate *priv = GV_PLAYLIST(object)->priv; TRACE("%p", object); /* Free any allocated resources */ g_free(priv->uri); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_playlist, object); } static void gv_playlist_constructed(GObject *object) { GvPlaylist *self = GV_PLAYLIST(object); GvPlaylistPrivate *priv = self->priv; TRACE("%p", object); /* Ensure construct-only properties have been set */ g_assert_nonnull(priv->uri); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_playlist, object); } static void gv_playlist_init(GvPlaylist *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_playlist_get_instance_private(self); } static void gv_playlist_class_init(GvPlaylistClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_playlist_finalize; object_class->constructed = gv_playlist_constructed; /* Properties */ object_class->get_property = gv_playlist_get_property; object_class->set_property = gv_playlist_set_property; properties[PROP_URI] = g_param_spec_string("uri", "Uri", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_STREAM_LIST] = g_param_spec_pointer("stream-list", "Stream list", NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); g_object_class_install_properties(object_class, PROP_N, properties); /* Signals */ signals[SIGNAL_DOWNLOADED] = g_signal_new("downloaded", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } /* * Class methods */ GvPlaylistFormat gv_playlist_get_format(const gchar *uri_string) { GvPlaylistFormat fmt = GV_PLAYLIST_FORMAT_UNKNOWN; SoupURI *uri; const gchar *path; const gchar *ext; /* Parse the uri */ uri = soup_uri_new(uri_string); if (uri == NULL) { INFO("Invalid uri '%s'", uri_string); return GV_PLAYLIST_FORMAT_UNKNOWN; } /* Get the path */ path = soup_uri_get_path(uri); /* Get the extension of the path */ ext = strrchr(path, '.'); if (ext) ext++; else ext = "\0"; /* Match with supported extensions */ if (!g_ascii_strcasecmp(ext, "m3u") || !g_ascii_strcasecmp(ext, "m3u8")) fmt = GV_PLAYLIST_FORMAT_M3U; else if (!g_ascii_strcasecmp(ext, "ram")) fmt = GV_PLAYLIST_FORMAT_M3U; else if (!g_ascii_strcasecmp(ext, "pls")) fmt = GV_PLAYLIST_FORMAT_PLS; else if (!g_ascii_strcasecmp(ext, "asx")) fmt = GV_PLAYLIST_FORMAT_ASX; else if (!g_ascii_strcasecmp(ext, "xspf")) fmt = GV_PLAYLIST_FORMAT_XSPF; /* Cleanup */ soup_uri_free(uri); return fmt; } goodvibes-v0.4.2/src/core/gv-playlist.h000066400000000000000000000031531337147241200200350ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_PLAYLIST_H__ #define __GOODVIBES_CORE_GV_PLAYLIST_H__ #include /* GObject declarations */ #define GV_TYPE_PLAYLIST gv_playlist_get_type() G_DECLARE_FINAL_TYPE(GvPlaylist, gv_playlist, GV, PLAYLIST, GObject) /* Data types */ typedef enum { GV_PLAYLIST_FORMAT_UNKNOWN, GV_PLAYLIST_FORMAT_M3U, GV_PLAYLIST_FORMAT_PLS, GV_PLAYLIST_FORMAT_ASX, GV_PLAYLIST_FORMAT_XSPF } GvPlaylistFormat; /* Class methods */ GvPlaylistFormat gv_playlist_get_format(const gchar *uri); /* Methods */ GvPlaylist *gv_playlist_new (const gchar *uri); void gv_playlist_download(GvPlaylist *playlist, const gchar *user_agent); /* Property accessors */ const gchar *gv_playlist_get_uri (GvPlaylist *self); GSList *gv_playlist_get_stream_list(GvPlaylist *playlist); #endif /* __GOODVIBES_CORE_GV_PLAYLIST_H__ */ goodvibes-v0.4.2/src/core/gv-station-list.c000066400000000000000000001035031337147241200206210ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-station-list.h" // WISHED Try with a huge number of stations to see how it behaves. // It might be slow. The implementation never tried to be fast. /* * FIP * Just the best radios you'll ever listen to. */ #define FIP_STATIONS \ "" \ " FIP Paris" \ " https://direct.fipradio.fr/live/fip-midfi.mp3" \ "" \ "" \ " FIP Autour du Rock" \ " https://direct.fipradio.fr/live/fip-webradio1.mp3" \ "" \ "" \ " FIP Autour du Jazz" \ " https://direct.fipradio.fr/live/fip-webradio2.mp3" \ "" \ "" \ " FIP Autour du Groove" \ " https://direct.fipradio.fr/live/fip-webradio3.mp3" \ "" \ "" \ " FIP Autour du Monde" \ " https://direct.fipradio.fr/live/fip-webradio4.mp3" \ "" \ "" \ " FIP Tout nouveau, tout FIP" \ " https://direct.fipradio.fr/live/fip-webradio5.mp3" \ "" \ "" \ " FIP Autour du Reggae" \ " https://direct.fipradio.fr/live/fip-webradio6.mp3" \ "" \ "" \ " FIP Autour de l'Électro" \ " https://direct.fipradio.fr/live/fip-webradio8.mp3" \ "" /* * Nova * Another killer radio from France. */ #define NOVA_STATIONS \ "" \ " Nova" \ " http://novazz.ice.infomaniak.ch/novazz-128.mp3" \ "" \ "" \ " Nova Vintage" \ " http://nova-vnt.ice.infomaniak.ch/nova-vnt-128" \ "" \ "" \ " Nova la Nuit" \ " http://nova-ln.ice.infomaniak.ch/nova-ln-128" \ "" \ "" \ " Nova V.F." \ " http://nova-vf.ice.infomaniak.ch/nova-vf-128" \ "" /* * More of my favorite french radios. * - Grenouille */ #define FRENCH_STATIONS \ "" \ " Radio Grenouille" \ " http://live.radiogrenouille.com/live" \ "" /* * Broken stations. */ #define TESTING_BROKEN_STATIONS \ "" \ " Broken - FIP old url" \ " http://audio.scdn.arkena.com/11016/fip-midfi128.mp3" \ "" /* * Various playlist formats. * - Swiss Internet Radio (Public Domain Radio) */ #define TESTING_PLAYLIST_STATIONS \ "" \ " M3U - Swiss Internet Radio Classical" \ " http://www.swissradio.ch/streams/6034.m3u" \ "" \ "" \ " ASX - Swiss Internet Radio Classical" \ " http://www.swissradio.ch/streams/6034.asx" \ "" \ "" \ " RAM - Swiss Internet Radio Classical" \ " http://www.swissradio.ch/streams/6034.ram" \ "" /* * More radios, for testing. */ #define TESTING_MORE_STATIONS \ "" \ " http://www.netradio.fr:8000/A0RemzouilleRadio.xspf" \ "" \ "" \ " http://player.100p.nl/livestream.asx" \ "" \ "" \ " http://vt-net.org/WebRadio/live/8056.m3u" \ "" \ "" \ " 'http://www.neradio.se/listen.pls" \ "" /* * Default station list, loaded if no station list file is found */ #define DEFAULT_STATIONS_DEV \ FIP_STATIONS \ NOVA_STATIONS \ FRENCH_STATIONS \ TESTING_BROKEN_STATIONS \ TESTING_PLAYLIST_STATIONS \ TESTING_MORE_STATIONS #define DEFAULT_STATIONS_PROD \ FIP_STATIONS \ NOVA_STATIONS \ FRENCH_STATIONS #define DEFAULT_STATION_LIST \ "" \ DEFAULT_STATIONS_PROD \ "" /* * More defines... */ #define SAVE_DELAY 1 // how long to wait before writing changes to disk #define STATION_LIST_FILE "stations.xml" // where to write the stations /* * Signals */ enum { SIGNAL_LOADED, SIGNAL_STATION_ADDED, SIGNAL_STATION_REMOVED, SIGNAL_STATION_MODIFIED, SIGNAL_STATION_MOVED, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * GObject definitions */ struct _GvStationListPrivate { /* Load/save paths */ gchar **load_paths; gchar *save_path; /* Timeout id, > 0 if a save operation is scheduled */ guint save_source_id; /* Ordered list of stations */ GList *stations; /* Shuffled list of stations, automatically created * and destroyed when needed. */ GList *shuffled; }; typedef struct _GvStationListPrivate GvStationListPrivate; struct _GvStationList { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvStationListPrivate *priv; }; G_DEFINE_TYPE_WITH_CODE(GvStationList, gv_station_list, G_TYPE_OBJECT, G_ADD_PRIVATE(GvStationList) G_IMPLEMENT_INTERFACE(GV_TYPE_ERRORABLE, NULL)) /* * Helpers */ static gpointer copy_func_object_ref(gconstpointer src, gpointer data G_GNUC_UNUSED) { return g_object_ref((gpointer) src); } /* * Markup handling */ struct _GvMarkupParsing { /* Persistent during the whole parsing process */ GList *list; /* Current iteration */ gchar **cur; gchar *name; gchar *uri; gchar *user_agent; }; typedef struct _GvMarkupParsing GvMarkupParsing; static void markup_on_text(GMarkupParseContext *context G_GNUC_UNUSED, const gchar *text, gsize text_len G_GNUC_UNUSED, gpointer user_data, GError **error G_GNUC_UNUSED) { GvMarkupParsing *parsing = user_data; /* Happens all the time */ if (parsing->cur == NULL) return; /* Should never happen */ g_assert_null(*parsing->cur); /* Save text */ *parsing->cur = g_strdup(text); /* Cleanup */ parsing->cur = NULL; } static void markup_on_end_element(GMarkupParseContext *context G_GNUC_UNUSED, const gchar *element_name G_GNUC_UNUSED, gpointer user_data, GError **error G_GNUC_UNUSED) { GvMarkupParsing *parsing = user_data; GvStation *station; /* We only care when we leave a station node */ if (g_strcmp0(element_name, "Station")) return; /* Discard stations with no uri */ if (parsing->uri == NULL || parsing->uri[0] == '\0') { DEBUG("Encountered station without uri (named '%s')", parsing->name); goto cleanup; } /* Create a new station */ station = gv_station_new(parsing->name, parsing->uri); if (parsing->user_agent) gv_station_set_user_agent(station, parsing->user_agent); /* We must take ownership right now */ g_object_ref_sink(station); /* Add to list, use prepend for efficiency */ parsing->list = g_list_prepend(parsing->list, station); cleanup: /* Cleanup */ g_free(parsing->name); g_free(parsing->uri); g_free(parsing->user_agent); parsing->name = NULL; parsing->uri = NULL; parsing->user_agent = NULL; } static void markup_on_start_element(GMarkupParseContext *context G_GNUC_UNUSED, const gchar *element_name, const gchar **attribute_names G_GNUC_UNUSED, const gchar **attribute_values G_GNUC_UNUSED, gpointer user_data, GError **error G_GNUC_UNUSED) { GvMarkupParsing *parsing = user_data; /* Expected top node */ if (!g_strcmp0(element_name, "Stations")) return; /* Entering a station node */ if (!g_strcmp0(element_name, "Station")) { g_assert_null(parsing->name); g_assert_null(parsing->uri); g_assert_null(parsing->user_agent); return; } /* Name property */ if (!g_strcmp0(element_name, "name")) { g_assert_null(parsing->cur); parsing->cur = &parsing->name; return; } /* Uri property */ if (!g_strcmp0(element_name, "uri")) { g_assert_null(parsing->cur); parsing->cur = &parsing->uri; return; } /* User-agent property */ if (!g_strcmp0(element_name, "user-agent")) { g_assert_null(parsing->user_agent); parsing->cur = &parsing->user_agent; return; } WARNING("Unexpected element: '%s'", element_name); } static void markup_on_error(GMarkupParseContext *context G_GNUC_UNUSED, GError *error G_GNUC_UNUSED, gpointer user_data) { GvMarkupParsing *parsing = user_data; parsing->cur = NULL; g_free(parsing->name); parsing->name = NULL; g_free(parsing->uri); parsing->uri = NULL; g_free(parsing->user_agent); parsing->user_agent = NULL; } static GList * parse_markup(const gchar *text, GError **err) { GMarkupParseContext *context; GMarkupParser parser = { markup_on_start_element, markup_on_end_element, markup_on_text, NULL, markup_on_error, }; GvMarkupParsing parsing = { NULL, NULL, NULL, NULL, NULL }; context = g_markup_parse_context_new(&parser, 0, &parsing, NULL); g_markup_parse_context_parse(context, text, -1, err); g_markup_parse_context_free(context); return g_list_reverse(parsing.list); } static GString * g_string_append_markup_tag_escaped(GString *string, const gchar *tag, const gchar *value) { gchar *escaped; escaped = g_markup_printf_escaped(" <%s>%s\n", tag, value, tag); g_string_append(string, escaped); g_free(escaped); return string; } static gchar * print_markup_station(GvStation *station) { const gchar *name = gv_station_get_name(station); const gchar *uri = gv_station_get_uri(station); const gchar *user_agent = gv_station_get_user_agent(station); GString *string; /* A station is supposed to have an uri */ if (uri == NULL) { WARNING("Station (%s) has no uri !", name); return NULL; } /* Write station in markup fashion */ string = g_string_new(" \n"); if (uri) g_string_append_markup_tag_escaped(string, "uri", uri); if (name) g_string_append_markup_tag_escaped(string, "name", name); if (user_agent) g_string_append_markup_tag_escaped(string, "user-agent", user_agent); g_string_append(string, " \n"); /* Return */ return g_string_free(string, FALSE); } static gchar * print_markup(GList *list, GError **err G_GNUC_UNUSED) { GList *item; GString *string = g_string_new(NULL); g_string_append(string, "\n"); for (item = list; item; item = item->next) { GvStation *station = GV_STATION(item->data); gchar *text; text = print_markup_station(station); if (text == NULL) continue; g_string_append(string, text); g_free(text); } g_string_append(string, ""); return g_string_free(string, FALSE); } /* * Iterator implementation */ struct _GvStationListIter { GList *head, *item; }; GvStationListIter * gv_station_list_iter_new(GvStationList *self) { GList *list = self->priv->stations; GvStationListIter *iter; iter = g_new0(GvStationListIter, 1); iter->head = g_list_copy_deep(list, copy_func_object_ref, NULL); iter->item = iter->head; return iter; } void gv_station_list_iter_free(GvStationListIter *iter) { g_return_if_fail(iter != NULL); g_list_free_full(iter->head, g_object_unref); g_free(iter); } gboolean gv_station_list_iter_loop(GvStationListIter *iter, GvStation **station) { g_return_val_if_fail(iter != NULL, FALSE); g_return_val_if_fail(station != NULL, FALSE); *station = NULL; if (iter->item == NULL) return FALSE; *station = iter->item->data; iter->item = iter->item->next; return TRUE; } /* * GList additions */ static gint glist_sortfunc_random(gconstpointer a G_GNUC_UNUSED, gconstpointer b G_GNUC_UNUSED) { return g_random_boolean() ? 1 : -1; } static GList * g_list_shuffle(GList *list) { /* Shuffle twice. Credits goes to: * http://www.linuxforums.org/forum/programming-scripting/ * 202125-fastest-way-shuffle-linked-list.html */ list = g_list_sort(list, glist_sortfunc_random); list = g_list_sort(list, glist_sortfunc_random); return list; } static GList * g_list_copy_deep_shuffle(GList *list, GCopyFunc func, gpointer user_data) { list = g_list_copy_deep(list, func, user_data); list = g_list_shuffle(list); return list; } /* * Helpers */ static gint are_stations_similar(GvStation *s1, GvStation *s2) { if (s1 == s2) { WARNING("Stations %p and %p are the same", s1, s2); return 0; } if (s1 == NULL || s2 == NULL) return -1; /* Compare uids */ const gchar *s1_uid, *s2_uid; s1_uid = gv_station_get_uid(s1); s2_uid = gv_station_get_uid(s2); if (!g_strcmp0(s1_uid, s2_uid)) { WARNING("Stations %p and %p have the same uid '%s'", s1, s2, s1_uid); return 0; } /* Compare names. * Two stations who don't have name are different. */ const gchar *s1_name, *s2_name; s1_name = gv_station_get_name(s1); s2_name = gv_station_get_name(s2); if (s1_name == NULL && s2_name == NULL) return -1; if (!g_strcmp0(s1_name, s2_name)) { DEBUG("Stations %p and %p have the same name '%s'", s1, s2, s1_name); return 0; } /* Compare uris */ const gchar *s1_uri, *s2_uri; s1_uri = gv_station_get_uri(s1); s2_uri = gv_station_get_uri(s2); if (!g_strcmp0(s1_uri, s2_uri)) { DEBUG("Stations %p and %p have the same uri '%s'", s1, s2, s1_uri); return 0; } return -1; } /* * Signal handlers */ static gboolean when_timeout_save_station_list(gpointer data) { GvStationList *self = GV_STATION_LIST(data); GvStationListPrivate *priv = self->priv; gv_station_list_save(self); priv->save_source_id = 0; return G_SOURCE_REMOVE; } static void gv_station_list_save_delayed(GvStationList *self) { GvStationListPrivate *priv = self->priv; if (priv->save_source_id > 0) g_source_remove(priv->save_source_id); priv->save_source_id = g_timeout_add_seconds(SAVE_DELAY, when_timeout_save_station_list, self); } static void on_station_notify(GvStation *station, GParamSpec *pspec, GvStationList *self) { const gchar *property_name = g_param_spec_get_name(pspec); TRACE("%s, %s, %p", gv_station_get_uid(station), property_name, self); /* We might want to save changes */ if (!g_strcmp0(property_name, "uri") || !g_strcmp0(property_name, "name")) { gv_station_list_save_delayed(self); } /* Emit signal */ g_signal_emit(self, signals[SIGNAL_STATION_MODIFIED], 0, station); } /* * Public functions */ void gv_station_list_remove(GvStationList *self, GvStation *station) { GvStationListPrivate *priv = self->priv; GList *item; /* Ensure a valid station was given */ if (station == NULL) { WARNING("Attempting to remove NULL station"); return; } /* Give info */ INFO("Removing station '%s'", gv_station_get_name_or_uri(station)); /* Check that we own this station at first. If we don't find it * in our internal list, it's probably a programming error. */ item = g_list_find(priv->stations, station); if (item == NULL) { WARNING("GvStation %p (%s) not found in list", station, gv_station_get_uid(station)); return; } /* Disconnect signal handlers */ g_signal_handlers_disconnect_by_data(station, self); /* Remove from list */ priv->stations = g_list_remove_link(priv->stations, item); g_list_free(item); /* Unown the station */ g_object_unref(station); /* Rebuild the shuffled station list */ if (priv->shuffled) { g_list_free_full(priv->shuffled, g_object_unref); priv->shuffled = g_list_copy_deep_shuffle(priv->stations, copy_func_object_ref, NULL); } /* Emit a signal */ g_signal_emit(self, signals[SIGNAL_STATION_REMOVED], 0, station); /* Save */ gv_station_list_save_delayed(self); } void gv_station_list_insert(GvStationList *self, GvStation *station, gint pos) { GvStationListPrivate *priv = self->priv; GList *similar_item; /* Ensure a valid station was given */ if (station == NULL) { WARNING("Attempting to insert NULL station"); return; } /* Give info */ INFO("Inserting station '%s'", gv_station_get_name_or_uri(station)); /* Check that the station is not already part of the list. * Duplicates are a programming error, we must warn about that. * Identical fields are an user error. * Warnings and such are encapsulated in the GCompareFunc used * here, this is messy but temporary (hopefully). */ similar_item = g_list_find_custom(priv->stations, station, (GCompareFunc) are_stations_similar); if (similar_item) return; /* Take ownership of the station */ g_object_ref_sink(station); /* Add to the list at the right position */ priv->stations = g_list_insert(priv->stations, station, pos); /* Connect to notify signal */ g_signal_connect_object(station, "notify", G_CALLBACK(on_station_notify), self, 0); /* Rebuild the shuffled station list */ if (priv->shuffled) { g_list_free_full(priv->shuffled, g_object_unref); priv->shuffled = g_list_copy_deep_shuffle(priv->stations, copy_func_object_ref, NULL); } /* Emit a signal */ g_signal_emit(self, signals[SIGNAL_STATION_ADDED], 0, station); /* Save */ gv_station_list_save_delayed(self); } /* Insert a station before another. * If 'before' is NULL or is not found, the station is appended at the end of the list. */ void gv_station_list_insert_before(GvStationList *self, GvStation *station, GvStation *before) { GvStationListPrivate *priv = self->priv; gint pos = -1; pos = g_list_index(priv->stations, before); gv_station_list_insert(self, station, pos); } /* Insert a station after another. * If 'after' is NULL or not found, the station is appended at the beginning of the list. */ void gv_station_list_insert_after(GvStationList *self, GvStation *station, GvStation *after) { GvStationListPrivate *priv = self->priv; gint pos = 0; pos = g_list_index(priv->stations, after); pos += 1; /* tricky but does what we want even for pos == -1 */ gv_station_list_insert(self, station, pos); } void gv_station_list_prepend(GvStationList *self, GvStation *station) { gv_station_list_insert_after(self, station, NULL); } void gv_station_list_append(GvStationList *self, GvStation *station) { gv_station_list_insert_before(self, station, NULL); } void gv_station_list_move(GvStationList *self, GvStation *station, gint pos) { GvStationListPrivate *priv = self->priv; GList *item; /* Find the station */ item = g_list_find(priv->stations, station); if (item == NULL) { WARNING("GvStation %p (%s) not found in list", station, gv_station_get_uid(station)); return; } /* Move it */ priv->stations = g_list_remove_link(priv->stations, item); g_list_free(item); priv->stations = g_list_insert(priv->stations, station, pos); /* Emit a signal */ g_signal_emit(self, signals[SIGNAL_STATION_MOVED], 0, station); /* Save */ gv_station_list_save_delayed(self); } /* Move a station before another. * If 'before' is NULL or not found, the station is inserted at the end of the list. */ void gv_station_list_move_before(GvStationList *self, GvStation *station, GvStation *before) { GvStationListPrivate *priv = self->priv; gint pos = -1; pos = g_list_index(priv->stations, before); gv_station_list_move(self, station, pos); } void gv_station_list_move_after(GvStationList *self, GvStation *station, GvStation *after) { GvStationListPrivate *priv = self->priv; gint pos = 0; pos = g_list_index(priv->stations, after); pos += 1; /* tricky but does what we want even for pos == -1 */ gv_station_list_move(self, station, pos); } void gv_station_list_move_first(GvStationList *self, GvStation *station) { gv_station_list_move_after(self, station, NULL); } void gv_station_list_move_last(GvStationList *self, GvStation *station) { gv_station_list_move_before(self, station, NULL); } GvStation * gv_station_list_prev(GvStationList *self, GvStation *station, gboolean repeat, gboolean shuffle) { GvStationListPrivate *priv = self->priv; GList *stations, *item; /* Pickup the right station list, create shuffle list if needed */ if (shuffle) { if (priv->shuffled == NULL) { priv->shuffled = g_list_copy_deep_shuffle(priv->stations, copy_func_object_ref, NULL); } stations = priv->shuffled; } else { if (priv->shuffled) { g_list_free_full(priv->shuffled, g_object_unref); priv->shuffled = NULL; } stations = priv->stations; } /* If the station list is empty, bail out */ if (stations == NULL) return NULL; /* Return last station for NULL argument */ if (station == NULL) return g_list_last(stations)->data; /* Try to find station in station list */ item = g_list_find(stations, station); if (item == NULL) return NULL; /* Return previous station if any */ item = item->prev; if (item) return item->data; /* Without repeat, there's no more station */ if (!repeat) return NULL; /* With repeat, we may re-shuffle, then return the last station */ if (shuffle) { GList *last_item; stations = g_list_shuffle(priv->shuffled); /* In case the last station (that we're about to return) happens to be * the same as the current station, we do a little a magic trick. */ last_item = g_list_last(stations); if (last_item->data == station) { stations = g_list_remove_link(stations, last_item); stations = g_list_prepend(stations, last_item->data); g_list_free(last_item); } priv->shuffled = stations; } return g_list_last(stations)->data; } GvStation * gv_station_list_next(GvStationList *self, GvStation *station, gboolean repeat, gboolean shuffle) { GvStationListPrivate *priv = self->priv; GList *stations, *item; /* Pickup the right station list, create shuffle list if needed */ if (shuffle) { if (priv->shuffled == NULL) { priv->shuffled = g_list_copy_deep_shuffle(priv->stations, copy_func_object_ref, NULL); } stations = priv->shuffled; } else { if (priv->shuffled) { g_list_free_full(priv->shuffled, g_object_unref); priv->shuffled = NULL; } stations = priv->stations; } /* If the station list is empty, bail out */ if (stations == NULL) return NULL; /* Return first station for NULL argument */ if (station == NULL) return stations->data; /* Try to find station in station list */ item = g_list_find(stations, station); if (item == NULL) return NULL; /* Return next station if any */ item = item->next; if (item) return item->data; /* Without repeat, there's no more station */ if (!repeat) return NULL; /* With repeat, we may re-shuffle, then return the first station */ if (shuffle) { GList *first_item; stations = g_list_shuffle(priv->shuffled); /* In case the first station (that we're about to return) happens to be * the same as the current station, we do a little a magic trick. */ first_item = g_list_first(stations); if (first_item->data == station) { stations = g_list_remove_link(stations, first_item); stations = g_list_append(stations, first_item->data); g_list_free(first_item); } priv->shuffled = stations; } return stations->data; } GvStation * gv_station_list_first(GvStationList *self) { GList *stations = self->priv->stations; if (stations == NULL) return NULL; return g_list_first(stations)->data; } GvStation * gv_station_list_last(GvStationList *self) { GList *stations = self->priv->stations; if (stations == NULL) return NULL; return g_list_last(stations)->data; } GvStation * gv_station_list_find(GvStationList *self, GvStation *station) { GList *stations = self->priv->stations; GList *item; item = g_list_find(stations, station); return item ? item->data : NULL; } GvStation * gv_station_list_find_by_name(GvStationList *self, const gchar *name) { GList *item; /* Ensure station name is valid */ if (name == NULL) { WARNING("Attempting to find a station with NULL name"); return NULL; } /* Forbid empty names */ if (!g_strcmp0(name, "")) return NULL; /* Iterate on station list */ for (item = self->priv->stations; item; item = item->next) { GvStation *station = item->data; if (!g_strcmp0(name, gv_station_get_name(station))) return station; } return NULL; } GvStation * gv_station_list_find_by_uri(GvStationList *self, const gchar *uri) { GList *item; /* Ensure station name is valid */ if (uri == NULL) { WARNING("Attempting to find a station with NULL uri"); return NULL; } /* Iterate on station list */ for (item = self->priv->stations; item; item = item->next) { GvStation *station = item->data; if (!g_strcmp0(uri, gv_station_get_uri(station))) return station; } return NULL; } GvStation * gv_station_list_find_by_uid(GvStationList *self, const gchar *uid) { GList *item; /* Ensure station name is valid */ if (uid == NULL) { WARNING("Attempting to find a station with NULL uid"); return NULL; } /* Iterate on station list */ for (item = self->priv->stations; item; item = item->next) { GvStation *station = item->data; if (!g_strcmp0(uid, gv_station_get_uid(station))) return station; } return NULL; } GvStation * gv_station_list_find_by_guessing(GvStationList *self, const gchar *string) { if (is_uri_scheme_supported(string)) return gv_station_list_find_by_uri(self, string); else /* Assume it's the station name */ return gv_station_list_find_by_name(self, string); } void gv_station_list_save(GvStationList *self) { GvStationListPrivate *priv = self->priv; GError *err = NULL; gchar *dirname = NULL; gchar *text = NULL; /* Stringify data */ text = print_markup(priv->stations, &err); if (err) goto cleanup; /* Create directory */ dirname = g_path_get_dirname(priv->save_path); if (g_mkdir_with_parents(dirname, S_IRWXU) != 0) { WARNING("Failed to make directory '%s': %s", dirname, strerror(errno)); goto cleanup; } /* Write to file */ g_file_set_contents(priv->save_path, text, -1, &err); cleanup: /* Cleanup */ g_free(dirname); g_free(text); /* Handle error */ if (err == NULL) { INFO("Station list saved to '%s'", priv->save_path); } else { WARNING("Failed to save station list: %s", err->message); gv_errorable_emit_error(GV_ERRORABLE(self), _("%s: %s"), _("Failed to save station list"), err->message); g_error_free(err); } } void gv_station_list_load(GvStationList *self) { GvStationListPrivate *priv = self->priv; const gchar *loaded_path = NULL; guint i, n_paths; GList *item; TRACE("%p", self); /* This should be called only once at startup */ g_assert_null(priv->stations); /* Load from a list of paths */ n_paths = g_strv_length(priv->load_paths); for (i = 0; i < n_paths; i++) { const gchar *path = priv->load_paths[i]; GError *err = NULL; gchar *text; /* Check if the file exists */ if (!g_file_test(path, G_FILE_TEST_EXISTS)) continue; /* Attempt to read file */ g_file_get_contents(path, &text, NULL, &err); if (err) { WARNING("%s", err->message); g_clear_error(&err); continue; } /* Attempt to parse it */ priv->stations = parse_markup(text, &err); g_free(text); if (err) { WARNING("Failed to parse '%s': %s", path, err->message); g_clear_error(&err); continue; } /* Success */ loaded_path = path; break; } /* Check if we got something */ if (loaded_path) { INFO("Station list loaded from file '%s'", loaded_path); } else { GError *err = NULL; INFO("No valid station list file found, using hard-coded default"); priv->stations = parse_markup(DEFAULT_STATION_LIST, &err); if (err) { ERROR("%s", err->message); /* Program execution stops here */ } } /* Dump the number of stations */ DEBUG("Station list has %u stations", gv_station_list_length(self)); /* Register a notify handler for each station */ for (item = priv->stations; item; item = item->next) { GvStation *station = item->data; g_signal_connect_object(station, "notify", G_CALLBACK(on_station_notify), self, 0); } /* Emit a signal to indicate that the list has been loaded */ g_signal_emit(self, signals[SIGNAL_LOADED], 0); } guint gv_station_list_length(GvStationList *self) { GvStationListPrivate *priv = self->priv; return g_list_length(priv->stations); } GvStationList * gv_station_list_new(void) { return g_object_new(GV_TYPE_STATION_LIST, NULL); } /* * GObject methods */ static gchar ** make_station_list_load_paths(void) { const gchar *const *system_dirs; const gchar *user_dir; guint i, n_dirs; gchar **dirs; system_dirs = gv_get_app_system_data_dirs(); user_dir = gv_get_app_user_data_dir(); n_dirs = g_strv_length((gchar **) system_dirs) + 1; dirs = g_malloc0_n(n_dirs + 1, sizeof(gchar *)); dirs[0] = g_build_filename(user_dir, STATION_LIST_FILE, NULL); for (i = 1; i < n_dirs; i++) { const gchar *dir; dir = system_dirs[i - 1]; dirs[i] = g_build_filename(dir, STATION_LIST_FILE, NULL); } return dirs; } static gchar * make_station_list_save_path(void) { const gchar *user_dir; gchar *dir; user_dir = gv_get_app_user_data_dir(); dir = g_build_filename(user_dir, STATION_LIST_FILE, NULL); return dir; } static void move_station_list_file(const gchar *new_file) { const gchar *user_config_dir; gchar *old_file = NULL; gchar *dirname = NULL; user_config_dir = gv_get_app_user_config_dir(); old_file = g_build_filename(user_config_dir, "stations", NULL); if (!g_file_test(old_file, G_FILE_TEST_EXISTS)) goto cleanup; INFO("Station list migration: '%s' > '%s'", old_file, new_file); dirname = g_path_get_dirname(new_file); if (g_mkdir_with_parents(dirname, S_IRWXU) != 0) { WARNING("Failed to make directory '%s': %s", dirname, strerror(errno)); goto cleanup; } g_free(dirname); if (g_rename(old_file, new_file) != 0) { WARNING("Failed to rename file '%s' to '%s': %s", old_file, new_file, strerror(errno)); goto cleanup; } dirname = g_path_get_dirname(old_file); g_rmdir(dirname); cleanup: g_free(dirname); g_free(old_file); } static void gv_station_list_finalize(GObject *object) { GvStationList *self = GV_STATION_LIST(object); GvStationListPrivate *priv = self->priv; GList *item; TRACE("%p", object); /* Run any pending save operation */ if (priv->save_source_id > 0) when_timeout_save_station_list(self); /* Free shuffled station list */ g_list_free_full(priv->shuffled, g_object_unref); /* Free station list and ensure no memory is leaked. This works only if the * station list is the last object to hold references to stations. In other * words, the station list must be the last object finalized. */ for (item = priv->stations; item; item = item->next) { g_object_add_weak_pointer(G_OBJECT(item->data), &(item->data)); g_object_unref(item->data); if (item->data != NULL) WARNING("Station '%s' has not been finalized !", gv_station_get_name_or_uri(GV_STATION(item->data))); } g_list_free(priv->stations); /* Free paths */ g_free(priv->save_path); g_strfreev(priv->load_paths); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_station_list, object); } static void gv_station_list_constructed(GObject *object) { GvStationList *self = GV_STATION_LIST(object); GvStationListPrivate *priv = self->priv; /* Initialize paths */ priv->load_paths = make_station_list_load_paths(); priv->save_path = make_station_list_save_path(); /* In version 4.1, the station file moved */ move_station_list_file(priv->save_path); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_station_list, object); } static void gv_station_list_init(GvStationList *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_station_list_get_instance_private(self); } static void gv_station_list_class_init(GvStationListClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_station_list_finalize; object_class->constructed = gv_station_list_constructed; /* Signals */ signals[SIGNAL_LOADED] = g_signal_new("loaded", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); signals[SIGNAL_STATION_ADDED] = g_signal_new("station-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT); signals[SIGNAL_STATION_REMOVED] = g_signal_new("station-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT); signals[SIGNAL_STATION_MODIFIED] = g_signal_new("station-modified", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT); signals[SIGNAL_STATION_MOVED] = g_signal_new("station-moved", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_OBJECT); } goodvibes-v0.4.2/src/core/gv-station-list.h000066400000000000000000000065671337147241200206420ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_STATION_LIST_H__ #define __GOODVIBES_CORE_GV_STATION_LIST_H__ #include #include "core/gv-station.h" /* GObject declarations */ #define GV_TYPE_STATION_LIST gv_station_list_get_type() G_DECLARE_FINAL_TYPE(GvStationList, gv_station_list, GV, STATION_LIST, GObject) /* Data types */ typedef struct _GvStationListIter GvStationListIter; /* Methods */ GvStationList *gv_station_list_new (void); void gv_station_list_load (GvStationList *self); void gv_station_list_save (GvStationList *self); guint gv_station_list_length(GvStationList *self); void gv_station_list_prepend (GvStationList *self, GvStation *station); void gv_station_list_append (GvStationList *self, GvStation *station); void gv_station_list_insert (GvStationList *self, GvStation *station, gint position); void gv_station_list_insert_before(GvStationList *self, GvStation *station, GvStation *before); void gv_station_list_insert_after (GvStationList *self, GvStation *station, GvStation *after); void gv_station_list_remove (GvStationList *self, GvStation *station); void gv_station_list_move (GvStationList *self, GvStation *station, gint position); void gv_station_list_move_before(GvStationList *self, GvStation *station, GvStation *before); void gv_station_list_move_after (GvStationList *self, GvStation *station, GvStation *after); void gv_station_list_move_first (GvStationList *self, GvStation *station); void gv_station_list_move_last (GvStationList *self, GvStation *station); GvStation *gv_station_list_first(GvStationList *self); GvStation *gv_station_list_last (GvStationList *self); GvStation *gv_station_list_prev (GvStationList *self, GvStation *station, gboolean repeat, gboolean shuffle); GvStation *gv_station_list_next (GvStationList *self, GvStation *station, gboolean repeat, gboolean shuffle); GvStation *gv_station_list_find (GvStationList *self, GvStation *station); GvStation *gv_station_list_find_by_name (GvStationList *self, const gchar *name); GvStation *gv_station_list_find_by_uri (GvStationList *self, const gchar *uri); GvStation *gv_station_list_find_by_uid (GvStationList *self, const gchar *uid); GvStation *gv_station_list_find_by_guessing(GvStationList *self, const gchar *string); /* Iterator methods */ GvStationListIter *gv_station_list_iter_new (GvStationList *self); void gv_station_list_iter_free(GvStationListIter *iter); gboolean gv_station_list_iter_loop(GvStationListIter *iter, GvStation **station); #endif /* __GOODVIBES_CORE_GV_STATION_LIST_H__ */ goodvibes-v0.4.2/src/core/gv-station.c000066400000000000000000000256711337147241200176610ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core-internal.h" #include "core/gv-playlist.h" #include "core/gv-station.h" /* * Properties */ enum { /* Reserved */ PROP_0, /* Set at construct-time */ PROP_UID, /* Set by user - station definition */ PROP_NAME, PROP_URI, /* Set by user - customization */ PROP_USER_AGENT, /* Learnt along the way */ PROP_STREAM_URIS, PROP_NOMINAL_BITRATE, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * Signals */ enum { SIGNAL_PLAYLIST_DOWNLOADED, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * GObject definitions */ struct _GvStationPrivate { /* * Properties */ /* Set at construct-time */ gchar *uid; /* Set by user - station definition */ gchar *name; gchar *uri; /* Set by user - customization */ gchar *user_agent; /* Learnt along the way */ GSList *stream_uris; guint nominal_bitrate; }; typedef struct _GvStationPrivate GvStationPrivate; struct _GvStation { /* Parent instance structure */ GInitiallyUnowned parent_instance; /* Private data */ GvStationPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvStation, gv_station, G_TYPE_INITIALLY_UNOWNED) /* * Helpers */ static gpointer copy_func_strdup(gconstpointer src, gpointer data G_GNUC_UNUSED) { return g_strdup(src); } static void gv_station_set_stream_uris(GvStation *self, GSList *list) { GvStationPrivate *priv = self->priv; if (priv->stream_uris) g_slist_free_full(priv->stream_uris, g_free); priv->stream_uris = g_slist_copy_deep(list, copy_func_strdup, NULL); g_object_notify(G_OBJECT(self), "stream-uris"); } static void gv_station_set_stream_uri(GvStation *self, const gchar *uri) { GSList *list = NULL; list = g_slist_append(list, g_strdup(uri)); gv_station_set_stream_uris(self, list); g_slist_free_full(list, g_free); } /* * Signal handlers */ static void on_playlist_downloaded(GvPlaylist *playlist, GvStation *self) { GSList *streams; streams = gv_playlist_get_stream_list(playlist); gv_station_set_stream_uris(self, streams); g_object_unref(playlist); } /* * Property accessors */ const gchar * gv_station_get_uid(GvStation *self) { return self->priv->uid; } const gchar * gv_station_get_name(GvStation *self) { return self->priv->name; } void gv_station_set_name(GvStation *self, const gchar *name) { GvStationPrivate *priv = self->priv; /* What should be used when there's no name for the station ? * NULL or empty string ? Let's settle the question here. */ if (name && *name == '\0') name = NULL; if (!g_strcmp0(priv->name, name)) return; g_free(priv->name); priv->name = g_strdup(name); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_NAME]); } const gchar * gv_station_get_uri(GvStation *self) { return self->priv->uri; } void gv_station_set_uri(GvStation *self, const gchar *uri) { GvStationPrivate *priv = self->priv; /* Setting the uri to NULL is forbidden */ if (uri == NULL) { if (priv->uri == NULL) { /* Construct-time set. Setting the uri to NULL * at this moment is an error in the code. */ ERROR("Creating station with an empty uri"); /* Program execution stops here */ } else { /* User is trying to set the uri to null, we just * silently discard the request. */ DEBUG("Trying to set station uri to null. Ignoring."); return; } } /* Set uri */ if (!g_strcmp0(priv->uri, uri)) return; g_free(priv->uri); priv->uri = g_strdup(uri); /* If this is not a playlist uri, then it's a stream uri */ if (gv_playlist_get_format(uri) == GV_PLAYLIST_FORMAT_UNKNOWN) gv_station_set_stream_uri(self, uri); /* Notify */ g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_URI]); } const gchar * gv_station_get_name_or_uri(GvStation *self) { GvStationPrivate *priv = self->priv; return priv->name ? priv->name : priv->uri; } const gchar * gv_station_get_user_agent(GvStation *self) { return self->priv->user_agent; } void gv_station_set_user_agent(GvStation *self, const gchar *user_agent) { GvStationPrivate *priv = self->priv; if (user_agent && user_agent[0] == '\0') user_agent = NULL; if (!g_strcmp0(priv->user_agent, user_agent)) return; g_free(priv->user_agent); priv->user_agent = g_strdup(user_agent); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_USER_AGENT]); } GSList * gv_station_get_stream_uris(GvStation *self) { return self->priv->stream_uris; } const gchar * gv_station_get_first_stream_uri(GvStation *self) { GSList *uris; uris = self->priv->stream_uris; if (uris == NULL) return NULL; return (const gchar *) uris->data; } guint gv_station_get_nominal_bitrate(GvStation *self) { return self->priv->nominal_bitrate; } void gv_station_set_nominal_bitrate(GvStation *self, guint bitrate) { GvStationPrivate *priv = self->priv; if (priv->nominal_bitrate == bitrate) return; priv->nominal_bitrate = bitrate; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_NOMINAL_BITRATE]); } static void gv_station_get_property(GObject *object, guint property_id, GValue *value G_GNUC_UNUSED, GParamSpec *pspec) { GvStation *self = GV_STATION(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_UID: g_value_set_string(value, gv_station_get_uid(self)); break; case PROP_NAME: g_value_set_string(value, gv_station_get_name(self)); break; case PROP_URI: g_value_set_string(value, gv_station_get_uri(self)); break; case PROP_USER_AGENT: g_value_set_string(value, gv_station_get_user_agent(self)); break; case PROP_STREAM_URIS: g_value_set_pointer(value, gv_station_get_stream_uris(self)); break; case PROP_NOMINAL_BITRATE: g_value_set_uint(value, gv_station_get_nominal_bitrate(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_station_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvStation *self = GV_STATION(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_NAME: gv_station_set_name(self, g_value_get_string(value)); break; case PROP_URI: gv_station_set_uri(self, g_value_get_string(value)); break; case PROP_USER_AGENT: gv_station_set_user_agent(self, g_value_get_string(value)); break; case PROP_NOMINAL_BITRATE: gv_station_set_nominal_bitrate(self, g_value_get_uint(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ gboolean gv_station_download_playlist(GvStation *self) { GvStationPrivate *priv = self->priv; GvPlaylist *playlist; if (priv->uri == NULL) { WARNING("No uri to download"); return FALSE; } if (gv_playlist_get_format(priv->uri) == GV_PLAYLIST_FORMAT_UNKNOWN) { WARNING("Uri doesn't seem to be a playlist"); return FALSE; } /* No need to keep track of that, it's unreferenced in the callback */ playlist = gv_playlist_new(priv->uri); g_signal_connect_object(playlist, "downloaded", G_CALLBACK(on_playlist_downloaded), self, 0); gv_playlist_download(playlist, priv->user_agent ? priv->user_agent : gv_core_user_agent); return TRUE; } gchar * gv_station_make_name(GvStation *self, gboolean escape) { GvStationPrivate *priv = self->priv; gchar *str; str = priv->name ? priv->name : priv->uri; if (escape) str = g_markup_escape_text(str, -1); else str = g_strdup(str); return str; } GvStation * gv_station_new(const gchar *name, const gchar *uri) { return g_object_new(GV_TYPE_STATION, "name", name, "uri", uri, NULL); } /* * GObject methods */ static void gv_station_finalize(GObject *object) { GvStationPrivate *priv = GV_STATION(object)->priv; TRACE("%p", object); /* Free any allocated resources */ if (priv->stream_uris) g_slist_free_full(priv->stream_uris, g_free); g_free(priv->uid); g_free(priv->name); g_free(priv->uri); g_free(priv->user_agent); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_station, object); } static void gv_station_init(GvStation *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_station_get_instance_private(self); /* Initialize properties */ self->priv->uid = g_strdup_printf("%p", self); } static void gv_station_class_init(GvStationClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_station_finalize; /* Properties */ object_class->get_property = gv_station_get_property; object_class->set_property = gv_station_set_property; properties[PROP_UID] = g_param_spec_string("uid", "UID", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_NAME] = g_param_spec_string("name", "Name", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT); properties[PROP_URI] = g_param_spec_string("uri", "Uri", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT); properties[PROP_USER_AGENT] = g_param_spec_string("user-agent", "User agent", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_STREAM_URIS] = g_param_spec_pointer("stream-uris", "Stream uris", NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_NOMINAL_BITRATE] = g_param_spec_uint("nominal-bitrate", "Nominal bitrate", NULL, 0, G_MAXUINT, 0, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); g_object_class_install_properties(object_class, PROP_N, properties); /* Signals */ signals[SIGNAL_PLAYLIST_DOWNLOADED] = g_signal_new("playlist-downloaded", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } goodvibes-v0.4.2/src/core/gv-station.h000066400000000000000000000041701337147241200176550ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_CORE_GV_STATION_H__ #define __GOODVIBES_CORE_GV_STATION_H__ #include /* GObject declarations */ #define GV_TYPE_STATION gv_station_get_type() G_DECLARE_FINAL_TYPE(GvStation, gv_station, GV, STATION, GInitiallyUnowned) /* Methods */ GvStation *gv_station_new (const gchar *name, const gchar *uri); gchar *gv_station_make_name (GvStation *self, gboolean escape); gboolean gv_station_download_playlist(GvStation *self); /* Property accessors */ const gchar *gv_station_get_uid (GvStation *self); const gchar *gv_station_get_name (GvStation *self); void gv_station_set_name (GvStation *self, const gchar *name); const gchar *gv_station_get_uri (GvStation *self); void gv_station_set_uri (GvStation *self, const gchar *uri); const gchar *gv_station_get_name_or_uri (GvStation *self); GSList *gv_station_get_stream_uris (GvStation *self); const gchar *gv_station_get_first_stream_uri(GvStation *self); const gchar *gv_station_get_user_agent (GvStation *self); void gv_station_set_user_agent (GvStation *self, const gchar *user_agent); guint gv_station_get_nominal_bitrate (GvStation *self); void gv_station_set_nominal_bitrate (GvStation *self, guint bitrate); #endif /* __GOODVIBES_CORE_GV_STATION_H__ */ goodvibes-v0.4.2/src/core/meson.build000066400000000000000000000015071337147241200175540ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Sources and dependencies core_sources = [ 'gst-additions.c', 'gv-core.c', 'gv-engine.c', 'gv-metadata.c', 'gv-player.c', 'gv-playlist.c', 'gv-station.c', 'gv-station-list.c', ] core_dependencies = [ glib_dep, gobject_dep, gio_dep, gst_dep, gst_audio_dep, gst_base_dep, libsoup_dep, gvframework_dep, ] core_enum_headers = [ 'gv-engine.h', 'gv-player.h' ] core_enums = gnome.mkenums_simple('gv-core-enum-types', sources: core_enum_headers ) core_enum_h = core_enums[1] # Library definition gvcore = static_library('gvcore', sources: [ core_enums, core_sources ], dependencies: core_dependencies, include_directories: root_inc, ) gvcore_dep = declare_dependency( dependencies: core_dependencies, sources: [ core_enum_h ], link_with: gvcore, ) goodvibes-v0.4.2/src/feat/000077500000000000000000000000001337147241200153765ustar00rootroot00000000000000goodvibes-v0.4.2/src/feat/gv-console-output.c000066400000000000000000000126741337147241200211660ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "framework/glib-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-console-output.h" /* * GObject definitions */ struct _GvConsoleOutput { /* Parent instance structure */ GvFeature parent_instance; }; G_DEFINE_TYPE(GvConsoleOutput, gv_console_output, GV_TYPE_FEATURE) /* * Helpers */ #define PRINT(fmt, ...) g_print(fmt "\n", ##__VA_ARGS__) static const gchar * time_now(void) { static gchar *now_str; GDateTime *now; g_free(now_str); now = g_date_time_new_now_local(); now_str = g_date_time_format(now, "%T"); g_date_time_unref(now); return now_str; } static void print_hello_line(void) { PRINT("---- " GV_NAME_CAPITAL " " PACKAGE_VERSION " ----"); PRINT("Hit Ctrl+C to quit..."); } static void print_goodbye_line(void) { PRINT("---- Bye ----"); } static void print_error(const gchar *error_string) { PRINT(VT_BOLD("Error !") " %s", error_string); } static void print_station(GvStation *station) { const gchar *str; if (station == NULL) return; str = gv_station_get_name(station); if (str) { PRINT(VT_BOLD("> %s Playing %s"), time_now(), str); } else { str = gv_station_get_uri(station); PRINT(VT_BOLD("> %s Playing <%s>"), time_now(), str); } } static void print_metadata(GvMetadata *metadata) { const gchar *artist; const gchar *title; const gchar *album; const gchar *year; const gchar *genre; if (metadata == NULL) return; artist = gv_metadata_get_artist(metadata); title = gv_metadata_get_title(metadata); album = gv_metadata_get_album(metadata); year = gv_metadata_get_year(metadata); genre = gv_metadata_get_genre(metadata); /* Ensure this first line is printed, with a timestamp */ if (title == NULL) title = "(Unknown title)"; PRINT(". %s %s", time_now(), title); /* Other fields are optional */ if (artist) PRINT(" %s", artist); if (album && year) PRINT(" %s (%s)", album, year); else if (album) PRINT(" %s", album); else if (year) PRINT(" (%s)", year); if (genre) PRINT(" %s", genre); } /* * Signal handlers */ static void on_player_notify(GvPlayer *player, GParamSpec *pspec, GvConsoleOutput *self G_GNUC_UNUSED) { const gchar *property_name = g_param_spec_get_name(pspec); if (!g_strcmp0(property_name, "state")) { GvPlayerState state; state = gv_player_get_state(player); if (state == GV_PLAYER_STATE_PLAYING) { GvStation *station; station = gv_player_get_station(player); print_station(station); } } else if (!g_strcmp0(property_name, "metadata")) { GvMetadata *metadata; metadata = gv_player_get_metadata(player); print_metadata(metadata); } } static void on_errorable_error(GvErrorable *errorable G_GNUC_UNUSED, const gchar *error_string, GvConsoleOutput *self G_GNUC_UNUSED) { print_error(error_string); } /* * GvFeature methods */ static void gv_console_output_disable(GvFeature *feature) { GvPlayer *player = gv_core_player; GList *item; /* Disconnect signal handlers */ for (item = gv_framework_get_objects(); item; item = item->next) { GObject *object = G_OBJECT(item->data); if (GV_IS_ERRORABLE(object) == FALSE) continue; g_signal_handlers_disconnect_by_data(object, feature); } g_signal_handlers_disconnect_by_data(player, feature); /* Say good-bye */ print_goodbye_line(); /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_console_output, feature); } static void gv_console_output_enable(GvFeature *feature) { GvPlayer *player = gv_core_player; GList *item; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_console_output, feature); /* Say hello */ print_hello_line(); /* Connect to player 'notify' */ g_signal_connect_object(player, "notify", G_CALLBACK(on_player_notify), feature, 0); /* Connect to objects that emit 'error' */ for (item = gv_framework_get_objects(); item; item = item->next) { GObject *object = G_OBJECT(item->data); if (GV_IS_ERRORABLE(object) == FALSE) continue; g_signal_connect_object(object, "error", G_CALLBACK(on_errorable_error), feature, 0); } } /* * Public methods */ GvFeature * gv_console_output_new(void) { return gv_feature_new(GV_TYPE_CONSOLE_OUTPUT, "ConsoleOutput", GV_FEATURE_EARLY); } /* * GObject methods */ static void gv_console_output_init(GvConsoleOutput *self) { TRACE("%p", self); } static void gv_console_output_class_init(GvConsoleOutputClass *class) { GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GvFeature methods */ feature_class->enable = gv_console_output_enable; feature_class->disable = gv_console_output_disable; } goodvibes-v0.4.2/src/feat/gv-console-output.h000066400000000000000000000023121337147241200211570ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_CONSOLE_OUTPUT_H__ #define __GOODVIBES_FEAT_GV_CONSOLE_OUTPUT_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_CONSOLE_OUTPUT gv_console_output_get_type() G_DECLARE_FINAL_TYPE(GvConsoleOutput, gv_console_output, GV, CONSOLE_OUTPUT, GvFeature) /* Public methods */ GvFeature *gv_console_output_new(void); #endif /* __GOODVIBES_FEAT_GV_CONSOLE_OUTPUT_H__ */ goodvibes-v0.4.2/src/feat/gv-dbus-server-mpris2.c000066400000000000000000001117011337147241200216300ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include "framework/glib-additions.h" #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #ifdef GV_UI_ENABLED #include "ui/gv-ui.h" #endif #include "feat/gv-dbus-server.h" #include "feat/gv-dbus-server-mpris2.h" #define TRACKID_PATH GV_APPLICATION_PATH "/TrackList" #define PLAYLISTID_PATH GV_APPLICATION_PATH "/Playlist" #define DBUS_NAME "org.mpris.MediaPlayer2." GV_NAME_CAPITAL #define DBUS_PATH "/org/mpris/MediaPlayer2" #define DBUS_IFACE_ROOT "org.mpris.MediaPlayer2" #define DBUS_IFACE_PLAYER DBUS_IFACE_ROOT ".Player" #define DBUS_IFACE_TRACKLIST DBUS_IFACE_ROOT ".TrackList" #define DBUS_IFACE_PLAYLISTS DBUS_IFACE_ROOT ".Playlists" static const gchar *DBUS_INTROSPECTION = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; /* * GObject definitions */ struct _GvDbusServerMpris2 { /* Parent instance structure */ GvDbusServer parent_instance; }; G_DEFINE_TYPE(GvDbusServerMpris2, gv_dbus_server_mpris2, GV_TYPE_DBUS_SERVER) /* * Helpers */ static gchar * make_playlist_id(GvStation *station) { /* As suggested in the MPRIS2 specifications, "/" should be used if NULL. * https://specifications.freedesktop.org/mpris-spec/latest/ * Playlists_Interface.html#Struct:Maybe_Playlist */ if (station == NULL) return g_strdup("/"); return g_strdup_printf(PLAYLISTID_PATH "/%s", gv_station_get_uid(station)); } static gboolean parse_playlist_id(const gchar *playlist_id, GvStationList *station_list, GvStation **station) { const gchar *station_uid; g_assert_nonnull(station); *station = NULL; if (!g_strcmp0(playlist_id, "/")) return TRUE; if (!g_str_has_prefix(playlist_id, PLAYLISTID_PATH "/")) return FALSE; station_uid = playlist_id + strlen(PLAYLISTID_PATH "/"); *station = gv_station_list_find_by_uid(station_list, station_uid); if (*station == NULL) return FALSE; return TRUE; } static gchar * make_track_id(GvStation *station) { if (station == NULL) return g_strdup(DBUS_PATH "/TrackList/NoTrack"); return g_strdup_printf(TRACKID_PATH "/%s", gv_station_get_uid(station)); } static gboolean parse_track_id(const gchar *track_id, GvStationList *station_list, GvStation **station) { const gchar *station_uid; g_assert_nonnull(station); *station = NULL; if (!g_strcmp0(track_id, DBUS_PATH "/TrackList/NoTrack")) return TRUE; if (!g_str_has_prefix(track_id, TRACKID_PATH "/")) return FALSE; station_uid = track_id + strlen(TRACKID_PATH "/"); *station = gv_station_list_find_by_uid(station_list, station_uid); if (*station == NULL) return FALSE; return TRUE; } static gint compare_alphabetically(GvStation *a, GvStation *b) { const gchar *str1 = gv_station_get_name_or_uri(a); const gchar *str2 = gv_station_get_name_or_uri(b); return g_strcmp0(str1, str2); } static GList * build_station_list(GvStationList *station_list, gboolean alphabetical, gboolean reverse, guint start_index, guint max_count) { GvStationListIter *iter; GvStation *station; GList *list = NULL; /* Build a GList */ iter = gv_station_list_iter_new(station_list); while (gv_station_list_iter_loop(iter, &station)) list = g_list_append(list, station); gv_station_list_iter_free(iter); /* Order alphabetically if needed */ if (alphabetical) list = g_list_sort(list, (GCompareFunc) compare_alphabetically); /* Reverse order if needed */ if (reverse) list = g_list_reverse(list); /* Handle start index */ while (start_index > 0) { if (list == NULL) break; list = g_list_delete_link(list, list); start_index--; } /* Handle max count */ GList *tmp = list; while (max_count > 0) { if (tmp == NULL) break; tmp = tmp->next; max_count--; } if (tmp != NULL) { if (tmp == list) { g_list_free(list); list = NULL; } else { tmp->prev->next = NULL; tmp->prev = NULL; g_list_free(tmp); } } return list; } /* * GVariant helpers for MPRIS2 types */ static GVariant * g_variant_new_playlist(GvStation *station) { gchar *playlist_id; playlist_id = make_playlist_id(station); GVariant *tuples[] = { g_variant_new_object_path(playlist_id), g_variant_new_string(station ? gv_station_get_name_or_uri(station) : ""), g_variant_new_string("") }; g_free(playlist_id); return g_variant_new_tuple(tuples, 3); } static GVariant * g_variant_new_maybe_playlist(GvStation *station) { GVariant *tuples[] = { g_variant_new_boolean(station ? TRUE : FALSE), g_variant_new_playlist(station) }; return g_variant_new_tuple(tuples, 2); } static GVariant * g_variant_new_metadata_map(GvStation *station, GvMetadata *metadata) { GVariantBuilder b; gchar *track_id; const gchar *uri; const gchar *name; const gchar *artist; const gchar *title; const gchar *album; const gchar *genre; const gchar *year; const gchar *comment; g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); /* Station properties */ if (station == NULL) goto end; track_id = make_track_id(station); g_variant_builder_add_dictentry_object_path(&b, "mpris:trackid", track_id); g_free(track_id); name = gv_station_get_name(station); if (name) g_variant_builder_add_dictentry_string(&b, "goodvibes:station", name); uri = gv_station_get_uri(station); if (uri) g_variant_builder_add_dictentry_string(&b, "xesam:url", uri); /* Metadata if any */ if (metadata == NULL) goto end; artist = gv_metadata_get_artist(metadata); if (artist) g_variant_builder_add_dictentry_array_string(&b, "xesam:artist", artist, NULL); title = gv_metadata_get_title(metadata); if (title) g_variant_builder_add_dictentry_string(&b, "xesam:title", title); album = gv_metadata_get_album(metadata); if (album) g_variant_builder_add_dictentry_string(&b, "xesam:album", album); genre = gv_metadata_get_genre(metadata); if (genre) g_variant_builder_add_dictentry_array_string(&b, "xesam:genre", genre, NULL); year = gv_metadata_get_year(metadata); if (year) g_variant_builder_add_dictentry_string(&b, "xesam:contentCreated", year); comment = gv_metadata_get_comment(metadata); if (comment) g_variant_builder_add_dictentry_array_string(&b, "xesam:comment", comment, NULL); end: return g_variant_builder_end(&b); } static GVariant * g_variant_new_playback_status(GvPlayer *player) { GvPlayerState state; gchar *state_str; state = gv_player_get_state(player); switch (state) { case GV_PLAYER_STATE_PLAYING: state_str = "Playing"; break; case GV_PLAYER_STATE_STOPPED: default: state_str = "Stopped"; break; } return g_variant_new_string(state_str); } static GVariant * g_variant_new_loop_status(GvPlayer *player) { gboolean repeat; repeat = gv_player_get_repeat(player); return g_variant_new_string(repeat ? "Playlist" : "None"); } static GVariant * g_variant_new_shuffle(GvPlayer *player) { gboolean shuffle; shuffle = gv_player_get_shuffle(player); return g_variant_new_boolean(shuffle); } static GVariant * g_variant_new_volume(GvPlayer *player) { guint volume; volume = gv_player_get_volume(player); return g_variant_new_double((gdouble) volume / 100.0); } /* * Dbus method handlers */ static GVariant * method_raise(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { #ifdef GV_UI_ENABLED gv_ui_present_main(); #endif return NULL; } static GVariant * method_quit(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { gv_core_quit(); return NULL; } static GvDbusMethod root_methods[] = { { "Raise", method_raise }, { "Quit", method_quit }, { NULL, NULL } }; static GVariant * method_play(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_play(player); return NULL; } static GVariant * method_stop(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_stop(player); return NULL; } static GVariant * method_toggle(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_toggle(player); return NULL; } static GVariant * method_next(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; if (!gv_player_next(player)) gv_player_stop(player); return NULL; } static GVariant * method_prev(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; if (!gv_player_prev(player)) gv_player_stop(player); return NULL; } static GVariant * method_open_uri(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; GvStation *station; const gchar *uri; g_variant_get(params, "(&s)", &uri); /* Ensure URI is valid */ if (!is_uri_scheme_supported(uri)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "URI scheme not supported."); return NULL; } /* Check if the station is part of the station list */ station = gv_station_list_find_by_uri(station_list, uri); /* Create a new station if needed */ if (station == NULL) { station = gv_station_new(NULL, uri); gv_station_list_append(station_list, station); } /* Play station */ gv_player_set_station(player, station); gv_player_play(player); return NULL; } static GvDbusMethod player_methods[] = { { "Play", method_play }, { "Pause", method_stop }, { "PlayPause", method_toggle }, { "Stop", method_stop }, { "Next", method_next }, { "Previous", method_prev }, { "Seek", NULL }, { "SetPosition", NULL }, { "OpenUri", method_open_uri }, { NULL, NULL } }; static GVariant * method_get_tracks_metadata(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; GVariantBuilder b; GVariantIter *iter; const gchar *track_id; g_variant_get(params, "(ao)", &iter); g_variant_builder_init(&b, G_VARIANT_TYPE("aa{sv}")); while (g_variant_iter_loop(iter, "&o", &track_id)) { GvStation *station; if (!parse_track_id(track_id, station_list, &station)) /* Ignore silently */ continue; g_variant_builder_add_value(&b, g_variant_new_metadata_map(station, NULL)); } return g_variant_builder_end(&b); } static GVariant * method_add_track(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; const gchar *uri; const gchar *after_track_id; gboolean set_as_current; GvStation *station, *after_station; g_variant_get(params, "(&s&ob)", &uri, &after_track_id, &set_as_current); /* Ensure URI is valid */ if (!is_uri_scheme_supported(uri)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid URI scheme for param 'Uri'."); return NULL; } /* Handle 'after_track' */ if (!parse_track_id(after_track_id, station_list, &after_station)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid param 'AfterTrack'."); return NULL; } /* Add a new station to station list. If 'after_station' is NULL, the MPRIS2 * specification says that the track should be placed at the beginning of the * track list. */ station = gv_station_new(NULL, uri); if (after_station) gv_station_list_insert_after(station_list, station, after_station); else gv_station_list_prepend(station_list, station); /* Play new station if needed */ if (set_as_current) { gv_player_set_station(player, station); if (gv_player_get_state(player) != GV_PLAYER_STATE_STOPPED) gv_player_play(player); } return NULL; } static GVariant * method_remove_track(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvStationList *station_list = gv_core_station_list; const gchar *track_id; GvStation *station; g_variant_get(params, "(&o)", &track_id); if (!parse_track_id(track_id, station_list, &station)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid param 'TrackId'."); return NULL; } gv_station_list_remove(station_list, station); return NULL; } static GVariant * method_go_to(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; const gchar *track_id; GvStation *station; // WISHED What about the last line in MPRIS2 specs ? What does that mean ? g_variant_get(params, "(&o)", &track_id); if (!parse_track_id(track_id, station_list, &station)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid param 'TrackId'."); return NULL; } gv_player_set_station(player, station); if (gv_player_get_state(player) != GV_PLAYER_STATE_STOPPED) gv_player_play(player); return NULL; } static GvDbusMethod tracklist_methods[] = { { "GetTracksMetadata", method_get_tracks_metadata }, { "AddTrack", method_add_track }, { "RemoveTrack", method_remove_track }, { "GoTo", method_go_to }, { NULL, NULL } }; static GVariant * method_activate_playlist(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; const gchar *playlist_id; GvStation *station; g_variant_get(params, "(&o)", &playlist_id); if (!parse_playlist_id(playlist_id, station_list, &station)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid param 'PlaylistId'."); return NULL; } if (station == NULL) return NULL; gv_player_set_station(player, station); gv_player_play(player); return NULL; } static GVariant * method_get_playlists(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; GVariantBuilder b; GList *list, *item; guint32 start_index, max_count; const gchar *order; gboolean reverse_order; gboolean alphabetical; g_variant_get(params, "(uu&sb)", &start_index, &max_count, &order, &reverse_order); /* We only support 'Alphabetical' */ if (!g_strcmp0(order, "Alphabetical")) alphabetical = TRUE; else alphabetical = FALSE; /* Make a list */ list = build_station_list(station_list, alphabetical, reverse_order, start_index, max_count); /* Make a GVariant */ g_variant_builder_init(&b, G_VARIANT_TYPE("a(oss)")); for (item = list; item; item = item->next) { GvStation *station = item->data; g_variant_builder_add_value(&b, g_variant_new_playlist(station)); } /* Cleanup */ g_list_free(list); /* Return */ return g_variant_builder_end(&b); } static GvDbusMethod playlists_methods[] = { { "ActivatePlaylist", method_activate_playlist }, { "GetPlaylists", method_get_playlists }, { NULL, NULL } }; /* * Dbus property handlers */ static GVariant * prop_get_true(GvDbusServer *dbus_server G_GNUC_UNUSED) { /* Dummy accessor to return TRUE all the time */ return g_variant_new_boolean(TRUE); } static GVariant * prop_get_false(GvDbusServer *dbus_server G_GNUC_UNUSED) { /* Dummy accessor to return FALSE all the time */ return g_variant_new_boolean(FALSE); } static gboolean prop_set_error(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value G_GNUC_UNUSED, GError **error) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Setting this property is not supported."); return FALSE; } static GVariant * prop_get_can_raise(GvDbusServer *dbus_server G_GNUC_UNUSED) { #ifdef GV_UI_ENABLED return g_variant_new_boolean(TRUE); #else return g_variant_new_boolean(FALSE); #endif } static GVariant * prop_get_identity(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_string(g_get_application_name()); } static GVariant * prop_get_desktop_entry(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_string(GV_APPLICATION_ID); } static GVariant * prop_get_supported_uri_schemes(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_strv(SUPPORTED_URI_SCHEMES, -1); } static GVariant * prop_get_supported_mime_types(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_strv(SUPPORTED_MIME_TYPES, -1); } static GvDbusProperty root_properties[] = { { "CanRaise", prop_get_can_raise, NULL }, { "CanQuit", prop_get_true, NULL }, { "Fullscreen", prop_get_false, prop_set_error }, { "CanSetFullscreen", prop_get_false, NULL }, { "HasTrackList", prop_get_true, NULL }, { "Identity", prop_get_identity, NULL }, { "DesktopEntry", prop_get_desktop_entry, NULL }, { "SupportedUriSchemes", prop_get_supported_uri_schemes, NULL }, { "SupportedMimeTypes", prop_get_supported_mime_types, NULL }, { NULL, NULL, NULL } }; static GVariant * prop_get_playback_status(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; return g_variant_new_playback_status(player); } static GVariant * prop_get_loop_status(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; return g_variant_new_loop_status(player); } static gboolean prop_set_loop_status(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error) { GvPlayer *player = gv_core_player; const gchar *loop_status; gboolean repeat; repeat = FALSE; loop_status = g_variant_get_string(value, NULL); if (!g_strcmp0(loop_status, "Playlist")) { repeat = TRUE; } else if (!g_strcmp0(loop_status, "Track")) { /* 'Track' makes no sense here */ repeat = FALSE; } else if (!g_strcmp0(loop_status, "None")) { repeat = FALSE; } else { /* Any other value should raise an error */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid value."); return FALSE; } gv_player_set_repeat(player, repeat); return TRUE; } static GVariant * prop_get_shuffle(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; return g_variant_new_shuffle(player); } static gboolean prop_set_shuffle(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean shuffle; shuffle = g_variant_get_boolean(value); gv_player_set_shuffle(player, shuffle); return TRUE; } static GVariant * prop_get_volume(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; return g_variant_new_volume(player); } static gboolean prop_set_volume(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gdouble volume; volume = g_variant_get_double(value); volume = round(volume * 100); gv_player_set_volume(player, (guint) volume); return TRUE; } static GVariant * prop_get_rate(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_double(1.0); } static GVariant * prop_get_metadata(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; GvStation *station; GvMetadata *metadata; station = gv_player_get_station(player); metadata = gv_player_get_metadata(player); return g_variant_new_metadata_map(station, metadata); } static GVariant * prop_get_can_play(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; guint n_stations; n_stations = gv_station_list_length(station_list); return g_variant_new_boolean(n_stations > 0 ? TRUE : FALSE); } static GVariant * prop_get_can_go_prev(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean has_prev; has_prev = gv_player_get_prev_station(player) ? TRUE : FALSE; return g_variant_new_boolean(has_prev); } static GVariant * prop_get_can_go_next(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean has_next; has_next = gv_player_get_next_station(player) ? TRUE : FALSE; return g_variant_new_boolean(has_next); } static GvDbusProperty player_properties[] = { { "PlaybackStatus", prop_get_playback_status, NULL }, { "LoopStatus", prop_get_loop_status, prop_set_loop_status }, { "Shuffle", prop_get_shuffle, prop_set_shuffle }, { "Volume", prop_get_volume, prop_set_volume }, { "Rate", prop_get_rate, prop_set_error }, { "MinimumRate", prop_get_rate, NULL }, { "MaximumRate", prop_get_rate, NULL }, { "Metadata", prop_get_metadata, NULL }, { "CanPlay", prop_get_can_play, NULL }, { "CanPause", prop_get_can_play, NULL }, { "CanGoNext", prop_get_can_go_next, NULL }, { "CanGoPrevious", prop_get_can_go_prev, NULL }, { "CanSeek", prop_get_false, NULL }, { "CanControl", prop_get_true, NULL }, { NULL, NULL, NULL } }; static GVariant * prop_get_tracks(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; GvStationListIter *iter; GvStation *station; GVariantBuilder b; g_variant_builder_init(&b, G_VARIANT_TYPE("ao")); iter = gv_station_list_iter_new(station_list); while (gv_station_list_iter_loop(iter, &station)) { gchar *track_id; track_id = make_track_id(station); g_variant_builder_add(&b, "o", track_id); g_free(track_id); } gv_station_list_iter_free(iter); return g_variant_builder_end(&b); } static GvDbusProperty tracklist_properties[] = { { "Tracks", prop_get_tracks, NULL }, { "CanEditTracks", prop_get_true, NULL }, { NULL, NULL, NULL } }; static GVariant * prop_get_playlist_count(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; guint n_stations = gv_station_list_length(station_list); return g_variant_new_uint32(n_stations); } static GVariant * prop_get_orderings(GvDbusServer *dbus_server G_GNUC_UNUSED) { static const gchar *supported_orderings[] = { "Alphabetical", "UserDefined", NULL }; return g_variant_new_strv(supported_orderings, -1); } static GVariant * prop_get_active_playlist(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; GvStation *station = gv_player_get_station(player); return g_variant_new_maybe_playlist(station); } static GvDbusProperty playlists_properties[] = { { "PlaylistCount", prop_get_playlist_count, NULL }, { "Orderings", prop_get_orderings, NULL }, { "ActivePlaylist", prop_get_active_playlist, NULL }, { NULL, NULL, NULL } }; /* * Dbus interfaces */ static GvDbusInterface dbus_interfaces[] = { { DBUS_IFACE_ROOT, root_methods, root_properties }, { DBUS_IFACE_PLAYER, player_methods, player_properties }, { DBUS_IFACE_TRACKLIST, tracklist_methods, tracklist_properties }, { DBUS_IFACE_PLAYLISTS, playlists_methods, playlists_properties }, { NULL, NULL, NULL } }; /* * Signal handlers & callbacks */ static void on_player_notify(GvPlayer *player, GParamSpec *pspec, GvDbusServerMpris2 *self) { GvDbusServer *dbus_server = GV_DBUS_SERVER(self); const gchar *property_name = g_param_spec_get_name(pspec); if (!g_strcmp0(property_name, "state")) { GvPlayerState state = gv_player_get_state(player); if (state != GV_PLAYER_STATE_PLAYING && state != GV_PLAYER_STATE_STOPPED) return; gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "PlaybackStatus", g_variant_new_playback_status(player)); } else if (!g_strcmp0(property_name, "repeat")) { gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "LoopStatus", g_variant_new_loop_status(player)); } else if (!g_strcmp0(property_name, "shuffle")) { gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "Shuffle", g_variant_new_shuffle(player)); } else if (!g_strcmp0(property_name, "volume")) { gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "Volume", g_variant_new_volume(player)); } else if (!g_strcmp0(property_name, "station")) { GvStation *station = gv_player_get_station(player); GvMetadata *metadata = gv_player_get_metadata(player); gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "Metadata", g_variant_new_metadata_map(station, metadata)); /* This signal should be send only if the station's name * or the station's icon was changed. */ gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYLISTS, "PlaylistChanged", g_variant_new_playlist(station)); } else if (!g_strcmp0(property_name, "metadata")) { GvStation *station = gv_player_get_station(player); GvMetadata *metadata = gv_player_get_metadata(player); gv_dbus_server_emit_signal_property_changed (dbus_server, DBUS_IFACE_PLAYER, "Metadata", g_variant_new_metadata_map(station, metadata)); } } static void on_station_list_station_added(GvStationList *station_list, GvStation *station, GvDbusServerMpris2 *self) { GvDbusServer *dbus_server = GV_DBUS_SERVER(self); GVariantBuilder b; GvStation *after_station; gchar *after_track_id; after_station = gv_station_list_prev(station_list, station, FALSE, FALSE); after_track_id = make_track_id(after_station); g_variant_builder_init(&b, G_VARIANT_TYPE("(a{sv}o)")); g_variant_builder_add_value(&b, g_variant_new_metadata_map(station, NULL)); g_variant_builder_add(&b, "o", after_track_id); gv_dbus_server_emit_signal(dbus_server, DBUS_IFACE_TRACKLIST, "TrackAdded", g_variant_builder_end(&b)); g_free(after_track_id); } static void on_station_list_station_removed(GvStationList *station_list G_GNUC_UNUSED, GvStation *station, GvDbusServerMpris2 *self) { GvDbusServer *dbus_server = GV_DBUS_SERVER(self); GVariantBuilder b; gchar *track_id; track_id = make_track_id(station); g_variant_builder_init(&b, G_VARIANT_TYPE("(o)")); g_variant_builder_add(&b, "o", track_id); gv_dbus_server_emit_signal(dbus_server, DBUS_IFACE_TRACKLIST, "TrackRemoved", g_variant_builder_end(&b)); g_free(track_id); } static void on_station_list_station_modified(GvStationList *station_list G_GNUC_UNUSED, GvStation *station, GvDbusServerMpris2 *self) { GvDbusServer *dbus_server = GV_DBUS_SERVER(self); GVariantBuilder b; gchar *track_id; track_id = make_track_id(station); g_variant_builder_init(&b, G_VARIANT_TYPE("(oa{sv})")); g_variant_builder_add(&b, "o", track_id); g_variant_builder_add_value(&b, g_variant_new_metadata_map(station, NULL)); gv_dbus_server_emit_signal(dbus_server, DBUS_IFACE_TRACKLIST, "TrackMetadataChanged", g_variant_builder_end(&b)); g_free(track_id); } /* * GvFeature methods */ static void gv_dbus_server_mpris2_disable(GvFeature *feature) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; /* Signal handlers */ g_signal_handlers_disconnect_by_data(station_list, feature); g_signal_handlers_disconnect_by_data(player, feature); /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_dbus_server_mpris2, feature); } static void gv_dbus_server_mpris2_enable(GvFeature *feature) { GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_dbus_server_mpris2, feature); /* Signal handlers */ g_signal_connect_object(player, "notify", G_CALLBACK(on_player_notify), feature, 0); g_signal_connect_object(station_list, "station-added", G_CALLBACK(on_station_list_station_added), feature, 0); g_signal_connect_object(station_list, "station-removed", G_CALLBACK(on_station_list_station_removed), feature, 0); g_signal_connect_object(station_list, "station-modified", G_CALLBACK(on_station_list_station_modified), feature, 0); } /* * Public methods */ GvFeature * gv_dbus_server_mpris2_new(void) { return gv_feature_new(GV_TYPE_DBUS_SERVER_MPRIS2, "DBusServerMpris2", GV_FEATURE_DEFAULT); } /* * GObject methods */ static void gv_dbus_server_mpris2_constructed(GObject *object) { GvDbusServer *dbus_server = GV_DBUS_SERVER(object); TRACE("%p", object); /* Set dbus server properties */ gv_dbus_server_set_dbus_name(dbus_server, DBUS_NAME); gv_dbus_server_set_dbus_path(dbus_server, DBUS_PATH); gv_dbus_server_set_dbus_introspection(dbus_server, DBUS_INTROSPECTION); gv_dbus_server_set_dbus_interface_table(dbus_server, dbus_interfaces); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_dbus_server_mpris2, object); } static void gv_dbus_server_mpris2_init(GvDbusServerMpris2 *self) { TRACE("%p", self); } static void gv_dbus_server_mpris2_class_init(GvDbusServerMpris2Class *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->constructed = gv_dbus_server_mpris2_constructed; /* Override GvFeature methods */ feature_class->enable = gv_dbus_server_mpris2_enable; feature_class->disable = gv_dbus_server_mpris2_disable; } goodvibes-v0.4.2/src/feat/gv-dbus-server-mpris2.h000066400000000000000000000024571337147241200216440ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_DBUS_SERVER_MPRIS2_H__ #define __GOODVIBES_FEAT_GV_DBUS_SERVER_MPRIS2_H__ #include #include "framework/gv-feature.h" #include "feat/gv-dbus-server.h" /* GObject declarations */ #define GV_TYPE_DBUS_SERVER_MPRIS2 gv_dbus_server_mpris2_get_type() G_DECLARE_FINAL_TYPE(GvDbusServerMpris2, gv_dbus_server_mpris2, \ GV, DBUS_SERVER_MPRIS2, GvDbusServer) /* Public methods */ GvFeature *gv_dbus_server_mpris2_new(void); #endif /* __GOODVIBES_FEAT_GV_DBUS_SERVER_MPRIS2_H__ */ goodvibes-v0.4.2/src/feat/gv-dbus-server-native.c000066400000000000000000000413031337147241200217020ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-additions.h" #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-dbus-server.h" #include "feat/gv-dbus-server-native.h" #define DBUS_PATH GV_APPLICATION_PATH #define DBUS_IFACE_ROOT GV_APPLICATION_ID #define DBUS_IFACE_PLAYER DBUS_IFACE_ROOT ".Player" #define DBUS_IFACE_STATIONS DBUS_IFACE_ROOT ".Stations" static const gchar *DBUS_INTROSPECTION = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; /* * GObject definitions */ struct _GvDbusServerNative { /* Parent instance structure */ GvDbusServer parent_instance; }; G_DEFINE_TYPE(GvDbusServerNative, gv_dbus_server_native, GV_TYPE_DBUS_SERVER) /* * Helpers */ static GVariant * g_variant_new_station(GvStation *station, GvMetadata *metadata) { GVariantBuilder b; const gchar *uri; const gchar *name; const gchar *artist; const gchar *title; const gchar *album; const gchar *genre; const gchar *year; const gchar *comment; g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); /* Station properties */ if (station == NULL) goto end; uri = gv_station_get_uri(station); if (uri) g_variant_builder_add_dictentry_string(&b, "uri", uri); name = gv_station_get_name(station); if (name) g_variant_builder_add_dictentry_string(&b, "name", name); /* Metadata if any */ if (metadata == NULL) goto end; artist = gv_metadata_get_artist(metadata); if (artist) g_variant_builder_add_dictentry_string(&b, "artist", artist); title = gv_metadata_get_title(metadata); if (title) g_variant_builder_add_dictentry_string(&b, "title", title); album = gv_metadata_get_album(metadata); if (album) g_variant_builder_add_dictentry_string(&b, "album", album); genre = gv_metadata_get_genre(metadata); if (genre) g_variant_builder_add_dictentry_string(&b, "genre", genre); year = gv_metadata_get_year(metadata); if (year) g_variant_builder_add_dictentry_string(&b, "year", year); comment = gv_metadata_get_comment(metadata); if (comment) g_variant_builder_add_dictentry_string(&b, "comment", comment); end: return g_variant_builder_end(&b); } /* * Dbus method handlers */ static GVariant * method_quit(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { gv_core_quit(); return NULL; } static GvDbusMethod root_methods[] = { { "Quit", method_quit }, { NULL, NULL } }; static GVariant * method_play(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvPlayer *player = gv_core_player; gchar *string; g_variant_get(params, "(&s)", &string); /* Empty string: play current station */ if (!g_strcmp0(string, "")) { gv_player_play(player); return NULL; } /* Otherwise, string may be a station URI, or a station name. * It may be part of station list, or may be a new URI. */ if (gv_player_set_station_by_guessing(player, string)) { gv_player_play(player); return NULL; } if (is_uri_scheme_supported(string)) { GvStation *station; station = gv_station_new(NULL, string); gv_player_set_station(player, station); gv_player_play(player); return NULL; } g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "'%s' is neither a known station or a valid uri", string); return NULL; } static GVariant * method_stop(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_stop(player); return NULL; } static GVariant * method_play_stop(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_toggle(player); return NULL; } static GVariant * method_next(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_next(player); return NULL; } static GVariant * method_prev(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gv_player_prev(player); return NULL; } static GvDbusMethod player_methods[] = { { "Play", method_play }, { "Stop", method_stop }, { "PlayStop", method_play_stop }, { "Next", method_next }, { "Previous", method_prev }, { NULL, NULL } }; static GVariant * method_list(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params G_GNUC_UNUSED, GError **error G_GNUC_UNUSED) { GvStationList *station_list = gv_core_station_list; GvStationListIter *iter; GvStation *station; GVariantBuilder b; g_variant_builder_init(&b, G_VARIANT_TYPE("aa{sv}")); iter = gv_station_list_iter_new(station_list); while (gv_station_list_iter_loop(iter, &station)) g_variant_builder_add_value(&b, g_variant_new_station(station, NULL)); gv_station_list_iter_free(iter); return g_variant_builder_end(&b); } static GVariant * method_add(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvStationList *station_list = gv_core_station_list; GvStation *new_station; GvStation *around_station; gchar *uri; gchar *name; gchar *where; gchar *around; g_variant_get(params, "(&s&s&s&s)", &uri, &name, &where, &around); /* Handle new station */ if (!is_uri_scheme_supported(uri)) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "URI scheme not supported"); return NULL; } new_station = gv_station_new(name, uri); /* Handle where to add */ around_station = gv_station_list_find_by_guessing(station_list, around); if (!g_strcmp0(where, "first")) gv_station_list_prepend(station_list, new_station); else if (!g_strcmp0(where, "last") || !g_strcmp0(where, "")) gv_station_list_append(station_list, new_station); else if (!g_strcmp0(where, "before")) gv_station_list_insert_before(station_list, new_station, around_station); else if (!g_strcmp0(where, "after")) gv_station_list_insert_after(station_list, new_station, around_station); else { g_object_unref(new_station); g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid keyword '%s'", where); } return NULL; } static GVariant * method_remove(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvStationList *station_list = gv_core_station_list; GvStation *match; gchar *station; g_variant_get(params, "(&s)", &station); match = gv_station_list_find_by_guessing(station_list, station); if (match) gv_station_list_remove(station_list, match); else g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Station '%s' not found", station); return NULL; } static GVariant * method_rename(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvStationList *station_list = gv_core_station_list; GvStation *match; gchar *station; gchar *name; g_variant_get(params, "(&s&s)", &station, &name); match = gv_station_list_find_by_guessing(station_list, station); if (match) gv_station_set_name(match, name); else g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Station '%s' not found", station); return NULL; } static GVariant * method_move(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *params, GError **error) { GvStationList *station_list = gv_core_station_list; GvStation *moving_station; GvStation *around_station; gchar *moving; gchar *around; gchar *where; g_variant_get(params, "(&s&s&s)", &moving, &where, &around); /* Handle station to move */ moving_station = gv_station_list_find_by_guessing(station_list, moving); if (moving_station == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Station '%s' not found", moving); return NULL; } /* Move the station */ around_station = gv_station_list_find_by_guessing(station_list, around); if (!g_strcmp0(where, "first")) gv_station_list_move_first(station_list, moving_station); else if (!g_strcmp0(where, "last")) gv_station_list_move_last(station_list, moving_station); else if (!g_strcmp0(where, "before")) gv_station_list_move_before(station_list, moving_station, around_station); else if (!g_strcmp0(where, "after")) gv_station_list_move_after(station_list, moving_station, around_station); else g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Invalid keyword '%s'", where); return NULL; } static GvDbusMethod stations_methods[] = { { "List", method_list }, { "Add", method_add }, { "Remove", method_remove }, { "Rename", method_rename }, { "Move", method_move }, { NULL, NULL } }; /* * Dbus property handlers */ static GVariant * prop_get_version(GvDbusServer *dbus_server G_GNUC_UNUSED) { return g_variant_new_string(PACKAGE_VERSION); } static GvDbusProperty root_properties[] = { { "Version", prop_get_version, NULL }, { NULL, NULL, NULL } }; static GVariant * prop_get_current(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; GvStation *station; GvMetadata *metadata; station = gv_player_get_station(player); metadata = gv_player_get_metadata(player); return g_variant_new_station(station, metadata); } static GVariant * prop_get_playing(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; GvPlayerState player_state; gboolean is_playing; player_state = gv_player_get_state(player); is_playing = player_state == GV_PLAYER_STATE_PLAYING ? TRUE : FALSE; return g_variant_new_boolean(is_playing); } static GVariant * prop_get_repeat(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean repeat; repeat = gv_player_get_repeat(player); return g_variant_new_boolean(repeat); } static gboolean prop_set_repeat(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean repeat; repeat = g_variant_get_boolean(value); gv_player_set_repeat(player, repeat); return TRUE; } static GVariant * prop_get_shuffle(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean shuffle; shuffle = gv_player_get_shuffle(player); return g_variant_new_boolean(shuffle); } static gboolean prop_set_shuffle(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean shuffle; shuffle = g_variant_get_boolean(value); gv_player_set_shuffle(player, shuffle); return TRUE; } static GVariant * prop_get_volume(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; guint volume; volume = gv_player_get_volume(player); return g_variant_new_uint32(volume); } static gboolean prop_set_volume(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; guint volume; volume = g_variant_get_uint32(value); gv_player_set_volume(player, volume); return TRUE; } static GVariant * prop_get_mute(GvDbusServer *dbus_server G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean mute; mute = gv_player_get_mute(player); return g_variant_new_boolean(mute); } static gboolean prop_set_mute(GvDbusServer *dbus_server G_GNUC_UNUSED, GVariant *value, GError **error G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; gboolean mute; mute = g_variant_get_boolean(value); gv_player_set_mute(player, mute); return TRUE; } static GvDbusProperty player_properties[] = { { "Current", prop_get_current, NULL }, { "Playing", prop_get_playing, NULL }, { "Repeat", prop_get_repeat, prop_set_repeat }, { "Shuffle", prop_get_shuffle, prop_set_shuffle }, { "Volume", prop_get_volume, prop_set_volume }, { "Mute", prop_get_mute, prop_set_mute }, { NULL, NULL, NULL } }; /* * Dbus interfaces */ static GvDbusInterface dbus_interfaces[] = { { DBUS_IFACE_ROOT, root_methods, root_properties }, { DBUS_IFACE_PLAYER, player_methods, player_properties }, { DBUS_IFACE_STATIONS, stations_methods, NULL }, { NULL, NULL, NULL } }; /* * Public methods */ GvFeature * gv_dbus_server_native_new(void) { return gv_feature_new(GV_TYPE_DBUS_SERVER_NATIVE, "DBusServerNative", GV_FEATURE_DEFAULT); } /* * GObject methods */ static void gv_dbus_server_native_constructed(GObject *object) { GvDbusServer *dbus_server = GV_DBUS_SERVER(object); /* Set dbus server properties - we don't set a name as we don't want to acquire any */ gv_dbus_server_set_dbus_path(dbus_server, DBUS_PATH); gv_dbus_server_set_dbus_introspection(dbus_server, DBUS_INTROSPECTION); gv_dbus_server_set_dbus_interface_table(dbus_server, dbus_interfaces); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_dbus_server_native, object); } static void gv_dbus_server_native_init(GvDbusServerNative *self) { TRACE("%p", self); } static void gv_dbus_server_native_class_init(GvDbusServerNativeClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->constructed = gv_dbus_server_native_constructed; } goodvibes-v0.4.2/src/feat/gv-dbus-server-native.h000066400000000000000000000024511337147241200217100ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_DBUS_SERVER_NATIVE_H__ #define __GOODVIBES_FEAT_GV_DBUS_SERVER_NATIVE_H__ #include #include "framework/gv-feature.h" #include "feat/gv-dbus-server.h" /* GObject declarations */ #define GV_TYPE_DBUS_SERVER_NATIVE gv_dbus_server_native_get_type() G_DECLARE_FINAL_TYPE(GvDbusServerNative, gv_dbus_server_native, \ GV, DBUS_SERVER_NATIVE, GvDbusServer) /* Public methods */ GvFeature *gv_dbus_server_native_new(void); #endif /* __GOODVIBES_FEAT_GV_DBUS_SERVER_NATIVE_H__ */ goodvibes-v0.4.2/src/feat/gv-dbus-server.c000066400000000000000000000534521337147241200204260ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-dbus-server.h" #define MAX_INTERFACES 4 #undef DEBUG_INTERFACES /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties - refer to class_init() for more details */ PROP_DBUS_NAME, PROP_DBUS_PATH, PROP_DBUS_INTROSPECTION, PROP_DBUS_INTERFACE_TABLE, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvDbusServerPrivate { /* Properties */ const gchar *name; const gchar *path; const gchar *introspection; GvDbusInterface *interface_table; /* Dbus stuff */ GDBusNodeInfo *introspection_data; guint bus_owner_id; GDBusConnection *bus_connection; guint registration_ids[MAX_INTERFACES + 1]; }; typedef struct _GvDbusServerPrivate GvDbusServerPrivate; G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(GvDbusServer, gv_dbus_server, GV_TYPE_FEATURE) /* * Debug helpers */ #ifdef DEBUG_INTERFACES static void debug_interfaces(GvDbusServer *self) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); GDBusNodeInfo *info = priv->introspection_data; const GvDbusInterface *iface; const GvDbusMethod *method; const GvDbusProperty *prop; GDBusInterfaceInfo **g_ifaces; GDBusInterfaceInfo *g_iface; GDBusMethodInfo **g_methods; GDBusMethodInfo *g_method; GDBusPropertyInfo **g_props; GDBusPropertyInfo *g_prop; /* Find missing things in introspection. * We iterate on methods and properties provided by the implementation, * and ensure they exist in introspection. */ DEBUG("Debugging introspection data..."); for (iface = priv->interface_table; iface->name; iface++) { g_iface = NULL; /* Look for matching interface in introspection data */ g_ifaces = info->interfaces; while (g_ifaces && (g_iface = *g_ifaces++)) { if (!g_strcmp0(iface->name, g_iface->name)) break; } if (g_iface == NULL) ERROR("Implemented interface '%s': no match", iface->name); /* Look for matching methods in interface */ for (method = iface->methods; method && method->name; method++) { g_methods = g_iface->methods; while (g_methods && (g_method = *g_methods++)) { if (!g_strcmp0(method->name, g_method->name)) break; } if (g_method == NULL) ERROR("Implemented interface '%s': method '%s': no match", iface->name, method->name); } /* Look for matching properties in interface */ for (prop = iface->properties; prop && prop->name; prop++) { g_prop = NULL; g_props = g_iface->properties; while (g_props && (g_prop = *g_props++)) { if (!g_strcmp0(prop->name, g_prop->name)) break; } if (g_prop == NULL) ERROR("Implemented interface '%s': property '%s': no match", iface->name, prop->name); /* Check that 'get' and 'set' match in the introspection */ if (prop->get && !(g_prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)) ERROR("Implemented interface '%s': property '%s': get: no match", iface->name, prop->name); if (prop->set && !(g_prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE)) ERROR("Implemented interface '%s': property '%s': set: no match", iface->name, prop->name); } } /* Find missing things in implementation. * We iterate on methods and properties provided by the introspection, * and ensure they are implemented. */ DEBUG("Debugging implementation..."); g_ifaces = info->interfaces; while (g_ifaces && (g_iface = *g_ifaces++)) { /* Look for matching interface in implementation */ for (iface = priv->interface_table; iface->name; iface++) { if (!g_strcmp0(g_iface->name, iface->name)) break; } if (iface->name == NULL) ERROR("Instrospection interface '%s': no match", g_iface->name); /* Look for matching methods in implementation */ g_methods = g_iface->methods; while (g_methods && (g_method = *g_methods++)) { for (method = iface->methods; method && method->name; method++) { if (!g_strcmp0(g_method->name, method->name)) break; } if (method == NULL || method->name == NULL) ERROR("Introspection interface '%s': method '%s': no match", g_iface->name, g_method->name); } /* Look for matching properties in implementation */ g_props = g_iface->properties; while (g_props && (g_prop = *g_props++)) { for (prop = iface->properties; prop && prop->name; prop++) { if (!g_strcmp0(g_prop->name, prop->name)) break; } if (prop == NULL || prop->name == NULL) ERROR("Introspection interface '%s': property '%s': no match", g_iface->name, g_prop->name); /* Check that 'get' and 'set' match in the implementation */ if ((g_prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE) && !prop->get) ERROR("Introspection interface '%s': property '%s': " "get is not implemented", g_iface->name, g_prop->name); if ((g_prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE) && !prop->set) ERROR("Introspection interface '%s': property '%s': " "set is not implemented", g_iface->name, g_prop->name); } } } #endif /* * GDBus helpers */ static void handle_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { GvDbusServer *self = GV_DBUS_SERVER(user_data); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); const GvDbusInterface *iface; const GvDbusMethod *method; GVariant *ret = NULL; GError *error = NULL; const gchar *bus_name = connection ? g_dbus_connection_get_unique_name(connection) : "(null)"; TRACE("%s, %s, %s, %s, %s, ...", bus_name, sender, object_path, interface_name, method_name); /* Iterate over interfaces */ for (iface = priv->interface_table; iface && iface->name; iface++) { if (g_strcmp0(iface->name, interface_name)) continue; /* Iterate over methods */ for (method = iface->methods; method && method->name; method++) { if (g_strcmp0(method->name, method_name)) continue; if (method->call) ret = method->call(self, parameters, &error); else g_set_error(&error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Method is not implemented."); break; } /* Check if method was found */ if (method == NULL || method->name == NULL) g_set_error(&error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "Method not found."); break; } /* Check if interface was found */ if (iface == NULL || iface->name == NULL) g_set_error(&error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE, "Interface not found."); /* Return with error if any */ if (error) { g_dbus_method_invocation_return_gerror(invocation, error); g_error_free(error); return; } /* Return value if any */ if (ret == NULL) g_dbus_method_invocation_return_value(invocation, NULL); else g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&ret, 1)); } static GVariant * handle_get_property(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { GvDbusServer *self = GV_DBUS_SERVER(user_data); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); const GvDbusInterface *iface; const GvDbusProperty *prop; const gchar *bus_name = connection ? g_dbus_connection_get_unique_name(connection) : "(null)"; TRACE("%s, %s, %s, %s, %s, ...", bus_name, sender, object_path, interface_name, property_name); /* Iterate over interfaces */ for (iface = priv->interface_table; iface && iface->name; iface++) { if (g_strcmp0(iface->name, interface_name)) continue; /* Iterate over properties */ for (prop = iface->properties; prop && prop->name; prop++) { if (g_strcmp0(prop->name, property_name)) continue; if (prop->get == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Property reader is not implemented."); return NULL; } return prop->get(self); } /* Property not found */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Property not found."); return NULL; } /* Interface not found */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE, "Interface not found."); return NULL; } static gboolean handle_set_property(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant *value, GError **error, gpointer user_data) { GvDbusServer *self = GV_DBUS_SERVER(user_data); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); const GvDbusInterface *iface = NULL; const GvDbusProperty *prop = NULL; const gchar *bus_name = connection ? g_dbus_connection_get_unique_name(connection) : "(null)"; TRACE("%s, %s, %s, %s, %s, ...", bus_name, sender, object_path, interface_name, property_name); /* Iterate over interfaces */ for (iface = priv->interface_table; iface && iface->name; iface++) { if (g_strcmp0(iface->name, interface_name)) continue; /* Iterate over properties */ for (prop = iface->properties; prop && prop->name; prop++) { if (g_strcmp0(prop->name, property_name)) continue; if (prop->set == NULL) { g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Property writer is not implemented."); return FALSE; } return prop->set(self, value, error); } /* Property not found */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "Property not found."); return FALSE; } /* Interface not found */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE, "Interface not found."); return FALSE; } static const GDBusInterfaceVTable interface_vtable = { .method_call = handle_method_call, .get_property = handle_get_property, .set_property = handle_set_property, }; /* * Private methods */ static void gv_dbus_server_register_objects(GvDbusServer *self) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); GDBusInterfaceInfo **interfaces = priv->introspection_data->interfaces; GDBusInterfaceInfo *interface; guint i; // TODO: checks i = 0; while (interfaces && (interface = *interfaces++)) { guint id; g_assert(i < MAX_INTERFACES); id = g_dbus_connection_register_object(priv->bus_connection, priv->path, interface, &interface_vtable, self, NULL, NULL); g_assert(id > 0); priv->registration_ids[i++] = id; INFO("Interface '%s' registered", interface->name); } } static void gv_dbus_server_unregister_objects(GvDbusServer *self) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); guint i; for (i = 0; priv->registration_ids[i] > 0; i++) { g_dbus_connection_unregister_object(priv->bus_connection, priv->registration_ids[i]); priv->registration_ids[i] = 0; } } /* * GDBus signal handlers */ static void on_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data G_GNUC_UNUSED) { const gchar *bus_name = connection ? g_dbus_connection_get_unique_name(connection) : "(null)"; TRACE("%s, %s, ...", bus_name, name); } static void on_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data G_GNUC_UNUSED) { const gchar *bus_name = connection ? g_dbus_connection_get_unique_name(connection) : "(null)"; TRACE("%s, %s, ...", bus_name, name); if (connection == NULL) WARNING("Name '%s' can't connect to the bus", name); else WARNING("Name '%s' lost on the bus", name); } /* * Property accessors */ void gv_dbus_server_set_dbus_name(GvDbusServer *self, const gchar *value) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); g_assert_null(priv->name); priv->name = value; } void gv_dbus_server_set_dbus_path(GvDbusServer *self, const gchar *value) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); g_assert_null(priv->path); priv->path = value; } void gv_dbus_server_set_dbus_introspection(GvDbusServer *self, const gchar *value) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); g_assert_null(priv->introspection); priv->introspection = value; } void gv_dbus_server_set_dbus_interface_table(GvDbusServer *self, GvDbusInterface *value) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); g_assert_null(priv->interface_table); priv->interface_table = value; } static void gv_dbus_server_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { TRACE_GET_PROPERTY(object, property_id, value, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } static void gv_dbus_server_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvDbusServer *self = GV_DBUS_SERVER(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_DBUS_NAME: gv_dbus_server_set_dbus_name(self, g_value_get_string(value)); break; case PROP_DBUS_PATH: gv_dbus_server_set_dbus_path(self, g_value_get_string(value)); break; case PROP_DBUS_INTROSPECTION: gv_dbus_server_set_dbus_introspection(self, g_value_get_string(value)); break; case PROP_DBUS_INTERFACE_TABLE: gv_dbus_server_set_dbus_interface_table(self, g_value_get_pointer(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ void gv_dbus_server_emit_signal(GvDbusServer *self, const gchar *interface_name, const gchar *signal_name, GVariant *parameters) { GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); GError *error = NULL; /* We're not sure to have a connection to dbus. Connection might fail * (for example, if the name is already owned). Or, early at startup, * we might still be waiting for the connection to finish when we're * asked to send the first signals. In any case, we must check that the * connection exists before using it. */ if (priv->bus_connection == NULL) return; g_dbus_connection_emit_signal(priv->bus_connection, NULL, priv->path, interface_name, signal_name, parameters, &error); if (error) { WARNING("Failed to emit dbus signal: %s", error->message); g_error_free(error); } } void gv_dbus_server_emit_signal_property_changed(GvDbusServer *self, const gchar *interface_name, const gchar *property_name, GVariant *value) { GVariantBuilder b; g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&b, "{sv}", property_name, value); GVariant *tuples[] = { g_variant_new_string(interface_name), g_variant_builder_end(&b), g_variant_new_strv(NULL, 0) }; gv_dbus_server_emit_signal(self, "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new_tuple(tuples, 3)); } GvDbusServer * gv_dbus_server_new(void) { return g_object_new(GV_TYPE_DBUS_SERVER, NULL); } /* * Feature methods */ static void gv_dbus_server_disable(GvFeature *feature) { GvDbusServer *self = GV_DBUS_SERVER(feature); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); /* Unref DBus connection & objects registered */ if (priv->bus_connection != NULL) { gv_dbus_server_unregister_objects(self); g_object_unref(priv->bus_connection); priv->bus_connection = NULL; } /* Unown name */ if (priv->bus_owner_id != 0) { g_bus_unown_name(priv->bus_owner_id); priv->bus_owner_id = 0; } /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_dbus_server, feature); } static void gv_dbus_server_enable(GvFeature *feature) { GvDbusServer *self = GV_DBUS_SERVER(feature); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); GDBusConnection *connection; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_dbus_server, feature); /* Get dbus connection */ connection = g_application_get_dbus_connection(gv_core_application); g_assert_nonnull(connection); /* Add a reference */ priv->bus_connection = g_object_ref(connection); /* Register objects */ gv_dbus_server_register_objects(self); /* We might want to acquire a name or not */ if (priv->name) { /* Acquire a name on the bus (objects will be registered in the callback) */ priv->bus_owner_id = g_bus_own_name_on_connection (connection, priv->name, G_BUS_NAME_OWNER_FLAGS_NONE, on_name_acquired, on_name_lost, self, NULL); g_assert(priv->bus_owner_id > 0); } } /* * GObject methods */ static void gv_dbus_server_finalize(GObject *object) { GvDbusServer *self = GV_DBUS_SERVER(object); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); TRACE("%p", object); /* Unref introspection data */ if (priv->introspection_data != NULL) g_dbus_node_info_unref(priv->introspection_data); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_dbus_server, object); } static void gv_dbus_server_constructed(GObject *object) { GvDbusServer *self = GV_DBUS_SERVER(object); GvDbusServerPrivate *priv = gv_dbus_server_get_instance_private(self); TRACE("%p", object); /* The 'name' property might be left to NULL if we don't want to * own a bus name. Other properties, though, must have been set. */ g_assert_nonnull(priv->path); g_assert_nonnull(priv->introspection); g_assert_nonnull(priv->interface_table); /* Parse XML introspection data */ priv->introspection_data = g_dbus_node_info_new_for_xml(priv->introspection, NULL); g_assert_nonnull(priv->introspection_data); #ifdef DEBUG_INTERFACES /* Ensure that the interface table matches the introspection data. * Be sure to enable this test if you work on this part. */ debug_interfaces(self); #endif /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_dbus_server, object); } static void gv_dbus_server_init(GvDbusServer *self) { TRACE("%p", self); } static void gv_dbus_server_class_init(GvDbusServerClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_dbus_server_finalize; object_class->constructed = gv_dbus_server_constructed; /* Override GvFeature methods */ feature_class->enable = gv_dbus_server_enable; feature_class->disable = gv_dbus_server_disable; /* Properties * All the strings are supposed to be static, we won't duplicate them. * This makes a little optimization in memory usage. * See the set() accessors if don't get it. */ object_class->get_property = gv_dbus_server_get_property; object_class->set_property = gv_dbus_server_set_property; properties[PROP_DBUS_NAME] = g_param_spec_string("dbus-name", "Dbus well-known name", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE); properties[PROP_DBUS_PATH] = g_param_spec_string("dbus-path", "Dbus path", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE); properties[PROP_DBUS_INTROSPECTION] = g_param_spec_string("dbus-introspection", "Dbus instrospection", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE); properties[PROP_DBUS_INTERFACE_TABLE] = g_param_spec_pointer("dbus-interface-table", "Dbus interface table", NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/feat/gv-dbus-server.h000066400000000000000000000056131337147241200204270ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_DBUS_SERVER_H__ #define __GOODVIBES_FEAT_GV_DBUS_SERVER_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_DBUS_SERVER gv_dbus_server_get_type() G_DECLARE_DERIVABLE_TYPE(GvDbusServer, gv_dbus_server, GV, DBUS_SERVER, GvFeature) /* Data types */ struct _GvDbusServerClass { /* Parent class */ GvFeatureClass parent_class; }; typedef GVariant *(*GvDbusMethodCall) (GvDbusServer *, GVariant *, GError **); typedef GVariant *(*GvDbusPropertyGet) (GvDbusServer *); typedef gboolean (*GvDbusPropertySet) (GvDbusServer *, GVariant *, GError **); struct _GvDbusMethod { const gchar *name; const GvDbusMethodCall call; }; typedef struct _GvDbusMethod GvDbusMethod; struct _GvDbusProperty { const gchar *name; const GvDbusPropertyGet get; const GvDbusPropertySet set; }; typedef struct _GvDbusProperty GvDbusProperty; struct _GvDbusInterface { const gchar *name; const GvDbusMethod *methods; const GvDbusProperty *properties; }; typedef struct _GvDbusInterface GvDbusInterface; /* Methods */ GvDbusServer *gv_dbus_server_new(void); void gv_dbus_server_emit_signal(GvDbusServer *self, const gchar *interface_name, const gchar *signal_name, GVariant *parameters); void gv_dbus_server_emit_signal_property_changed(GvDbusServer *self, const gchar *interface_name, const gchar *property_name, GVariant *value); /* Property accessors */ void gv_dbus_server_set_dbus_name (GvDbusServer *self, const gchar *name); void gv_dbus_server_set_dbus_path (GvDbusServer *self, const gchar *path); void gv_dbus_server_set_dbus_introspection (GvDbusServer *self, const gchar *introspection); void gv_dbus_server_set_dbus_interface_table(GvDbusServer *self, GvDbusInterface *interface_table); #endif /* __GOODVIBES_FEAT_GV_DBUS_SERVER_H__ */ goodvibes-v0.4.2/src/feat/gv-feat.c000066400000000000000000000072571337147241200171060ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "framework/gv-framework.h" #ifdef GV_FEAT_CONSOLE_OUTPUT #include "feat/gv-console-output.h" #endif #ifdef GV_FEAT_DBUS_SERVER #include "feat/gv-dbus-server-native.h" #include "feat/gv-dbus-server-mpris2.h" #endif #ifdef GV_FEAT_HOTKEYS #include "feat/gv-hotkeys.h" #endif #ifdef GV_FEAT_INHIBITOR #include "feat/gv-inhibitor.h" #endif #ifdef GV_FEAT_NOTIFICATIONS #include "feat/gv-notifications.h" #endif static GList *feat_objects; /* * Public functions */ GvFeature * gv_feat_find(const gchar *name_to_find) { GList *item; for (item = feat_objects; item; item = item->next) { GvFeature *feature = GV_FEATURE(item->data); const gchar *feature_name = gv_feature_get_name(feature); if (!g_strcmp0(feature_name, name_to_find)) return feature; } return NULL; } void gv_feat_configure_early(void) { GList *item; for (item = feat_objects; item; item = item->next) { GvFeature *feature = GV_FEATURE(item->data); GvFeatureFlags flags = gv_feature_get_flags(feature); if (flags & GV_FEATURE_EARLY) gv_configurable_configure(GV_CONFIGURABLE(feature)); } } void gv_feat_configure_late(void) { GList *item; for (item = feat_objects; item; item = item->next) { GvFeature *feature = GV_FEATURE(item->data); GvFeatureFlags flags = gv_feature_get_flags(feature); if (flags & GV_FEATURE_EARLY) continue; gv_configurable_configure(GV_CONFIGURABLE(feature)); } } void gv_feat_cleanup(void) { feat_objects = g_list_reverse(feat_objects); g_list_free_full(feat_objects, (GDestroyNotify) g_object_unref); } void gv_feat_init(void) { GvFeature *feature; GList *item; /* * Create every feature enabled at compile-time. * * Notice that some features don't really make sense if the program * has been compiled without ui. However we don't bother about that * here: all features are equals ! * The distinction between 'core' features and 'ui' features is done * in the build system, see the `configure.ac` for more details. */ #ifdef GV_FEAT_CONSOLE_OUTPUT feature = gv_console_output_new(); feat_objects = g_list_append(feat_objects, feature); #endif #ifdef GV_FEAT_DBUS_SERVER feature = gv_dbus_server_native_new(); feat_objects = g_list_append(feat_objects, feature); feature = gv_dbus_server_mpris2_new(); feat_objects = g_list_append(feat_objects, feature); #endif #ifdef GV_FEAT_INHIBITOR feature = gv_inhibitor_new(); feat_objects = g_list_append(feat_objects, feature); #endif #ifdef GV_FEAT_HOTKEYS feature = gv_hotkeys_new(); feat_objects = g_list_append(feat_objects, feature); #endif #ifdef GV_FEAT_NOTIFICATIONS feature = gv_notifications_new(); feat_objects = g_list_append(feat_objects, feature); #endif /* Register objects in the framework */ for (item = feat_objects; item; item = item->next) { feature = GV_FEATURE(item->data); gv_framework_register_object(feature); /* Drop a line */ INFO("Feature compiled in: '%s'", gv_feature_get_name(feature)); } } goodvibes-v0.4.2/src/feat/gv-feat.h000066400000000000000000000023021337147241200170750ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This header contains definitions to be used by feat users */ #ifndef __GOODVIBES_FEAT_GV_FEAT_H__ #define __GOODVIBES_FEAT_GV_FEAT_H__ #include "framework/gv-feature.h" /* Functions */ void gv_feat_init (void); void gv_feat_cleanup (void); void gv_feat_configure_early(void); void gv_feat_configure_late (void); GvFeature *gv_feat_find (const gchar *name_to_find); #endif /* __GOODVIBES_FEAT_GV_FEAT_H__ */ goodvibes-v0.4.2/src/feat/gv-hotkeys.c000066400000000000000000000117771337147241200176570ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-hotkeys.h" /* * GObject definitions */ struct _GvHotkeys { /* Parent instance structure */ GvFeature parent_instance; }; G_DEFINE_TYPE_WITH_CODE(GvHotkeys, gv_hotkeys, GV_TYPE_FEATURE, G_IMPLEMENT_INTERFACE(GV_TYPE_ERRORABLE, NULL)) /* * Signal handlers & callbacks */ static void on_key_pressed(const gchar *keystring, void *data G_GNUC_UNUSED) { GvPlayer *player = gv_core_player; DEBUG("Key '%s' pressed", keystring); /* * Here are the key definitions, from libinput (input.h), and how * they show up from keybinder as xorg keystrings (XF86keysym.h). * * KEY_NEXTSONG 163 XF86AudioNext * KEY_PLAYPAUSE 164 XF86AudioPlay * KEY_PREVIOUSSONG 165 XF86AudioPrev * ? ? XF86AudioStop * ? ? XF86AudioPause */ if (!g_strcmp0(keystring, "XF86AudioPlay")) gv_player_toggle(player); else if (!g_strcmp0(keystring, "XF86AudioStop")) gv_player_toggle(player); else if (!g_strcmp0(keystring, "XF86AudioPause")) gv_player_toggle(player); else if (!g_strcmp0(keystring, "XF86AudioPrev")) gv_player_prev(player); else if (!g_strcmp0(keystring, "XF86AudioNext")) gv_player_next(player); else WARNING("Unhandled key '%s'", keystring); } /* * Private methods */ struct _Binding { const gchar *key; gboolean bound; }; typedef struct _Binding Binding; static Binding bindings[] = { { "XF86AudioPlay", FALSE }, { "XF86AudioPause", FALSE }, { "XF86AudioStop", FALSE }, { "XF86AudioPrev", FALSE }, { "XF86AudioNext", FALSE }, { NULL, FALSE } }; static void gv_hotkeys_unbind(GvHotkeys *self G_GNUC_UNUSED) { Binding *binding; for (binding = bindings; binding->key != NULL; binding++) { if (binding->bound == TRUE) { keybinder_unbind(binding->key, on_key_pressed); binding->bound = FALSE; } } INFO("Multimedia keys undound"); } static void gv_hotkeys_bind(GvHotkeys *self) { Binding *binding; GString *unbound_keys; unbound_keys = NULL; for (binding = bindings; binding->key != NULL; binding++) { const gchar *key; gboolean bound; key = binding->key; bound = keybinder_bind(key, on_key_pressed, self); if (bound == FALSE) { if (unbound_keys == NULL) unbound_keys = g_string_new(key); else g_string_append_printf(unbound_keys, ", %s", key); } binding->bound = bound; } if (unbound_keys) { gchar *text; text = g_string_free(unbound_keys, FALSE); WARNING("Failed to bind the following keys: %s", text); gv_errorable_emit_error(GV_ERRORABLE(self), _("%s:\n%s"), _("Failed to bind the following keys"), text); g_free(text); } else { INFO("Multimedia keys successfully bound"); } } /* * Feature methods */ static void gv_hotkeys_disable(GvFeature *feature) { /* Unbind keys */ gv_hotkeys_unbind(GV_HOTKEYS(feature)); /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_hotkeys, feature); } static void gv_hotkeys_enable(GvFeature *feature) { /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_hotkeys, feature); /* Unbind keys */ gv_hotkeys_bind(GV_HOTKEYS(feature)); } /* * Public methods */ GvFeature * gv_hotkeys_new(void) { return gv_feature_new(GV_TYPE_HOTKEYS, "Hotkeys", GV_FEATURE_DEFAULT); } /* * GObject methods */ static void gv_hotkeys_finalize(GObject *object) { TRACE("%p", object); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_hotkeys, object); } static void gv_hotkeys_constructed(GObject *object) { TRACE("%p", object); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_hotkeys, object); } static void gv_hotkeys_init(GvHotkeys *self) { TRACE("%p", self); } static void gv_hotkeys_class_init(GvHotkeysClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_hotkeys_finalize; object_class->constructed = gv_hotkeys_constructed; /* Override GvFeature methods */ feature_class->enable = gv_hotkeys_enable; feature_class->disable = gv_hotkeys_disable; /* Init keybinder */ keybinder_init(); } goodvibes-v0.4.2/src/feat/gv-hotkeys.h000066400000000000000000000022141337147241200176460ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_HOTKEYS_H__ #define __GOODVIBES_FEAT_GV_HOTKEYS_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_HOTKEYS gv_hotkeys_get_type() G_DECLARE_FINAL_TYPE(GvHotkeys, gv_hotkeys, GV, HOTKEYS, GvFeature) /* Public methods */ GvFeature *gv_hotkeys_new(void); #endif /* __GOODVIBES_FEAT_GV_HOTKEYS_H__ */ goodvibes-v0.4.2/src/feat/gv-inhibitor.c000066400000000000000000000140451337147241200201470ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "libcaphe/caphe.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-inhibitor.h" /* * GObject definitions */ struct _GvInhibitorPrivate { guint check_playback_status_source_id; gboolean error_emited; }; typedef struct _GvInhibitorPrivate GvInhibitorPrivate; struct _GvInhibitor { /* Parent instance structure */ GvFeature parent_instance; /* Private data */ GvInhibitorPrivate *priv; }; G_DEFINE_TYPE_WITH_CODE(GvInhibitor, gv_inhibitor, GV_TYPE_FEATURE, G_ADD_PRIVATE(GvInhibitor) G_IMPLEMENT_INTERFACE(GV_TYPE_ERRORABLE, NULL)) /* * Private method */ static void gv_inhibitor_check_playback_status_now(GvInhibitor *self G_GNUC_UNUSED) { GvPlayerState player_state = gv_player_get_state(gv_core_player); /* Not interested about the transitional states */ if (player_state == GV_PLAYER_STATE_PLAYING) caphe_cup_inhibit(caphe_cup_get_default(), "Playing"); else if (player_state == GV_PLAYER_STATE_STOPPED) caphe_cup_uninhibit(caphe_cup_get_default()); } static gboolean when_timeout_check_playback_status(GvInhibitor *self) { GvInhibitorPrivate *priv = self->priv; gv_inhibitor_check_playback_status_now(self); priv->check_playback_status_source_id = 0; return G_SOURCE_REMOVE; } static void gv_inhibitor_check_playback_status_delayed(GvInhibitor *self) { GvInhibitorPrivate *priv = self->priv; if (priv->check_playback_status_source_id > 0) g_source_remove(priv->check_playback_status_source_id); priv->check_playback_status_source_id = g_timeout_add_seconds(1, (GSourceFunc) when_timeout_check_playback_status, self); } /* * Signal handlers & callbacks */ static void on_caphe_cup_inhibit_failure(CapheCup *caphe_cup G_GNUC_UNUSED, GvInhibitor *self) { GvInhibitorPrivate *priv = self->priv; WARNING("Failed to inhibit system sleep"); if (priv->error_emited) return; gv_errorable_emit_error(GV_ERRORABLE(self), _("Failed to inhibit system sleep")); priv->error_emited = TRUE; } static void on_caphe_cup_notify_inhibitor(CapheCup *caphe_cup, GParamSpec *pspec G_GNUC_UNUSED, GvInhibitor *self G_GNUC_UNUSED) { CapheInhibitor *inhibitor = caphe_cup_get_inhibitor(caphe_cup); if (inhibitor) INFO("System sleep inhibited (%s)", caphe_inhibitor_get_name(inhibitor)); else INFO("System sleep uninhibited"); } static void on_player_notify_state(GvPlayer *player, GParamSpec *pspec G_GNUC_UNUSED, GvInhibitor *self) { GvPlayerState player_state = gv_player_get_state(player); /* Not interested about the transitional states */ if (player_state != GV_PLAYER_STATE_PLAYING && player_state != GV_PLAYER_STATE_STOPPED) return; /* We might take action now, however we delay our decision a bit, * just in case player state is changing fast and getting crazy. */ gv_inhibitor_check_playback_status_delayed(self); } /* * Feature methods */ static void gv_inhibitor_disable(GvFeature *feature) { GvInhibitor *self = GV_INHIBITOR(feature); GvInhibitorPrivate *priv = self->priv; GvPlayer *player = gv_core_player; /* Remove pending operation */ if (priv->check_playback_status_source_id) { g_source_remove(priv->check_playback_status_source_id); priv->check_playback_status_source_id = 0; } /* Reset error emission */ priv->error_emited = FALSE; /* Signal handlers */ g_signal_handlers_disconnect_by_data(player, feature); /* Cleanup libcaphe */ g_signal_handlers_disconnect_by_data(caphe_cup_get_default(), self); caphe_cleanup(); /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_inhibitor, feature); } static void gv_inhibitor_enable(GvFeature *feature) { GvInhibitor *self = GV_INHIBITOR(feature); GvInhibitorPrivate *priv = self->priv; GvPlayer *player = gv_core_player; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_inhibitor, feature); /* Init libcaphe */ caphe_init(); g_signal_connect_object(caphe_cup_get_default(), "notify::inhibitor", G_CALLBACK(on_caphe_cup_notify_inhibitor), self, 0); g_signal_connect_object(caphe_cup_get_default(), "inhibit-failure", G_CALLBACK(on_caphe_cup_inhibit_failure), self, 0); /* Connect to signal handlers */ g_signal_connect_object(player, "notify::state", G_CALLBACK(on_player_notify_state), self, 0); /* Schedule a check for the current playback status */ g_assert(priv->check_playback_status_source_id == 0); priv->check_playback_status_source_id = g_timeout_add_seconds(1, (GSourceFunc) when_timeout_check_playback_status, self); } /* * Public methods */ GvFeature * gv_inhibitor_new(void) { return gv_feature_new(GV_TYPE_INHIBITOR, "Inhibitor", GV_FEATURE_DEFAULT); } /* * GObject methods */ static void gv_inhibitor_init(GvInhibitor *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_inhibitor_get_instance_private(self); } static void gv_inhibitor_class_init(GvInhibitorClass *class) { GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GvFeature methods */ feature_class->enable = gv_inhibitor_enable; feature_class->disable = gv_inhibitor_disable; } goodvibes-v0.4.2/src/feat/gv-inhibitor.h000066400000000000000000000022361337147241200201530ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_INHIBITOR_H__ #define __GOODVIBES_FEAT_GV_INHIBITOR_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_INHIBITOR gv_inhibitor_get_type() G_DECLARE_FINAL_TYPE(GvInhibitor, gv_inhibitor, GV, INHIBITOR, GvFeature) /* Public methods */ GvFeature *gv_inhibitor_new(void); #endif /* __GOODVIBES_FEAT_GV_INHIBITOR_H__ */ goodvibes-v0.4.2/src/feat/gv-notifications.c000066400000000000000000000140331337147241200210260ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-notifications.h" /* * GObject definitions */ struct _GvNotifications { /* Parent instance structure */ GvFeature parent_instance; }; G_DEFINE_TYPE(GvNotifications, gv_notifications, GV_TYPE_FEATURE) /* * Helpers */ static GNotification * make_station_notification(GvStation *station) { GNotification *notif; const gchar *str; gchar *body; if (station == NULL) return NULL; str = gv_station_get_name(station); if (str) { body = g_strdup_printf(_("Playing %s"), str); } else { str = gv_station_get_uri(station); body = g_strdup_printf(_("Playing <%s>"), str); } notif = g_notification_new(_("Playing Station")); g_notification_set_body(notif, body); g_free(body); return notif; } static GNotification * make_metadata_notification(GvMetadata *metadata) { GNotification *notif; const gchar *artist; const gchar *title; const gchar *album; const gchar *year; const gchar *genre; gchar *body; if (metadata == NULL) return NULL; artist = gv_metadata_get_artist(metadata); title = gv_metadata_get_title(metadata); album = gv_metadata_get_album(metadata); year = gv_metadata_get_year(metadata); genre = gv_metadata_get_genre(metadata); if (title && !artist && !album && !year && !genre) { /* If there's only the 'title' field, don't bother being smart, * just display it as it. Actually, most radios fill only this field, * and put everything in it (title + artist + some more stuff). */ body = g_strdup(title); } else { /* Otherwise, each existing field is displayed on a line */ gchar *album_year; if (title == NULL) title = _("(Unknown title)"); album_year = gv_metadata_make_album_year(metadata, FALSE); body = g_strjoin_null("\n", 4, title, artist, album_year, genre); g_free(album_year); } notif = g_notification_new(_("New Track")); g_notification_set_body(notif, body); g_free(body); return notif; } static GNotification * make_error_notification(const gchar *error_string) { GNotification *notif; notif = g_notification_new(_("Error")); g_notification_set_body(notif, error_string); return notif; } /* * Signal handlers & callbacks */ static void on_player_notify(GvPlayer *player, GParamSpec *pspec, GvNotifications *self G_GNUC_UNUSED) { const gchar *property_name = g_param_spec_get_name(pspec); GApplication *app = gv_core_application; if (!g_strcmp0(property_name, "state")) { GNotification *notif; GvPlayerState state; GvStation *station; state = gv_player_get_state(player); if (state != GV_PLAYER_STATE_PLAYING) return; station = gv_player_get_station(player); notif = make_station_notification(station); if (notif == NULL) return; g_application_send_notification(app, "station", notif); g_object_unref(notif); } else if (!g_strcmp0(property_name, "metadata")) { GNotification *notif; GvMetadata *metadata; metadata = gv_player_get_metadata(player); notif = make_metadata_notification(metadata); if (notif == NULL) return; g_application_send_notification(app, "metadata", notif); g_object_unref(notif); } } static void on_errorable_error(GvErrorable *errorable, const gchar *error_string, GvNotifications *self) { GApplication *app = gv_core_application; GNotification *notif; TRACE("%p, %s, %p", errorable, error_string, self); notif = make_error_notification(error_string); g_application_send_notification(app, "error", notif); g_object_unref(notif); } /* * Feature methods */ static void gv_notifications_disable(GvFeature *feature) { GvPlayer *player = gv_core_player; GList *item; /* Disconnect signal handlers */ for (item = gv_framework_get_objects(); item; item = item->next) { GObject *object = G_OBJECT(item->data); if (GV_IS_ERRORABLE(object) == FALSE) continue; g_signal_handlers_disconnect_by_data(object, feature); } g_signal_handlers_disconnect_by_data(player, feature); /* Chain up */ GV_FEATURE_CHAINUP_DISABLE(gv_notifications, feature); } static void gv_notifications_enable(GvFeature *feature) { GvPlayer *player = gv_core_player; GList *item; /* Chain up */ GV_FEATURE_CHAINUP_ENABLE(gv_notifications, feature); /* Connect to player 'notify' */ g_signal_connect_object(player, "notify", G_CALLBACK(on_player_notify), feature, 0); /* Connect to objects that emit 'error' */ for (item = gv_framework_get_objects(); item; item = item->next) { GObject *object = G_OBJECT(item->data); if (GV_IS_ERRORABLE(object) == FALSE) continue; g_signal_connect_object(object, "error", G_CALLBACK(on_errorable_error), feature, 0); } } /* * Public methods */ GvFeature * gv_notifications_new(void) { return gv_feature_new(GV_TYPE_NOTIFICATIONS, "Notifications", GV_FEATURE_EARLY); } /* * GObject methods */ static void gv_notifications_init(GvNotifications *self) { TRACE("%p", self); } static void gv_notifications_class_init(GvNotificationsClass *class) { GvFeatureClass *feature_class = GV_FEATURE_CLASS(class); TRACE("%p", class); /* Override GvFeature methods */ feature_class->enable = gv_notifications_enable; feature_class->disable = gv_notifications_disable; } goodvibes-v0.4.2/src/feat/gv-notifications.h000066400000000000000000000023021337147241200210270ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FEAT_GV_NOTIFICATIONS_H__ #define __GOODVIBES_FEAT_GV_NOTIFICATIONS_H__ #include #include "framework/gv-feature.h" /* GObject declarations */ #define GV_TYPE_NOTIFICATIONS gv_notifications_get_type() G_DECLARE_FINAL_TYPE(GvNotifications, gv_notifications, GV, NOTIFICATIONS, GvFeature) /* Public methods */ GvFeature *gv_notifications_new(void); #endif /* __GOODVIBES_FEAT_GV_NOTIFICATIONS_H__ */ goodvibes-v0.4.2/src/feat/meson.build000066400000000000000000000017041337147241200175420ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Sources and dependencies feat_sources = [ 'gv-feat.c' ] feat_dependencies = [ glib_dep, gobject_dep, gvframework_dep, ] if gv_feat_console_output feat_sources += 'gv-console-output.c' endif if gv_feat_dbus_server feat_sources += [ 'gv-dbus-server.c', 'gv-dbus-server-native.c', 'gv-dbus-server-mpris2.c' ] if gv_ui_enabled feat_dependencies += gvui_dep endif endif if gv_feat_inhibitor feat_sources += 'gv-inhibitor.c' feat_dependencies += libcaphe_dep endif if gv_feat_hotkeys feat_sources += 'gv-hotkeys.c' feat_dependencies += keybinder_dep endif if gv_feat_notifications feat_sources += 'gv-notifications.c' endif # Library definition gvfeat = static_library('gvfeat', sources: [ feat_sources ], dependencies: feat_dependencies, include_directories: root_inc, ) gvfeat_dep = declare_dependency( dependencies: feat_dependencies, link_with: gvfeat, ) goodvibes-v0.4.2/src/framework/000077500000000000000000000000001337147241200164545ustar00rootroot00000000000000goodvibes-v0.4.2/src/framework/glib-additions.c000066400000000000000000000055361337147241200215220ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include /* * Version Information */ const gchar * glib_get_runtime_version_string(void) { static gchar *version_string; if (version_string == NULL) { version_string = g_strdup_printf("GLib %u.%u.%u", glib_major_version, glib_minor_version, glib_micro_version); } return version_string; } const gchar * glib_get_compile_version_string(void) { static gchar *version_string; if (version_string == NULL) { version_string = g_strdup_printf("GLib %u.%u.%u", GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION, GLIB_MICRO_VERSION); } return version_string; } /* * String Utilities Functions */ gchar * g_strjoin_null(const gchar *separator, unsigned int n_strings, ...) { gchar *string, *s; va_list args; gsize len; gsize separator_len; gchar *ptr; guint i; if (separator == NULL) separator = ""; separator_len = strlen(separator); /* First part, getting length */ len = 1; va_start(args, n_strings); for (i = 0; i < n_strings; i++) { s = va_arg(args, gchar *); if (s) { if (len != 1) len += separator_len; len += strlen(s); } } va_end(args); /* Second part, building string */ string = g_new(gchar, len); ptr = string; va_start(args, n_strings); for (i = 0; i < n_strings; i++) { s = va_arg(args, gchar *); if (s) { if (ptr != string) ptr = g_stpcpy(ptr, separator); ptr = g_stpcpy(ptr, s); } } va_end(args); if (ptr == string) string[0] = '\0'; return string; } /* * GVariant */ void g_variant_builder_add_dictentry_array_string(GVariantBuilder *b, const gchar *key, ...) { GVariantBuilder ab; va_list ap; gchar *s; g_variant_builder_init(&ab, G_VARIANT_TYPE_ARRAY); va_start(ap, key); s = va_arg(ap, gchar *); while (s) { GVariant *tmp = g_variant_new_string(s); g_variant_builder_add(&ab, "v", tmp); s = va_arg(ap, gchar *); } g_variant_builder_add(b, "{sv}", key, g_variant_builder_end(&ab)); } goodvibes-v0.4.2/src/framework/glib-additions.h000066400000000000000000000033171337147241200215220ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GLIB_ADDITIONS_H__ #define __GOODVIBES_FRAMEWORK_GLIB_ADDITIONS_H__ #include /* * Version Information */ const gchar *glib_get_runtime_version_string(void); const gchar *glib_get_compile_version_string(void); /* * String Utilities Functions */ gchar *g_strjoin_null(const gchar *separator, unsigned int n_strings, ...); /* * GVariant */ #define g_variant_builder_add_dictentry_string(b, key, val) \ g_variant_builder_add(b, "{sv}", key, g_variant_new_string(val)) #define g_variant_builder_add_dictentry_object_path(b, key, val) \ g_variant_builder_add(b, "{sv}", key, g_variant_new_object_path(val)) void g_variant_builder_add_dictentry_array_string(GVariantBuilder *b, const gchar *key, ...) G_GNUC_NULL_TERMINATED; #endif /* __GOODVIBES_FRAMEWORK_GLIB_ADDITIONS_H__ */ goodvibes-v0.4.2/src/framework/glib-object-additions.c000066400000000000000000000070471337147241200227650ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "glib-object-additions.h" /* * GType */ gchar * g_type_dup_name(GType type) { return g_strdup(g_type_name(type)); } gchar * g_type_dup_name_no_prefix(GType type) { const gchar *type_name; const gchar *ptr; type_name = g_type_name(type); ptr = type_name; if (*ptr != '\0') ptr++; while (*ptr != '\0') { if (g_ascii_isupper(*ptr)) break; ptr++; } if (*ptr == '\0') return g_strdup(type_name); else return g_strdup(ptr); } /* * GObject */ gboolean g_object_get_boolean(GObject *object, const gchar *property_name) { gboolean value; g_object_get(object, property_name, &value, NULL); return value; } guint g_object_get_uint(GObject *object, const gchar *property_name) { guint value; g_object_get(object, property_name, &value, NULL); return value; } gchar * g_object_get_string(GObject *object, const gchar *property_name) { gchar *value; g_object_get(object, property_name, &value, NULL); return value; } const gchar * g_object_get_property_desc(GObject *object, const gchar *property_name) { GParamSpec *pspec; const gchar *desc; pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(object), property_name); g_assert_nonnull(pspec); desc = g_param_spec_get_blurb(pspec); g_assert_nonnull(desc); return desc; } void g_object_get_property_uint_bounds(GObject *object, const gchar *property_name, guint *minimum, guint *maximum) { GParamSpec *pspec; GParamSpecUInt *pspec_uint; if (minimum) *minimum = 0; if (maximum) *maximum = 0; pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(object), property_name); g_assert_nonnull(pspec); g_assert_true(G_IS_PARAM_SPEC_UINT(pspec)); pspec_uint = G_PARAM_SPEC_UINT(pspec); if (minimum) *minimum = pspec_uint->minimum; if (maximum) *maximum = pspec_uint->maximum; } /* * Signals */ void g_signal_handlers_connect_object(gpointer instance, GSignalHandler *handlers, gpointer gobject, GConnectFlags connect_flags) { GSignalHandler *handler; if (handlers == NULL) return; for (handler = handlers; handler->name; handler++) g_signal_connect_object(instance, handler->name, handler->callback, gobject, connect_flags); } void g_signal_handlers_block(gpointer instance, GSignalHandler *handlers, gpointer data) { GSignalHandler *handler; if (handlers == NULL) return; for (handler = handlers; handler->name; handler++) g_signal_handlers_block_by_func(instance, handler->callback, data); } void g_signal_handlers_unblock(gpointer instance, GSignalHandler *handlers, gpointer data) { GSignalHandler *handler; if (handlers == NULL) return; for (handler = handlers; handler->name; handler++) g_signal_handlers_unblock_by_func(instance, handler->callback, data); } goodvibes-v0.4.2/src/framework/glib-object-additions.h000066400000000000000000000056261337147241200227730ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GLIB_OBJECT_ADDITIONS_H__ #define __GOODVIBES_FRAMEWORK_GLIB_OBJECT_ADDITIONS_H__ #include /* * GType */ /* Duplicate type name, eventually without the prefix */ gchar *g_type_dup_name(GType type); gchar *g_type_dup_name_no_prefix(GType type); /* * GObject */ /* Duplicate type name, eventually without the prefix */ #define g_object_dup_type_name(obj) \ g_type_dup_name(G_OBJECT_TYPE(obj)) #define g_object_dup_type_name_no_prefix(obj) \ g_type_dup_name_no_prefix(G_OBJECT_TYPE(obj)) /* Chain up for finalize() */ #define G_OBJECT_CHAINUP_FINALIZE(module_obj_name, obj) \ G_OBJECT_CLASS(module_obj_name##_parent_class)->finalize(obj) /* Chain up for constructed() - beware, constructed() is not guaranteed to exist */ #define G_OBJECT_CHAINUP_CONSTRUCTED(module_obj_name, obj) \ do { \ if (G_OBJECT_CLASS(module_obj_name##_parent_class)->constructed) \ G_OBJECT_CLASS(module_obj_name##_parent_class)->constructed(obj); \ } while (0) /* get() for fundamental types */ gboolean g_object_get_boolean(GObject *object, const gchar *property_name); guint g_object_get_uint (GObject *object, const gchar *property_name); gchar *g_object_get_string (GObject *object, const gchar *property_name); const gchar *g_object_get_property_desc (GObject *object, const gchar *property_name); void g_object_get_property_uint_bounds(GObject *object, const gchar *property_name, guint *minimum, guint *maximum); /* * Signals */ struct _GSignalHandler { const gchar *name; GCallback callback; }; typedef struct _GSignalHandler GSignalHandler; void g_signal_handlers_connect_object(gpointer instance, GSignalHandler *handlers, gpointer gobject, GConnectFlags connect_flags); void g_signal_handlers_block (gpointer instance, GSignalHandler *handlers, gpointer data); void g_signal_handlers_unblock (gpointer instance, GSignalHandler *handlers, gpointer data); #endif /* __GOODVIBES_FRAMEWORK_GLIB_OBJECT_ADDITIONS_H__ */ goodvibes-v0.4.2/src/framework/gv-configurable.c000066400000000000000000000023551337147241200216770ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "gv-configurable.h" #include "log.h" /* * GObject definitions */ G_DEFINE_INTERFACE(GvConfigurable, gv_configurable, G_TYPE_OBJECT) /* * Public methods */ void gv_configurable_configure(GvConfigurable *self) { g_return_if_fail(GV_IS_CONFIGURABLE(self)); return GV_CONFIGURABLE_GET_IFACE(self)->configure(self); } /* * GObject methods */ static void gv_configurable_default_init(GvConfigurableInterface *iface) { TRACE("%p", iface); } goodvibes-v0.4.2/src/framework/gv-configurable.h000066400000000000000000000025201337147241200216760ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GV_CONFIGURABLE_H__ #define __GOODVIBES_FRAMEWORK_GV_CONFIGURABLE_H__ #include /* GObject declarations */ #define GV_TYPE_CONFIGURABLE gv_configurable_get_type() G_DECLARE_INTERFACE(GvConfigurable, gv_configurable, GV, CONFIGURABLE, GObject) struct _GvConfigurableInterface { /* Parent interface */ GTypeInterface parent_iface; /* Virtual methods */ void (*configure) (GvConfigurable *self); }; /* Public methods */ void gv_configurable_configure(GvConfigurable *self); #endif /* __GOODVIBES_FRAMEWORK_GV_CONFIGURABLE_H__ */ goodvibes-v0.4.2/src/framework/gv-errorable.c000066400000000000000000000032431337147241200212110ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "gv-errorable.h" #include "log.h" /* * GObject definitions */ enum { SIGNAL_ERROR, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; G_DEFINE_INTERFACE(GvErrorable, gv_errorable, G_TYPE_OBJECT) /* * Public methods */ void gv_errorable_emit_error(GvErrorable *self, const gchar *fmt, ...) { va_list args; gchar *string; va_start(args, fmt); string = g_strdup_vprintf(fmt, args); va_end(args); g_signal_emit(self, signals[SIGNAL_ERROR], 0, string); g_free(string); } /* * GObject methods */ static void gv_errorable_default_init(GvErrorableInterface *iface) { TRACE("%p", iface); signals[SIGNAL_ERROR] = g_signal_new("error", G_TYPE_FROM_INTERFACE(iface), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(GvErrorableInterface, error), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); } goodvibes-v0.4.2/src/framework/gv-errorable.h000066400000000000000000000024611337147241200212170ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GV_ERRORABLE_H__ #define __GOODVIBES_FRAMEWORK_GV_ERRORABLE_H__ #include /* GObject declarations */ #define GV_TYPE_ERRORABLE gv_errorable_get_type() G_DECLARE_INTERFACE(GvErrorable, gv_errorable, GV, ERRORABLE, GObject) struct _GvErrorableInterface { /* Parent interface */ GTypeInterface parent_iface; /* Signals */ void (*error) (GvErrorable *self); }; /* Methods */ void gv_errorable_emit_error(GvErrorable *self, const gchar *fmt, ...); #endif /* __GOODVIBES_FRAMEWORK_GV_ERRORABLE_H__ */ goodvibes-v0.4.2/src/framework/gv-feature.c000066400000000000000000000203161337147241200206670ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include "config.h" #include "glib-additions.h" #include "glib-object-additions.h" #include "gv-configurable.h" #include "gv-feature.h" #include "gv-framework-enum-types.h" #include "gv-param-specs.h" #include "log.h" #include "utils.h" /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_NAME, PROP_FLAGS, PROP_SETTINGS, PROP_ENABLED, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvFeaturePrivate { /* Properties */ gchar *name; GvFeatureFlags flags; GSettings *settings; gboolean enabled; }; typedef struct _GvFeaturePrivate GvFeaturePrivate; static void gv_feature_configurable_interface_init(GvConfigurableInterface *iface); G_DEFINE_ABSTRACT_TYPE_WITH_CODE(GvFeature, gv_feature, G_TYPE_OBJECT, G_ADD_PRIVATE(GvFeature) G_IMPLEMENT_INTERFACE(GV_TYPE_CONFIGURABLE, gv_feature_configurable_interface_init)) /* * Property accessors */ const gchar * gv_feature_get_name(GvFeature *self) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); return priv->name; } static void gv_feature_set_name(GvFeature *self, const gchar *name) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); /* Construct-only property */ g_assert_null(priv->name); g_assert_nonnull(name); priv->name = g_strdup(name); } GvFeatureFlags gv_feature_get_flags(GvFeature *self) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); return priv->flags; } static void gv_feature_set_flags(GvFeature *self, GvFeatureFlags flags) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); /* Construct-only property */ g_assert(priv->flags == GV_FEATURE_DEFAULT); priv->flags = flags; } GSettings * gv_feature_get_settings(GvFeature *self) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); return priv->settings; } gboolean gv_feature_get_enabled(GvFeature *self) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); return priv->enabled; } void gv_feature_set_enabled(GvFeature *self, gboolean enabled) { GvFeaturePrivate *priv = gv_feature_get_instance_private(self); GvFeatureClass *feature_class = GV_FEATURE_GET_CLASS(self); /* Bail out if needed */ if (priv->enabled == enabled) return; priv->enabled = enabled; /* Invoke virtual function */ if (enabled == TRUE) { INFO("Enabling feature '%s'...", priv->name); feature_class->enable(self); } else { INFO("Disabling feature '%s'...", priv->name); feature_class->disable(self); } /* Don't forget to notify */ g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_ENABLED]); } static void gv_feature_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvFeature *self = GV_FEATURE(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_NAME: g_value_set_string(value, gv_feature_get_name(self)); break; case PROP_FLAGS: g_value_set_flags(value, gv_feature_get_flags(self)); break; case PROP_SETTINGS: g_value_set_object(value, gv_feature_get_settings(self)); break; case PROP_ENABLED: g_value_set_boolean(value, gv_feature_get_enabled(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_feature_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvFeature *self = GV_FEATURE(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_NAME: gv_feature_set_name(self, g_value_get_string(value)); break; case PROP_FLAGS: gv_feature_set_flags(self, g_value_get_flags(value)); break; case PROP_ENABLED: gv_feature_set_enabled(self, g_value_get_boolean(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public functions */ GvFeature * gv_feature_new(GType object_type, const gchar *name, GvFeatureFlags flags) { return g_object_new(object_type, "name", name, "flags", flags, NULL); } /* * GvConfigurable interface */ static void gv_feature_configure(GvConfigurable *configurable) { GvFeature *self = GV_FEATURE(configurable); GvFeaturePrivate *priv = gv_feature_get_instance_private(self); GSettings *settings = priv->settings; TRACE("%p", self); g_assert(settings); g_settings_bind(settings, "enabled", self, "enabled", G_SETTINGS_BIND_DEFAULT); } static void gv_feature_configurable_interface_init(GvConfigurableInterface *iface) { iface->configure = gv_feature_configure; } /* * GObject methods */ static void gv_feature_finalize(GObject *object) { GvFeature *self = GV_FEATURE(object); GvFeaturePrivate *priv = gv_feature_get_instance_private(self); TRACE("%p", object); /* Unload */ if (priv->enabled) gv_feature_set_enabled(self, FALSE); /* Free resources */ g_object_unref(priv->settings); g_free(priv->name); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_feature, object); } static void gv_feature_constructed(GObject *object) { GvFeature *self = GV_FEATURE(object); GvFeatureClass *class = GV_FEATURE_GET_CLASS(self); GvFeaturePrivate *priv = gv_feature_get_instance_private(self); gchar *schema_id_suffix; TRACE("%p", object); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_feature, object); /* Ensure virtual methods are implemented */ g_assert_nonnull(class->enable); g_assert_nonnull(class->disable); /* Ensure construct-only properties have been set */ g_assert_nonnull(priv->name); /* Create settings */ schema_id_suffix = g_strjoin(".", "Feat", priv->name, NULL); priv->settings = gv_get_settings(schema_id_suffix); g_free(schema_id_suffix); } static void gv_feature_init(GvFeature *self) { TRACE("%p", self); } static void gv_feature_class_init(GvFeatureClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_feature_finalize; object_class->constructed = gv_feature_constructed; /* Properties */ object_class->get_property = gv_feature_get_property; object_class->set_property = gv_feature_set_property; properties[PROP_NAME] = g_param_spec_string("name", "Name", NULL, NULL, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_FLAGS] = g_param_spec_flags("flags", "Feature flags", NULL, GV_TYPE_FEATURE_FLAGS, GV_FEATURE_DEFAULT, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_SETTINGS] = g_param_spec_object("settings", "Settings", NULL, G_TYPE_SETTINGS, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_ENABLED] = g_param_spec_boolean("enabled", "Enabled", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/framework/gv-feature.h000066400000000000000000000044541337147241200207010ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GV_FEATURE_H__ #define __GOODVIBES_FRAMEWORK_GV_FEATURE_H__ #include #include /* GObject declarations */ #define GV_TYPE_FEATURE gv_feature_get_type() G_DECLARE_DERIVABLE_TYPE(GvFeature, gv_feature, GV, FEATURE, GObject) /* Chain up macros */ #define GV_FEATURE_CHAINUP_ENABLE(type_name, obj) \ do { \ if (GV_FEATURE_CLASS(type_name##_parent_class)->enable) \ GV_FEATURE_CLASS(type_name##_parent_class)->enable(obj); \ } while (0) #define GV_FEATURE_CHAINUP_DISABLE(type_name, obj) \ do { \ if (GV_FEATURE_CLASS(type_name##_parent_class)->disable) \ GV_FEATURE_CLASS(type_name##_parent_class)->disable(obj); \ } while (0) /* Data types */ typedef enum { /*< flags >*/ GV_FEATURE_DEFAULT, GV_FEATURE_EARLY, } GvFeatureFlags; struct _GvFeatureClass { /* Parent class */ GObjectClass parent_class; /* Virtual methods */ void (*enable) (GvFeature *); void (*disable)(GvFeature *); }; /* Public methods */ GvFeature *gv_feature_new(GType object_type, const gchar *name, GvFeatureFlags flags); /* Property accessors */ const gchar *gv_feature_get_name (GvFeature *self); GvFeatureFlags gv_feature_get_flags (GvFeature *self); GSettings *gv_feature_get_settings(GvFeature *self); gboolean gv_feature_get_enabled (GvFeature *self); void gv_feature_set_enabled (GvFeature *self, gboolean enabled); #endif /* __GOODVIBES_FRAMEWORK_GV_FEATURE_H__ */ goodvibes-v0.4.2/src/framework/gv-framework.c000066400000000000000000000046711337147241200212370ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "log.h" static gboolean initialized = FALSE; /* * Global object list * * This list contains all the global objects, ie. objects which lifetime * last for the whole program duration. It must be populated during * initialization, and accessed only after. */ static GList *object_list; /* Return the list of global objects. This list must be treated as read-only by * the caller, it's not supposed to be modified. */ GList * gv_framework_get_objects(void) { /* This should happen only after initialization is complete */ g_assert(initialized == TRUE); return object_list; } /* Register a global object. */ void gv_framework_register_object(gpointer data) { GObject *object = G_OBJECT(data); /* This should happen only during initialization, never after */ g_assert(initialized == FALSE); /* Add to the object list (we don't take ownership) */ object_list = g_list_prepend(object_list, object); /* Add a weak pointer, for cleanup checks */ g_object_add_weak_pointer(object, (gpointer *) &(object_list->data)); } void gv_framework_cleanup(void) { GList *item; g_assert(initialized == TRUE); /* Objects in list should be empty, thanks to the magic of weak pointers */ for (item = object_list; item; item = item->next) { GObject *object = G_OBJECT(item->data); if (object == NULL) continue; WARNING("Object of type '%s' has not been finalized !", G_OBJECT_TYPE_NAME(object)); } /* Free list */ g_list_free(object_list); } void gv_framework_init_completed(void) { g_assert(initialized == FALSE); initialized = TRUE; } void gv_framework_init(void) { /* Dummy */ } goodvibes-v0.4.2/src/framework/gv-framework.h000066400000000000000000000030751337147241200212410ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GV_FRAMEWORK_H__ #define __GOODVIBES_FRAMEWORK_GV_FRAMEWORK_H__ #include #include /* _() is defined here */ #include "framework/config.h" /* generated by the build system */ #include "framework/gv-configurable.h" #include "framework/gv-errorable.h" #include "framework/gv-feature.h" #include "framework/gv-framework-enum-types.h" #include "framework/gv-param-specs.h" #include "framework/log.h" #include "framework/uri-schemes.h" #include "framework/utils.h" #include "framework/vt-codes.h" void gv_framework_init (void); void gv_framework_init_completed(void); void gv_framework_cleanup (void); void gv_framework_register_object(gpointer object); GList *gv_framework_get_objects (void); #endif /* __GOODVIBES_FRAMEWORK_GV_FRAMEWORK_H__ */ goodvibes-v0.4.2/src/framework/gv-param-specs.h000066400000000000000000000024251337147241200214550ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GV_PARAM_SPECS_H__ #define __GOODVIBES_FRAMEWORK_GV_PARAM_SPECS_H__ #include /* Default flags for objects: * - STATIC_STRINGS because we only use static strings. * - EXPLICIT_NOTIFY because we want to notify only when a property * is changed. It's a design choice, every object should stick to it, * otherwise expect surprises. */ #define GV_PARAM_DEFAULT_FLAGS G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY #endif /* __GOODVIBES_FRAMEWORK_GV_PARAM_SPECS_H__ */ goodvibes-v0.4.2/src/framework/log.c000066400000000000000000000220251337147241200174020ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include #include #include "vt-codes.h" /* Additional log level for traces */ #define LOG_LEVEL_TRACE G_LOG_LEVEL_DEBUG << 1 /* Error printing */ #define perrorf(fmt, ...) fprintf(stderr, fmt ": %s\n", ##__VA_ARGS__, strerror(errno)) #define print_err(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__) /* Predefined log strings */ struct _log_strings { /* Log level prefixes */ const gchar *error; const gchar *critical; const gchar *warning; const gchar *message; const gchar *info; const gchar *debug; const gchar *trace; const gchar *dfl; /* Colors codes */ const gchar *reset; const gchar *dim; }; typedef struct _log_strings LogStrings; static LogStrings log_strings_colorless = { /* Log level prefixes */ .error = "ERR ", .critical = "CRIT", .warning = "WARN", .message = "MSG ", .info = "INFO", .debug = "DBG ", .trace = " -> ", .dfl = "LOG ", /* Colors codes */ .reset = "", .dim = "" }; static LogStrings log_strings_colorful = { /* Log level prefixes */ .error = VT_RED ("ERR "), .critical = VT_RED ("CRIT"), .warning = VT_YELLOW("WARN"), .message = VT_GREEN ("MSG "), .info = VT_GREEN ("INFO"), .debug = VT_DIM ("DBG "), .trace = VT_DIM (" -> "), .dfl = "LOG ", /* Color codes */ .reset = VT_CODE_ESC VT_CODE_RESET, .dim = VT_CODE_ESC VT_CODE_DIM }; /* Global variables that control the behavior of logs */ static FILE *log_stream; static gint log_level; static LogStrings *log_strings = &log_strings_colorless; /* Copies of std{out/err}, in case we redirect it */ static int stdout_copy = -1; static int stderr_copy = -1; /* Convert from string to log level */ static gint string_to_log_level(const gchar *str) { GLogLevelFlags level = G_LOG_LEVEL_MESSAGE; if (str == NULL) return level; if (!strcasecmp(str, "error") || !strcasecmp(str, "err")) { level = G_LOG_LEVEL_ERROR; } else if (!strcasecmp(str, "critical") || !strcasecmp(str, "crit")) { level = G_LOG_LEVEL_CRITICAL; } else if (!strcasecmp(str, "warning") || !strcasecmp(str, "warn")) { level = G_LOG_LEVEL_WARNING; } else if (!strcasecmp(str, "message") || !strcasecmp(str, "msg")) { level = G_LOG_LEVEL_MESSAGE; } else if (!strcasecmp(str, "info")) { level = G_LOG_LEVEL_INFO; } else if (!strcasecmp(str, "debug") || !strcasecmp(str, "dbg")) { level = G_LOG_LEVEL_DEBUG; } else if (!strcasecmp(str, "trace")) { level = LOG_LEVEL_TRACE; } else { print_err("Invalid log level '%s'", str); } return level; } /* Default log handler. * We DON'T honor any environment variables, such as * G_MESSAGES_PREFIXED, G_MESSAGES_DEBUG, ... */ static void log_default_handler(const gchar *domain, GLogLevelFlags level, const gchar *msg, gpointer unused_data G_GNUC_UNUSED) { GDateTime *now; gchar *now_str; const gchar *prefix; level &= G_LOG_LEVEL_MASK; /* Last chance to discard the log */ if (level > log_level) return; /* Discard debug messages that don't belong to us */ if (domain && level > G_LOG_LEVEL_INFO) return; /* Start by getting the current time. Note that it would be more * accurate to get the time earlier, but we don't need such accuracy * I believe, plus it's more convenient to do it here. */ now = g_date_time_new_now_local(); now_str = g_date_time_format(now, "%T"); /* Prefix depends on log level. * Cast is needed at the moment to avoid a gcc warning. * This is because GLogLevelFlags *may be* 8-bits long * due to the way it's defined. * Check the net for more info: * https://mail.gnome.org/archives/gtk-devel-list/2014-May/msg00029.html * https://bugzilla.gnome.org/show_bug.cgi?id=730932 */ switch ((gint) level) { case G_LOG_LEVEL_ERROR: prefix = log_strings->error; break; case G_LOG_LEVEL_CRITICAL: prefix = log_strings->critical; break; case G_LOG_LEVEL_WARNING: prefix = log_strings->warning; break; case G_LOG_LEVEL_MESSAGE: prefix = log_strings->message; break; case G_LOG_LEVEL_INFO: prefix = log_strings->info; break; case G_LOG_LEVEL_DEBUG: prefix = log_strings->debug; break; case LOG_LEVEL_TRACE: prefix = log_strings->trace; break; default: prefix = log_strings->dfl; break; } /* Send everything to the log stream */ fputs(prefix, log_stream); fputs(" ", log_stream); fputs(log_strings->dim, log_stream); fputs(now_str, log_stream); fputs(log_strings->reset, log_stream); fputs(" ", log_stream); if (domain) fprintf(log_stream, "[%s] ", domain); fputs(msg, log_stream); fputs("\n", log_stream); /* Cleanup */ g_date_time_unref(now); g_free(now_str); } void log_trace_property_access(const gchar *file, const gchar *func, GObject *object, guint property_id, const GValue *value, GParamSpec *pspec, gboolean print_value) { gchar *value_string; guint max_len = 128; if (LOG_LEVEL_TRACE > log_level) return; if (print_value) { value_string = g_strdup_value_contents(value); if (value_string && strlen(value_string) > max_len) { guint idx; if (value_string[0] == '"') idx = max_len - 4; else idx = max_len - 3; value_string[idx++] = '.'; value_string[idx++] = '.'; value_string[idx++] = '.'; if (value_string[0] == '"') value_string[idx++] = '"'; value_string[idx] = '\0'; } } else { value_string = g_strdup_printf("(%s)", G_VALUE_TYPE_NAME(value)); } g_log(G_LOG_DOMAIN, LOG_LEVEL_TRACE, "%s%s: %s%s(%p, %d, %s, '%s')", log_strings->dim, file, func, log_strings->reset, object, property_id, value_string, pspec->name); g_free(value_string); } void log_trace(const gchar *file, const gchar *func, const gchar *fmt, ...) { va_list ap; gchar fmt2[512]; if (LOG_LEVEL_TRACE > log_level) return; snprintf(fmt2, sizeof fmt2, "%s%s: %s()%s: (%s)", log_strings->dim, file, func, log_strings->reset, fmt); va_start(ap, fmt); g_logv(G_LOG_DOMAIN, LOG_LEVEL_TRACE, fmt2, ap); va_end(ap); } void log_msg(GLogLevelFlags level, const gchar *file, const gchar *func, const gchar *fmt, ...) { va_list ap; gchar fmt2[512]; if (level > log_level) return; if (!file && !func) snprintf(fmt2, sizeof fmt2, "%s", fmt); else snprintf(fmt2, sizeof fmt2, "%s%s: %s()%s: %s", log_strings->dim, file, func, log_strings->reset, fmt); va_start(ap, fmt); g_logv(G_LOG_DOMAIN, level, fmt2, ap); va_end(ap); } void log_cleanup(void) { /* Restore standard output */ if (stdout_copy > 0) { if (dup2(stdout_copy, STDOUT_FILENO) == -1) perror("Failed to restore stdout"); if (close(stdout_copy) == -1) perror("Failed to close stdout copy"); } /* Restore error output */ if (stderr_copy > 0) { if (dup2(stderr_copy, STDERR_FILENO) == -1) perror("Failed to restore stderr"); if (close(stderr_copy) == -1) perror("Failed to close stderr copy"); } } void log_init(const gchar *log_level_str, gboolean colorless, const gchar *output_file) { /* We send every log message to stderr, so that it's easy * to separate logs (intended for developpers) and messages * (intended for users). */ log_stream = stderr; /* We have our own log handler */ g_log_set_default_handler(log_default_handler, NULL); /* Set log level */ log_level = string_to_log_level(log_level_str); /* Redirect output to a log file */ if (output_file) { FILE *fp; stdout_copy = dup(STDOUT_FILENO); if (stdout_copy == -1) perror("Failed to duplicate stdout"); stderr_copy = dup(STDERR_FILENO); if (stderr_copy == -1) perror("Failed to duplicate stderr"); fp = fopen(output_file, "w"); if (fp) { if (dup2(fileno(fp), STDOUT_FILENO) == -1) perror("Failed to redirect stdout"); if (dup2(fileno(fp), STDERR_FILENO) == -1) perror("Failed to redirect stderr"); if (fclose(fp) == -1) perrorf("Failed to close log file '%s'", output_file); } else perrorf("Failed to open log file '%s'", output_file); } /* Set colorful log prefixes. * Colors only make sense if logs are sent to a terminal, * since they're implemented with VT commands. */ if (isatty(fileno(log_stream)) && !colorless) log_strings = &log_strings_colorful; } goodvibes-v0.4.2/src/framework/log.h000066400000000000000000000050431337147241200174100ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_LOG_H__ #define __GOODVIBES_FRAMEWORK_LOG_H__ #include #include void log_init(const gchar *log_level, gboolean colorless, const gchar *output_file); void log_cleanup(void); void log_msg(GLogLevelFlags level, const gchar *file, const gchar *func, const gchar *fmt, ...); void log_trace(const gchar *file, const gchar *func, const gchar *fmt, ...); void log_trace_property_access(const gchar *file, const gchar *func, GObject *object, guint property_id, const GValue *value, GParamSpec *pspec, gboolean print_value); /* * Wrappers to GLib message logging functions. * Use that for logs intended for developers. */ #define ERROR(fmt, ...) do { \ log_msg(G_LOG_LEVEL_ERROR, __FILE__, __func__, fmt, ##__VA_ARGS__); \ __builtin_unreachable(); \ } while (0) #define CRITICAL(fmt, ...) log_msg(G_LOG_LEVEL_CRITICAL, __FILE__, __func__, fmt, ##__VA_ARGS__) #define WARNING(fmt, ...) log_msg(G_LOG_LEVEL_WARNING, __FILE__, __func__, fmt, ##__VA_ARGS__) #define INFO(fmt, ...) log_msg(G_LOG_LEVEL_INFO, __FILE__, __func__, fmt, ##__VA_ARGS__) #define DEBUG(fmt, ...) log_msg(G_LOG_LEVEL_DEBUG, __FILE__, __func__, fmt, ##__VA_ARGS__) #define DEBUG_NO_CONTEXT(fmt, ...) log_msg(G_LOG_LEVEL_DEBUG, NULL, NULL, fmt, ##__VA_ARGS__) #define TRACE(fmt, ...) log_trace(__FILE__, __func__, fmt, ##__VA_ARGS__) #define TRACE_GET_PROPERTY(obj, prop_id, value, pspec) \ log_trace_property_access(__FILE__, __func__, obj, prop_id, value, pspec, FALSE) #define TRACE_SET_PROPERTY(obj, prop_id, value, pspec) \ log_trace_property_access(__FILE__, __func__, obj, prop_id, value, pspec, TRUE) #endif /* __GOODVIBES_FRAMEWORK_LOG_H__ */ goodvibes-v0.4.2/src/framework/meson.build000066400000000000000000000036011337147241200206160ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Configuration file config = configuration_data() config.set_quoted('GETTEXT_PACKAGE', gv_name_lowercase) config.set_quoted('GV_LOCALEDIR', localedir) config.set_quoted('PACKAGE_NAME', gv_name_lowercase) config.set_quoted('PACKAGE_VERSION', gv_version) config.set_quoted('GV_NAME_CAPITAL', gv_name_camelcase) config.set_quoted('GV_APPLICATION_ID', gv_application_id) config.set_quoted('GV_APPLICATION_PATH', gv_application_path) config.set_quoted('GV_ICON_NAME', gv_icon_name) config.set_quoted('GV_HOMEPAGE', gv_homepage) config.set_quoted('GV_COPYRIGHT', gv_copyright) config.set_quoted('GV_AUTHOR_NAME', gv_author_name) config.set_quoted('GV_AUTHOR_EMAIL', gv_author_email) config.set('GV_FEAT_CONSOLE_OUTPUT', gv_feat_console_output) config.set('GV_FEAT_DBUS_SERVER', gv_feat_dbus_server) config.set('GV_FEAT_INHIBITOR', gv_feat_inhibitor) config.set('GV_UI_ENABLED', gv_ui_enabled) config.set('GV_FEAT_HOTKEYS', gv_feat_hotkeys) config.set('GV_FEAT_NOTIFICATIONS', gv_feat_notifications) configure_file( output: 'config.h', configuration: config, ) # Sources and dependencies framework_sources = [ 'glib-additions.c', 'glib-object-additions.c', 'gv-configurable.c', 'gv-errorable.c', 'gv-feature.c', 'gv-framework.c', 'log.c', 'uri-schemes.c', 'utils.c', ] framework_dependencies = [ glib_dep, gobject_dep, gio_dep, ] framework_enum_headers = [ 'gv-feature.h' ] framework_enums = gnome.mkenums_simple('gv-framework-enum-types', sources: framework_enum_headers ) framework_enum_h = framework_enums[1] # Library definition gvframework = static_library('gvframework', sources: [ framework_sources, framework_enums ], dependencies: framework_dependencies, include_directories: root_inc, ) gvframework_dep = declare_dependency( dependencies: framework_dependencies, sources: [ framework_enum_h ], link_with: gvframework, ) goodvibes-v0.4.2/src/framework/uri-schemes.c000066400000000000000000000024341337147241200210470ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include const gchar *SUPPORTED_URI_SCHEMES[] = { "http", "https", NULL }; const gchar *SUPPORTED_MIME_TYPES[] = { "audio/*", NULL }; gboolean is_uri_scheme_supported(const gchar *uri) { gchar *uri_scheme; const gchar **schemes = SUPPORTED_URI_SCHEMES; const gchar *scheme; gboolean uri_ok; uri_scheme = g_uri_parse_scheme(uri); uri_ok = FALSE; while (schemes && (scheme = *schemes++)) { if (g_strcmp0(scheme, uri_scheme) == 0) { uri_ok = TRUE; break; } } g_free(uri_scheme); return uri_ok; } goodvibes-v0.4.2/src/framework/uri-schemes.h000066400000000000000000000020501337147241200210460ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_URI_SCHEMES_H__ #define __GOODVIBES_FRAMEWORK_URI_SCHEMES_H__ extern const gchar *SUPPORTED_URI_SCHEMES[]; extern const gchar *SUPPORTED_MIME_TYPES[]; gboolean is_uri_scheme_supported(const gchar *uri); #endif /* __GOODVIBES_FRAMEWORK_URI_SCHEMES_H__ */ goodvibes-v0.4.2/src/framework/utils.c000066400000000000000000000100041337147241200177530ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "config.h" #include "log.h" #define GV_OLD_APPLICATION_ID "com.elboulangero.Goodvibes" /* * Settings */ static GSettings * get_current_settings(const gchar *component) { gchar *schema_id; GSettings *settings; schema_id = g_strdup_printf("%s.%s", GV_APPLICATION_ID, component); settings = g_settings_new(schema_id); g_free(schema_id); return settings; } static GSettings * get_old_settings(const gchar *component) { gchar *schema_id; GSettingsSchema *schema; GSettingsSchemaSource *source; source = g_settings_schema_source_get_default(); if (source == NULL) return NULL; schema_id = g_strdup_printf("%s.%s", GV_OLD_APPLICATION_ID, component); schema = g_settings_schema_source_lookup(source, schema_id, TRUE); g_free(schema_id); if (schema == NULL) return NULL; return g_settings_new_full(schema, NULL, NULL); } static void merge_settings(const gchar *component, GSettings *from, GSettings *to) { gint i; gchar **keys; gboolean changed; GSettingsSchema *schema; g_object_get(from, "settings-schema", &schema, NULL); keys = g_settings_schema_list_keys(schema); changed = FALSE; for (i = 0; keys[i]; i++) { const gchar *key; GVariant *value; key = keys[i]; value = g_settings_get_user_value(from, key); if (value == NULL) continue; INFO("Settings migration: %s: moving key '%s'", component, key); g_settings_set_value(to, key, value); g_settings_reset(from, key); g_variant_unref(value); changed = TRUE; } if (changed) { INFO("Settings migration: %s: syncing", component); g_settings_sync(); } g_strfreev(keys); g_settings_schema_unref(schema); } GSettings * gv_get_settings(const gchar *component) { GSettings *settings, *old_settings; settings = get_current_settings(component); old_settings = get_old_settings(component); if (old_settings) { merge_settings(component, old_settings, settings); g_object_unref(old_settings); } return settings; } /* * XDG */ const gchar * gv_get_app_user_config_dir(void) { static gchar *dir; if (dir == NULL) { const gchar *user_dir; user_dir = g_get_user_config_dir(); dir = g_build_filename(user_dir, PACKAGE_NAME, NULL); } return dir; } const gchar * gv_get_app_user_data_dir(void) { static gchar *dir; if (dir == NULL) { const gchar *user_dir; user_dir = g_get_user_data_dir(); dir = g_build_filename(user_dir, PACKAGE_NAME, NULL); } return dir; } const gchar *const * gv_get_app_system_config_dirs(void) { static gchar **dirs; if (dirs == NULL) { const gchar *const *system_dirs; guint i, n_dirs; system_dirs = g_get_system_config_dirs(); n_dirs = g_strv_length((gchar **) system_dirs); dirs = g_malloc0_n(n_dirs + 1, sizeof(gchar *)); for (i = 0; i < n_dirs; i++) dirs[i] = g_build_filename(system_dirs[i], PACKAGE_NAME, NULL); } return (const gchar * const *) dirs; } const gchar *const * gv_get_app_system_data_dirs(void) { static gchar **dirs; if (dirs == NULL) { const gchar *const *system_dirs; guint i, n_dirs; system_dirs = g_get_system_data_dirs(); n_dirs = g_strv_length((gchar **) system_dirs); dirs = g_malloc0_n(n_dirs + 1, sizeof(gchar *)); for (i = 0; i < n_dirs; i++) dirs[i] = g_build_filename(system_dirs[i], PACKAGE_NAME, NULL); } return (const gchar * const *) dirs; } goodvibes-v0.4.2/src/framework/utils.h000066400000000000000000000022321337147241200177640ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_UTILS_H__ #define __GOODVIBES_FRAMEWORK_UTILS_H__ #include GSettings *gv_get_settings(const gchar *component); const gchar *gv_get_app_user_config_dir(void); const gchar *gv_get_app_user_data_dir(void); const gchar *const *gv_get_app_system_config_dirs(void); const gchar *const *gv_get_app_system_data_dirs(void); #endif /* __GOODVIBES_FRAMEWORK_UTILS_H__ */ goodvibes-v0.4.2/src/framework/vt-codes.h000066400000000000000000000062041337147241200203530ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_VT_CODES_H__ #define __GOODVIBES_FRAMEWORK_VT_CODES_H__ /* Terminal codes (ANSI/VT100) */ #define VT_CODE_ESC "\033" #define VT_CODE_RESET "[0m" #define VT_CODE_BOLD "[1m" #define VT_CODE_DIM "[2m" #define VT_CODE_BLACK "[0;30m" #define VT_CODE_RED "[0;31m" #define VT_CODE_GREEN "[0;32m" #define VT_CODE_BROWN "[0;33m" #define VT_CODE_BLUE "[0;34m" #define VT_CODE_PURPLE "[0;35m" #define VT_CODE_CYAN "[0;36m" #define VT_CODE_GREY "[0;37m" #define VT_CODE_DARK_GREY "[1;30m" #define VT_CODE_LIGHT_RED "[1;31m" #define VT_CODE_LIGHT_GREEN "[1;32m" #define VT_CODE_YELLOW "[1;33m" #define VT_CODE_LIGHT_BLUE "[1;34m" #define VT_CODE_LIGHT_PURPLE "[1;35m" #define VT_CODE_LIGHT_CYAN "[1;36m" #define VT_CODE_WHITE "[1;37m" /* Convenient macros to wrap static strings */ #define VT_BOLD(str) VT_CODE_ESC VT_CODE_BOLD str VT_CODE_ESC VT_CODE_RESET #define VT_DIM(str) VT_CODE_ESC VT_CODE_DIM str VT_CODE_ESC VT_CODE_RESET #define VT_BLACK(str) VT_CODE_ESC VT_CODE_BLACK str VT_CODE_ESC VT_CODE_RESET #define VT_RED(str) VT_CODE_ESC VT_CODE_RED str VT_CODE_ESC VT_CODE_RESET #define VT_GREEN(str) VT_CODE_ESC VT_CODE_GREEN str VT_CODE_ESC VT_CODE_RESET #define VT_BROWN(str) VT_CODE_ESC VT_CODE_BROWN str VT_CODE_ESC VT_CODE_RESET #define VT_BLUE(str) VT_CODE_ESC VT_CODE_BLUE str VT_CODE_ESC VT_CODE_RESET #define VT_PURPLE(str) VT_CODE_ESC VT_CODE_PURPLE str VT_CODE_ESC VT_CODE_RESET #define VT_CYAN(str) VT_CODE_ESC VT_CODE_CYAN str VT_CODE_ESC VT_CODE_RESET #define VT_GREY(str) VT_CODE_ESC VT_CODE_GREY str VT_CODE_ESC VT_CODE_RESET #define VT_DARK_GREY(str) VT_CODE_ESC VT_CODE_DARK_GREY str VT_CODE_ESC VT_CODE_RESET #define VT_LIGHT_RED(str) VT_CODE_ESC VT_CODE_LIGHT_RED str VT_CODE_ESC VT_CODE_RESET #define VT_LIGHT_GREEN(str) VT_CODE_ESC VT_CODE_LIGHT_GREEN str VT_CODE_ESC VT_CODE_RESET #define VT_YELLOW(str) VT_CODE_ESC VT_CODE_YELLOW str VT_CODE_ESC VT_CODE_RESET #define VT_LIGHT_BLUE(str) VT_CODE_ESC VT_CODE_LIGHT_BLUE str VT_CODE_ESC VT_CODE_RESET #define VT_LIGHT_PURPLE(str) VT_CODE_ESC VT_CODE_LIGHT_PURPLE str VT_CODE_ESC VT_CODE_RESET #define VT_LIGHT_CYAN(str) VT_CODE_ESC VT_CODE_LIGHT_CYAN str VT_CODE_ESC VT_CODE_RESET #define VT_WHITE(str) VT_CODE_ESC VT_CODE_WHITE str VT_CODE_ESC VT_CODE_RESET #endif /* __GOODVIBES_FRAMEWORK_VT_CODES_H__ */ goodvibes-v0.4.2/src/gv-console-application.c000066400000000000000000000075221337147241200212060ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2017-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-feat.h" #include "gv-console-application.h" #include "options.h" /* * GObject definitions */ struct _GvConsoleApplication { /* Parent instance structure */ GApplication parent_instance; }; G_DEFINE_TYPE(GvConsoleApplication, gv_console_application, G_TYPE_APPLICATION) /* * Public methods */ GApplication * gv_console_application_new(const gchar *application_id) { return G_APPLICATION(g_object_new(GV_TYPE_CONSOLE_APPLICATION, "application-id", application_id, "flags", G_APPLICATION_FLAGS_NONE, NULL)); } /* * GApplication methods */ static void gv_console_application_shutdown(GApplication *app) { DEBUG_NO_CONTEXT(">>>> Main loop terminated <<<<"); /* Cleanup */ DEBUG_NO_CONTEXT("---- Cleaning up ----"); gv_feat_cleanup(); gv_core_cleanup(); gv_framework_cleanup(); /* Mandatory chain-up */ G_APPLICATION_CLASS(gv_console_application_parent_class)->shutdown(app); } static void gv_console_application_startup(GApplication *app) { DEBUG_NO_CONTEXT("---- Starting application ----"); /* Mandatory chain-up, see: * https://developer.gnome.org/gtk3/stable/GtkApplication.html#gtk-application-new */ G_APPLICATION_CLASS(gv_console_application_parent_class)->startup(app); /* Initialization */ DEBUG_NO_CONTEXT("---- Initializing ----"); gv_framework_init(); gv_core_init(app); gv_feat_init(); gv_framework_init_completed(); /* Configuration */ DEBUG_NO_CONTEXT("---- Configuring ----"); gv_feat_configure_early(); gv_core_configure(); gv_feat_configure_late(); /* Hold application */ g_application_hold(app); } static gboolean when_idle_go_player(gpointer user_data) { const gchar *uri_to_play = user_data; gv_player_go(gv_core_player, uri_to_play); return G_SOURCE_REMOVE; } static void gv_console_application_activate(GApplication *app G_GNUC_UNUSED) { static gboolean first_invocation = TRUE; if (first_invocation) { first_invocation = FALSE; DEBUG_NO_CONTEXT(">>>> Main loop started <<<<"); /* Schedule a callback to play music. * DO NOT start playing now ! It's too early ! * There's still some init code pending, and we want to ensure * (as much as possible) that this init code is run before we * start the playback. Therefore we schedule with a low priority. */ g_idle_add_full(G_PRIORITY_LOW, when_idle_go_player, (void *) options.uri_to_play, NULL); } } /* * GObject methods */ static void gv_console_application_init(GvConsoleApplication *self) { TRACE("%p", self); } static void gv_console_application_class_init(GvConsoleApplicationClass *class) { GApplicationClass *application_class = G_APPLICATION_CLASS(class); TRACE("%p", class); /* Override GApplication methods */ application_class->startup = gv_console_application_startup; application_class->shutdown = gv_console_application_shutdown; application_class->activate = gv_console_application_activate; } goodvibes-v0.4.2/src/gv-console-application.h000066400000000000000000000024051337147241200212060ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2017-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_GV_CONSOLE_APPLICATION_H__ #define __GOODVIBES_GV_CONSOLE_APPLICATION_H__ #include #include /* GObject declarations */ #define GV_TYPE_CONSOLE_APPLICATION gv_console_application_get_type() G_DECLARE_FINAL_TYPE(GvConsoleApplication, gv_console_application, GV, CONSOLE_APPLICATION, GApplication) /* Methods */ GApplication *gv_console_application_new(const gchar *application_id); #endif /* __GOODVIBES_GV_CONSOLE_APPLICATION_H__ */ goodvibes-v0.4.2/src/gv-graphical-application.c000066400000000000000000000174541337147241200215030ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2017-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gv-ui.h" #include "feat/gv-feat.h" #include "gv-graphical-application.h" #include "options.h" #define APPMENU_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/app-menu.glade" #define MENUBAR_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/menubar.glade" /* * GObject definitions */ struct _GvGraphicalApplication { /* Parent instance structure */ GtkApplication parent_instance; }; G_DEFINE_TYPE(GvGraphicalApplication, gv_graphical_application, GTK_TYPE_APPLICATION) /* * Public methods */ GApplication * gv_graphical_application_new(const gchar *application_id) { return G_APPLICATION(g_object_new(GV_TYPE_GRAPHICAL_APPLICATION, "application-id", application_id, "flags", G_APPLICATION_FLAGS_NONE, NULL)); } /* * GApplication actions */ static void add_station_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gv_ui_present_add_station(); } static void preferences_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gv_ui_present_preferences(); } static void help_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { g_app_info_launch_default_for_uri(GV_HOMEPAGE, NULL, NULL); } static void about_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gv_ui_present_about(); } static void close_ui_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gv_ui_hide(); } static void quit_action_cb(GSimpleAction *action G_GNUC_UNUSED, GVariant *parameters G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { gv_core_quit(); } static const GActionEntry gv_graphical_application_actions[] = { { "add-station", add_station_action_cb, NULL, NULL, NULL, {0} }, { "preferences", preferences_action_cb, NULL, NULL, NULL, {0} }, { "help", help_action_cb, NULL, NULL, NULL, {0} }, { "about", about_action_cb, NULL, NULL, NULL, {0} }, { "close-ui", close_ui_action_cb, NULL, NULL, NULL, {0} }, { "quit", quit_action_cb, NULL, NULL, NULL, {0} }, { NULL, NULL, NULL, NULL, NULL, {0} } }; /* * GApplication methods */ static void gv_graphical_application_shutdown(GApplication *app) { DEBUG_NO_CONTEXT(">>>> Main loop terminated <<<<"); /* Cleanup */ DEBUG_NO_CONTEXT("---- Cleaning up ----"); gv_feat_cleanup(); gv_ui_cleanup(); gv_core_cleanup(); gv_framework_cleanup(); /* Mandatory chain-up */ G_APPLICATION_CLASS(gv_graphical_application_parent_class)->shutdown(app); } static void gv_graphical_application_startup(GApplication *app) { DEBUG_NO_CONTEXT("---- Starting application ----"); /* Mandatory chain-up, see: * https://developer.gnome.org/gtk3/stable/GtkApplication.html#gtk-application-new */ G_APPLICATION_CLASS(gv_graphical_application_parent_class)->startup(app); /* Add actions to the application */ g_action_map_add_action_entries(G_ACTION_MAP(app), gv_graphical_application_actions, -1, NULL); /* The 'close-ui' action makes no sense in the status icon mode */ if (options.status_icon) g_action_map_remove_action(G_ACTION_MAP(app), "close-ui"); /* Now time to setup the menus. In status icon mode, we do nothing, this is handled * by the status icon code itself later on. */ if (!options.status_icon) { gboolean prefers_app_menu; /* Check how the application prefers to display it's main menu */ prefers_app_menu = gtk_application_prefers_app_menu(GTK_APPLICATION(app)); DEBUG("Application prefers... %s", prefers_app_menu ? "app-menu" : "menubar"); /* Gnome-based desktop environments prefer an application menu */ if (prefers_app_menu) { GtkBuilder *builder; GMenuModel *model; builder = gtk_builder_new_from_resource(APPMENU_RESOURCE_PATH); model = G_MENU_MODEL(gtk_builder_get_object(builder, "app-menu")); gtk_application_set_app_menu(GTK_APPLICATION(app), model); g_object_unref(builder); } /* Unity-based and traditional desktop environments prefer a menu bar */ if (!prefers_app_menu) { GtkBuilder *builder; GMenuModel *model; builder = gtk_builder_new_from_resource(MENUBAR_RESOURCE_PATH); model = G_MENU_MODEL(gtk_builder_get_object(builder, "menubar")); gtk_application_set_menubar(GTK_APPLICATION(app), model); g_object_unref(builder); } } /* Initialization */ DEBUG_NO_CONTEXT("---- Initializing ----"); gv_framework_init(); gv_core_init(app); gv_ui_init(app, options.status_icon); gv_feat_init(); gv_framework_init_completed(); /* Configuration */ DEBUG_NO_CONTEXT("---- Configuring ----"); gv_feat_configure_early(); gv_core_configure(); gv_ui_configure(); gv_feat_configure_late(); /* Hold application */ g_application_hold(app); } static gboolean when_idle_go_player(gpointer user_data) { const gchar *uri_to_play = user_data; gv_player_go(gv_core_player, uri_to_play); return G_SOURCE_REMOVE; } static void gv_graphical_application_activate(GApplication *app G_GNUC_UNUSED) { static gboolean first_invocation = TRUE; if (first_invocation) { first_invocation = FALSE; DEBUG_NO_CONTEXT(">>>> Main loop started <<<<"); /* Schedule a callback to play music. * DO NOT start playing now ! It's too early ! * There's still some init code pending, and we want to ensure * (as much as possible) that this init code is run before we * start the playback. Therefore we schedule with a low priority. */ g_idle_add_full(G_PRIORITY_LOW, when_idle_go_player, (void *) options.uri_to_play, NULL); /* Present the main window, depending on options */ if (!options.without_ui) { DEBUG("Presenting main window"); gv_ui_present_main(); } else { DEBUG("NOT presenting main window (--without-ui)"); } } else { /* Present the main window, unconditionally */ DEBUG("Presenting main window"); gv_ui_present_main(); } } /* * GObject methods */ static void gv_graphical_application_init(GvGraphicalApplication *self) { TRACE("%p", self); } static void gv_graphical_application_class_init(GvGraphicalApplicationClass *class) { GApplicationClass *application_class = G_APPLICATION_CLASS(class); TRACE("%p", class); /* Override GApplication methods */ application_class->startup = gv_graphical_application_startup; application_class->shutdown = gv_graphical_application_shutdown; application_class->activate = gv_graphical_application_activate; } goodvibes-v0.4.2/src/gv-graphical-application.h000066400000000000000000000024561337147241200215040ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2017-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_GV_GRAPHICAL_APPLICATION_H__ #define __GOODVIBES_GV_GRAPHICAL_APPLICATION_H__ #include #include #include /* GObject declarations */ #define GV_TYPE_GRAPHICAL_APPLICATION gv_graphical_application_get_type() G_DECLARE_FINAL_TYPE(GvGraphicalApplication, gv_graphical_application, GV, GRAPHICAL_APPLICATION, GtkApplication) /* Methods */ GApplication *gv_graphical_application_new(const gchar *application_id); #endif /* __GOODVIBES_GV_GRAPHICAL_APPLICATION_H__ */ goodvibes-v0.4.2/src/libcaphe/000077500000000000000000000000001337147241200162265ustar00rootroot00000000000000goodvibes-v0.4.2/src/libcaphe/README.md000066400000000000000000000020011337147241200174760ustar00rootroot00000000000000Libcaphe ======== Libcaphe is a little library that inhibits sleep/suspend. You use it in your application if you want to prevent the system from going to sleep. A typical use-case is for audio/video players, who might want to inhibit sleep while playing. The code is inspired by [Caffeine-ng](https://gitlab.com/hobarrera/caffeine-ng). Libcaphe will work if one of the following D-Bus services is present on your system: - `org.gnome.SessionManager` - `org.xfce.SessionManager` - `org.freedesktop.PowerManagement` - `org.freedesktop.login1` Notice that the prefered way to inhibit for GTK+ applications is to use `gtk_application_inhibit()`. I use this library mainly to support old systems that rely on `org.freedesktop.PowerManagement`, which has been deprecated a while ago, but is still widely used. About the name -------------- "Cà phê" is the Vietnamese for coffee. At the moment of this writing, Vietnam is the second largest producer in the world after Brazil, and I thought the world ought to know :) goodvibes-v0.4.2/src/libcaphe/caphe-cup.c000066400000000000000000000265421337147241200202500ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-inhibitor.h" #include "caphe-inhibitor-list.h" #include "caphe-cup.h" /* * Signals */ enum { SIGNAL_INHIBIT_FAILURE, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * Properties */ enum { /* Reserved */ PROP_0, /* Construct-only properties */ PROP_APPLICATION_NAME, /* Properties */ PROP_INHIBITOR, /* Total number of properties */ LAST_PROP }; static GParamSpec *properties[LAST_PROP]; /* * GObject definitions */ typedef struct { /* If NULL, means an unhibibit request. * If non NULL, means an inhibit request. */ gchar *reason; } CapheRequest; struct _CapheCupPrivate { /* List of inhibitors */ CapheInhibitorList *inhibitor_list; gboolean ready; /* Application name */ gchar *application_name; /* Current state */ CapheInhibitor *current_inhibitor; gchar *current_reason; /* Pending request */ CapheRequest *pending_request; /* Pending processing */ guint when_idle_process_id; }; typedef struct _CapheCupPrivate CapheCupPrivate; struct _CapheCup { /* Parent instance structure */ GObject parent_instance; /* Private data */ CapheCupPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CapheCup, caphe_cup, G_TYPE_OBJECT) /* * Helpers */ CapheRequest * caphe_request_new(const gchar *reason) { CapheRequest *self; self = g_new0(CapheRequest, 1); self->reason = g_strdup(reason); return self; } void caphe_request_free(CapheRequest *self) { if (self == NULL) return; g_free(self->reason); g_free(self); } /* * Private methods */ static void caphe_cup_set_inhibitor(CapheCup *self, CapheInhibitor *inhibitor); static void caphe_cup_process(CapheCup *self) { CapheCupPrivate *priv = self->priv; /* As long as we're not ready, we can't do anything */ if (priv->ready == FALSE) return; /* If there's nothing pending, do nothing */ if (priv->pending_request == NULL) return; /* We can start processing the pending request. * In case we're trying to inhibit while already inhibited * (it's possible in case the user wants to change the reason), * we must slip an unhibit request in between. */ if (priv->current_reason && priv->pending_request->reason) { g_debug("Slipping uninhibit request between two inhibit requests"); /* Uninhibit */ if (priv->current_inhibitor) { caphe_inhibitor_uninhibit(priv->current_inhibitor); priv->current_inhibitor = NULL; g_free(priv->current_reason); priv->current_reason = NULL; } } /* Process now */ if (priv->pending_request->reason) { CapheInhibitor **inhibitors; gboolean success; g_debug("Inhibition: processing request"); for (inhibitors = caphe_inhibitor_list_get_inhibitors(priv->inhibitor_list); *inhibitors; inhibitors++) { CapheInhibitor *inhibitor = *inhibitors; GError *error = NULL; /* Ignore inhibitors that are not available */ if (caphe_inhibitor_get_available(inhibitor) == FALSE) continue; /* Attempt inhibition */ success = caphe_inhibitor_inhibit(inhibitor, priv->application_name, priv->pending_request->reason, &error); if (success == FALSE) { g_warning("Error while inhibiting: %s", error->message); g_clear_error(&error); continue; } /* Save new status */ caphe_cup_set_inhibitor(self, inhibitor); g_free(priv->current_reason); priv->current_reason = g_strdup(priv->pending_request->reason); /* Done */ break; } if (success == FALSE) { g_signal_emit(self, signals[SIGNAL_INHIBIT_FAILURE], 0); } /* Consume request */ caphe_request_free(priv->pending_request); priv->pending_request = NULL; } else { g_debug("Uninhibition: processing request"); /* Uninhibit */ if (priv->current_inhibitor) { caphe_inhibitor_uninhibit(priv->current_inhibitor); caphe_cup_set_inhibitor(self, NULL); g_free(priv->current_reason); priv->current_reason = NULL; } /* Consume request */ caphe_request_free(priv->pending_request); priv->pending_request = NULL; } } /* * Signal handlers */ static void on_inhibitor_list_ready(CapheInhibitorList *inhibitor_list G_GNUC_UNUSED, CapheCup *self) { CapheCupPrivate *priv = self->priv; /* We're ready, let's remember that */ priv->ready = TRUE; /* Process */ caphe_cup_process(self); } /* * Public */ gboolean caphe_cup_is_inhibited(CapheCup *self) { CapheCupPrivate *priv = self->priv; return priv->current_inhibitor != NULL; } static gboolean when_idle_process(CapheCup *self) { CapheCupPrivate *priv = self->priv; caphe_cup_process(self); priv->when_idle_process_id = 0; return G_SOURCE_REMOVE; } void caphe_cup_uninhibit(CapheCup *self) { CapheCupPrivate *priv = self->priv; /* We might ignore the request in the following case: * - we're already uninhibited * - we're sure there's nothing happening at the moment */ if (priv->current_inhibitor == NULL && priv->pending_request == NULL) return; /* Otherwise, create a request */ caphe_request_free(priv->pending_request); priv->pending_request = caphe_request_new(NULL); g_debug("Uninhibition: request created and pending"); /* Schedule process */ if (priv->when_idle_process_id == 0) priv->when_idle_process_id = g_idle_add((GSourceFunc) when_idle_process, self); } void caphe_cup_inhibit(CapheCup *self, const gchar *reason) { CapheCupPrivate *priv = self->priv; g_return_if_fail(reason != NULL); /* We might ignore the request in the following case: * - we're already inhibited with the same reason * - we're sure there's nothing happening at the moment * ... */ if (priv->current_inhibitor && !g_strcmp0(reason, priv->current_reason) && priv->pending_request == NULL) return; /* Otherwise, create a request */ caphe_request_free(priv->pending_request); priv->pending_request = caphe_request_new(reason); g_debug("Inhibition: request created and pending"); /* Schedule process */ if (priv->when_idle_process_id == 0) priv->when_idle_process_id = g_idle_add((GSourceFunc) when_idle_process, self); } CapheCup * caphe_cup_new(void) { return g_object_new(CAPHE_TYPE_CUP, NULL); } /* * Property accessors */ void caphe_cup_set_application_name(CapheCup *self, const gchar *application_name) { CapheCupPrivate *priv = self->priv; if (!g_strcmp0(priv->application_name, application_name)) return; g_free(priv->application_name); priv->application_name = g_strdup(application_name); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_APPLICATION_NAME]); } CapheInhibitor * caphe_cup_get_inhibitor(CapheCup *self) { return self->priv->current_inhibitor; } static void caphe_cup_set_inhibitor(CapheCup *self, CapheInhibitor *inhibitor) { CapheCupPrivate *priv = self->priv; if (g_set_object(&priv->current_inhibitor, inhibitor)) g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_INHIBITOR]); } static void caphe_cup_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { CapheCup *self = CAPHE_CUP(object); switch (property_id) { case PROP_INHIBITOR: g_value_set_object(value, caphe_cup_get_inhibitor(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void caphe_cup_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { CapheCup *self = CAPHE_CUP(object); switch (property_id) { case PROP_APPLICATION_NAME: caphe_cup_set_application_name(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * GObject methods */ static void caphe_cup_finalize(GObject *object) { CapheCup *self = CAPHE_CUP(object); CapheCupPrivate *priv = self->priv; TRACE("%p", self); if (priv->when_idle_process_id) g_source_remove(priv->when_idle_process_id); caphe_request_free(priv->pending_request); g_free(priv->application_name); /* Unref current status */ g_clear_object(&priv->current_inhibitor); g_free(priv->current_reason); /* Destroy inhibitor list */ g_clear_object(&priv->inhibitor_list); /* Chain up */ if (G_OBJECT_CLASS(caphe_cup_parent_class)->finalize) G_OBJECT_CLASS(caphe_cup_parent_class)->finalize(object); } static void caphe_cup_constructed(GObject *object) { CapheCup *self = CAPHE_CUP(object); CapheCupPrivate *priv = self->priv; TRACE("%p", self); /* Create inhibitor list */ priv->inhibitor_list = caphe_inhibitor_list_new(); g_signal_connect_object(priv->inhibitor_list, "ready", G_CALLBACK(on_inhibitor_list_ready), self, 0); /* Chain up */ if (G_OBJECT_CLASS(caphe_cup_parent_class)->constructed) G_OBJECT_CLASS(caphe_cup_parent_class)->constructed(object); } static void caphe_cup_init(CapheCup *self) { CapheCupPrivate *priv = caphe_cup_get_instance_private(self); const gchar *app_name; TRACE("%p", self); /* Default values */ app_name = g_get_application_name(); if (app_name == NULL) app_name = "Unknown"; priv->application_name = g_strdup(app_name); /* Set private pointer */ self->priv = priv; } static void caphe_cup_class_init(CapheCupClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = caphe_cup_finalize; object_class->constructed = caphe_cup_constructed; /* Install properties */ object_class->get_property = caphe_cup_get_property; object_class->set_property = caphe_cup_set_property; properties[PROP_APPLICATION_NAME] = g_param_spec_string("application-name", "Application name", NULL, NULL, G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE); properties[PROP_INHIBITOR] = g_param_spec_object("inhibitor", "Current inhibitor", NULL, CAPHE_TYPE_INHIBITOR, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); g_object_class_install_properties(object_class, LAST_PROP, properties); /* Signals */ signals[SIGNAL_INHIBIT_FAILURE] = g_signal_new("inhibit-failure", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } /* * Convenience functions */ CapheCup *caphe_cup_default_instance; CapheCup * caphe_cup_get_default(void) { if (caphe_cup_default_instance == NULL) caphe_cup_default_instance = caphe_cup_new(); return caphe_cup_default_instance; } goodvibes-v0.4.2/src/libcaphe/caphe-cup.h000066400000000000000000000030201337147241200202370ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_CUP_H__ #define __LIBCAPHE_CAPHE_CUP_H__ #include #include "caphe-inhibitor.h" /* GObject declarations */ #define CAPHE_TYPE_CUP caphe_cup_get_type() G_DECLARE_FINAL_TYPE(CapheCup, caphe_cup, CAPHE, CUP, GObject) /* Global instance (private, don't access) */ extern CapheCup *caphe_cup_default_instance; /* Methods */ CapheCup *caphe_cup_get_default(void); void caphe_cup_inhibit (CapheCup *self, const gchar *reason); void caphe_cup_uninhibit (CapheCup *self); gboolean caphe_cup_is_inhibited(CapheCup *self); /* Property accessors */ void caphe_cup_set_application_name(CapheCup *self, const gchar *name); CapheInhibitor *caphe_cup_get_inhibitor (CapheCup *self); #endif /* __LIBCAPHE_CAPHE_CUP_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-dbus-inhibitor.c000066400000000000000000000253341337147241200224010ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-login-dbus-invokator.h" #include "caphe-portal-dbus-invokator.h" #include "caphe-power-dbus-invokator.h" #include "caphe-session-dbus-invokator.h" #include "caphe-dbus-invokator.h" #include "caphe-dbus-proxy.h" #include "caphe-dbus-inhibitor.h" /* * Properties */ enum { /* Reserved */ PROP_0, /* Number of installable properties */ LAST_INSTALLABLE_PROP, /* Overriden properties */ PROP_NAME, PROP_AVAILABLE, /* Total number of properties */ LAST_PROP }; static GParamSpec *properties[LAST_PROP]; /* * GObject definitions */ struct _CapheDbusInhibitorPrivate { /* Properties */ gchar *name; /* Internal */ CapheDbusProxy *proxy; GType invokator_type; CapheDbusInvokator *invokator; }; typedef struct _CapheDbusInhibitorPrivate CapheDbusInhibitorPrivate; struct _CapheDbusInhibitor { /* Parent instance structure */ GObject parent_instance; /* Private data */ CapheDbusInhibitorPrivate *priv; }; static void caphe_inhibitor_interface_init(CapheInhibitorInterface *iface); G_DEFINE_TYPE_WITH_CODE(CapheDbusInhibitor, caphe_dbus_inhibitor, G_TYPE_OBJECT, G_ADD_PRIVATE(CapheDbusInhibitor) G_IMPLEMENT_INTERFACE(CAPHE_TYPE_INHIBITOR, caphe_inhibitor_interface_init)) /* * Signal handlers */ static void on_caphe_dbus_proxy_notify_proxy(CapheDbusProxy *caphe_proxy, GParamSpec *pspec G_GNUC_UNUSED, CapheDbusInhibitor *self) { CapheDbusInhibitorPrivate *priv = self->priv; GDBusProxy *dbus_proxy = caphe_dbus_proxy_get_proxy(caphe_proxy); /* If the proxy disappeared, we must dispose of the invokator. * Otherwise, we must create an invokator. */ if (dbus_proxy == NULL) { g_clear_object(&priv->invokator); } else { g_assert(priv->invokator == NULL); priv->invokator = caphe_dbus_invokator_new(priv->invokator_type, dbus_proxy); } /* Notify that availability changed */ g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_AVAILABLE]); } /* * Inhibitor methods */ static gboolean caphe_dbus_inhibitor_is_inhibited(CapheInhibitor *inhibitor) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; if (priv->invokator == NULL) return FALSE; return caphe_dbus_invokator_is_inhibited(priv->invokator); } static void caphe_dbus_inhibitor_uninhibit(CapheInhibitor *inhibitor) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; g_return_if_fail(priv->invokator != NULL); caphe_dbus_invokator_uninhibit(priv->invokator); } static gboolean caphe_dbus_inhibitor_inhibit(CapheInhibitor *inhibitor, const gchar *application, const gchar *reason, GError **error) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; g_return_val_if_fail(application != NULL, FALSE); g_return_val_if_fail(reason != NULL, FALSE); g_return_val_if_fail(priv->invokator != NULL, FALSE); return caphe_dbus_invokator_inhibit(priv->invokator, application, reason, error); } /* * Inhibitor property accessors */ static const gchar * caphe_dbus_inhibitor_get_name(CapheInhibitor *inhibitor) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; return priv->name; } static void caphe_dbus_inhibitor_set_name(CapheInhibitor *inhibitor, const gchar *name) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; /* Construct-only property */ g_assert_null(priv->name); g_assert_nonnull(name); priv->name = g_strdup(name); } static gboolean caphe_dbus_inhibitor_get_available(CapheInhibitor *inhibitor) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(inhibitor); CapheDbusInhibitorPrivate *priv = self->priv; return priv->invokator ? TRUE : FALSE; } static void caphe_dbus_inhibitor_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { CapheInhibitor *inhibitor = CAPHE_INHIBITOR(object); switch (property_id) { /* Inhibitor */ case PROP_NAME: g_value_set_string(value, caphe_dbus_inhibitor_get_name(inhibitor)); break; case PROP_AVAILABLE: g_value_set_boolean(value, caphe_dbus_inhibitor_get_available(inhibitor)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void caphe_dbus_inhibitor_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { CapheInhibitor *inhibitor = CAPHE_INHIBITOR(object); switch (property_id) { case PROP_NAME: caphe_dbus_inhibitor_set_name(inhibitor, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ CapheInhibitor * caphe_dbus_inhibitor_new(const gchar *name) { return g_object_new(CAPHE_TYPE_DBUS_INHIBITOR, "name", name, NULL); } /* * GObject methods */ static void caphe_dbus_inhibitor_finalize(GObject *object) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(object); CapheDbusInhibitorPrivate *priv = self->priv; TRACE("%p", object); if (priv->invokator) g_object_unref(priv->invokator); if (priv->proxy) g_object_unref(priv->proxy); g_free(priv->name); /* Chain up */ if (G_OBJECT_CLASS(caphe_dbus_inhibitor_parent_class)->finalize) G_OBJECT_CLASS(caphe_dbus_inhibitor_parent_class)->finalize(object); } static void caphe_dbus_inhibitor_constructed(GObject *object) { CapheDbusInhibitor *self = CAPHE_DBUS_INHIBITOR(object); CapheDbusInhibitorPrivate *priv = self->priv; TRACE("%p", object); /* Create the dbus proxy */ if (!g_strcmp0(priv->name, "gnome-session-manager")) { priv->invokator_type = CAPHE_TYPE_SESSION_DBUS_INVOKATOR; priv->proxy = caphe_dbus_proxy_new(G_BUS_TYPE_SESSION, "org.gnome.SessionManager", "/org/gnome/SessionManager", "org.gnome.SessionManager"); } else if (!g_strcmp0(priv->name, "xfce-session-manager")) { priv->invokator_type = CAPHE_TYPE_SESSION_DBUS_INVOKATOR; priv->proxy = caphe_dbus_proxy_new(G_BUS_TYPE_SESSION, "org.xfce.SessionManager", "/org/xfce/SessionManager", "org.xfce.SessionManager"); } else if (!g_strcmp0(priv->name, "xdg-portal")) { priv->invokator_type = CAPHE_TYPE_PORTAL_DBUS_INVOKATOR; priv->proxy = caphe_dbus_proxy_new(G_BUS_TYPE_SESSION, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Inhibit"); } else if (!g_strcmp0(priv->name, "xdg-power-management")) { /* Also provided by the following names: * - org.xfce.PowerManager * - org.kde.Solid.PowerManagement */ priv->invokator_type = CAPHE_TYPE_POWER_DBUS_INVOKATOR; priv->proxy = caphe_dbus_proxy_new(G_BUS_TYPE_SESSION, "org.freedesktop.PowerManagement", "/org/freedesktop/PowerManagement/Inhibit", "org.freedesktop.PowerManagement.Inhibit"); } else if (!g_strcmp0(priv->name, "xdg-login1")) { priv->invokator_type = CAPHE_TYPE_LOGIN_DBUS_INVOKATOR; priv->proxy = caphe_dbus_proxy_new(G_BUS_TYPE_SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager"); } else { g_error("Invalid inhibitor name '%s'", priv->name); /* Program execution stops here */ } /* Connect to the 'proxy' signal to be notified as soon as the proxy * is created. The signal is ensured to be sent once at init time. */ g_signal_connect_object(priv->proxy, "notify::proxy", G_CALLBACK(on_caphe_dbus_proxy_notify_proxy), self, 0); /* Chain up */ if (G_OBJECT_CLASS(caphe_dbus_inhibitor_parent_class)->constructed) G_OBJECT_CLASS(caphe_dbus_inhibitor_parent_class)->constructed(object); } static void caphe_dbus_inhibitor_init(CapheDbusInhibitor *self) { CapheDbusInhibitorPrivate *priv = caphe_dbus_inhibitor_get_instance_private(self); TRACE("%p", self); /* Set private pointer */ self->priv = priv; } static void caphe_dbus_inhibitor_class_init(CapheDbusInhibitorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = caphe_dbus_inhibitor_finalize; object_class->constructed = caphe_dbus_inhibitor_constructed; /* Install properties */ object_class->get_property = caphe_dbus_inhibitor_get_property; object_class->set_property = caphe_dbus_inhibitor_set_property; /* Override Inhibitor properties */ g_object_class_override_property(object_class, PROP_NAME, "name"); properties[PROP_NAME] = g_object_class_find_property(object_class, "name"); g_object_class_override_property(object_class, PROP_AVAILABLE, "available"); properties[PROP_AVAILABLE] = g_object_class_find_property(object_class, "available"); } static void caphe_inhibitor_interface_init(CapheInhibitorInterface *iface) { /* Implement methods */ iface->inhibit = caphe_dbus_inhibitor_inhibit; iface->uninhibit = caphe_dbus_inhibitor_uninhibit; iface->is_inhibited = caphe_dbus_inhibitor_is_inhibited; iface->get_name = caphe_dbus_inhibitor_get_name; iface->get_available = caphe_dbus_inhibitor_get_available; } goodvibes-v0.4.2/src/libcaphe/caphe-dbus-inhibitor.h000066400000000000000000000023321337147241200223770ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_DBUS_INHIBITOR_H__ #define __LIBCAPHE_CAPHE_DBUS_INHIBITOR_H__ #include #include "caphe-inhibitor.h" /* GObject declarations */ #define CAPHE_TYPE_DBUS_INHIBITOR caphe_dbus_inhibitor_get_type() G_DECLARE_FINAL_TYPE(CapheDbusInhibitor, caphe_dbus_inhibitor, CAPHE, DBUS_INHIBITOR, GObject) /* Property accessors */ CapheInhibitor *caphe_dbus_inhibitor_new(const gchar *service_id); #endif /* __LIBCAPHE_CAPHE_DBUS_INHIBITOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-dbus-invokator.c000066400000000000000000000122371337147241200224240ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-dbus-invokator.h" /* * Properties */ enum { /* Reserved */ PROP_0, /* Construct-only properties */ PROP_PROXY, /* Total number of properties */ LAST_PROP }; static GParamSpec *properties[LAST_PROP]; /* * GObject definitions */ struct _CapheDbusInvokatorPrivate { /* Properties */ GDBusProxy *proxy; }; typedef struct _CapheDbusInvokatorPrivate CapheDbusInvokatorPrivate; G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(CapheDbusInvokator, caphe_dbus_invokator, G_TYPE_OBJECT) /* * Property accessors */ GDBusProxy * caphe_dbus_invokator_get_proxy(CapheDbusInvokator *self) { CapheDbusInvokatorPrivate *priv = caphe_dbus_invokator_get_instance_private(self); return priv->proxy; } static void caphe_dbus_invokator_set_proxy(CapheDbusInvokator *self, GDBusProxy *proxy) { CapheDbusInvokatorPrivate *priv = caphe_dbus_invokator_get_instance_private(self); /* Construct-only property */ g_assert_null(priv->proxy); g_assert_nonnull(proxy); priv->proxy = g_object_ref(proxy); } static void caphe_dbus_invokator_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { CapheDbusInvokator *self = CAPHE_DBUS_INVOKATOR(object); switch (property_id) { case PROP_PROXY: g_value_set_object(value, caphe_dbus_invokator_get_proxy(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void caphe_dbus_invokator_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { CapheDbusInvokator *self = CAPHE_DBUS_INVOKATOR(object); switch (property_id) { case PROP_PROXY: caphe_dbus_invokator_set_proxy(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ gboolean caphe_dbus_invokator_is_inhibited(CapheDbusInvokator *self) { g_return_val_if_fail(CAPHE_IS_DBUS_INVOKATOR(self), FALSE); return CAPHE_DBUS_INVOKATOR_GET_CLASS(self)->is_inhibited(self); } void caphe_dbus_invokator_uninhibit(CapheDbusInvokator *self) { g_return_if_fail(CAPHE_IS_DBUS_INVOKATOR(self)); return CAPHE_DBUS_INVOKATOR_GET_CLASS(self)->uninhibit(self); } gboolean caphe_dbus_invokator_inhibit(CapheDbusInvokator *self, const gchar *application, const gchar *reason, GError **error) { g_return_val_if_fail(CAPHE_IS_DBUS_INVOKATOR(self), FALSE); return CAPHE_DBUS_INVOKATOR_GET_CLASS(self)->inhibit(self, application, reason, error); } CapheDbusInvokator * caphe_dbus_invokator_new(GType invokator_type, GDBusProxy *proxy) { g_return_val_if_fail(g_type_is_a(invokator_type, CAPHE_TYPE_DBUS_INVOKATOR), NULL); return g_object_new(invokator_type, "proxy", proxy, NULL); } /* * GObject methods */ static void caphe_dbus_invokator_finalize(GObject *object) { CapheDbusInvokator *self = CAPHE_DBUS_INVOKATOR(object); CapheDbusInvokatorPrivate *priv = caphe_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Free resources */ g_object_unref(priv->proxy); /* Chain up */ if (G_OBJECT_CLASS(caphe_dbus_invokator_parent_class)->finalize) G_OBJECT_CLASS(caphe_dbus_invokator_parent_class)->finalize(object); } static void caphe_dbus_invokator_init(CapheDbusInvokator *self) { CapheDbusInvokatorPrivate *priv = caphe_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Construct-only properties, initialized later on */ priv->proxy = NULL; } static void caphe_dbus_invokator_class_init(CapheDbusInvokatorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = caphe_dbus_invokator_finalize; /* Install properties */ object_class->get_property = caphe_dbus_invokator_get_property; object_class->set_property = caphe_dbus_invokator_set_property; properties[PROP_PROXY] = g_param_spec_object("proxy", "Proxy", NULL, G_TYPE_DBUS_PROXY, G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); g_object_class_install_properties(object_class, LAST_PROP, properties); } goodvibes-v0.4.2/src/libcaphe/caphe-dbus-invokator.h000066400000000000000000000041671337147241200224340ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_DBUS_INVOKATOR_H__ #define __LIBCAPHE_CAPHE_DBUS_INVOKATOR_H__ #include #include /* GObject declarations */ #define CAPHE_TYPE_DBUS_INVOKATOR caphe_dbus_invokator_get_type() G_DECLARE_DERIVABLE_TYPE(CapheDbusInvokator, caphe_dbus_invokator, CAPHE, DBUS_INVOKATOR, GObject) struct _CapheDbusInvokatorClass { /* Parent class */ GObjectClass parent_class; /* Virtual Methods */ gboolean (*inhibit) (CapheDbusInvokator *self, const gchar *application, const gchar *reason, GError **error); void (*uninhibit) (CapheDbusInvokator *self); gboolean (*is_inhibited)(CapheDbusInvokator *self); }; /* Methods */ CapheDbusInvokator *caphe_dbus_invokator_new(GType invokator_type, GDBusProxy *proxy); gboolean caphe_dbus_invokator_inhibit (CapheDbusInvokator *self, const gchar *application, const gchar *reason, GError **error); void caphe_dbus_invokator_uninhibit (CapheDbusInvokator *self); gboolean caphe_dbus_invokator_is_inhibited(CapheDbusInvokator *self); /* Public property accessors */ GDBusProxy *caphe_dbus_invokator_get_proxy (CapheDbusInvokator *self); #endif /* __LIBCAPHE_CAPHE_DBUS_INVOKATOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-dbus-proxy.c000066400000000000000000000240521337147241200215670ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "caphe-trace.h" #include "caphe-dbus-proxy.h" #define debug(self, fmt, ...) \ g_debug("[%s]: " fmt, strrchr(self->priv->well_known_name, '.') + 1, ##__VA_ARGS__) #define warning(self, fmt, ...) \ g_warning("[%s]: " fmt, strrchr(self->priv->well_known_name, '.') + 1, ##__VA_ARGS__) /* * Properties */ enum { /* Reserved */ PROP_0, /* Construct-only properties */ PROP_BUS_TYPE, PROP_WELL_KNOWN_NAME, PROP_OBJECT_PATH, PROP_INTERFACE_NAME, /* Readable properties */ PROP_PROXY, /* Total number of properties */ LAST_PROP }; static GParamSpec *properties[LAST_PROP]; /* * GObject definitions */ struct _CapheDbusProxyPrivate { /* Properties */ GBusType bus_type; gchar *well_known_name; gchar *object_path; gchar *interface_name; /* Internal */ guint watch_id; GDBusProxy *proxy; }; typedef struct _CapheDbusProxyPrivate CapheDbusProxyPrivate; struct _CapheDbusProxy { /* Parent instance structure */ GObject parent_instance; /* Private data */ CapheDbusProxyPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CapheDbusProxy, caphe_dbus_proxy, G_TYPE_OBJECT) /* * Property setters */ static void caphe_dbus_proxy_set_proxy(CapheDbusProxy *self, GDBusProxy *proxy) { CapheDbusProxyPrivate *priv = self->priv; g_set_object(&priv->proxy, proxy); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_PROXY]); } /* * Callbacks */ static void on_proxy_created(GDBusConnection *connection G_GNUC_UNUSED, GAsyncResult *result, CapheDbusProxy *self) { GDBusProxy *proxy; GError *error = NULL; debug(self, "Proxy created"); proxy = g_dbus_proxy_new_finish(result, &error); if (error) { warning(self, "%s", error->message); g_error_free(error); } caphe_dbus_proxy_set_proxy(self, proxy); } static void on_name_vanished(GDBusConnection *connection G_GNUC_UNUSED, const gchar *name, CapheDbusProxy *self) { debug(self, "Name '%s' vanished", name); caphe_dbus_proxy_set_proxy(self, NULL); } static void on_name_appeared(GDBusConnection *connection, const gchar *name, const gchar *name_owner, CapheDbusProxy *self) { CapheDbusProxyPrivate *priv = self->priv; debug(self, "Name '%s' appeared (owner %s)", name, name_owner); g_dbus_proxy_new(connection, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, priv->well_known_name, priv->object_path, priv->interface_name, NULL, (GAsyncReadyCallback) on_proxy_created, self); } /* * Property accessors */ GDBusProxy * caphe_dbus_proxy_get_proxy(CapheDbusProxy *self) { CapheDbusProxyPrivate *priv = self->priv; return priv->proxy; } static void caphe_dbus_proxy_set_bus_type(CapheDbusProxy *self, GBusType bus_type) { CapheDbusProxyPrivate *priv = self->priv; /* Construct-only property */ g_assert(priv->bus_type == G_BUS_TYPE_NONE); g_assert(bus_type != G_BUS_TYPE_NONE); priv->bus_type = bus_type; } static void caphe_dbus_proxy_set_well_known_name(CapheDbusProxy *self, const gchar *name) { CapheDbusProxyPrivate *priv = self->priv; /* Construct-only property */ g_assert_null(priv->well_known_name); g_assert_nonnull(name); priv->well_known_name = g_strdup(name); } static void caphe_dbus_proxy_set_object_path(CapheDbusProxy *self, const gchar *object_path) { CapheDbusProxyPrivate *priv = self->priv; /* Construct-only property */ g_assert_null(priv->object_path); g_assert_nonnull(object_path); priv->object_path = g_strdup(object_path); } static void caphe_dbus_proxy_set_interface_name(CapheDbusProxy *self, const gchar *interface_name) { CapheDbusProxyPrivate *priv = self->priv; /* Construct-only property */ g_assert_null(priv->interface_name); g_assert_nonnull(interface_name); priv->interface_name = g_strdup(interface_name); } static void caphe_dbus_proxy_get_property(GObject *object, guint property_id, GValue *value G_GNUC_UNUSED, GParamSpec *pspec) { CapheDbusProxy *self = CAPHE_DBUS_PROXY(object); switch (property_id) { case PROP_PROXY: g_value_set_object(value, caphe_dbus_proxy_get_proxy(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void caphe_dbus_proxy_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { CapheDbusProxy *self = CAPHE_DBUS_PROXY(object); switch (property_id) { case PROP_BUS_TYPE: caphe_dbus_proxy_set_bus_type(self, g_value_get_enum(value)); break; case PROP_WELL_KNOWN_NAME: caphe_dbus_proxy_set_well_known_name(self, g_value_get_string(value)); break; case PROP_OBJECT_PATH: caphe_dbus_proxy_set_object_path(self, g_value_get_string(value)); break; case PROP_INTERFACE_NAME: caphe_dbus_proxy_set_interface_name(self, g_value_get_string(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ CapheDbusProxy * caphe_dbus_proxy_new(GBusType bus_type, const gchar *well_known_name, const gchar *object_path, const gchar *interface_name) { return g_object_new(CAPHE_TYPE_DBUS_PROXY, "bus-type", bus_type, "well-known-name", well_known_name, "object-path", object_path, "interface-name", interface_name, NULL); } /* * GObject methods */ static void caphe_dbus_proxy_finalize(GObject *object) { CapheDbusProxy *self = CAPHE_DBUS_PROXY(object); CapheDbusProxyPrivate *priv = self->priv; TRACE("%p", object); if (priv->watch_id) g_bus_unwatch_name(priv->watch_id); if (priv->proxy) g_object_unref(priv->proxy); g_free(priv->well_known_name); g_free(priv->object_path); g_free(priv->interface_name); /* Chain up */ if (G_OBJECT_CLASS(caphe_dbus_proxy_parent_class)->finalize) G_OBJECT_CLASS(caphe_dbus_proxy_parent_class)->finalize(object); } static void caphe_dbus_proxy_constructed(GObject *object) { CapheDbusProxy *self = CAPHE_DBUS_PROXY(object); CapheDbusProxyPrivate *priv = self->priv; TRACE("%p", object); debug(self, "Starting to watch '%s'", priv->well_known_name); priv->watch_id = g_bus_watch_name(priv->bus_type, priv->well_known_name, G_BUS_NAME_WATCHER_FLAGS_NONE, (GBusNameAppearedCallback) on_name_appeared, (GBusNameVanishedCallback) on_name_vanished, self, NULL); /* Chain up */ if (G_OBJECT_CLASS(caphe_dbus_proxy_parent_class)->constructed) G_OBJECT_CLASS(caphe_dbus_proxy_parent_class)->constructed(object); } static void caphe_dbus_proxy_init(CapheDbusProxy *self) { CapheDbusProxyPrivate *priv = caphe_dbus_proxy_get_instance_private(self); TRACE("%p", self); /* Construct-only properties, initialized later on */ priv->bus_type = G_BUS_TYPE_NONE; priv->well_known_name = NULL; priv->object_path = NULL; priv->interface_name = NULL; /* Save private pointer */ self->priv = priv; } static void caphe_dbus_proxy_class_init(CapheDbusProxyClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = caphe_dbus_proxy_finalize; object_class->constructed = caphe_dbus_proxy_constructed; /* Install properties */ object_class->get_property = caphe_dbus_proxy_get_property; object_class->set_property = caphe_dbus_proxy_set_property; properties[PROP_BUS_TYPE] = g_param_spec_enum("bus-type", "Bus type", NULL, G_TYPE_BUS_TYPE, G_BUS_TYPE_NONE, G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_WELL_KNOWN_NAME] = g_param_spec_string("well-known-name", "Well-known bus name", NULL, NULL, G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_OBJECT_PATH] = g_param_spec_string("object-path", "Object path", NULL, NULL, G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_INTERFACE_NAME] = g_param_spec_string("interface-name", "Interface name", NULL, NULL, G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_PROXY] = g_param_spec_object("proxy", "Proxy", NULL, G_TYPE_DBUS_PROXY, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); g_object_class_install_properties(object_class, LAST_PROP, properties); } goodvibes-v0.4.2/src/libcaphe/caphe-dbus-proxy.h000066400000000000000000000025661337147241200216020ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_DBUS_PROXY_H__ #define __LIBCAPHE_CAPHE_DBUS_PROXY_H__ #include #include /* GObject declarations */ #define CAPHE_TYPE_DBUS_PROXY caphe_dbus_proxy_get_type() G_DECLARE_FINAL_TYPE(CapheDbusProxy, caphe_dbus_proxy, CAPHE, DBUS_PROXY, GObject) /* Public methods */ CapheDbusProxy *caphe_dbus_proxy_new(GBusType bus_type, const gchar *well_known_name, const gchar *object_path, const gchar *interface_name); /* Property accessors */ GDBusProxy *caphe_dbus_proxy_get_proxy(CapheDbusProxy *self); #endif /* __LIBCAPHE_CAPHE_DBUS_PROXY_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-inhibitor-list.c000066400000000000000000000133001337147241200224050ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-inhibitor.h" #include "caphe-dbus-inhibitor.h" #include "caphe-inhibitor-list.h" /* Inhibitors we know of. This list is ordered by priority. */ enum { CAPHE_GNOME_SESSION_MANAGER_IDX, CAPHE_XFCE_SESSION_MANAGER_IDX, CAPHE_XDG_PORTAL_IDX, CAPHE_XDG_POWER_MANAGEMENT_IDX, CAPHE_XDG_LOGIN1_IDX, CAPHE_N_INHIBITORS, }; static const gchar *caphe_inhibitor_ids[] = { "gnome-session-manager", "xfce-session-manager", "xdg-portal", "xdg-power-management", "xdg-login1", NULL }; /* * Signals */ enum { SIGNAL_READY, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * GObject definitions */ struct _CapheInhibitorListPrivate { /* List of inhibitors */ CapheInhibitor *inhibitors[CAPHE_N_INHIBITORS]; /* For init-time */ gboolean inhibitors_ready[CAPHE_N_INHIBITORS]; gboolean ready; }; typedef struct _CapheInhibitorListPrivate CapheInhibitorListPrivate; struct _CapheInhibitorList { /* Parent instance structure */ GObject parent_instance; /* Private data */ CapheInhibitorListPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CapheInhibitorList, caphe_inhibitor_list, G_TYPE_OBJECT) /* * Helpers */ static gint get_inhibitor_index(CapheInhibitor *inhibitors[], CapheInhibitor *inhibitor) { gint i; for (i = 0; i < CAPHE_N_INHIBITORS; i++) { if (inhibitors[i] == inhibitor) return i; } return -1; } const gchar * get_inhibitor_id(CapheInhibitor *inhibitors[], CapheInhibitor *inhibitor) { gint i; for (i = 0; i < CAPHE_N_INHIBITORS; i++) { if (inhibitors[i] == inhibitor) return caphe_inhibitor_ids[i]; } return "unknown"; } /* * Signal handlers */ static void on_inhibitor_notify_available(CapheInhibitor *inhibitor, GParamSpec *pspec G_GNUC_UNUSED, CapheInhibitorList *self) { CapheInhibitorListPrivate *priv = self->priv; gboolean available = caphe_inhibitor_get_available(inhibitor); /* Log */ g_debug("Inhibitor '%s' availability changed: %s", get_inhibitor_id(priv->inhibitors, inhibitor), available ? "true" : "false"); /* We watch this signals only to know when all the inhibitors are ready */ if (priv->ready == TRUE) return; gint index; /* Update ready list */ index = get_inhibitor_index(priv->inhibitors, inhibitor); priv->inhibitors_ready[index] = TRUE; /* Check if every inhibitors are ready */ for (index = 0; index < CAPHE_N_INHIBITORS; index++) { if (priv->inhibitors_ready[index] == FALSE) return; } /* We're ready */ g_debug("All inhibitors ready"); priv->ready = TRUE; g_signal_emit(self, signals[SIGNAL_READY], 0); } /* * Public methods */ CapheInhibitor ** caphe_inhibitor_list_get_inhibitors(CapheInhibitorList *self) { CapheInhibitorListPrivate *priv = self->priv; return priv->inhibitors; } CapheInhibitorList * caphe_inhibitor_list_new(void) { return g_object_new(CAPHE_TYPE_INHIBITOR_LIST, NULL); } /* * GObject methods */ static void caphe_inhibitor_list_finalize(GObject *object) { CapheInhibitorList *self = CAPHE_INHIBITOR_LIST(object); CapheInhibitorListPrivate *priv = self->priv; guint i; TRACE("%p", self); /* Disconnect and unref inhibitors */ for (i = 0; i < CAPHE_N_INHIBITORS; i++) { g_signal_handlers_disconnect_by_data(priv->inhibitors[i], self); g_object_unref(priv->inhibitors[i]); } /* Chain up */ if (G_OBJECT_CLASS(caphe_inhibitor_list_parent_class)->finalize) G_OBJECT_CLASS(caphe_inhibitor_list_parent_class)->finalize(object); } static void caphe_inhibitor_list_constructed(GObject *object) { CapheInhibitorList *self = CAPHE_INHIBITOR_LIST(object); CapheInhibitorListPrivate *priv = self->priv; guint i; TRACE("%p", self); /* Create inhibitors */ for (i = 0; i < CAPHE_N_INHIBITORS; i++) { priv->inhibitors[i] = caphe_dbus_inhibitor_new(caphe_inhibitor_ids[i]); g_signal_connect_object(priv->inhibitors[i], "notify::available", G_CALLBACK(on_inhibitor_notify_available), self, 0); } /* Chain up */ if (G_OBJECT_CLASS(caphe_inhibitor_list_parent_class)->constructed) G_OBJECT_CLASS(caphe_inhibitor_list_parent_class)->constructed(object); } static void caphe_inhibitor_list_init(CapheInhibitorList *self) { CapheInhibitorListPrivate *priv = caphe_inhibitor_list_get_instance_private(self); TRACE("%p", self); /* Init some stuff */ priv->ready = FALSE; /* Set private pointer */ self->priv = priv; } static void caphe_inhibitor_list_class_init(CapheInhibitorListClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = caphe_inhibitor_list_finalize; object_class->constructed = caphe_inhibitor_list_constructed; /* Signals */ signals[SIGNAL_READY] = g_signal_new("ready", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } goodvibes-v0.4.2/src/libcaphe/caphe-inhibitor-list.h000066400000000000000000000024401337147241200224150ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_INHIBITOR_LIST_H__ #define __LIBCAPHE_CAPHE_INHIBITOR_LIST_H__ #include #include "caphe-inhibitor.h" /* GObject declarations */ #define CAPHE_TYPE_INHIBITOR_LIST caphe_inhibitor_list_get_type() G_DECLARE_FINAL_TYPE(CapheInhibitorList, caphe_inhibitor_list, CAPHE, INHIBITOR_LIST, GObject) /* Methods */ CapheInhibitorList *caphe_inhibitor_list_new (void); CapheInhibitor **caphe_inhibitor_list_get_inhibitors(CapheInhibitorList *self); #endif /* __LIBCAPHE_CAPHE_INHIBITOR_LIST_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-inhibitor.c000066400000000000000000000047311337147241200214440ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "caphe-trace.h" #include "caphe-inhibitor.h" /* * GObject definitions */ G_DEFINE_INTERFACE(CapheInhibitor, caphe_inhibitor, G_TYPE_OBJECT) /* * Methods */ gboolean caphe_inhibitor_is_inhibited(CapheInhibitor *self) { g_return_val_if_fail(CAPHE_IS_INHIBITOR(self), FALSE); return CAPHE_INHIBITOR_GET_IFACE(self)->is_inhibited(self); } void caphe_inhibitor_uninhibit(CapheInhibitor *self) { g_return_if_fail(CAPHE_IS_INHIBITOR(self)); return CAPHE_INHIBITOR_GET_IFACE(self)->uninhibit(self); } gboolean caphe_inhibitor_inhibit(CapheInhibitor *self, const gchar *application, const gchar *reason, GError **error) { g_return_val_if_fail(CAPHE_IS_INHIBITOR(self), FALSE); return CAPHE_INHIBITOR_GET_IFACE(self)->inhibit(self, application, reason, error); } /* * Property accessors */ const gchar * caphe_inhibitor_get_name(CapheInhibitor *self) { g_return_val_if_fail(CAPHE_IS_INHIBITOR(self), FALSE); return CAPHE_INHIBITOR_GET_IFACE(self)->get_name(self); } gboolean caphe_inhibitor_get_available(CapheInhibitor *self) { g_return_val_if_fail(CAPHE_IS_INHIBITOR(self), FALSE); return CAPHE_INHIBITOR_GET_IFACE(self)->get_available(self); } /* * GObject methods */ static void caphe_inhibitor_default_init(CapheInhibitorInterface *iface) { TRACE("%p", iface); g_object_interface_install_property (iface, g_param_spec_string("name", "Name", NULL, NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); g_object_interface_install_property (iface, g_param_spec_boolean("available", "Available", NULL, FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE)); } goodvibes-v0.4.2/src/libcaphe/caphe-inhibitor.h000066400000000000000000000041541337147241200214500ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_INHIBITOR_H__ #define __LIBCAPHE_CAPHE_INHIBITOR_H__ #include /* GObject declarations */ #define CAPHE_TYPE_INHIBITOR caphe_inhibitor_get_type() G_DECLARE_INTERFACE(CapheInhibitor, caphe_inhibitor, CAPHE, INHIBITOR, GObject) struct _CapheInhibitorInterface { /* Parent class */ GTypeInterface parent_iface; /* Virtual Methods */ gboolean (*inhibit) (CapheInhibitor *self, const gchar *application, const gchar *reason, GError **error); void (*uninhibit) (CapheInhibitor *self); gboolean (*is_inhibited)(CapheInhibitor *self); /* Virtual property accessors */ const gchar *(*get_name) (CapheInhibitor *self); gboolean (*get_available)(CapheInhibitor *self); }; /* Methods */ gboolean caphe_inhibitor_inhibit (CapheInhibitor *self, const gchar *application, const gchar *reason, GError **error); void caphe_inhibitor_uninhibit (CapheInhibitor *self); gboolean caphe_inhibitor_is_inhibited(CapheInhibitor *self); /* Property accessors */ const gchar *caphe_inhibitor_get_name (CapheInhibitor *self); gboolean caphe_inhibitor_get_available(CapheInhibitor *self); #endif /* __LIBCAPHE_CAPHE_INHIBITOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-login-dbus-invokator.c000066400000000000000000000122061337147241200235260ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "caphe-trace.h" #include "caphe-dbus-invokator.h" #include "caphe-login-dbus-invokator.h" /* * Debug macros */ #define debug(proxy, fmt, ...) \ g_debug("[%s]: " fmt, \ proxy ? g_dbus_proxy_get_name(proxy) : "SessionManager", \ ##__VA_ARGS__) /* * GObject definitions */ struct _CapheLoginDbusInvokatorPrivate { gint32 fd; }; typedef struct _CapheLoginDbusInvokatorPrivate CapheLoginDbusInvokatorPrivate; struct _CapheLoginDbusInvokator { /* Parent instance structure */ CapheDbusInvokator parent_instance; /* Private data */ CapheLoginDbusInvokatorPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CapheLoginDbusInvokator, caphe_login_dbus_invokator, CAPHE_TYPE_DBUS_INVOKATOR) /* * DbusInvokator methods */ static gboolean caphe_login_dbus_invokator_is_inhibited(CapheDbusInvokator *dbus_invokator) { CapheLoginDbusInvokator *self = CAPHE_LOGIN_DBUS_INVOKATOR(dbus_invokator); CapheLoginDbusInvokatorPrivate *priv = self->priv; return priv->fd != -1; } static void caphe_login_dbus_invokator_uninhibit(CapheDbusInvokator *dbus_invokator) { CapheLoginDbusInvokator *self = CAPHE_LOGIN_DBUS_INVOKATOR(dbus_invokator); CapheLoginDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); if (priv->fd < 0) { debug(proxy, "Already uninhibited (no fd)"); return; } close(priv->fd); priv->fd = -1; debug(proxy, "Uninhibited"); } static gboolean caphe_login_dbus_invokator_inhibit(CapheDbusInvokator *dbus_invokator, const gchar *application, const gchar *reason, GError **error) { CapheLoginDbusInvokator *self = CAPHE_LOGIN_DBUS_INVOKATOR(dbus_invokator); CapheLoginDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); GUnixFDList *fd_list = NULL; gint32 fd_index; GVariant *res; if (priv->fd >= 0) { debug(proxy, "Already inhibited (fd: %d)", priv->fd); return TRUE; } if (proxy == NULL) { debug(proxy, "No proxy"); return FALSE; } /* * Documentation at: * https://www.freedesktop.org/wiki/Software/systemd/inhibit/ */ res = g_dbus_proxy_call_with_unix_fd_list_sync(proxy, "Inhibit", g_variant_new( "(ssss)", "sleep", application, reason, "block"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, /* in_fd_list */ &fd_list, /* out_fd_list */ NULL, /* cancellable */ error); if (res == NULL) return FALSE; g_variant_get(res, "(h)", &fd_index); g_variant_unref(res); debug(proxy, "Fd index: %d", fd_index); priv->fd = g_unix_fd_list_get(fd_list, fd_index, NULL); g_object_unref(fd_list); debug(proxy, "Inhibited (fd: %d)", priv->fd); return TRUE; } /* * GObject methods */ static void caphe_login_dbus_invokator_finalize(GObject *object) { CapheDbusInvokator *dbus_invokator = CAPHE_DBUS_INVOKATOR(object); TRACE("%p", object); /* Ensure uninhibition */ caphe_login_dbus_invokator_uninhibit(dbus_invokator); if (G_OBJECT_CLASS(caphe_login_dbus_invokator_parent_class)->finalize) G_OBJECT_CLASS(caphe_login_dbus_invokator_parent_class)->finalize(object); } static void caphe_login_dbus_invokator_init(CapheLoginDbusInvokator *self) { CapheLoginDbusInvokatorPrivate *priv = caphe_login_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Initialize inhibit file descriptor */ priv->fd = -1; /* Initialize private pointer */ self->priv = priv; } static void caphe_login_dbus_invokator_class_init(CapheLoginDbusInvokatorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); CapheDbusInvokatorClass *dbus_invokator_class = CAPHE_DBUS_INVOKATOR_CLASS(class); TRACE("%p", class); /* GObject methods */ object_class->finalize = caphe_login_dbus_invokator_finalize; /* Implement methods */ dbus_invokator_class->inhibit = caphe_login_dbus_invokator_inhibit; dbus_invokator_class->uninhibit = caphe_login_dbus_invokator_uninhibit; dbus_invokator_class->is_inhibited = caphe_login_dbus_invokator_is_inhibited; } goodvibes-v0.4.2/src/libcaphe/caphe-login-dbus-invokator.h000066400000000000000000000024321337147241200235330ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_LOGIN_DBUS_INVOKATOR_H__ #define __LIBCAPHE_CAPHE_LOGIN_DBUS_INVOKATOR_H__ #include #include "caphe-dbus-invokator.h" /* GObject declarations */ #define CAPHE_TYPE_LOGIN_DBUS_INVOKATOR caphe_login_dbus_invokator_get_type() G_DECLARE_FINAL_TYPE(CapheLoginDbusInvokator, caphe_login_dbus_invokator, CAPHE, LOGIN_DBUS_INVOKATOR, CapheDbusInvokator) /* Methods */ CapheDbusInvokator *caphe_login_dbus_invokator_new(void); #endif /* __LIBCAPHE_CAPHE_LOGIN_DBUS_INVOKATOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-portal-dbus-invokator.c000066400000000000000000000130571337147241200237240ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-dbus-invokator.h" #include "caphe-portal-dbus-invokator.h" /* * Debug macros */ #define debug(proxy, fmt, ...) \ g_debug("[%s]: " fmt, \ proxy ? g_dbus_proxy_get_name(proxy) : "Portal", \ ##__VA_ARGS__) /* * GObject definitions */ struct _CaphePortalDbusInvokatorPrivate { char *handle; }; typedef struct _CaphePortalDbusInvokatorPrivate CaphePortalDbusInvokatorPrivate; struct _CaphePortalDbusInvokator { /* Parent instance structure */ CapheDbusInvokator parent_instance; /* Private data */ CaphePortalDbusInvokatorPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CaphePortalDbusInvokator, caphe_portal_dbus_invokator, CAPHE_TYPE_DBUS_INVOKATOR) /* * DbusInvokator methods */ static gboolean caphe_portal_dbus_invokator_is_inhibited(CapheDbusInvokator *dbus_invokator) { CaphePortalDbusInvokator *self = CAPHE_PORTAL_DBUS_INVOKATOR(dbus_invokator); CaphePortalDbusInvokatorPrivate *priv = self->priv; return priv->handle != NULL; } static void caphe_portal_dbus_invokator_uninhibit(CapheDbusInvokator *dbus_invokator) { CaphePortalDbusInvokator *self = CAPHE_PORTAL_DBUS_INVOKATOR(dbus_invokator); CaphePortalDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); if (priv->handle == NULL) { debug(proxy, "Not inhibited (no handle)"); return; } if (proxy == NULL) { debug(proxy, "No proxy"); return; } g_dbus_connection_call(g_dbus_proxy_get_connection(proxy), "org.freedesktop.portal.Desktop", priv->handle, "org.freedesktop.portal.Request", "Close", g_variant_new("()"), G_VARIANT_TYPE_UNIT, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); g_free(priv->handle); priv->handle = NULL; debug(proxy, "Uninhibited"); } static gboolean caphe_portal_dbus_invokator_inhibit(CapheDbusInvokator *dbus_invokator, const gchar *application G_GNUC_UNUSED, const gchar *reason, GError **error) { CaphePortalDbusInvokator *self = CAPHE_PORTAL_DBUS_INVOKATOR(dbus_invokator); CaphePortalDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); GVariant *res; if (priv->handle) { debug(proxy, "Already inhibited (handle: %s)", priv->handle); return TRUE; } if (proxy == NULL) { debug(proxy, "No proxy"); return FALSE; } GVariantBuilder options; g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&options, "{sv}", "reason", g_variant_new_string(reason)); res = g_dbus_proxy_call_sync(proxy, "Inhibit", g_variant_new("(su@a{sv})", "", /* window */ 4, g_variant_builder_end(&options)), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (res == NULL) return FALSE; g_variant_get(res, "(o)", &priv->handle); g_variant_unref(res); debug(proxy, "Inhibited (handle: %s)", priv->handle); return TRUE; } /* * GObject methods */ static void caphe_portal_dbus_invokator_finalize(GObject *object) { CapheDbusInvokator *dbus_invokator = CAPHE_DBUS_INVOKATOR(object); TRACE("%p", object); /* Ensure uninhibition */ caphe_portal_dbus_invokator_uninhibit(dbus_invokator); /* Chain up */ if (G_OBJECT_CLASS(caphe_portal_dbus_invokator_parent_class)->finalize) G_OBJECT_CLASS(caphe_portal_dbus_invokator_parent_class)->finalize(object); } static void caphe_portal_dbus_invokator_init(CaphePortalDbusInvokator *self) { CaphePortalDbusInvokatorPrivate *priv = caphe_portal_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Initialize private pointer */ self->priv = priv; } static void caphe_portal_dbus_invokator_class_init(CaphePortalDbusInvokatorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); CapheDbusInvokatorClass *dbus_invokator_class = CAPHE_DBUS_INVOKATOR_CLASS(class); TRACE("%p", class); /* GObject methods */ object_class->finalize = caphe_portal_dbus_invokator_finalize; /* Implement methods */ dbus_invokator_class->inhibit = caphe_portal_dbus_invokator_inhibit; dbus_invokator_class->uninhibit = caphe_portal_dbus_invokator_uninhibit; dbus_invokator_class->is_inhibited = caphe_portal_dbus_invokator_is_inhibited; } goodvibes-v0.4.2/src/libcaphe/caphe-portal-dbus-invokator.h000066400000000000000000000024431337147241200237260ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_PORTAL_DBUS_INVOKATOR_H__ #define __LIBCAPHE_CAPHE_PORTAL_DBUS_INVOKATOR_H__ #include #include "caphe-dbus-invokator.h" /* GObject declarations */ #define CAPHE_TYPE_PORTAL_DBUS_INVOKATOR caphe_portal_dbus_invokator_get_type() G_DECLARE_FINAL_TYPE(CaphePortalDbusInvokator, caphe_portal_dbus_invokator, CAPHE, PORTAL_DBUS_INVOKATOR, CapheDbusInvokator) /* Methods */ CapheDbusInvokator *caphe_portal_dbus_invokator_new(void); #endif /* __LIBCAPHE_CAPHE_PORTAL_DBUS_INVOKATOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-power-dbus-invokator.c000066400000000000000000000120411337147241200235470ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-dbus-invokator.h" #include "caphe-power-dbus-invokator.h" /* * Debug macros */ #define debug(proxy, fmt, ...) \ g_debug("[%s]: " fmt, \ proxy ? g_dbus_proxy_get_name(proxy) : "PowerManagement", \ ##__VA_ARGS__) /* * GObject definitions */ struct _CaphePowerDbusInvokatorPrivate { guint32 cookie; }; typedef struct _CaphePowerDbusInvokatorPrivate CaphePowerDbusInvokatorPrivate; struct _CaphePowerDbusInvokator { /* Parent instance structure */ CapheDbusInvokator parent_instance; /* Private data */ CaphePowerDbusInvokatorPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CaphePowerDbusInvokator, caphe_power_dbus_invokator, CAPHE_TYPE_DBUS_INVOKATOR) /* * DbusInvokator methods */ static gboolean caphe_power_dbus_invokator_is_inhibited(CapheDbusInvokator *dbus_invokator) { CaphePowerDbusInvokator *self = CAPHE_POWER_DBUS_INVOKATOR(dbus_invokator); CaphePowerDbusInvokatorPrivate *priv = self->priv; return priv->cookie != 0; } static void caphe_power_dbus_invokator_uninhibit(CapheDbusInvokator *dbus_invokator) { CaphePowerDbusInvokator *self = CAPHE_POWER_DBUS_INVOKATOR(dbus_invokator); CaphePowerDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); if (priv->cookie == 0) { debug(proxy, "Not inhibited (no cookie)"); return; } if (proxy == NULL) { debug(proxy, "No proxy"); return; } g_dbus_proxy_call(proxy, "UnInhibit", g_variant_new("(u)", priv->cookie), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); priv->cookie = 0; debug(proxy, "Uninhibited"); } static gboolean caphe_power_dbus_invokator_inhibit(CapheDbusInvokator *dbus_invokator, const gchar *application, const gchar *reason, GError **error) { CaphePowerDbusInvokator *self = CAPHE_POWER_DBUS_INVOKATOR(dbus_invokator); CaphePowerDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); GVariant *res; if (priv->cookie != 0) { debug(proxy, "Already inhibited (cookie: %u)", priv->cookie); return TRUE; } if (proxy == NULL) { debug(proxy, "No proxy"); return FALSE; } res = g_dbus_proxy_call_sync(proxy, "Inhibit", g_variant_new("(ss)", application, reason), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (res == NULL) return FALSE; g_variant_get(res, "(u)", &priv->cookie); g_variant_unref(res); debug(proxy, "Inhibited (cookie: %u)", priv->cookie); return TRUE; } /* * GObject methods */ static void caphe_power_dbus_invokator_finalize(GObject *object) { CapheDbusInvokator *dbus_invokator = CAPHE_DBUS_INVOKATOR(object); TRACE("%p", object); /* Ensure uninhibition */ caphe_power_dbus_invokator_uninhibit(dbus_invokator); /* Chain up */ if (G_OBJECT_CLASS(caphe_power_dbus_invokator_parent_class)->finalize) G_OBJECT_CLASS(caphe_power_dbus_invokator_parent_class)->finalize(object); } static void caphe_power_dbus_invokator_init(CaphePowerDbusInvokator *self) { CaphePowerDbusInvokatorPrivate *priv = caphe_power_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Initialize private pointer */ self->priv = priv; } static void caphe_power_dbus_invokator_class_init(CaphePowerDbusInvokatorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); CapheDbusInvokatorClass *dbus_invokator_class = CAPHE_DBUS_INVOKATOR_CLASS(class); TRACE("%p", class); /* GObject methods */ object_class->finalize = caphe_power_dbus_invokator_finalize; /* Implement methods */ dbus_invokator_class->inhibit = caphe_power_dbus_invokator_inhibit; dbus_invokator_class->uninhibit = caphe_power_dbus_invokator_uninhibit; dbus_invokator_class->is_inhibited = caphe_power_dbus_invokator_is_inhibited; } goodvibes-v0.4.2/src/libcaphe/caphe-power-dbus-invokator.h000066400000000000000000000024321337147241200235570ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_POWER_DBUS_INVOKATOR_H__ #define __LIBCAPHE_CAPHE_POWER_DBUS_INVOKATOR_H__ #include #include "caphe-dbus-invokator.h" /* GObject declarations */ #define CAPHE_TYPE_POWER_DBUS_INVOKATOR caphe_power_dbus_invokator_get_type() G_DECLARE_FINAL_TYPE(CaphePowerDbusInvokator, caphe_power_dbus_invokator, CAPHE, POWER_DBUS_INVOKATOR, CapheDbusInvokator) /* Methods */ CapheDbusInvokator *caphe_power_dbus_invokator_new(void); #endif /* __LIBCAPHE_CAPHE_POWER_DBUS_INVOKATOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-session-dbus-invokator.c000066400000000000000000000134241337147241200241040ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "caphe-trace.h" #include "caphe-dbus-invokator.h" #include "caphe-session-dbus-invokator.h" /* * Debug macros */ #define debug(proxy, fmt, ...) \ g_debug("[%s]: " fmt, \ proxy ? g_dbus_proxy_get_name(proxy) : "SessionManager", \ ##__VA_ARGS__) /* * GObject definitions */ struct _CapheSessionDbusInvokatorPrivate { guint32 cookie; }; typedef struct _CapheSessionDbusInvokatorPrivate CapheSessionDbusInvokatorPrivate; struct _CapheSessionDbusInvokator { /* Parent instance structure */ CapheDbusInvokator parent_instance; /* Private data */ CapheSessionDbusInvokatorPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(CapheSessionDbusInvokator, caphe_session_dbus_invokator, CAPHE_TYPE_DBUS_INVOKATOR) /* * DbusInvokator methods */ static gboolean caphe_session_dbus_invokator_is_inhibited(CapheDbusInvokator *dbus_invokator) { CapheSessionDbusInvokator *self = CAPHE_SESSION_DBUS_INVOKATOR(dbus_invokator); CapheSessionDbusInvokatorPrivate *priv = self->priv; return priv->cookie != 0; } static void caphe_session_dbus_invokator_uninhibit(CapheDbusInvokator *dbus_invokator) { CapheSessionDbusInvokator *self = CAPHE_SESSION_DBUS_INVOKATOR(dbus_invokator); CapheSessionDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); if (priv->cookie == 0) { debug(proxy, "Not inhibited (no cookie)"); return; } if (proxy == NULL) { debug(proxy, "No proxy"); return; } g_dbus_proxy_call(proxy, "Uninhibit", g_variant_new("(u)", priv->cookie), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); priv->cookie = 0; debug(proxy, "Uninhibited"); } static gboolean caphe_session_dbus_invokator_inhibit(CapheDbusInvokator *dbus_invokator, const gchar *application, const gchar *reason, GError **error) { CapheSessionDbusInvokator *self = CAPHE_SESSION_DBUS_INVOKATOR(dbus_invokator); CapheSessionDbusInvokatorPrivate *priv = self->priv; GDBusProxy *proxy = caphe_dbus_invokator_get_proxy(dbus_invokator); GVariant *res; if (priv->cookie != 0) { debug(proxy, "Already inhibited (cookie: %u)", priv->cookie); return TRUE; } if (proxy == NULL) { debug(proxy, "No proxy"); return FALSE; } /* * From org.gnome.SessionManager.xml, the 4 arguments to pass are: * - app_id The application identifier * - toplevel_xid The toplevel X window identifier * - reason The reason for the inhibit * - flags Flags that specify what should be inhibited * * The flags parameter must include at least one of the following: * - 1 Inhibit logging out * - 2 Inhibit user switching * - 4 Inhibit suspending the session or computer * - 8 Inhibit the session being marked as idle * - 16 Inhibit auto-mounting removable media for the session */ res = g_dbus_proxy_call_sync(proxy, "Inhibit", g_variant_new("(susu)", application, 0, reason, 4), G_DBUS_CALL_FLAGS_NONE, -1, NULL, error); if (res == NULL) return FALSE; g_variant_get(res, "(u)", &priv->cookie); g_variant_unref(res); debug(proxy, "Inhibited (cookie: %u)", priv->cookie); return TRUE; } /* * GObject methods */ static void caphe_session_dbus_invokator_finalize(GObject *object) { CapheDbusInvokator *dbus_invokator = CAPHE_DBUS_INVOKATOR(object); TRACE("%p", object); /* Ensure uninhibition */ caphe_session_dbus_invokator_uninhibit(dbus_invokator); /* Chain up */ if (G_OBJECT_CLASS(caphe_session_dbus_invokator_parent_class)->finalize) G_OBJECT_CLASS(caphe_session_dbus_invokator_parent_class)->finalize(object); } static void caphe_session_dbus_invokator_init(CapheSessionDbusInvokator *self) { CapheSessionDbusInvokatorPrivate *priv = caphe_session_dbus_invokator_get_instance_private(self); TRACE("%p", self); /* Initialize private pointer */ self->priv = priv; } static void caphe_session_dbus_invokator_class_init(CapheSessionDbusInvokatorClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); CapheDbusInvokatorClass *dbus_invokator_class = CAPHE_DBUS_INVOKATOR_CLASS(class); TRACE("%p", class); /* GObject methods */ object_class->finalize = caphe_session_dbus_invokator_finalize; /* Implement methods */ dbus_invokator_class->inhibit = caphe_session_dbus_invokator_inhibit; dbus_invokator_class->uninhibit = caphe_session_dbus_invokator_uninhibit; dbus_invokator_class->is_inhibited = caphe_session_dbus_invokator_is_inhibited; } goodvibes-v0.4.2/src/libcaphe/caphe-session-dbus-invokator.h000066400000000000000000000024541337147241200241120ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_SESSION_DBUS_INVOKATOR_H__ #define __LIBCAPHE_CAPHE_SESSION_DBUS_INVOKATOR_H__ #include #include "caphe-dbus-invokator.h" /* GObject declarations */ #define CAPHE_TYPE_SESSION_DBUS_INVOKATOR caphe_session_dbus_invokator_get_type() G_DECLARE_FINAL_TYPE(CapheSessionDbusInvokator, caphe_session_dbus_invokator, CAPHE, SESSION_DBUS_INVOKATOR, CapheDbusInvokator) /* Methods */ CapheDbusInvokator *caphe_session_dbus_invokator_new(void); #endif /* __LIBCAPHE_CAPHE_SESSION_DBUS_INVOKATOR_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe-trace.h000066400000000000000000000022221337147241200205510ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_TRACE_H__ #define __LIBCAPHE_CAPHE_TRACE_H__ #include #ifdef ENABLE_TRACE #define CAPHE_TRACE_ENABLED 1 #else #define CAPHE_TRACE_ENABLED 0 #endif #define TRACE(fmt, ...) \ do { \ if (CAPHE_TRACE_ENABLED) \ g_debug("-> %s(" fmt ")", __func__, ##__VA_ARGS__); \ } while (0) #endif /* __LIBCAPHE_CAPHE_TRACE_H__ */ goodvibes-v0.4.2/src/libcaphe/caphe.c000066400000000000000000000021221337147241200174470ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "caphe-cup.h" void caphe_cleanup(void) { g_object_add_weak_pointer(G_OBJECT(caphe_cup_default_instance), (gpointer *) &caphe_cup_default_instance); g_object_unref(caphe_cup_default_instance); g_assert_null(caphe_cup_default_instance); } void caphe_init(void) { /* Dummy */ } goodvibes-v0.4.2/src/libcaphe/caphe.h000066400000000000000000000016561337147241200174670ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __LIBCAPHE_CAPHE_H__ #define __LIBCAPHE_CAPHE_H__ #include "libcaphe/caphe-cup.h" void caphe_init (void); void caphe_cleanup(void); #endif /* __LIBCAPHE_CAPHE_H__ */ goodvibes-v0.4.2/src/libcaphe/meson.build000066400000000000000000000013341337147241200203710ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later caphe_sources = [ 'caphe.c', 'caphe-cup.c', 'caphe-dbus-inhibitor.c', 'caphe-dbus-invokator.c', 'caphe-dbus-proxy.c', 'caphe-inhibitor.c', 'caphe-inhibitor-list.c', 'caphe-login-dbus-invokator.c', 'caphe-portal-dbus-invokator.c', 'caphe-power-dbus-invokator.c', 'caphe-session-dbus-invokator.c', ] caphe_dependencies = [ glib_dep, gobject_dep, gio_dep, gio_unix_dep ] libcaphe = static_library('caphe', sources: [ caphe_sources ], dependencies: caphe_dependencies, c_args: [ '-DG_LOG_DOMAIN="Caphe"', #'-DENABLE_TRACE', ], ) libcaphe_dep = declare_dependency( dependencies: caphe_dependencies, link_with: libcaphe, ) subdir('tests') goodvibes-v0.4.2/src/libcaphe/tests/000077500000000000000000000000001337147241200173705ustar00rootroot00000000000000goodvibes-v0.4.2/src/libcaphe/tests/main.c000066400000000000000000000106561337147241200204700ustar00rootroot00000000000000/* * Libcaphe * * Copyright (C) 2016-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * Compilation: * packages="glib-2.0 gio-2.0 gio-unix-2.0" * gcc $(pkg-config --cflags --libs $packages) *.c -o main * gcc $(pkg-config --cflags --libs $packages) -DENABLE_TRACE *.c -o main * * Execution: * G_MESSAGES_DEBUG=all ./main */ #include #include #include #include #include #include "libcaphe/caphe.h" #define INHIBIT_REASON "Ice-coffee with milk" #define print(fmt, ...) g_print(fmt "\n", ##__VA_ARGS__) static GMainLoop *main_loop; static void on_caphe_notify_inhibitor(CapheCup *caphe, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { CapheInhibitor *inhibitor = caphe_cup_get_inhibitor(caphe); if (inhibitor) print("Inhibited with inhibitor: %s", caphe_inhibitor_get_name(inhibitor)); else print("Uninhibited"); } static gboolean on_stress_step_4(CapheCup *caphe) { if (!caphe_cup_is_inhibited(caphe)) g_error("Should be inhibited !"); return G_SOURCE_REMOVE; } static gboolean on_stress_step_3(CapheCup *caphe) { if (!caphe_cup_is_inhibited(caphe)) g_error("Should be inhibited !"); print("Sending inhibit request for a different reason"); caphe_cup_inhibit(caphe, "Expresso"); print("Waiting 1 sec. Expect inhibited."); g_timeout_add_seconds(1, (GSourceFunc) on_stress_step_4, caphe); return G_SOURCE_REMOVE; } static gboolean on_stress_step_2(CapheCup *caphe) { if (caphe_cup_is_inhibited(caphe)) g_error("Shouldn't be inhibited !"); print("Sending batch of requests."); caphe_cup_uninhibit(caphe); caphe_cup_inhibit(caphe, INHIBIT_REASON); caphe_cup_inhibit(caphe, INHIBIT_REASON "dfdf"); caphe_cup_uninhibit(caphe); caphe_cup_inhibit(caphe, INHIBIT_REASON); print("Waiting 1 sec. Expect inhibited."); g_timeout_add_seconds(1, (GSourceFunc) on_stress_step_3, caphe); return G_SOURCE_REMOVE; } static gboolean when_idle_stress(CapheCup *caphe) { print("Sending 1st batch of requests."); caphe_cup_inhibit(caphe, INHIBIT_REASON); caphe_cup_inhibit(caphe, INHIBIT_REASON); caphe_cup_uninhibit(caphe); caphe_cup_inhibit(caphe, INHIBIT_REASON); caphe_cup_uninhibit(caphe); print("Waiting 1 second. Expect uninhibited."); g_timeout_add_seconds(1, (GSourceFunc) on_stress_step_2, caphe); return G_SOURCE_REMOVE; } static gboolean when_idle_inhibit(CapheCup *caphe) { print("Inhibiting..."); caphe_cup_inhibit(caphe, INHIBIT_REASON); return G_SOURCE_REMOVE; } static gboolean sigint_handler(gpointer user_data G_GNUC_UNUSED) { g_main_loop_quit(main_loop); return FALSE; } static void print_usage(const char *progname) { print("Usage: %s ", progname); } int main(int argc, char *argv[]) { CapheCup *caphe; GSourceFunc idle_func; /* Process input arguments */ if (argc == 1 || argc > 2) { print_usage(argv[0]); exit(EXIT_FAILURE); } if (!g_strcmp0(argv[1], "inhibit")) idle_func = (GSourceFunc) when_idle_inhibit; else if (!g_strcmp0(argv[1], "stress")) idle_func = (GSourceFunc) when_idle_stress; else { print_usage(argv[0]); exit(EXIT_FAILURE); } /* Init */ caphe_init(); caphe = caphe_cup_get_default(); g_signal_connect(caphe, "notify::inhibitor", G_CALLBACK(on_caphe_notify_inhibitor), NULL); /* Add idle function. Priorities to try: * - G_PRIORITY_LOW * - G_PRIORITY_HIGH */ g_idle_add_full(G_PRIORITY_LOW, idle_func, caphe, NULL); /* Be ready to catch interruptions */ g_unix_signal_add(SIGINT, sigint_handler, NULL); /* Main loop */ print("-- Running the main loop --"); main_loop = g_main_loop_new(NULL, FALSE); g_main_loop_run(main_loop); /* Cleanup */ print("-- Main loop exited --"); g_main_loop_unref(main_loop); caphe_cleanup(); return EXIT_SUCCESS; } goodvibes-v0.4.2/src/libcaphe/tests/meson.build000066400000000000000000000002461337147241200215340ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later executable('caphe-test', [ 'main.c' ], dependencies: libcaphe_dep, include_directories: root_inc, install: false ) goodvibes-v0.4.2/src/main.c000066400000000000000000000120321337147241200155450ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include "framework/glib-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #ifdef GV_UI_ENABLED #include "ui/gv-ui.h" #endif #ifdef GV_UI_ENABLED #include "gv-graphical-application.h" #else #include "gv-console-application.h" #endif #include "options.h" /* * Print informations about program. */ static const gchar * string_package_info(void) { return GV_NAME_CAPITAL " " PACKAGE_VERSION; } static const gchar * string_copyright(void) { return GV_COPYRIGHT " " GV_AUTHOR_NAME " <" GV_AUTHOR_EMAIL ">"; } static const gchar * string_date_now(void) { GDateTime *now; static gchar *text; g_free(text); now = g_date_time_new_now_local(); text = g_date_time_format(now, "%F, %T"); g_date_time_unref(now); return text; } static const gchar * string_compile_libraries(void) { static gchar *text; if (text == NULL) text = g_strjoin(", ", glib_get_compile_version_string(), gv_core_audio_backend_compile_version_string(), #ifdef GV_UI_ENABLED gv_ui_toolkit_compile_version_string(), #endif NULL); return text; } static const gchar * string_runtime_libraries(void) { static gchar *text; if (text == NULL) text = g_strjoin(", ", glib_get_runtime_version_string(), gv_core_audio_backend_runtime_version_string(), #ifdef GV_UI_ENABLED gv_ui_toolkit_runtime_version_string(), #endif NULL); return text; } /* * Main - this is where everything starts... */ static gboolean sigint_handler(gpointer user_data) { GApplication *application = G_APPLICATION(user_data); /* There's probably a '^C' written on the console line by now. * Let's start a new line to keep logs clean. */ putchar('\n'); /* Stop application */ g_application_quit(application); return FALSE; } int main(int argc, char *argv[]) { GApplication *app; int ret; /* Initialize i18n */ setlocale(LC_ALL, NULL); bindtextdomain(GETTEXT_PACKAGE, GV_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); /* Set application name */ g_set_prgname(PACKAGE_NAME); g_set_application_name(_("Goodvibes")); #ifdef GV_UI_ENABLED /* Set application icon */ gtk_window_set_default_icon_name(GV_ICON_NAME); #endif /* Parse command-line, and run some init code at the same time */ options_parse(&argc, &argv); /* We might just want to print the version and exit */ if (options.print_version) { g_print("%s\n", string_package_info()); g_print("%s\n", string_copyright()); g_print("Compiled against: %s\n", string_compile_libraries()); g_print("Running along : %s\n", string_runtime_libraries()); return EXIT_SUCCESS; } /* Run in background. * This option may be used along with the 'logfile' option, therefore the * arguments for daemon() must be as follow: * - nochdir = 1: we MUST NOT change the working directory * (otherwise a relative path for logfile won't work) * - noclose = 0: we MUST close std{in/out/err} BEFORE initializing the logs * (and let the log system re-open std{out/err} afterward if needed). */ if (options.background) { if (daemon(1, 0) == -1) { g_printerr("Failed to daemonize: %s\n", strerror(errno)); return EXIT_FAILURE; } } /* Initialize log system, warm it up with a few logs */ log_init(options.log_level, options.colorless, options.output_file); INFO("%s", string_package_info()); INFO("%s", string_copyright()); INFO("Started on %s, with pid %ld", string_date_now(), (long) getpid()); INFO("Compiled against : %s", string_compile_libraries()); INFO("Running along : %s", string_runtime_libraries()); INFO("Gettext locale dir: %s", GV_LOCALEDIR); /* Create the application */ #ifdef GV_UI_ENABLED app = gv_graphical_application_new(GV_APPLICATION_ID); #else app = gv_console_application_new(GV_APPLICATION_ID); #endif /* Quit on SIGINT */ g_unix_signal_add(SIGINT, sigint_handler, app); /* Run the application */ ret = g_application_run(app, 0, NULL); /* Cleanup */ log_cleanup(); options_cleanup(); return ret; } goodvibes-v0.4.2/src/meson.build000066400000000000000000000014501337147241200166210ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Subdirectories root_inc = include_directories('.') subdir('libcaphe') subdir('framework') subdir('core') if gv_ui_enabled subdir('ui') endif subdir('feat') # Source and dependencies goodvibes_sources = [ 'main.c', 'options.c', ] goodvibes_dependencies = [ gvframework_dep, gvcore_dep, gvfeat_dep, math_dep, ] if gv_ui_enabled goodvibes_sources += 'gv-graphical-application.c' goodvibes_dependencies += gvui_dep else goodvibes_sources += 'gv-console-application.c' endif # Executable definition executable('goodvibes', goodvibes_sources, dependencies: goodvibes_dependencies, include_directories: root_inc, install: true ) executable('goodvibes-client', [ 'client.c' ], dependencies: [ glib_dep, gio_dep ], install: true ) goodvibes-v0.4.2/src/options.c000066400000000000000000000071661337147241200163300ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/gv-framework.h" #include "core/gv-core.h" #ifdef GV_UI_ENABLED #include "ui/gv-ui.h" #endif #include "options.h" struct options options; static GOptionEntry entries[] = { { "background", 'b', 0, G_OPTION_ARG_NONE, &options.background, "Run in the background", NULL }, { "colorless", 'c', 0, G_OPTION_ARG_NONE, &options.colorless, "Disable colors in log messages", NULL }, { "log-level", 'l', 0, G_OPTION_ARG_STRING, &options.log_level, "Set the log level, amongst: trace, debug, info, warning, critical, error.", "warning" }, { "output-file", 'o', 0, G_OPTION_ARG_STRING, &options.output_file, "Redirect log messages to a file", "file" }, { "version", 'v', 0, G_OPTION_ARG_NONE, &options.print_version, "Print the version and exit", NULL }, #ifdef GV_UI_ENABLED { "without-ui", 0, 0, G_OPTION_ARG_NONE, &options.without_ui, "Disable the graphical user interface at startup", NULL }, { "status-icon", 0, 0, G_OPTION_ARG_NONE, &options.status_icon, "Launch as a status icon (deprecated on modern desktops)", NULL }, #endif { .long_name = NULL } }; static void print_help(GOptionContext *context) { gchar *help; help = g_option_context_get_help(context, TRUE, NULL); g_print("%s", help); g_free(help); } void options_parse(int *argc, char **argv[]) { GError *error = NULL; GOptionContext *context; /* Init options */ memset(&options, 0, sizeof(struct options)); /* Create context & entries */ context = g_option_context_new("[STATION]"); g_option_context_set_summary (context, GV_NAME_CAPITAL " is a lightweight internet radio player for GNU/Linux.\n" "It offers a simple way to have your favorite radio stations at easy reach.\n" "\n" "To control it via the command line, see the '" PACKAGE_NAME "-client' executable."); g_option_context_add_main_entries(context, entries, NULL); /* Add option groups and perform some init code at the same time */ g_option_context_add_group(context, gv_core_audio_backend_init_get_option_group()); #ifdef GV_UI_ENABLED g_option_context_add_group(context, gv_ui_toolkit_init_get_option_group()); #endif /* Parse the command-line arguments */ if (!g_option_context_parse(context, argc, argv, &error)) { g_print("Failed to parse options: %s\n", error->message); g_error_free(error); g_option_context_free(context); exit(EXIT_FAILURE); } /* There should be at most one argument left: the URI to play */ switch (*argc) { case 1: options.uri_to_play = NULL; break; case 2: options.uri_to_play = (*argv)[1]; break; default: print_help(context); g_option_context_free(context); exit(EXIT_FAILURE); break; } /* Leave no trace behind */ g_option_context_free(context); } void options_cleanup(void) { /* Run some cleanup code that matches the init code done in parse() */ gv_core_audio_backend_cleanup(); } goodvibes-v0.4.2/src/options.h000066400000000000000000000023771337147241200163340ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_OPTIONS_H__ #define __GOODVIBES_OPTIONS_H__ struct options { /* Options */ gboolean background; gboolean colorless; const gchar *log_level; const gchar *output_file; gboolean print_version; #ifdef GV_UI_ENABLED gboolean without_ui; gboolean status_icon; #endif /* Arguments */ const gchar *uri_to_play; }; extern struct options options; void options_parse(int *argc, char **argv[]); void options_cleanup(void); #endif /* __GOODVIBES_OPTIONS_H__ */ goodvibes-v0.4.2/src/ui/000077500000000000000000000000001337147241200150745ustar00rootroot00000000000000goodvibes-v0.4.2/src/ui/gtk-additions.c000066400000000000000000000030651337147241200200050ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include /* * Version Information */ const gchar * gtk_get_runtime_version_string(void) { static gchar *version_string; if (version_string == NULL) { version_string = g_strdup_printf("GTK+ %u.%u.%u", gtk_get_major_version(), gtk_get_minor_version(), gtk_get_micro_version()); } return version_string; } const gchar * gtk_get_compile_version_string(void) { static gchar *version_string; if (version_string == NULL) { version_string = g_strdup_printf("GTK+ %u.%u.%u", GTK_MAJOR_VERSION, GTK_MINOR_VERSION, GTK_MICRO_VERSION); } return version_string; } goodvibes-v0.4.2/src/ui/gtk-additions.h000066400000000000000000000046631337147241200200170ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_FRAMEWORK_GTK_ADDITIONS_H__ #define __GOODVIBES_FRAMEWORK_GTK_ADDITIONS_H__ #include /* * Version Information */ const gchar *gtk_get_runtime_version_string(void); const gchar *gtk_get_compile_version_string(void); /* * GtkBuilder */ #define gtk_builder_get_widget(builder, name) \ GTK_WIDGET(gtk_builder_get_object(builder, name)) #define gtk_builder_get_object(builder, name) \ G_OBJECT(gtk_builder_get_object(builder, name)) /* * Convenient macros to save widgets from a GtkBuilder instance. * If the widget is not found, it is considered as a programming error, * therefore ending the execution of the program. * This macro also ensures that the widget name in the ui file is the same * as the one used in the C file, therefore enforcing a consistent naming * across the code. */ #define GTK_BUILDER_SAVE_WIDGET(builder, str, name) \ do { \ str->name = gtk_builder_get_widget(builder, #name); \ g_assert(GTK_IS_WIDGET(str->name)); \ } while (0) #define GTK_BUILDER_SAVE_OBJECT(builder, str, name) \ do { \ str->name = gtk_builder_get_object(builder, #name); \ g_assert(G_IS_OBJECT(str->name)); \ } while (0) /* * GtkWidget */ #define gtk_widget_set_margins(widget, margin) \ do { \ gtk_widget_set_margin_start(widget, margin); \ gtk_widget_set_margin_end(widget, margin); \ gtk_widget_set_margin_top(widget, margin); \ gtk_widget_set_margin_bottom(widget, margin); \ } while (0) #endif /* __GOODVIBES_FRAMEWORK_GTK_ADDITIONS_H__ */ goodvibes-v0.4.2/src/ui/gv-about-dialog.c000066400000000000000000000051431337147241200202240ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "framework/gv-framework.h" static const gchar *authors[] = { GV_AUTHOR_NAME " <" GV_AUTHOR_EMAIL ">", NULL }; static const gchar *artists[] = { "Lahminèwski Lab http://lahminewski-lab.net", NULL }; static const gchar *translators = "Weblate https://hosted.weblate.org/projects/goodvibes/translations\n" \ "Lukáš Linhart - Czech (cs)\n" \ "Michal Čihař - Czech (cs)\n" \ "Andreas Kleinert - German (de)\n" \ "Vinz - German (de)\n" \ "Wolf Berg - German (de)\n" \ "Benages - Spanish (es)\n" \ "Étienne Deparis - French (fr)\n" \ "Allan Nordhøy - Norwegian Bokmål (nb_NO)\n" \ "Heimen Stoffels - Dutch (nl)\n" \ "Алексей Выскубов - Russian (ru)"; void gv_show_about_dialog(GtkWindow *parent, const gchar *audio_backend_string, const gchar *ui_toolkit_string) { // WISHED "license-type" shouldn't be hardcoded gchar *comments; comments = g_strdup_printf("Audio Backend: %s\n" "GUI Toolkit: %s", audio_backend_string, ui_toolkit_string); gtk_show_about_dialog(parent, "artists", artists, "authors", authors, "comments", comments, "copyright", GV_COPYRIGHT " " GV_AUTHOR_NAME, "license-type", GTK_LICENSE_GPL_3_0, "logo-icon-name", GV_ICON_NAME, "translator-credits", translators, "version", PACKAGE_VERSION, "website", GV_HOMEPAGE, NULL); g_free(comments); } goodvibes-v0.4.2/src/ui/gv-about-dialog.h000066400000000000000000000020621337147241200202260ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_ABOUT_DIALOG_H__ #define __GOODVIBES_UI_GV_ABOUT_DIALOG_H__ #include void gv_show_about_dialog(GtkWindow *parent, const gchar *audio_backend_string, const gchar *ui_toolkit_string); #endif /* __GOODVIBES_UI_GV_ABOUT_DIALOG_H__ */ goodvibes-v0.4.2/src/ui/gv-main-window-manager.c000066400000000000000000000370431337147241200215220ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "ui/gv-ui-internal.h" #include "ui/gv-main-window.h" #include "ui/gv-main-window-manager.h" /* * Reasons to have a separate object to deal with the main window configuration, * aka size, position, maximized. * * Delayed method calls * -------------------- * * We invoke methods on the main window in two contexts: * - at init time (to load configuration) * - in signal handlers (to save configuration) * * Both of these contexts are not suitable to query the window size, and because * of that we need to delay all the method calls. This logic is better taken out * of the main window, and handled here in the window manager. * * Window finalization * ------------------- * * Saving the window configuration is an operation that is delayed by a timeout, * and therefore there's a possibility that the main window is finalized before * the timeout is reached. If this 'save timeout' is handled by the main window, * then we have to try to save the window configuration from within the finalize * method of the window itself, and it doesn't work. It's too late to query the * window at this moment. * * The window manager solves this problem elegantly, since the 'save timeout' is * taken out of the main window, and handled by the manager. The manager is sure * to be finalized before the main window, and therefore can safely query it at * this moment. */ /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_MAIN_WINDOW, PROP_STATUS_ICON_MODE, PROP_AUTOSET_HEIGHT, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvMainWindowManagerPrivate { /* Properties */ GvMainWindow *main_window; gboolean status_icon_mode; gboolean autoset_height; /* Window configuration */ guint save_window_configuration_source_id; }; typedef struct _GvMainWindowManagerPrivate GvMainWindowManagerPrivate; struct _GvMainWindowManager { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvMainWindowManagerPrivate *priv; }; static void gv_main_window_manager_configurable_interface_init(GvConfigurableInterface *iface); G_DEFINE_TYPE_WITH_CODE(GvMainWindowManager, gv_main_window_manager, G_TYPE_OBJECT, G_ADD_PRIVATE(GvMainWindowManager) G_IMPLEMENT_INTERFACE(GV_TYPE_CONFIGURABLE, gv_main_window_manager_configurable_interface_init)) /* * Private methods */ static void gv_main_window_manager_save_configuration_now(GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; GtkWindow *window = GTK_WINDOW(priv->main_window); GSettings *settings = gv_ui_settings; gint width, height, old_width, old_height; gint x, y, old_x, old_y; TRACE("%p", self); /* This is never invoked if the window is in status icon mode */ g_assert_false(priv->status_icon_mode); /* Don't save anything when window is maximized */ if (gtk_window_is_maximized(window)) return; /* Save size if changed */ gtk_window_get_size(window, &width, &height); g_settings_get(settings, "window-size", "(ii)", &old_width, &old_height); if ((width != old_width) || (height != old_height)) g_settings_set(settings, "window-size", "(ii)", width, height); /* Save position if changed */ gtk_window_get_position(window, &x, &y); g_settings_get(settings, "window-position", "(ii)", &old_x, &old_y); if ((x != old_x) || (y != old_y)) g_settings_set(settings, "window-position", "(ii)", x, y); } static gboolean when_timeout_save_window_configuration(GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; gv_main_window_manager_save_configuration_now(self); priv->save_window_configuration_source_id = 0; return G_SOURCE_REMOVE; } static void gv_main_window_manager_save_configuration_delayed(GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; if (priv->save_window_configuration_source_id > 0) g_source_remove(priv->save_window_configuration_source_id); priv->save_window_configuration_source_id = g_timeout_add_seconds(1, (GSourceFunc) when_timeout_save_window_configuration, self); } /* * Window signal handlers */ static void on_main_window_notify_natural_height(GvMainWindow *window, GParamSpec *pspec G_GNUC_UNUSED, GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; gint natural_height; /* This signal handler is invoked when the natural height of the main * window was changed, and therefore we want to resize it. */ /* Should never be invoked if autoset-height is FALSE */ g_assert_true(priv->autoset_height); natural_height = gv_main_window_get_natural_height(window); gv_main_window_resize_height(window, natural_height); } static gboolean on_main_window_configure_event(GtkWindow *window G_GNUC_UNUSED, GdkEventConfigure *event G_GNUC_UNUSED, GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; /* This signal handler is invoked multiple times during resizing (really). * It only means that resizing is in progress, and there is no way to know * that resizing is finished. * The (dumb) strategy to handle that is just to delay our reaction. */ /* Should never be invoked in status icon mode */ g_assert_false(priv->status_icon_mode); gv_main_window_manager_save_configuration_delayed(self); return GDK_EVENT_PROPAGATE; } /* * Public functions */ GvMainWindowManager * gv_main_window_manager_new(GvMainWindow *main_window, gboolean status_icon_mode) { return g_object_new(GV_TYPE_MAIN_WINDOW_MANAGER, "main-window", main_window, "status-icon-mode", status_icon_mode, NULL); } /* * Property accessors */ static void gv_main_window_manager_set_main_window(GvMainWindowManager *self, GvMainWindow *main_window) { GvMainWindowManagerPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_null(priv->main_window); g_assert_nonnull(main_window); g_set_object(&priv->main_window, main_window); } static void gv_main_window_manager_set_status_icon_mode(GvMainWindowManager *self, gboolean status_icon_mode) { GvMainWindowManagerPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_false(priv->status_icon_mode); priv->status_icon_mode = status_icon_mode; } gboolean gv_main_window_manager_get_autoset_height(GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; return priv->autoset_height; } void gv_main_window_manager_set_autoset_height(GvMainWindowManager *self, gboolean autoset_height) { GvMainWindowManagerPrivate *priv = self->priv; /* In status icon mode, this property is forced to TRUE */ if (priv->status_icon_mode) { DEBUG("Status icon mode: 'autoset-height' forced to TRUE"); autoset_height = TRUE; } if (priv->autoset_height == autoset_height) return; if (autoset_height) { g_signal_connect_object (priv->main_window, "notify::natural-height", G_CALLBACK(on_main_window_notify_natural_height), self, 0); } else { g_signal_handlers_disconnect_by_func (priv->main_window, G_CALLBACK(on_main_window_notify_natural_height), self); } priv->autoset_height = autoset_height; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_AUTOSET_HEIGHT]); } static void gv_main_window_manager_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvMainWindowManager *self = GV_MAIN_WINDOW_MANAGER(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_AUTOSET_HEIGHT: g_value_set_boolean(value, gv_main_window_manager_get_autoset_height(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_main_window_manager_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvMainWindowManager *self = GV_MAIN_WINDOW_MANAGER(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_MAIN_WINDOW: gv_main_window_manager_set_main_window(self, g_value_get_object(value)); break; case PROP_STATUS_ICON_MODE: gv_main_window_manager_set_status_icon_mode(self, g_value_get_boolean(value)); break; case PROP_AUTOSET_HEIGHT: gv_main_window_manager_set_autoset_height(self, g_value_get_boolean(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * GvConfigurable interface */ static void gv_main_window_manager_load_configuration(GvMainWindowManager *self) { GvMainWindowManagerPrivate *priv = self->priv; GSettings *settings = gv_ui_settings; gint width, height; gint x, y; TRACE("%p", self); /* Get settings */ g_settings_get(settings, "window-size", "(ii)", &width, &height); g_settings_get(settings, "window-position", "(ii)", &x, &y); g_settings_bind(settings, "window-autoset-height", self, "autoset-height", G_SETTINGS_BIND_DEFAULT); /* Set initial window size */ if (width != -1 && height != -1) { GtkWindow *window = GTK_WINDOW(priv->main_window); DEBUG("Restoring window size (%d, %d)", width, height); gtk_window_resize(window, width, height); } else { GtkWindow *window = GTK_WINDOW(priv->main_window); gint win_height; /* * Now is the tricky part. I wish to resize the main window to its * 'natural size', so that it can accomodate the station tree view * (whatever the number of stations), without showing the scroll bar. * I don't think I'm wishing anything fancy here. * * However, it seems impossible to find a reliable way to do that. * * Querying the station tree view for its natural size RIGHT NOW is * too early. Ok, that's fine to me GTK, so when is the right moment ? * * Well, the problem is that there doesn't seem to be a right moment to * speak of. I tried delaying to the latest, that's to say connecting to * the 'map-event' signal of the station tree view, then delaying with a * 'g_idle_add()', then at last querying the tree view for its natural size. * Even at this moment, it might fail and report a size that is too small. * After that I have no other signal to connect to, I'm left with delaying * with a timeout to query AGAIN the tree view for its natural size, and * discover, o surprise, that it reports a different natural size. * * And as far as I know, there was no signal that I could have used to * get notified of this change. * * So, screw it, no more autosize, it's way too much hassle for such a * trivial result. */ gtk_window_get_size(window, NULL, &win_height); win_height += 200; DEBUG("Setting default window size (%d, %d)", 1, win_height); gtk_window_resize(window, 1, win_height); } /* Set initial window position */ if (x != -1 || y != -1) { GtkWindow *window = GTK_WINDOW(priv->main_window); DEBUG("Restoring window position (%d, %d)", x, y); gtk_window_move(window, x, y); } /* Connect to the 'configure-event' signal to save changes when the * window size or position is modified. */ g_signal_connect_object(priv->main_window, "configure-event", G_CALLBACK(on_main_window_configure_event), self, 0); } static void gv_main_window_manager_configure(GvConfigurable *configurable) { GvMainWindowManager *self = GV_MAIN_WINDOW_MANAGER(configurable); GvMainWindowManagerPrivate *priv = self->priv; /* In status icon mode, configuration is ignored. We neither read nor write it. * The only thing we want is to ensure that the height is automatically set. * We don't even need to resize the window now, it will be done the first time * the window is realized. */ if (priv->status_icon_mode) { gv_main_window_manager_set_autoset_height(self, TRUE); return; } /* Load configuration */ gv_main_window_manager_load_configuration(self); } static void gv_main_window_manager_configurable_interface_init(GvConfigurableInterface *iface) { iface->configure = gv_main_window_manager_configure; } /* * GObject methods */ static void gv_main_window_manager_finalize(GObject *object) { GvMainWindowManager *self = GV_MAIN_WINDOW_MANAGER(object); GvMainWindowManagerPrivate *priv = self->priv; TRACE("%p", object); /* Run any pending save operation */ if (priv->save_window_configuration_source_id > 0) when_timeout_save_window_configuration(self); /* Release resources */ g_object_unref(priv->main_window); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_main_window_manager, object); } static void gv_main_window_manager_constructed(GObject *object) { GvMainWindowManager *self = GV_MAIN_WINDOW_MANAGER(object); GvMainWindowManagerPrivate *priv =self->priv; TRACE("%p", self); /* Ensure construct-only properties have been set */ g_assert_nonnull(priv->main_window); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_main_window_manager, object); } static void gv_main_window_manager_init(GvMainWindowManager *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_main_window_manager_get_instance_private(self); } static void gv_main_window_manager_class_init(GvMainWindowManagerClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_main_window_manager_finalize; object_class->constructed = gv_main_window_manager_constructed; /* Properties */ object_class->get_property = gv_main_window_manager_get_property; object_class->set_property = gv_main_window_manager_set_property; properties[PROP_MAIN_WINDOW] = g_param_spec_object("main-window", "Main window", NULL, GV_TYPE_MAIN_WINDOW, GV_PARAM_DEFAULT_FLAGS | G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE); properties[PROP_STATUS_ICON_MODE] = g_param_spec_boolean("status-icon-mode", "Status icon mode", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE); properties[PROP_AUTOSET_HEIGHT] = g_param_spec_boolean("autoset-height", "Autoset height", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/ui/gv-main-window-manager.h000066400000000000000000000031751337147241200215260ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_MAIN_WINDOW_MANAGER_H__ #define __GOODVIBES_UI_GV_MAIN_WINDOW_MANAGER_H__ #include #include #include "ui/gv-main-window.h" /* GObject declarations */ #define GV_TYPE_MAIN_WINDOW_MANAGER gv_main_window_manager_get_type() G_DECLARE_FINAL_TYPE(GvMainWindowManager, gv_main_window_manager, GV, MAIN_WINDOW_MANAGER, GObject) /* Methods */ GvMainWindowManager *gv_main_window_manager_new(GvMainWindow *main_window, gboolean status_icon_mode); /* Property accessors */ gboolean gv_main_window_manager_get_autoset_height(GvMainWindowManager *self); void gv_main_window_manager_set_autoset_height(GvMainWindowManager *self, gboolean autoset_height); #endif /* __GOODVIBES_UI_GV_MAIN_WINDOW_MANAGER_H__ */ goodvibes-v0.4.2/src/ui/gv-main-window.c000066400000000000000000000765311337147241200201170ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gtk-additions.h" #include "ui/gv-ui-enum-types.h" #include "ui/gv-ui-helpers.h" #include "ui/gv-ui-internal.h" #include "ui/gv-stations-tree-view.h" #include "ui/gv-main-window.h" #define UI_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/main-window.glade" /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_STATUS_ICON_MODE, PROP_NATURAL_HEIGHT, PROP_THEME_VARIANT, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvMainWindowPrivate { /* * Properties */ gboolean status_icon_mode; gint natural_height; GvMainWindowThemeVariant theme_variant; /* * Widgets */ /* Top-level */ GtkWidget *window_vbox; /* Current status */ GtkWidget *info_vbox; GtkWidget *station_label; GtkWidget *status_label; /* Button box */ GtkWidget *play_button; GtkWidget *prev_button; GtkWidget *next_button; GtkWidget *repeat_toggle_button; GtkWidget *shuffle_toggle_button; GtkWidget *volume_button; /* Stations */ GtkWidget *scrolled_window; GtkWidget *stations_tree_view; /* * Internal */ GtkWidget *info_tooltip_grid; GBinding *volume_binding; gboolean system_prefer_dark_theme; }; typedef struct _GvMainWindowPrivate GvMainWindowPrivate; struct _GvMainWindow { /* Parent instance structure */ GtkApplicationWindow parent_instance; /* Private data */ GvMainWindowPrivate *priv; }; static void gv_main_window_configurable_interface_init(GvConfigurableInterface *iface); G_DEFINE_TYPE_WITH_CODE(GvMainWindow, gv_main_window, GTK_TYPE_APPLICATION_WINDOW, G_ADD_PRIVATE(GvMainWindow) G_IMPLEMENT_INTERFACE(GV_TYPE_CONFIGURABLE, gv_main_window_configurable_interface_init)) /* * Private methods */ static gint gv_main_window_compute_natural_height(GvMainWindow *self) { GtkWindow *window = GTK_WINDOW(self); GtkWidget *tree_view = self->priv->stations_tree_view; GtkAllocation allocated; GtkRequisition natural; gint width, height, diff, natural_height; gint min_height = 1; gint max_height; /* Get maximum height */ #if GTK_CHECK_VERSION(3,22,0) GdkDisplay *display; GdkWindow *gdk_window; GdkMonitor *monitor; GdkRectangle geometry; display = gdk_display_get_default(); gdk_window = gtk_widget_get_window(GTK_WIDGET(self)); monitor = gdk_display_get_monitor_at_window(display, gdk_window); gdk_monitor_get_workarea(monitor, &geometry); max_height = geometry.height; #else GdkScreen *screen; screen = gdk_screen_get_default(); max_height = gdk_screen_get_height(screen); #endif /* * Here comes a hacky piece of code ! * * Problem: from the moment the station tree view is within a scrolled * window, the height is not handled smartly by GTK+ anymore. By default, * it's ridiculously small. Then, when the number of rows in the tree view * is changed, the tree view is not resized. So if we want a smart height, * we have to do it manually. * * The success (or failure) of this function highly depends on the moment * it's called. * - too early, get_preferred_size() calls return junk. * - in some signal handlers, get_preferred_size() calls return junk. * * Plus, we resize the window here, is the context safe to do that ? * * For these reasons, it's safer to never call this function directly, * but instead always delay the call for an idle moment. */ /* Determine if the tree view is at its natural height */ gtk_widget_get_allocation(tree_view, &allocated); gtk_widget_get_preferred_size(tree_view, NULL, &natural); //DEBUG("allocated height: %d", allocated.height); //DEBUG("natural height: %d", natural.height); diff = natural.height - allocated.height; /* Determine what should be the new height */ gtk_window_get_size(window, &width, &height); natural_height = height + diff; if (natural_height < min_height) { DEBUG("Clamping natural height %d to minimum height %d", natural_height, min_height); natural_height = min_height; } if (natural_height > max_height) { DEBUG("Clamping natural height %d to maximum height %d", natural_height, max_height); natural_height = max_height; } return natural_height; } /* * Info custom tooltip */ static void grid_add_section(GtkGrid *grid, gint row, const gchar *section) { GtkWidget *label; PangoAttrList *attrs; PangoAttribute *bold; attrs = pango_attr_list_new(); bold = pango_attr_weight_new(PANGO_WEIGHT_BOLD); pango_attr_list_insert(attrs, bold); label = gtk_label_new(section); gtk_label_set_attributes(GTK_LABEL(label), attrs); gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_grid_attach(grid, label, 0, row, 2, 1); pango_attr_list_unref(attrs); } static void grid_add_field(GtkGrid *grid, gint row, gboolean mandatory, const gchar *key, const gchar *value) { GtkWidget *label; if (value == NULL && !mandatory) return; if (key) { label = gtk_label_new(key); gtk_widget_set_halign(label, GTK_ALIGN_END); gtk_grid_attach(grid, label, 0, row, 1, 1); } if (value) { label = gtk_label_new(value); gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_grid_attach(grid, label, 1, row, 1, 1); } } static GtkWidget * make_info_tooltip_grid(GvStation *station, GvMetadata *metadata, guint bitrate) { GtkGrid *grid; guint n; grid = GTK_GRID(gtk_grid_new()); g_object_set(grid, "row-spacing", 2, "column-spacing", 6, NULL); n = 0; if (station) { const gchar *name = gv_station_get_name(station); const gchar *uri = gv_station_get_uri(station); const gchar *user_agent = gv_station_get_user_agent(station); guint nominal_bitrate = gv_station_get_nominal_bitrate(station); GSList *stream_uris = gv_station_get_stream_uris(station); GSList *item; grid_add_section(grid, n++, _("Station Information")); grid_add_field(grid, n++, TRUE, _("Name"), name); grid_add_field(grid, n++, TRUE, _("URI"), uri); for (item = stream_uris; item; item = item->next) { const gchar *stream_uri = item->data; if (!g_strcmp0(stream_uri, uri)) continue; grid_add_field(grid, n++, FALSE, "-", stream_uri); } grid_add_field(grid, n++, FALSE, _("User-agent"), user_agent); if (bitrate != 0 || nominal_bitrate != 0) { gchar *str = g_strdup_printf("%u kb/s (real: %u kb/s)", nominal_bitrate, bitrate); grid_add_field(grid, n++, FALSE, _("Bitrate"), str); g_free(str); } } if (metadata) { const gchar *artist = gv_metadata_get_artist(metadata); const gchar *title = gv_metadata_get_title(metadata); const gchar *album = gv_metadata_get_album(metadata); const gchar *genre = gv_metadata_get_genre(metadata); const gchar *year = gv_metadata_get_year(metadata); const gchar *comment = gv_metadata_get_comment(metadata); grid_add_section(grid, n++, _("Metadata")); grid_add_field(grid, n++, FALSE, _("Artist"), artist); grid_add_field(grid, n++, FALSE, _("Title"), title); grid_add_field(grid, n++, FALSE, _("Album"), album); grid_add_field(grid, n++, FALSE, _("Genre"), genre); grid_add_field(grid, n++, FALSE, _("Year"), year); grid_add_field(grid, n++, FALSE, _("Comment"), comment); } gtk_widget_show_all(GTK_WIDGET(grid)); return GTK_WIDGET(grid); } static gboolean on_info_vbox_query_tooltip(GtkWidget *widget G_GNUC_UNUSED, gint x G_GNUC_UNUSED, gint y G_GNUC_UNUSED, gboolean keyboard_tip G_GNUC_UNUSED, GtkTooltip *tooltip, GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; /* We must take ownership of the grid, otherwise it gets destroyed * as soon as the tooltip window vanishes (aka is finalized). */ if (priv->info_tooltip_grid == NULL) { GvPlayer *player = gv_core_player; GvStation *station = gv_player_get_station(player); GvMetadata *metadata = gv_player_get_metadata(player); guint bitrate = gv_player_get_bitrate(player); priv->info_tooltip_grid = make_info_tooltip_grid(station, metadata, bitrate); g_object_ref_sink(priv->info_tooltip_grid); } /* Set the custom widget */ gtk_tooltip_set_custom(tooltip, priv->info_tooltip_grid); /* If the grid is empty, there's nothing to display */ if (gtk_grid_get_child_at(GTK_GRID(priv->info_tooltip_grid), 0, 0) == NULL) return FALSE; return TRUE; } /* * Core Player signal handlers */ static void set_station_label(GtkLabel *label, GvStation *station) { const gchar *station_title; if (station) station_title = gv_station_get_name_or_uri(station); else station_title = _("No station selected"); gtk_label_set_text(label, station_title); } static void set_status_label(GtkLabel *label, GvPlayerState state, GvMetadata *metadata) { if (state != GV_PLAYER_STATE_PLAYING || metadata == NULL) { const gchar *state_str; switch (state) { case GV_PLAYER_STATE_PLAYING: state_str = _("Playing"); break; case GV_PLAYER_STATE_CONNECTING: state_str = _("Connecting…"); break; case GV_PLAYER_STATE_BUFFERING: state_str = _("Buffering…"); break; case GV_PLAYER_STATE_STOPPED: default: state_str = _("Stopped"); break; } gtk_label_set_text(label, state_str); } else { gchar *artist_title; gchar *album_year; gchar *str; artist_title = gv_metadata_make_title_artist(metadata, FALSE); album_year = gv_metadata_make_album_year(metadata, FALSE); if (artist_title && album_year) str = g_strdup_printf("%s/n%s", artist_title, album_year); else if (artist_title) str = g_strdup(artist_title); else str = g_strdup(_("Playing")); gtk_label_set_text(label, str); g_free(str); g_free(album_year); g_free(artist_title); } } static void set_play_button(GtkButton *button, GvPlayerState state) { GtkWidget *image; const gchar *icon_name; if (state == GV_PLAYER_STATE_STOPPED) icon_name = "media-playback-start-symbolic"; else icon_name = "media-playback-stop-symbolic"; image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON); gtk_button_set_image(button, image); } static void set_volume_button(GtkVolumeButton *volume_button, guint volume, gboolean mute) { GtkScaleButton *scale_button = GTK_SCALE_BUTTON(volume_button); if (mute) gtk_scale_button_set_value(scale_button, 0); else gtk_scale_button_set_value(scale_button, volume); } static void on_player_notify(GvPlayer *player, GParamSpec *pspec, GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; const gchar *property_name = g_param_spec_get_name(pspec); TRACE("%p, %s, %p", player, property_name, self); if (!g_strcmp0(property_name, "station")) { GtkLabel *label = GTK_LABEL(priv->station_label); GvStation *station = gv_player_get_station(player); set_station_label(label, station); g_clear_object(&priv->info_tooltip_grid); } else if (!g_strcmp0(property_name, "state")) { GtkLabel *label = GTK_LABEL(priv->status_label); GtkButton *button = GTK_BUTTON(priv->play_button); GvPlayerState state = gv_player_get_state(player); GvMetadata *metadata = gv_player_get_metadata(player); set_status_label(label, state, metadata); set_play_button(button, state); } else if (!g_strcmp0(property_name, "metadata")) { GtkLabel *label = GTK_LABEL(priv->status_label); GvPlayerState state = gv_player_get_state(player); GvMetadata *metadata = gv_player_get_metadata(player); set_status_label(label, state, metadata); g_clear_object(&priv->info_tooltip_grid); } else if (!g_strcmp0(property_name, "mute")) { GtkVolumeButton *volume_button = GTK_VOLUME_BUTTON(priv->volume_button); guint volume = gv_player_get_volume(player); gboolean mute = gv_player_get_mute(player); g_binding_unbind(priv->volume_binding); set_volume_button(volume_button, volume, mute); priv->volume_binding = g_object_bind_property (player, "volume", volume_button, "value", G_BINDING_BIDIRECTIONAL); } } /* * Widget signal handlers */ static void on_button_clicked(GtkButton *button, GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; GvPlayer *player = gv_core_player; GtkWidget *widget = GTK_WIDGET(button); if (widget == priv->play_button) { gv_player_toggle(player); } else if (widget == priv->prev_button) { gv_player_prev(player); } else if (widget == priv->next_button) { gv_player_next(player); } else { CRITICAL("Unhandled button %p", button); } } /* * Station tree view signal handlers */ static gboolean when_idle_compute_natural_height(GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; gint natural_height; natural_height = gv_main_window_compute_natural_height(self); if (natural_height != priv->natural_height) { priv->natural_height = natural_height; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_NATURAL_HEIGHT]); } return G_SOURCE_REMOVE; } static void on_stations_tree_view_populated(GtkWidget *stations_tree_view G_GNUC_UNUSED, GvMainWindow *self) { /* If the content of the stations tree view was modified, the natural size * changed also. However it's too early to compute the new size now. */ g_idle_add((GSourceFunc) when_idle_compute_natural_height, self); } static void on_stations_tree_view_realize(GtkWidget *stations_tree_view G_GNUC_UNUSED, GvMainWindow *self) { /* When the treeview is realized, we need to check AGAIN if the natural * height we have is correct. */ g_idle_add((GSourceFunc) when_idle_compute_natural_height, self); } static gboolean on_stations_tree_view_map_event(GtkWidget *stations_tree_view G_GNUC_UNUSED, GdkEvent *event G_GNUC_UNUSED, GvMainWindowManager *self) { /* When the treeview is mapped, we need to check AGAIN if the natural * height we have is correct. */ g_idle_add((GSourceFunc) when_idle_compute_natural_height, self); return GDK_EVENT_PROPAGATE; } /* * Popup window signal handlers */ #define POPUP_WINDOW_CLOSE_ON_FOCUS_OUT TRUE static gboolean on_popup_window_focus_change(GtkWindow *window, GdkEventFocus *focus_event, gpointer data G_GNUC_UNUSED) { GvMainWindow *self = GV_MAIN_WINDOW(window); GvMainWindowPrivate *priv = self->priv; GvStationsTreeView *stations_tree_view = GV_STATIONS_TREE_VIEW(priv->stations_tree_view); g_assert(focus_event->type == GDK_FOCUS_CHANGE); //DEBUG("Main window %s focus", focus_event->in ? "gained" : "lost"); if (focus_event->in) return GDK_EVENT_PROPAGATE; if (!POPUP_WINDOW_CLOSE_ON_FOCUS_OUT) return GDK_EVENT_PROPAGATE; if (gv_stations_tree_view_has_context_menu(stations_tree_view)) return GDK_EVENT_PROPAGATE; gtk_window_close(window); return GDK_EVENT_PROPAGATE; } static gboolean on_popup_window_key_press_event(GvMainWindow *self, GdkEventKey *event, gpointer data G_GNUC_UNUSED) { GvMainWindowPrivate *priv = self->priv; GvPlayer *player = gv_core_player; g_assert(event->type == GDK_KEY_PRESS); switch (event->keyval) { case GDK_KEY_Escape: /* Close window if is pressed */ gtk_window_close(GTK_WINDOW(self)); break; case GDK_KEY_space: /* Toggle playback if is pressed */ gtk_widget_grab_focus(priv->play_button); gv_player_toggle(player); return GDK_EVENT_STOP; // consume event default: break; } return GDK_EVENT_PROPAGATE; } /* * Standalone window signal handlers */ static gboolean on_standalone_window_delete_event(GvMainWindow *self G_GNUC_UNUSED, GdkEvent *event G_GNUC_UNUSED, gpointer data G_GNUC_UNUSED) { /* We're about to quit the application */ gv_core_quit(); /* Do not hide the window, as there might be a save operation pending, * and hiding would make the save operation fail. */ /* We must stop the event to prevent the window from being destroyed. * The window will be destroyed later on during the cleanup process. */ return GDK_EVENT_STOP; } static gboolean on_standalone_window_key_press_event(GvMainWindow *self, GdkEventKey *event, gpointer data G_GNUC_UNUSED) { GvMainWindowPrivate *priv = self->priv; GvPlayer *player = gv_core_player; g_assert(event->type == GDK_KEY_PRESS); switch (event->keyval) { case GDK_KEY_Escape: /* Iconify window if is pressed */ gtk_window_iconify(GTK_WINDOW(self)); break; case GDK_KEY_space: /* Toggle playback if is pressed */ gtk_widget_grab_focus(priv->play_button); gv_player_toggle(player); return GDK_EVENT_STOP; // consume event default: break; } return GDK_EVENT_PROPAGATE; } /* * Construct helpers */ static void setup_adjustment(GtkAdjustment *adjustment, GObject *obj, const gchar *obj_prop) { guint minimum, maximum; guint range; /* Get property bounds, and assign it to the adjustment */ g_object_get_property_uint_bounds(obj, obj_prop, &minimum, &maximum); range = maximum - minimum; gtk_adjustment_set_lower(adjustment, minimum); gtk_adjustment_set_upper(adjustment, maximum); gtk_adjustment_set_step_increment(adjustment, range / 100); gtk_adjustment_set_page_increment(adjustment, range / 10); } static void setup_action(GtkWidget *widget, const gchar *widget_signal, GCallback callback, GObject *object) { /* Signal handler */ g_signal_connect_object(widget, widget_signal, callback, object, 0); } static void setup_setting(GtkWidget *widget, const gchar *widget_prop, GObject *obj, const gchar *obj_prop) { /* Binding: obj 'prop' <-> widget 'prop' * Order matters, don't mix up source and target here... */ g_object_bind_property(obj, obj_prop, widget, widget_prop, G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); } /* * Public methods */ void gv_main_window_resize_height(GvMainWindow *self, gint height) { GtkWindow *window = GTK_WINDOW(self); gint width; DEBUG("Resizing height to %d", height); gtk_window_get_size(window, &width, NULL); gtk_window_resize(window, width, height); } GtkWidget * gv_main_window_new(GApplication *application, gboolean status_icon_mode) { return g_object_new(GV_TYPE_MAIN_WINDOW, "application", application, "status-icon-mode", status_icon_mode, NULL); } /* * Property accessors */ static void gv_main_window_set_status_icon_mode(GvMainWindow *self, gboolean status_icon_mode) { GvMainWindowPrivate *priv = self->priv; /* This is a construct-only property */ g_assert_false(priv->status_icon_mode); priv->status_icon_mode = status_icon_mode; } gint gv_main_window_get_natural_height(GvMainWindow *self) { return self->priv->natural_height; } GvMainWindowThemeVariant gv_main_window_get_theme_variant(GvMainWindow *self) { return self->priv->theme_variant; } void gv_main_window_set_theme_variant(GvMainWindow *self, GvMainWindowThemeVariant variant) { GvMainWindowPrivate *priv = self->priv; gboolean prefer_dark_theme; if (priv->theme_variant == variant) return; priv->theme_variant = variant; switch (variant) { case GV_MAIN_WINDOW_THEME_DARK: prefer_dark_theme = TRUE; break; case GV_MAIN_WINDOW_THEME_LIGHT: prefer_dark_theme = FALSE; break; case GV_MAIN_WINDOW_THEME_DEFAULT: default: prefer_dark_theme = priv->system_prefer_dark_theme; break; } g_object_set(gtk_settings_get_default(), "gtk-application-prefer-dark-theme", prefer_dark_theme, NULL); g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_THEME_VARIANT]); } static void gv_main_window_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvMainWindow *self = GV_MAIN_WINDOW(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_NATURAL_HEIGHT: g_value_set_int(value, gv_main_window_get_natural_height(self)); break; case PROP_THEME_VARIANT: g_value_set_enum(value, gv_main_window_get_theme_variant(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_main_window_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvMainWindow *self = GV_MAIN_WINDOW(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_STATUS_ICON_MODE: gv_main_window_set_status_icon_mode(self, g_value_get_boolean(value)); break; case PROP_THEME_VARIANT: gv_main_window_set_theme_variant(self, g_value_get_enum(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Construct helpers */ static void gv_main_window_populate_widgets(GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; GtkBuilder *builder; /* Build the ui */ builder = gtk_builder_new_from_resource(UI_RESOURCE_PATH); /* Save widget pointers */ /* Top-level */ GTK_BUILDER_SAVE_WIDGET(builder, priv, window_vbox); /* Current status */ GTK_BUILDER_SAVE_WIDGET(builder, priv, info_vbox); GTK_BUILDER_SAVE_WIDGET(builder, priv, station_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, status_label); /* Button box */ GTK_BUILDER_SAVE_WIDGET(builder, priv, play_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, prev_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, next_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, repeat_toggle_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, shuffle_toggle_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, volume_button); /* Stations tree view */ GTK_BUILDER_SAVE_WIDGET(builder, priv, scrolled_window); /* Now create the stations tree view */ priv->stations_tree_view = gv_stations_tree_view_new(); gtk_widget_show_all(priv->stations_tree_view); /* Add to scrolled window */ gtk_container_add(GTK_CONTAINER(priv->scrolled_window), priv->stations_tree_view); /* Add vbox to the window */ gtk_container_add(GTK_CONTAINER(self), priv->window_vbox); /* Cleanup */ g_object_unref(builder); } static void gv_main_window_setup_widgets(GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; GObject *player_obj = G_OBJECT(gv_core_player); /* Setup adjustments - must be done first, before setting widget values */ setup_adjustment(gtk_scale_button_get_adjustment(GTK_SCALE_BUTTON(priv->volume_button)), player_obj, "volume"); /* Setup funky label for info vbox */ gtk_widget_set_has_tooltip(priv->info_vbox, TRUE); g_signal_connect_object(priv->info_vbox, "query-tooltip", G_CALLBACK(on_info_vbox_query_tooltip), self, 0); /* Watch stations tree view */ g_signal_connect_object(priv->stations_tree_view, "populated", G_CALLBACK(on_stations_tree_view_populated), self, 0); g_signal_connect_object(priv->stations_tree_view, "realize", G_CALLBACK(on_stations_tree_view_realize), self, 0); g_signal_connect_object(priv->stations_tree_view, "map-event", G_CALLBACK(on_stations_tree_view_map_event), self, 0); /* * Setup settings and actions. * These function calls create the link between the widgets and the program. * - settings: the link is a binding between the gtk widget and an internal object. * - actions : the link is a callback invoked when the widget is clicked. This is a * one-way link from the ui to the application, and additional work would be needed * if the widget was to react to external changes. */ setup_action(priv->play_button, "clicked", G_CALLBACK(on_button_clicked), G_OBJECT(self)); setup_action(priv->prev_button, "clicked", G_CALLBACK(on_button_clicked), G_OBJECT(self)); setup_action(priv->next_button, "clicked", G_CALLBACK(on_button_clicked), G_OBJECT(self)); setup_setting(priv->repeat_toggle_button, "active", player_obj, "repeat"); setup_setting(priv->shuffle_toggle_button, "active", player_obj, "shuffle"); /* Volume button comes with automatic tooltip, so we just need to bind. * Plus, we need to remember the binding because we do strange things with it. */ priv->volume_binding = g_object_bind_property (player_obj, "volume", priv->volume_button, "value", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); } static void gv_main_window_setup_layout(GvMainWindow *self) { GvMainWindowPrivate *priv = self->priv; g_object_set(priv->window_vbox, "spacing", GV_UI_ELEM_SPACING, NULL); } static void gv_main_window_configure_for_popup(GvMainWindow *self) { GtkApplicationWindow *application_window = GTK_APPLICATION_WINDOW(self); GtkWindow *window = GTK_WINDOW(self); /* Basically, we want the window to appear and behave as a popup window */ /* Hide the menu bar in the main window */ gtk_application_window_set_show_menubar(application_window, FALSE); /* Window appearance */ gtk_window_set_decorated(window, FALSE); gtk_window_set_position(window, GTK_WIN_POS_MOUSE); /* We don't want the window to appear in pager or taskbar. * This has an undesired effect though: the window may not * have the focus when it's shown by the window manager. * But read on... */ gtk_window_set_skip_pager_hint(window, TRUE); gtk_window_set_skip_taskbar_hint(window, TRUE); /* Setting the window modal seems to ensure that the window * receives focus when shown by the window manager. */ gtk_window_set_modal(window, TRUE); /* We want the window to be hidden instead of destroyed when closed */ g_signal_connect_object(window, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL, 0); /* Handle some keys */ g_signal_connect_object(window, "key-press-event", G_CALLBACK(on_popup_window_key_press_event), NULL, 0); /* Handle keyboard focus changes, so that we can hide the * window on 'focus-out-event'. */ g_signal_connect_object(window, "focus-in-event", G_CALLBACK(on_popup_window_focus_change), NULL, 0); g_signal_connect_object(window, "focus-out-event", G_CALLBACK(on_popup_window_focus_change), NULL, 0); } static void gv_main_window_configure_for_standalone(GvMainWindow *self) { /* We want to quit the application when the window is closed */ g_signal_connect_object(self, "delete-event", G_CALLBACK(on_standalone_window_delete_event), NULL, 0); /* Handle some keys */ g_signal_connect_object(self, "key-press-event", G_CALLBACK(on_standalone_window_key_press_event), NULL, 0); } /* * GvConfigurable interface */ static void gv_main_window_configure(GvConfigurable *configurable) { GvMainWindow *self = GV_MAIN_WINDOW(configurable); TRACE("%p", self); /* We want to save the value of 'prefer-dark-theme' from GtkSettings * right now, as we might modify it later, and need a way to go back * to its default value. */ g_object_get(gtk_settings_get_default(), "gtk-application-prefer-dark-theme", &self->priv->system_prefer_dark_theme, NULL); g_assert(gv_ui_settings); g_settings_bind(gv_ui_settings, "theme-variant", self, "theme-variant", G_SETTINGS_BIND_DEFAULT); } static void gv_main_window_configurable_interface_init(GvConfigurableInterface *iface) { iface->configure = gv_main_window_configure; } /* * GObject methods */ static void gv_main_window_finalize(GObject *object) { GvMainWindow *self = GV_MAIN_WINDOW(object); GvMainWindowPrivate *priv = self->priv; TRACE("%p", object); /* Free resources */ g_clear_object(&priv->info_tooltip_grid); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_main_window, object); } static void gv_main_window_constructed(GObject *object) { GvMainWindow *self = GV_MAIN_WINDOW(object); GvMainWindowPrivate *priv = self->priv; GvPlayer *player = gv_core_player; TRACE("%p", self); /* Build window */ gv_main_window_populate_widgets(self); gv_main_window_setup_widgets(self); gv_main_window_setup_layout(self); /* Configure depending on the window mode */ if (priv->status_icon_mode) { DEBUG("Configuring main window for popup mode"); gv_main_window_configure_for_popup(self); } else { DEBUG("Configuring main window for standalone mode"); gv_main_window_configure_for_standalone(self); } /* Connect core signal handlers */ g_signal_connect_object(player, "notify", G_CALLBACK(on_player_notify), self, 0); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_main_window, object); } static void gv_main_window_init(GvMainWindow *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_main_window_get_instance_private(self); } static void gv_main_window_class_init(GvMainWindowClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_main_window_finalize; object_class->constructed = gv_main_window_constructed; /* Properties */ object_class->get_property = gv_main_window_get_property; object_class->set_property = gv_main_window_set_property; properties[PROP_STATUS_ICON_MODE] = g_param_spec_boolean("status-icon-mode", "Status icon mode", NULL, FALSE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE); properties[PROP_NATURAL_HEIGHT] = g_param_spec_int("natural-height", "Natural height", NULL, 0, G_MAXINT, 0, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READABLE); properties[PROP_THEME_VARIANT] = g_param_spec_enum("theme-variant", "Theme variant", NULL, GV_TYPE_MAIN_WINDOW_THEME_VARIANT, GV_MAIN_WINDOW_THEME_DEFAULT, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); /* Register transform function */ g_value_register_transform_func(GV_TYPE_MAIN_WINDOW_THEME_VARIANT, G_TYPE_STRING, gv_value_transform_enum_string); g_value_register_transform_func(G_TYPE_STRING, GV_TYPE_MAIN_WINDOW_THEME_VARIANT, gv_value_transform_string_enum); } goodvibes-v0.4.2/src/ui/gv-main-window.h000066400000000000000000000034261337147241200201150ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_MAIN_WINDOW_H__ #define __GOODVIBES_UI_GV_MAIN_WINDOW_H__ #include #include /* GObject declarations */ #define GV_TYPE_MAIN_WINDOW gv_main_window_get_type() G_DECLARE_FINAL_TYPE(GvMainWindow, gv_main_window, GV, MAIN_WINDOW, GtkApplicationWindow) /* Data types */ typedef enum { GV_MAIN_WINDOW_THEME_DEFAULT, GV_MAIN_WINDOW_THEME_DARK, GV_MAIN_WINDOW_THEME_LIGHT, } GvMainWindowThemeVariant; /* Methods */ GtkWidget *gv_main_window_new(GApplication *application, gboolean status_icon_mode); void gv_main_window_resize_height(GvMainWindow *self, gint height); /* Property accessors */ gint gv_main_window_get_natural_height (GvMainWindow *self); GvMainWindowThemeVariant gv_main_window_get_theme_variant (GvMainWindow *self); void gv_main_window_set_theme_variant (GvMainWindow *self, GvMainWindowThemeVariant variant); #endif /* __GOODVIBES_UI_GV_MAIN_WINDOW_H__ */ goodvibes-v0.4.2/src/ui/gv-prefs-window.c000066400000000000000000000447071337147241200203120ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "feat/gv-feat.h" #include "ui/gtk-additions.h" #include "ui/gv-ui-enum-types.h" #include "ui/gv-ui-internal.h" #include "ui/gv-prefs-window.h" #define UI_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/prefs-window.glade" /* * GObject definitions */ struct _GvPrefsWindowPrivate { /* * Features */ /* Controls */ GvFeature *hotkeys_feat; GvFeature *dbus_native_feat; GvFeature *dbus_mpris2_feat; /* Display */ GvFeature *notifications_feat; GvFeature *console_output_feat; /* Player */ GvFeature *inhibitor_feat; /* * Widgets */ /* Top-level */ GtkWidget *window_vbox; /* Misc */ GtkWidget *misc_vbox; GtkWidget *playback_frame; GtkWidget *playback_grid; GtkWidget *autoplay_check; GtkWidget *pipeline_check; GtkWidget *pipeline_entry; GtkWidget *pipeline_apply_button; GtkWidget *system_frame; GtkWidget *system_grid; GtkWidget *inhibitor_label; GtkWidget *inhibitor_switch; GtkWidget *dbus_frame; GtkWidget *dbus_grid; GtkWidget *dbus_native_label; GtkWidget *dbus_native_switch; GtkWidget *dbus_mpris2_label; GtkWidget *dbus_mpris2_switch; /* Display */ GtkWidget *display_vbox; GtkWidget *window_frame; GtkWidget *window_grid; GtkWidget *window_theme_combo; GtkWidget *window_autosize_check; GtkWidget *notif_frame; GtkWidget *notif_grid; GtkWidget *notif_enable_label; GtkWidget *notif_enable_switch; GtkWidget *console_frame; GtkWidget *console_grid; GtkWidget *console_output_label; GtkWidget *console_output_switch; /* Controls */ GtkWidget *controls_vbox; GtkWidget *keyboard_frame; GtkWidget *keyboard_grid; GtkWidget *hotkeys_label; GtkWidget *hotkeys_switch; GtkWidget *mouse_frame; GtkWidget *mouse_grid; GtkWidget *middle_click_action_label; GtkWidget *middle_click_action_combo; GtkWidget *scroll_action_label; GtkWidget *scroll_action_combo; /* Buttons */ GtkWidget *close_button; }; typedef struct _GvPrefsWindowPrivate GvPrefsWindowPrivate; struct _GvPrefsWindow { /* Parent instance structure */ GtkWindow parent_instance; /* Private data */ GvPrefsWindowPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvPrefsWindow, gv_prefs_window, GTK_TYPE_WINDOW) /* * Gtk signal handlers */ static void on_pipeline_apply_button_clicked(GtkButton *button G_GNUC_UNUSED, GvPrefsWindow *self) { GvPrefsWindowPrivate *priv = self->priv; GtkEntry *pipeline_entry = GTK_ENTRY(priv->pipeline_entry); const gchar *text; text = gtk_entry_get_text(pipeline_entry); if (text) { /* Strip leading and trailing whitespaces */ gchar *stripped; stripped = g_strstrip(g_strdup(text)); gtk_entry_set_text(pipeline_entry, stripped); g_free(stripped); text = gtk_entry_get_text(pipeline_entry); } if (text && strlen(text) > 0) { gv_player_set_pipeline_string(gv_core_player, text); } else { gv_player_set_pipeline_string(gv_core_player, NULL); } } static void on_close_button_clicked(GtkButton *button G_GNUC_UNUSED, GvPrefsWindow *self) { GtkWindow *window = GTK_WINDOW(self); gtk_window_close(window); } static gboolean on_window_key_press_event(GvPrefsWindow *self, GdkEventKey *event, gpointer data G_GNUC_UNUSED) { GtkWindow *window = GTK_WINDOW(self); g_assert(event->type == GDK_KEY_PRESS); if (event->keyval == GDK_KEY_Escape) gtk_window_close(window); return GDK_EVENT_PROPAGATE; } /* * GBinding transform functions */ static gboolean transform_pipeline_string_to(GBinding *binding G_GNUC_UNUSED, const GValue *from_value, GValue *to_value, gpointer user_data G_GNUC_UNUSED) { const gchar *str; /* GtkEntry complains if we set the 'text' property to NULL... * [Gtk] gtk_entry_set_text: assertion 'text != NULL' failed */ str = g_value_get_string(from_value); if (str == NULL) g_value_set_static_string(to_value, ""); else g_value_set_string(to_value, str); return TRUE; } /* * Construct helpers */ static void setup_notebook_page_appearance(GtkWidget *vbox) { g_return_if_fail(GTK_IS_BOX(vbox)); g_object_set(vbox, "margin", GV_UI_WINDOW_BORDER, "spacing", GV_UI_GROUP_SPACING, NULL); } static void setup_section_appearance(GtkWidget *frame, GtkWidget *grid) { static PangoAttrList *bold_attr_list; GtkWidget *label; g_return_if_fail(GTK_IS_FRAME(frame)); g_return_if_fail(GTK_IS_GRID(grid)); if (bold_attr_list == NULL) { bold_attr_list = pango_attr_list_new(); pango_attr_list_insert(bold_attr_list, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); } label = gtk_frame_get_label_widget(GTK_FRAME(frame)); if (label) { gtk_label_set_attributes(GTK_LABEL(label), bold_attr_list); gtk_widget_set_margin_bottom(label, GV_UI_ELEM_SPACING); } g_object_set(grid, "row-spacing", GV_UI_ELEM_SPACING, "column-spacing", GV_UI_COLUMN_SPACING, NULL); } static void setdown_widget(const gchar *tooltip_text, GtkWidget *widget) { if (tooltip_text) gtk_widget_set_tooltip_text(widget, tooltip_text); gtk_widget_set_sensitive(widget, FALSE); } static void setup_setting(const gchar *tooltip_text, GtkWidget *label, GtkWidget *widget, const gchar *widget_prop, GObject *obj, const gchar *obj_prop, GBindingTransformFunc transform_to, GBindingTransformFunc transform_from) { /* Tooltip */ if (tooltip_text) { if (widget) gtk_widget_set_tooltip_text(widget, tooltip_text); if (label) gtk_widget_set_tooltip_text(label, tooltip_text); } /* Binding: obj 'prop' <-> widget 'prop' * Order matters, don't mix up source and target here... */ if (!widget || !obj) return; g_object_bind_property_full(obj, obj_prop, widget, widget_prop, G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE, transform_to, transform_from, NULL, NULL); } static void setup_feature(const gchar *tooltip_text, GtkWidget *label, GtkWidget *sw, GvFeature *feat) { /* If feat is NULL, it's because it's been disabled at compile time */ if (feat == NULL) { tooltip_text = _("Feature disabled at compile-time."); gtk_widget_set_tooltip_text(sw, tooltip_text); gtk_widget_set_tooltip_text(label, tooltip_text); gtk_widget_set_sensitive(label, FALSE); gtk_widget_set_sensitive(sw, FALSE); return; } /* Tooltip */ if (tooltip_text) { gtk_widget_set_tooltip_text(sw, tooltip_text); gtk_widget_set_tooltip_text(label, tooltip_text); } /* Binding: feature 'enabled' <-> switch 'active' * Order matters, don't mix up source and target here... */ g_object_bind_property(feat, "enabled", sw, "active", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); } /* * Public methods */ GtkWidget * gv_prefs_window_new(void) { return g_object_new(GV_TYPE_PREFS_WINDOW, NULL); } /* * Construct helpers */ static void gv_prefs_window_populate_features(GvPrefsWindow *self) { GvPrefsWindowPrivate *priv = self->priv; /* Controls */ priv->hotkeys_feat = gv_feat_find("Hotkeys"); priv->dbus_native_feat = gv_feat_find("DBusServerNative"); priv->dbus_mpris2_feat = gv_feat_find("DBusServerMpris2"); /* Display */ priv->notifications_feat = gv_feat_find("Notifications"); priv->console_output_feat = gv_feat_find("ConsoleOutput"); /* Player */ priv->inhibitor_feat = gv_feat_find("Inhibitor"); } static void gv_prefs_window_populate_widgets(GvPrefsWindow *self) { GvPrefsWindowPrivate *priv = self->priv; GtkBuilder *builder; /* Build the ui */ builder = gtk_builder_new_from_resource(UI_RESOURCE_PATH); /* Save widget pointers */ /* Top-level */ GTK_BUILDER_SAVE_WIDGET(builder, priv, window_vbox); /* Misc */ GTK_BUILDER_SAVE_WIDGET(builder, priv, misc_vbox); GTK_BUILDER_SAVE_WIDGET(builder, priv, playback_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, playback_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, autoplay_check); GTK_BUILDER_SAVE_WIDGET(builder, priv, pipeline_check); GTK_BUILDER_SAVE_WIDGET(builder, priv, pipeline_entry); GTK_BUILDER_SAVE_WIDGET(builder, priv, pipeline_apply_button); GTK_BUILDER_SAVE_WIDGET(builder, priv, system_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, system_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, inhibitor_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, inhibitor_switch); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_native_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_native_switch); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_mpris2_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, dbus_mpris2_switch); /* Display */ GTK_BUILDER_SAVE_WIDGET(builder, priv, display_vbox); GTK_BUILDER_SAVE_WIDGET(builder, priv, window_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, window_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, window_theme_combo); GTK_BUILDER_SAVE_WIDGET(builder, priv, window_autosize_check); GTK_BUILDER_SAVE_WIDGET(builder, priv, notif_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, notif_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, notif_enable_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, notif_enable_switch); GTK_BUILDER_SAVE_WIDGET(builder, priv, console_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, console_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, console_output_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, console_output_switch); /* Controls */ GTK_BUILDER_SAVE_WIDGET(builder, priv, controls_vbox); GTK_BUILDER_SAVE_WIDGET(builder, priv, keyboard_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, keyboard_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, hotkeys_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, hotkeys_switch); GTK_BUILDER_SAVE_WIDGET(builder, priv, mouse_frame); GTK_BUILDER_SAVE_WIDGET(builder, priv, mouse_grid); GTK_BUILDER_SAVE_WIDGET(builder, priv, middle_click_action_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, middle_click_action_combo); GTK_BUILDER_SAVE_WIDGET(builder, priv, scroll_action_label); GTK_BUILDER_SAVE_WIDGET(builder, priv, scroll_action_combo); /* Action area */ GTK_BUILDER_SAVE_WIDGET(builder, priv, close_button); /* Pack that within the window */ gtk_container_add(GTK_CONTAINER(self), priv->window_vbox); /* Cleanup */ g_object_unref(builder); } static void gv_prefs_window_setup_widgets(GvPrefsWindow *self) { GvPrefsWindowPrivate *priv = self->priv; GObject *main_window_mgr_obj = G_OBJECT(gv_ui_main_window_manager); GObject *main_window_obj = G_OBJECT(gv_ui_main_window); GObject *status_icon_obj = G_OBJECT(gv_ui_status_icon); GObject *player_obj = G_OBJECT(gv_core_player); /* * Setup settings and features. * These function calls create a binding between a gtk widget and * an internal object, initializes the widget value, and set the * widgets tooltips (label + setting). */ /* Misc */ setup_setting(_("Whether to start playback automatically on startup."), NULL, priv->autoplay_check, "active", player_obj, "autoplay", NULL, NULL); setup_setting(_("Whether to use a custom output pipeline."), NULL, priv->pipeline_check, "active", player_obj, "pipeline-enabled", NULL, NULL); setup_setting(_("The GStreamer output pipeline used for playback. Refer to the" "online documentation for examples."), NULL, priv->pipeline_entry, NULL, NULL, NULL, NULL, NULL); g_object_bind_property_full(player_obj, "pipeline-string", priv->pipeline_entry, "text", G_BINDING_SYNC_CREATE, transform_pipeline_string_to, NULL, NULL, NULL); g_signal_connect_object(priv->pipeline_apply_button, "clicked", G_CALLBACK(on_pipeline_apply_button_clicked), self, 0); setup_feature(_("Prevent the system from going to sleep while playing."), priv->inhibitor_label, priv->inhibitor_switch, priv->inhibitor_feat); setup_feature(_("Enable the native D-Bus server " "(needed for the command-line interface)."), priv->dbus_native_label, priv->dbus_native_switch, priv->dbus_native_feat); setup_feature(_("Enable the MPRIS2 D-Bus server."), priv->dbus_mpris2_label, priv->dbus_mpris2_switch, priv->dbus_mpris2_feat); /* Display */ setup_setting(_("Prefer a different variant of the theme (if available)."), NULL, priv->window_theme_combo, "active-id", main_window_obj, "theme-variant", NULL, NULL); if (!status_icon_obj) { setup_setting(_("Automatically adjust the window height when a station " "is added or removed."), NULL, priv->window_autosize_check, "active", main_window_mgr_obj, "autoset-height", NULL, NULL); } else { setdown_widget(_("Setting not available in status icon mode."), priv->window_frame); } setup_feature(_("Show notification when the status changes."), priv->notif_enable_label, priv->notif_enable_switch, priv->notifications_feat); setup_feature(_("Display information on the standard output."), priv->console_output_label, priv->console_output_switch, priv->console_output_feat); /* Controls */ setup_feature(_("Bind mutimedia keys (play/pause/stop/previous/next)."), priv->hotkeys_label, priv->hotkeys_switch, priv->hotkeys_feat); if (status_icon_obj) { setup_setting(_("Action triggered by a middle click on the status icon."), priv->middle_click_action_label, priv->middle_click_action_combo, "active-id", status_icon_obj, "middle-click-action", NULL, NULL); setup_setting(_("Action triggered by mouse-scrolling on the status icon."), priv->scroll_action_label, priv->scroll_action_combo, "active-id", status_icon_obj, "scroll-action", NULL, NULL); } else { setdown_widget(_("Setting only available in status icon mode."), priv->mouse_frame); } /* * Some widgets have conditional sensitivity. */ g_object_bind_property(priv->pipeline_check, "active", priv->pipeline_entry, "sensitive", G_BINDING_SYNC_CREATE); g_object_bind_property(priv->pipeline_check, "active", priv->pipeline_apply_button, "sensitive", G_BINDING_SYNC_CREATE); } static void gv_prefs_window_setup_appearance(GvPrefsWindow *self) { GvPrefsWindowPrivate *priv = self->priv; /* Window */ g_object_set(priv->window_vbox, "margin", 0, "spacing", 0, NULL); /* Misc */ setup_notebook_page_appearance(priv->misc_vbox); setup_section_appearance(priv->playback_frame, priv->playback_grid); setup_section_appearance(priv->system_frame, priv->system_grid); setup_section_appearance(priv->dbus_frame, priv->dbus_grid); /* Display */ setup_notebook_page_appearance(priv->display_vbox); setup_section_appearance(priv->window_frame, priv->window_grid); setup_section_appearance(priv->notif_frame, priv->notif_grid); setup_section_appearance(priv->console_frame, priv->console_grid); /* Controls */ setup_notebook_page_appearance(priv->controls_vbox); setup_section_appearance(priv->keyboard_frame, priv->keyboard_grid); setup_section_appearance(priv->mouse_frame, priv->mouse_grid); } /* * GObject methods */ static void gv_prefs_window_finalize(GObject *object) { TRACE("%p", object); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_prefs_window, object); } static void gv_prefs_window_constructed(GObject *object) { GvPrefsWindow *self = GV_PREFS_WINDOW(object); GvPrefsWindowPrivate *priv = self->priv; /* Build the window */ gv_prefs_window_populate_features(self); gv_prefs_window_populate_widgets(self); gv_prefs_window_setup_widgets(self); gv_prefs_window_setup_appearance(self); /* Connect signal handlers */ g_signal_connect_object(priv->close_button, "clicked", G_CALLBACK(on_close_button_clicked), self, 0); g_signal_connect_object(self, "key_press_event", G_CALLBACK(on_window_key_press_event), NULL, 0); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_prefs_window, object); } static void gv_prefs_window_init(GvPrefsWindow *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_prefs_window_get_instance_private(self); } static void gv_prefs_window_class_init(GvPrefsWindowClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_prefs_window_finalize; object_class->constructed = gv_prefs_window_constructed; } /* * Convenience functions */ static GtkWidget * make_prefs_window(GtkWindow *parent) { GtkWidget *window; window = gv_prefs_window_new(); gtk_window_set_transient_for(GTK_WINDOW(window), parent); gtk_window_set_destroy_with_parent(GTK_WINDOW(window), TRUE); gtk_window_set_title(GTK_WINDOW(window), _("Preferences")); return window; } void gv_show_prefs_window(GtkWindow *parent) { static GtkWidget *prefs; /* Create if needed */ if (prefs == NULL) { prefs = make_prefs_window(parent); g_object_add_weak_pointer(G_OBJECT(prefs), (gpointer *) &prefs); } /* Present */ gtk_window_present(GTK_WINDOW(prefs)); } goodvibes-v0.4.2/src/ui/gv-prefs-window.h000066400000000000000000000023501337147241200203030ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_PREFS_WINDOW_H__ #define __GOODVIBES_UI_GV_PREFS_WINDOW_H__ #include #include /* GObject declarations */ #define GV_TYPE_PREFS_WINDOW gv_prefs_window_get_type() G_DECLARE_FINAL_TYPE(GvPrefsWindow, gv_prefs_window, GV, PREFS_WINDOW, GtkWindow) /* Convenience functions */ void gv_show_prefs_window(GtkWindow *parent); /* Methods */ GtkWidget *gv_prefs_window_new(void); #endif /* __GOODVIBES_UI_GV_PREFS_WINDOW_H__ */ goodvibes-v0.4.2/src/ui/gv-station-context-menu.c000066400000000000000000000163431337147241200217660ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gtk-additions.h" #include "ui/gv-station-dialog.h" #include "ui/gv-ui-internal.h" #include "ui/gv-station-context-menu.h" #define ADD_STATION_LABEL _("Add Station") #define REMOVE_STATION_LABEL _("Remove Station") #define EDIT_STATION_LABEL _("Edit Station") /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_STATION, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvStationContextMenuPrivate { /* Widgets */ GtkWidget *add_station_menu_item; GtkWidget *remove_station_menu_item; GtkWidget *edit_station_menu_item; /* Selected station if any */ GvStation *station; }; typedef struct _GvStationContextMenuPrivate GvStationContextMenuPrivate; struct _GvStationContextMenu { /* Parent instance structure */ GtkMenu parent_instance; /* Private data */ GvStationContextMenuPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvStationContextMenu, gv_station_context_menu, GTK_TYPE_MENU) /* * Signal handlers & callbacks */ static void on_menu_item_activate(GtkMenuItem *item, GvStationContextMenu *self) { GvStationList *station_list = gv_core_station_list; GvStationContextMenuPrivate *priv = self->priv; GvStation *selected_station = priv->station; GtkWidget *widget = GTK_WIDGET(item); if (widget == priv->add_station_menu_item) { GvStation *station; station = gv_show_add_station_dialog(GTK_WINDOW(gv_ui_main_window)); if (station) gv_station_list_insert_after(station_list, station, selected_station); } else if (widget == priv->edit_station_menu_item && selected_station) { gv_show_edit_station_dialog(GTK_WINDOW(gv_ui_main_window), selected_station); } else if (widget == priv->remove_station_menu_item && selected_station) { gv_station_list_remove(station_list, selected_station); } else { CRITICAL("Unhandled menu item %p", item); } } /* * Private methods */ static void gv_station_context_menu_populate(GvStationContextMenu *self) { GvStationContextMenuPrivate *priv = self->priv; GtkWidget *widget; /* This is supposed to be called once only */ g_assert_null(gtk_container_get_children(GTK_CONTAINER(self))); /* Add station */ widget = gtk_menu_item_new_with_label(ADD_STATION_LABEL); gtk_menu_shell_append(GTK_MENU_SHELL(self), widget); g_signal_connect_object(widget, "activate", G_CALLBACK(on_menu_item_activate), self, 0); priv->add_station_menu_item = widget; /* In case the station list is empty, we have no station here. * We must handle this case. */ if (priv->station) { /* Remove station */ widget = gtk_menu_item_new_with_label(REMOVE_STATION_LABEL); gtk_menu_shell_append(GTK_MENU_SHELL(self), widget); g_signal_connect_object(widget, "activate", G_CALLBACK(on_menu_item_activate), self, 0); priv->remove_station_menu_item = widget; /* Edit station */ widget = gtk_menu_item_new_with_label(EDIT_STATION_LABEL); gtk_menu_shell_append(GTK_MENU_SHELL(self), widget); g_signal_connect_object(widget, "activate", G_CALLBACK(on_menu_item_activate), self, 0); priv->edit_station_menu_item = widget; } /* Showtime */ gtk_widget_show_all(GTK_WIDGET(self)); } /* * Property accessors */ static void gv_station_context_menu_set_station(GvStationContextMenu *self, GvStation *station) { GvStationContextMenuPrivate *priv = self->priv; /* This is a construct-only property - NULL is allowed */ g_assert_null(priv->station); g_set_object(&priv->station, station); } static void gv_station_context_menu_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { TRACE_GET_PROPERTY(object, property_id, value, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } static void gv_station_context_menu_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvStationContextMenu *self = GV_STATION_CONTEXT_MENU(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_STATION: gv_station_context_menu_set_station(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ GtkWidget * gv_station_context_menu_new(void) { return g_object_new(GV_TYPE_STATION_CONTEXT_MENU, NULL); } GtkWidget * gv_station_context_menu_new_with_station(GvStation *station) { return g_object_new(GV_TYPE_STATION_CONTEXT_MENU, "station", station, NULL); } /* * GObject methods */ static void gv_station_context_menu_finalize(GObject *object) { GvStationContextMenu *self = GV_STATION_CONTEXT_MENU(object); GvStationContextMenuPrivate *priv = self->priv; TRACE("%p", object); /* Unref station */ if (priv->station) g_object_unref(priv->station); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_station_context_menu, object); } static void gv_station_context_menu_constructed(GObject *object) { GvStationContextMenu *self = GV_STATION_CONTEXT_MENU(object); TRACE("%p", object); /* Populate */ gv_station_context_menu_populate(self); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_station_context_menu, object); } static void gv_station_context_menu_init(GvStationContextMenu *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_station_context_menu_get_instance_private(self); } static void gv_station_context_menu_class_init(GvStationContextMenuClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_station_context_menu_finalize; object_class->constructed = gv_station_context_menu_constructed; /* Properties */ object_class->get_property = gv_station_context_menu_get_property; object_class->set_property = gv_station_context_menu_set_property; properties[PROP_STATION] = g_param_spec_object("station", "Station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); g_object_class_install_properties(object_class, PROP_N, properties); } goodvibes-v0.4.2/src/ui/gv-station-context-menu.h000066400000000000000000000025551337147241200217730ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_STATION_CONTEXT_MENU_H__ #define __GOODVIBES_UI_GV_STATION_CONTEXT_MENU_H__ #include #include #include "core/gv-station.h" /* GObject declarations */ #define GV_TYPE_STATION_CONTEXT_MENU gv_station_context_menu_get_type() G_DECLARE_FINAL_TYPE(GvStationContextMenu, gv_station_context_menu, \ GV, STATION_CONTEXT_MENU, GtkMenu) /* Methods */ GtkWidget *gv_station_context_menu_new (void); GtkWidget *gv_station_context_menu_new_with_station(GvStation *station); #endif /* __GOODVIBES_UI_GV_STATION_CONTEXT_MENU_H__ */ goodvibes-v0.4.2/src/ui/gv-station-dialog.c000066400000000000000000000306061337147241200205750ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gtk-additions.h" #include "ui/gv-ui-internal.h" #include "ui/gv-station-dialog.h" #define UI_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/station-dialog.glade" /* * Properties */ enum { /* Reserved */ PROP_0, /* Properties */ PROP_STATION, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvStationDialogPrivate { /* Widgets */ /* Top-level */ GtkWidget *main_grid; /* Entries */ GtkWidget *name_entry; GtkWidget *uri_entry; /* Buttons */ GtkWidget *save_button; /* Existing station if any */ GvStation *station; }; typedef struct _GvStationDialogPrivate GvStationDialogPrivate; struct _GvStationDialog { /* Parent instance structure */ GtkDialog parent_instance; /* Private data */ GvStationDialogPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvStationDialog, gv_station_dialog, GTK_TYPE_DIALOG) /* * Helpers */ static void remove_weird_chars(const gchar *text, gchar **out, guint *out_len) { gchar *start, *ptr; guint length, i; length = strlen(text); start = g_malloc(length + 1); for (i = 0, ptr = start; i < length; i++) { /* Discard every character below space */ if (text[i] > ' ') *ptr++ = text[i]; } *ptr = '\0'; if (length != ptr - start) { length = ptr - start; start = g_realloc(start, length); } *out = start; *out_len = length; } /* * Signal handlers */ static void on_uri_entry_insert_text(GtkEditable *editable, gchar *text, gint length G_GNUC_UNUSED, gpointer position, GvStationDialog *self) { gchar *new_text; guint new_len; /* Remove weird characters */ remove_weird_chars(text, &new_text, &new_len); /* Replace text in entry */ g_signal_handlers_block_by_func(editable, on_uri_entry_insert_text, self); gtk_editable_insert_text(editable, new_text, new_len, position); g_signal_handlers_unblock_by_func(editable, on_uri_entry_insert_text, self); /* We inserted the text in the entry, so now we just need to * prevent the default handler to run. */ g_signal_stop_emission_by_name(editable, "insert-text"); /* Free */ g_free(new_text); } static void on_uri_entry_changed(GtkEditable *editable, GvStationDialog *self) { GvStationDialogPrivate *priv = self->priv; guint text_len; text_len = gtk_entry_get_text_length(GTK_ENTRY(editable)); /* If the entry is empty, the save button is not clickable */ if (text_len > 0) gtk_widget_set_sensitive(priv->save_button, TRUE); else gtk_widget_set_sensitive(priv->save_button, FALSE); } /* * Property accessors */ static void gv_station_dialog_set_station(GvStationDialog *self, GvStation *station) { GvStationDialogPrivate *priv = self->priv; /* This is a construct-only property - NULL is allowed */ g_assert_null(priv->station); g_set_object(&priv->station, station); } static void gv_station_dialog_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { TRACE_GET_PROPERTY(object, property_id, value, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); } static void gv_station_dialog_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvStationDialog *self = GV_STATION_DIALOG(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_STATION: gv_station_dialog_set_station(self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ void gv_station_dialog_retrieve(GvStationDialog *self, GvStation *station) { GvStationDialogPrivate *priv = self->priv; GtkEntry *name_entry = GTK_ENTRY(priv->name_entry); GtkEntry *uri_entry = GTK_ENTRY(priv->uri_entry); gv_station_set_name(station, gtk_entry_get_text(name_entry)); gv_station_set_uri(station, gtk_entry_get_text(uri_entry)); } GvStation * gv_station_dialog_create(GvStationDialog *self) { GvStationDialogPrivate *priv = self->priv; GtkEntry *name_entry = GTK_ENTRY(priv->name_entry); GtkEntry *uri_entry = GTK_ENTRY(priv->uri_entry); const gchar *name, *uri; name = gtk_entry_get_text(name_entry); uri = gtk_entry_get_text(uri_entry); return gv_station_new(name, uri); } void gv_station_dialog_fill_uri(GvStationDialog *self, const gchar *uri) { GvStationDialogPrivate *priv = self->priv; GtkEntry *uri_entry = GTK_ENTRY(priv->uri_entry); gtk_entry_set_text(uri_entry, uri); } GtkWidget * gv_station_dialog_new(GvStation *station) { return g_object_new(GV_TYPE_STATION_DIALOG, "station", station, NULL); } /* * Construct helpers */ static void gv_station_dialog_populate_widgets(GvStationDialog *self) { GvStationDialogPrivate *priv = self->priv; GtkWidget *content_area; GtkBuilder *builder; /* Build the ui */ builder = gtk_builder_new_from_resource(UI_RESOURCE_PATH); /* Save widget pointers */ /* Top-level */ GTK_BUILDER_SAVE_WIDGET(builder, priv, main_grid); /* Text entries */ GTK_BUILDER_SAVE_WIDGET(builder, priv, name_entry); GTK_BUILDER_SAVE_WIDGET(builder, priv, uri_entry); /* Add to content area */ content_area = gtk_dialog_get_content_area(GTK_DIALOG(self)); gtk_container_add(GTK_CONTAINER(content_area), priv->main_grid); /* Populate the action area */ gtk_dialog_add_buttons(GTK_DIALOG(self), _("Cancel"), GTK_RESPONSE_CANCEL, _("Save"), GTK_RESPONSE_OK, NULL); priv->save_button = gtk_dialog_get_widget_for_response (GTK_DIALOG(self), GTK_RESPONSE_OK); /* Cleanup */ g_object_unref(builder); } static void gv_station_dialog_setup_widgets(GvStationDialog *self) { GvStationDialogPrivate *priv = self->priv; GtkEntry *name_entry = GTK_ENTRY(priv->name_entry); GtkEntry *uri_entry = GTK_ENTRY(priv->uri_entry); GvStation *station = priv->station; /* We don't allow creating a station without an empty uri, therefore * the save button is insensitive when the uri empty. We can set it * insensitive now because of the following code... * Watch out if you change something here ! */ gtk_widget_set_sensitive(priv->save_button, FALSE); /* Configure uri widget */ gtk_entry_set_input_purpose(uri_entry, GTK_INPUT_PURPOSE_URL); g_signal_connect_object(uri_entry, "insert-text", G_CALLBACK(on_uri_entry_insert_text), self, 0); g_signal_connect_object(uri_entry, "changed", G_CALLBACK(on_uri_entry_changed), self, 0); /* Fill widgets with station info */ if (station) { const gchar *name, *uri; name = gv_station_get_name(station); if (name) gtk_entry_set_text(name_entry, name); uri = gv_station_get_uri(station); if (uri) gtk_entry_set_text(uri_entry, uri); } } static void gv_station_dialog_setup_appearance(GvStationDialog *self) { GvStationDialogPrivate *priv = self->priv; /* Main window */ gtk_window_set_default_size(GTK_WINDOW(self), 400, -1); /* Main grid */ g_object_set(priv->main_grid, "margin", GV_UI_WINDOW_BORDER, "row-spacing", GV_UI_ELEM_SPACING, "column-spacing", GV_UI_COLUMN_SPACING, NULL); } /* * GObject methods */ static void gv_station_dialog_finalize(GObject *object) { GvStationDialog *self = GV_STATION_DIALOG(object); GvStationDialogPrivate *priv = self->priv; TRACE("%p", object); /* Unref station */ if (priv->station) g_object_unref(priv->station); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_station_dialog, object); } static void gv_station_dialog_constructed(GObject *object) { GvStationDialog *self = GV_STATION_DIALOG(object); /* Build window */ gv_station_dialog_populate_widgets(self); gv_station_dialog_setup_widgets(self); gv_station_dialog_setup_appearance(self); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_station_dialog, object); } static void gv_station_dialog_init(GvStationDialog *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_station_dialog_get_instance_private(self); } static void gv_station_dialog_class_init(GvStationDialogClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_station_dialog_finalize; object_class->constructed = gv_station_dialog_constructed; /* Properties */ object_class->get_property = gv_station_dialog_get_property; object_class->set_property = gv_station_dialog_set_property; properties[PROP_STATION] = g_param_spec_object("station", "Station", NULL, GV_TYPE_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); g_object_class_install_properties(object_class, PROP_N, properties); } /* * Convenience functions */ static GtkWidget * make_station_dialog(GtkWindow *parent, GvStation *station) { GtkWidget *dialog; dialog = gv_station_dialog_new(station); gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE); gtk_window_set_transient_for(GTK_WINDOW(dialog), parent); gtk_window_set_destroy_with_parent(GTK_WINDOW(dialog), TRUE); gtk_window_set_title(GTK_WINDOW(dialog), station ? _("Edit Station") : _("Add Station")); /* When 'Add Station' is invoked from the popup menu, in status icon mode, * we want a better position than centering on screen. */ if (!gtk_widget_is_visible(GTK_WIDGET(parent))) gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE); return dialog; } void gv_show_edit_station_dialog(GtkWindow *parent, GvStation *station) { GtkWidget *dialog; gint response; /* Create and configure the dialog */ dialog = make_station_dialog(parent, station); /* Run */ response = gtk_dialog_run(GTK_DIALOG(dialog)); if (response == GTK_RESPONSE_OK) gv_station_dialog_retrieve(GV_STATION_DIALOG(dialog), station); /* Cleanup */ gtk_widget_destroy(dialog); } GvStation * gv_show_add_station_dialog(GtkWindow *parent) { GvStation *station; GtkWidget *dialog; gint response; /* Create and configure the dialog */ dialog = make_station_dialog(parent, NULL); /* When we're asked to display an empty station dialog, we play a little trick. * If the application was started with an uri in argument, it's possible that * the current station is not part of the station list. In such case, we assume * that the user intends to add this station to the list, so we save him a bit * of time and populate the dialog with this uri. */ GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; GvStation *current_station; current_station = gv_player_get_station(player); if (current_station && gv_station_list_find(station_list, current_station) == NULL) { gv_station_dialog_fill_uri(GV_STATION_DIALOG(dialog), gv_station_get_uri(current_station)); } /* Run */ response = gtk_dialog_run(GTK_DIALOG(dialog)); if (response == GTK_RESPONSE_OK) station = gv_station_dialog_create(GV_STATION_DIALOG(dialog)); else station = NULL; /* Cleanup */ gtk_widget_destroy(dialog); return station; } goodvibes-v0.4.2/src/ui/gv-station-dialog.h000066400000000000000000000031521337147241200205760ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_STATION_DIALOG_H__ #define __GOODVIBES_UI_GV_STATION_DIALOG_H__ #include #include #include "core/gv-station.h" /* GObject declarations */ #define GV_TYPE_STATION_DIALOG gv_station_dialog_get_type() G_DECLARE_FINAL_TYPE(GvStationDialog, gv_station_dialog, GV, STATION_DIALOG, GtkDialog) /* Convenience functions */ GvStation *gv_show_add_station_dialog (GtkWindow *parent); void gv_show_edit_station_dialog(GtkWindow *parent, GvStation *station); /* Methods */ GtkWidget *gv_station_dialog_new (GvStation *station); void gv_station_dialog_fill_uri(GvStationDialog *dialog, const gchar *uri); void gv_station_dialog_retrieve(GvStationDialog *self, GvStation *station); GvStation *gv_station_dialog_create (GvStationDialog *self); #endif /* __GOODVIBES_UI_GV_STATION_DIALOG_H__ */ goodvibes-v0.4.2/src/ui/gv-stations-tree-view.c000066400000000000000000000551151337147241200214320ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gv-station-context-menu.h" #include "ui/gv-stations-tree-view.h" /* * Signals */ enum { SIGNAL_POPULATED, /* Number of signals */ SIGNAL_N }; static guint signals[SIGNAL_N]; /* * GObject definitions */ struct _GvStationsTreeViewPrivate { /* Current context menu */ GtkWidget *context_menu; /* Dragging operation in progress */ gboolean is_dragging; GvStation *station_dragged; gint station_new_pos; }; typedef struct _GvStationsTreeViewPrivate GvStationsTreeViewPrivate; struct _GvStationsTreeView { /* Parent instance structure */ GtkTreeView parent_instance; /* Private data */ GvStationsTreeViewPrivate *priv; }; G_DEFINE_TYPE_WITH_PRIVATE(GvStationsTreeView, gv_stations_tree_view, GTK_TYPE_TREE_VIEW) /* * Columns */ enum { STATION_COLUMN, STATION_NAME_COLUMN, STATION_WEIGHT_COLUMN, STATION_STYLE_COLUMN, N_COLUMNS }; /* * Player signal handlers */ static void on_player_notify_station(GvPlayer *player, GParamSpec *pspec G_GNUC_UNUSED, GvStationsTreeView *self) { GtkTreeView *tree_view = GTK_TREE_VIEW(self); GtkTreeModel *tree_model = gtk_tree_view_get_model(tree_view); GvStation *station = gv_player_get_station(player); GtkTreeIter iter; gboolean can_iter; DEBUG("Notify from player"); can_iter = gtk_tree_model_get_iter_first(tree_model, &iter); while (can_iter) { GvStation *iter_station; /* Get station from model */ gtk_tree_model_get(tree_model, &iter, STATION_COLUMN, &iter_station, -1); /* Make the current station bold */ if (station == iter_station) gtk_list_store_set(GTK_LIST_STORE(tree_model), &iter, STATION_WEIGHT_COLUMN, PANGO_WEIGHT_BOLD, -1); else gtk_list_store_set(GTK_LIST_STORE(tree_model), &iter, STATION_WEIGHT_COLUMN, PANGO_WEIGHT_NORMAL, -1); /* Unref the station */ if (iter_station) g_object_unref(iter_station); /* Next ! */ can_iter = gtk_tree_model_iter_next(tree_model, &iter); } } /* * Station List signal handlers * Needed to update the internal list store when the station list is modified. * (remember the station list might be updated through the D-Bus API). */ static void on_station_list_loaded(GvStationList *station_list G_GNUC_UNUSED, GvStationsTreeView *self) { gv_stations_tree_view_populate(self); } static void on_station_list_station_event(GvStationList *station_list G_GNUC_UNUSED, GvStation *station G_GNUC_UNUSED, GvStationsTreeView *self) { gv_stations_tree_view_populate(self); } static GSignalHandler station_list_handlers[] = { { "loaded", G_CALLBACK(on_station_list_loaded) }, { "station-added", G_CALLBACK(on_station_list_station_event) }, { "station-removed", G_CALLBACK(on_station_list_station_event) }, { "station-modified", G_CALLBACK(on_station_list_station_event) }, { "station-moved", G_CALLBACK(on_station_list_station_event) }, { NULL, NULL } }; #if 0 /* * Stations tree view row activated * Might be caused by mouse action (single click on the row), * or by keyboard action (Enter or similar key pressed). */ static void on_tree_view_row_activated(GvStationsTreeView *self, GtkTreePath *path, GtkTreeViewColumn *column G_GNUC_UNUSED, gpointer data G_GNUC_UNUSED) { GvStationsTreeViewPrivate *priv = self->priv; GtkTreeView *tree_view = GTK_TREE_VIEW(self); GtkTreeModel *tree_model = gtk_tree_view_get_model(tree_view); GtkTreeIter iter; GvStation *station; /* Get station from model */ gtk_tree_model_get_iter(tree_model, &iter, path); gtk_tree_model_get(tree_model, &iter, STATION_COLUMN, &station, -1); /* Play station */ if (station) { GvPlayer *player = gv_core_player; gv_player_set_station(player, station); gv_player_play(player); g_object_unref(station); } DEBUG("Row activated"); } #else static gboolean idle_tree_view_row_activated(GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; GtkTreeView *tree_view = GTK_TREE_VIEW(self); GtkTreeSelection *tree_selection = gtk_tree_view_get_selection(tree_view); GtkTreeModel *tree_model = gtk_tree_view_get_model(tree_view); GvPlayer *player = gv_core_player; GtkTreeIter iter; GvStation *station; /* Check if a drag operation is in progress */ if (priv->is_dragging) { DEBUG("Drag'n'drop operation in progress"); return FALSE; } /* Get station */ gtk_tree_selection_get_selected(tree_selection, &tree_model, &iter); gtk_tree_model_get(tree_model, &iter, STATION_COLUMN, &station, -1); /* Station might be NULL if the station list is empty */ if (station == NULL) return FALSE; /* Play station */ gv_player_set_station(player, station); gv_player_play(player); /* Cleanup */ g_object_unref(station); return FALSE; } /* * Stations Tree View row-activated * Might be caused by mouse action (single click on the row), * or by keyboard action (Enter or similar key pressed). */ static void on_tree_view_row_activated(GvStationsTreeView *self, GtkTreePath *path G_GNUC_UNUSED, GtkTreeViewColumn *column G_GNUC_UNUSED, gpointer data G_GNUC_UNUSED) { DEBUG("Row activated, delaying..."); /* This signal might be received when the user clicks an item, * when he hits 'Enter' or similar on the keyboards, * but also when a drag'n'drop operation is performed, BEFORE * the 'drag-begin' signal is send. * In such case, we want to do nothing, but we don't know yet * that a drag'n'drop was started... * So, we have to delay the execution of the code a little bit. * We must give time to the 'drag-begin' signal to be emitted. */ g_idle_add((GSourceFunc) idle_tree_view_row_activated, self); } #endif /* * Stations Tree View button-press-event, for context menu on right-click */ static void on_context_menu_hide(GtkWidget *widget, GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; /* Sanity check */ if (widget != priv->context_menu) CRITICAL("'hide' signal from unknown context menu"); /* Context menu can be destroyed */ g_object_unref(widget); priv->context_menu = NULL; } static gboolean on_tree_view_button_press_event(GvStationsTreeView *self, GdkEventButton *event, gpointer data G_GNUC_UNUSED) { GvStationsTreeViewPrivate *priv = self->priv; GtkTreeView *tree_view = GTK_TREE_VIEW(self); GtkTreeModel *tree_model = gtk_tree_view_get_model(tree_view); GtkTreePath *path; GtkTreeIter iter; GvStation *station; GtkWidget *context_menu; DEBUG("Button pressed: %d", event->button); /* Ensure the event happened on the expected window. * According from the doc, we MUST check that. */ if (event->window != gtk_tree_view_get_bin_window(tree_view)) return GDK_EVENT_PROPAGATE; /* Handle only single-click */ if (event->type != GDK_BUTTON_PRESS) return GDK_EVENT_PROPAGATE; /* Handle only right-click */ if (event->button != 3) return GDK_EVENT_PROPAGATE; /* Do nothing if there's already a context menu (this case shouldn't happen) */ if (priv->context_menu) { WARNING("Context menu already exists !"); return GDK_EVENT_PROPAGATE; } /* Get row at this position */ path = NULL; gtk_tree_view_get_path_at_pos(tree_view, event->x, event->y, &path, NULL, NULL, NULL); if (path == NULL) return GDK_EVENT_PROPAGATE; /* Get corresponding station */ gtk_tree_model_get_iter(tree_model, &iter, path); gtk_tree_model_get(tree_model, &iter, STATION_COLUMN, &station, -1); /* Create the context menu */ if (station) { context_menu = gv_station_context_menu_new_with_station(station); g_object_unref(station); } else { context_menu = gv_station_context_menu_new(); } /* Pop it up */ #if GTK_CHECK_VERSION(3,22,0) gtk_menu_popup_at_pointer(GTK_MENU(context_menu), NULL); #else gtk_menu_popup(GTK_MENU(context_menu), NULL, NULL, NULL, NULL, event->button, event->time); #endif /* Save it for later use, handle destruction in callback */ priv->context_menu = g_object_ref_sink(context_menu); g_signal_connect_object(context_menu, "hide", G_CALLBACK(on_context_menu_hide), self, 0); /* Free at last */ gtk_tree_path_free(path); return GDK_EVENT_PROPAGATE; } /* * Stations Tree View drag-and-drop source signal handlers. * We watch these signals to know when a dragging operation is in progress. */ static void on_tree_view_drag_begin(GvStationsTreeView *self, GdkDragContext *context G_GNUC_UNUSED, gpointer data G_GNUC_UNUSED) { GvStationsTreeViewPrivate *priv = self->priv; priv->is_dragging = TRUE; } static void on_tree_view_drag_end(GvStationsTreeView *self, GdkDragContext *context G_GNUC_UNUSED, gpointer data G_GNUC_UNUSED) { GvStationsTreeViewPrivate *priv = self->priv; priv->is_dragging = FALSE; } static gboolean on_tree_view_drag_failed(GvStationsTreeView *self, GdkDragContext *context G_GNUC_UNUSED, GtkDragResult result, gpointer data G_GNUC_UNUSED) { GvStationsTreeViewPrivate *priv = self->priv; DEBUG("Drag failed with result: %d", result); priv->is_dragging = FALSE; return TRUE; } static GSignalHandler tree_view_drag_handlers[] = { { "drag-begin", G_CALLBACK(on_tree_view_drag_begin) }, { "drag-end", G_CALLBACK(on_tree_view_drag_end) }, { "drag-failed", G_CALLBACK(on_tree_view_drag_failed) }, { NULL, NULL } }; /* * Stations List Store signal handlers. * * We watch these signals to be notified when a station is moved * in the list (this is done when user drag'n'drop). * After a station has been moved, we need to forward this change * to the core station list. * * The signal sequence when a station is moved is as follow: * - row-inserted: a new empty row is created * - row-changed: the new row has been populated * - row-deleted: the old row has been deleted */ static void on_list_store_row_inserted(GtkTreeModel *tree_model G_GNUC_UNUSED, GtkTreePath *path, GtkTreeIter *iter G_GNUC_UNUSED, GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; gint *indices; guint position; /* This should only happen when there's a drag-n-drop */ if (priv->is_dragging == FALSE) { WARNING("Not dragging at the moment, ignoring"); return; } /* We expect a clean status */ if (priv->station_dragged != NULL || priv->station_new_pos != -1) { WARNING("Current state is not clean, ignoring"); return; } /* Get position of the new row */ indices = gtk_tree_path_get_indices(path); position = indices[0]; priv->station_new_pos = position; DEBUG("Row inserted at %d", position); } static void on_list_store_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; gint *indices; gint position; /* We only care if it's caused by a drag'n'drop */ if (priv->is_dragging == FALSE) return; /* We expect the indice to be the one registered previously * in 'row-inserted' signal. */ indices = gtk_tree_path_get_indices(path); position = indices[0]; if (position != priv->station_new_pos) { WARNING("Unexpected position %d, doesn't match %d", position, priv->station_new_pos); return; } /* Get station */ GvStation *station; gtk_tree_model_get(tree_model, iter, STATION_COLUMN, &station, -1); /* Save it */ priv->station_dragged = station; /* Freedom for the braves */ g_object_unref(station); DEBUG("Row changed at %d", position); } static void on_list_store_row_deleted(GtkTreeModel *tree_model G_GNUC_UNUSED, GtkTreePath *path, GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; gint *indices; guint indice_deleted, indice_inserted; /* End of drag operation, let's commit that to station list */ GvStation *station = priv->station_dragged; if (station == NULL) { WARNING("Station dragged is null, wtf ?"); return; } /* Compute indice */ indices = gtk_tree_path_get_indices(path); indice_deleted = indices[0]; indice_inserted = priv->station_new_pos; if (indice_deleted < indice_inserted) indice_inserted -= 1; /* Move station in the station list */ GvStationList *station_list = gv_core_station_list; g_signal_handlers_block(station_list, station_list_handlers, self); gv_station_list_move(station_list, station, indice_inserted); g_signal_handlers_unblock(station_list, station_list_handlers, self); DEBUG("Row deleted, moving sta at %d", indice_inserted); /* Clean status */ priv->station_dragged = NULL; priv->station_new_pos = -1; } static GSignalHandler list_store_handlers[] = { { "row-inserted", G_CALLBACK(on_list_store_row_inserted) }, { "row-changed", G_CALLBACK(on_list_store_row_changed) }, { "row-deleted", G_CALLBACK(on_list_store_row_deleted) }, { NULL, NULL } }; /* * Helpers */ static void station_cell_data_func(GtkTreeViewColumn *tree_column G_GNUC_UNUSED, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data G_GNUC_UNUSED) { gchar *station_name; PangoWeight station_weight; PangoStyle station_style; /* According to the doc, there should be nothing heavy in this function, * since it's called intensively. No UTF-8 conversion, for example. */ gtk_tree_model_get(tree_model, iter, STATION_NAME_COLUMN, &station_name, STATION_WEIGHT_COLUMN, &station_weight, STATION_STYLE_COLUMN, &station_style, -1); g_object_set(cell, "text", station_name, "weight", station_weight, "style", station_style, NULL); g_free(station_name); } /* * Public methods */ void gv_stations_tree_view_populate(GvStationsTreeView *self) { GtkTreeView *tree_view = GTK_TREE_VIEW(self); GtkTreeModel *tree_model = gtk_tree_view_get_model(tree_view); GtkListStore *list_store = GTK_LIST_STORE(tree_model); GvStationList *station_list = gv_core_station_list; GvPlayer *player = gv_core_player; TRACE("%p", self); /* Block list store handlers */ g_signal_handlers_block(list_store, list_store_handlers, self); /* Make station list empty */ gtk_list_store_clear(list_store); /* Handle the special-case: empty station list */ if (gv_station_list_length(station_list) == 0) { GtkTreeIter tree_iter; /* Populate */ gtk_list_store_append(list_store, &tree_iter); gtk_list_store_set(list_store, &tree_iter, STATION_COLUMN, NULL, STATION_NAME_COLUMN, "Right click to add station", STATION_WEIGHT_COLUMN, PANGO_WEIGHT_NORMAL, STATION_STYLE_COLUMN, PANGO_STYLE_ITALIC, -1); /* Configure behavior */ gtk_tree_view_set_hover_selection(tree_view, FALSE); gtk_tree_view_set_activate_on_single_click(tree_view, FALSE); } else { GvStation *current_station = gv_player_get_station(player); GvStation *station; GvStationListIter *iter; /* Populate menu with every station */ iter = gv_station_list_iter_new(station_list); while (gv_station_list_iter_loop(iter, &station)) { GtkTreeIter tree_iter; const gchar *station_name; PangoWeight weight; station_name = gv_station_get_name_or_uri(station); if (station == current_station) weight = PANGO_WEIGHT_BOLD; else weight = PANGO_WEIGHT_NORMAL; gtk_list_store_append(list_store, &tree_iter); gtk_list_store_set(list_store, &tree_iter, STATION_COLUMN, station, STATION_NAME_COLUMN, station_name, STATION_WEIGHT_COLUMN, weight, STATION_STYLE_COLUMN, PANGO_STYLE_NORMAL, -1); } gv_station_list_iter_free(iter); /* Configure behavior */ gtk_tree_view_set_hover_selection(tree_view, TRUE); gtk_tree_view_set_activate_on_single_click(tree_view, TRUE); } /* Unblock list store handlers */ g_signal_handlers_unblock(list_store, list_store_handlers, self); /* Emit a signal */ g_signal_emit(self, signals[SIGNAL_POPULATED], 0); } gboolean gv_stations_tree_view_has_context_menu(GvStationsTreeView *self) { GvStationsTreeViewPrivate *priv = self->priv; return priv->context_menu ? TRUE : FALSE; } GtkWidget * gv_stations_tree_view_new(void) { return g_object_new(GV_TYPE_STATIONS_TREE_VIEW, NULL); } /* * GObject methods */ static void gv_stations_tree_view_constructed(GObject *object) { GtkTreeView *tree_view = GTK_TREE_VIEW(object); GvStationsTreeView *self = GV_STATIONS_TREE_VIEW(object); /* Hide headers */ gtk_tree_view_set_headers_visible(tree_view, FALSE); /* Enable hover selection mode, and single click activation */ gtk_tree_view_set_hover_selection(tree_view, TRUE); gtk_tree_view_set_activate_on_single_click(tree_view, TRUE); /* Allow re-ordering. The tree view then becomes a drag source and * a drag destination, and all the drag-and-drop mess is handled * by the tree view. We will just have to watch the drag-* signals. */ gtk_tree_view_set_reorderable(tree_view, TRUE); /* Horizontally, the tree view grows as wide as the longer station name. * I'm OK with this behavior, let it be. */ /* Vertically, the tree view grows forever. If someone has too many stations, * it might cause a problem. * I tried to put the tree view inside a scrolled window, but it creates more * problems than it solves, mainly because then we have to assign a fixed size * to the tree view. Finding the appropriate size (that would be the natural, * expanded size if NOT within a scrolled window, OR the screen height if too * many stations) seems VERY VERY tricky and slippery... */ /* * Create the stations list store. It has 4 columns: * - the station object * - the station represented by a string (for displaying) * - the station's font weight (bold characters for current station) * - the station's font style (italic characters if no station) */ /* Create a new list store */ GtkListStore *list_store; list_store = gtk_list_store_new(4, G_TYPE_OBJECT, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT); /* Associate it with the tree view */ gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(list_store)); g_object_unref(list_store); /* * Create the column that will be displayed */ /* Create a renderer */ GtkCellRenderer *renderer; renderer = gtk_cell_renderer_text_new(); g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); /* Create a column that uses this renderer */ GtkTreeViewColumn *column; column = gtk_tree_view_column_new_with_attributes ("Station", renderer, NULL); /* Set the function that will render this column */ gtk_tree_view_column_set_cell_data_func(column, renderer, station_cell_data_func, NULL, NULL); /* Append the column */ gtk_tree_view_append_column(tree_view, column); /* * Tree View signal handlers */ /* Left click or keyboard */ g_signal_connect_object(tree_view, "row-activated", G_CALLBACK(on_tree_view_row_activated), NULL, 0); /* We handle the right-click here */ g_signal_connect_object(tree_view, "button-press-event", G_CALLBACK(on_tree_view_button_press_event), NULL, 0); /* Drag-n-drop signal handlers. * We need to watch it just to know when a drag-n-drop is in progress. */ g_signal_handlers_connect_object(tree_view, tree_view_drag_handlers, NULL, 0); /* * List Store signal handlers */ /* If stations are re-ordered, we have to propagate to the core */ g_signal_handlers_connect_object(list_store, list_store_handlers, self, 0); /* * Core signal handlers */ GvPlayer *player = gv_core_player; GvStationList *station_list = gv_core_station_list; g_signal_connect_object(player, "notify::station", G_CALLBACK(on_player_notify_station), self, 0); g_signal_handlers_connect_object(station_list, station_list_handlers, self, 0); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_stations_tree_view, object); } static void gv_stations_tree_view_init(GvStationsTreeView *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_stations_tree_view_get_instance_private(self); /* Initialize internal state */ self->priv->station_new_pos = -1; } static void gv_stations_tree_view_class_init(GvStationsTreeViewClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->constructed = gv_stations_tree_view_constructed; /* Signals */ signals[SIGNAL_POPULATED] = g_signal_new("populated", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } goodvibes-v0.4.2/src/ui/gv-stations-tree-view.h000066400000000000000000000026061337147241200214340ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_STATIONS_TREE_VIEW_H__ #define __GOODVIBES_UI_GV_STATIONS_TREE_VIEW_H__ #include #include /* GObject declarations */ #define GV_TYPE_STATIONS_TREE_VIEW gv_stations_tree_view_get_type() G_DECLARE_FINAL_TYPE(GvStationsTreeView, gv_stations_tree_view, \ GV, STATIONS_TREE_VIEW, GtkTreeView) /* Methods */ GtkWidget *gv_stations_tree_view_new(void); gboolean gv_stations_tree_view_has_context_menu(GvStationsTreeView *self); void gv_stations_tree_view_populate (GvStationsTreeView *self); #endif /* __GOODVIBES_UI_GV_STATIONS_TREE_VIEW_H__ */ goodvibes-v0.4.2/src/ui/gv-status-icon.c000066400000000000000000000403771337147241200201360ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include #include #include #include #include "framework/glib-object-additions.h" #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gtk-additions.h" #include "ui/gv-ui-enum-types.h" #include "ui/gv-ui-helpers.h" #include "ui/gv-ui-internal.h" #include "ui/gv-main-window.h" #include "ui/gv-status-icon.h" #define UI_RESOURCE_PATH GV_APPLICATION_PATH "/Ui/status-icon-menu.glade" #define ICON_MIN_SIZE 16 /* * Properties */ #define DEFAULT_MIDDLE_CLICK_ACTION "toggle" #define DEFAULT_SCROLL_ACTION "station" enum { /* Reserved */ PROP_0, /* Properties */ PROP_MAIN_WINDOW, PROP_MIDDLE_CLICK_ACTION, PROP_SCROLL_ACTION, /* Number of properties */ PROP_N }; static GParamSpec *properties[PROP_N]; /* * GObject definitions */ struct _GvStatusIconPrivate { /* Properties */ GtkWindow *main_window; GvStatusIconMiddleClick middle_click_action; GvStatusIconScroll scroll_action; /* Right-click menu */ GtkWidget *popup_menu; /* Status icon */ GtkStatusIcon *status_icon; guint status_icon_size; }; typedef struct _GvStatusIconPrivate GvStatusIconPrivate; struct _GvStatusIcon { /* Parent instance structure */ GObject parent_instance; /* Private data */ GvStatusIconPrivate *priv; }; static void gv_status_icon_configurable_interface_init(GvConfigurableInterface *iface); G_DEFINE_TYPE_WITH_CODE(GvStatusIcon, gv_status_icon, G_TYPE_OBJECT, G_ADD_PRIVATE(GvStatusIcon) G_IMPLEMENT_INTERFACE(GV_TYPE_CONFIGURABLE, gv_status_icon_configurable_interface_init)) /* * Helpers */ static void gv_status_icon_update_icon_pixbuf(GvStatusIcon *self) { GvStatusIconPrivate *priv = self->priv; GtkStatusIcon *status_icon = priv->status_icon; gtk_status_icon_set_from_icon_name(status_icon, PACKAGE_NAME); } static void gv_status_icon_update_icon_tooltip(GvStatusIcon *self) { GvStatusIconPrivate *priv = self->priv; GtkStatusIcon *status_icon = priv->status_icon; GvPlayer *player = gv_core_player; GvPlayerState player_state; const gchar *player_state_str; guint player_volume; gboolean player_muted; gchar *player_str; GvStation *station; gchar *station_str; GvMetadata *metadata; gchar *metadata_str; gchar *tooltip; /* Player */ player_state = gv_player_get_state(player); player_volume = gv_player_get_volume(player); player_muted = gv_player_get_mute(player); switch (player_state) { case GV_PLAYER_STATE_STOPPED: player_state_str = _("stopped"); break; case GV_PLAYER_STATE_CONNECTING: player_state_str = _("connecting"); break; case GV_PLAYER_STATE_BUFFERING: player_state_str = _("buffering"); break; case GV_PLAYER_STATE_PLAYING: player_state_str = _("playing"); break; default: player_state_str = _("unknown state"); break; } if (player_muted) player_str = g_strdup_printf("%s (%s, %s)", g_get_application_name(), player_state_str, _("muted")); else player_str = g_strdup_printf("%s (%s, %s %u%%)", g_get_application_name(), player_state_str, _("vol."), player_volume); /* Current station */ station = gv_player_get_station(player); if (station) station_str = gv_station_make_name(station, TRUE); else station_str = g_strdup_printf("%s", _("No station")); /* Metadata */ metadata = gv_player_get_metadata(player); if (metadata) metadata_str = gv_metadata_make_title_artist(metadata, TRUE); else metadata_str = g_strdup_printf("%s", _("No metadata")); /* Set the tooltip */ tooltip = g_strdup_printf("%s\n%s\n%s", player_str, station_str, metadata_str); gtk_status_icon_set_tooltip_markup(status_icon, tooltip); /* Free */ g_free(player_str); g_free(station_str); g_free(metadata_str); g_free(tooltip); } static void gv_status_icon_update_icon(GvStatusIcon *self) { /* Update icon */ gv_status_icon_update_icon_pixbuf(self); /* Update tooltip */ gv_status_icon_update_icon_tooltip(self); /* Set visible */ gtk_status_icon_set_visible(self->priv->status_icon, TRUE); } /* * GtkStatusIcon signal handlers */ static void on_activate(GtkStatusIcon *status_icon G_GNUC_UNUSED, GvStatusIcon *self G_GNUC_UNUSED) { GvStatusIconPrivate *priv = self->priv; GtkWindow *window = priv->main_window; if (gtk_window_is_active(window)) gtk_window_close(window); else gtk_window_present(window); } static void on_popup_menu(GtkStatusIcon *status_icon, guint button, guint activate_time, GvStatusIcon *self) { GvStatusIconPrivate *priv = self->priv; GtkMenu *menu = GTK_MENU(priv->popup_menu); #if GTK_CHECK_VERSION(3,22,0) (void) status_icon; (void) button; (void) activate_time; gtk_menu_popup_at_pointer(menu, NULL); #else gtk_menu_popup(menu, NULL, NULL, gtk_status_icon_position_menu, status_icon, button, activate_time); #endif } static gboolean on_button_release_event(GtkStatusIcon *status_icon G_GNUC_UNUSED, GdkEventButton *event, GvStatusIcon *self G_GNUC_UNUSED) { GvStatusIconPrivate *priv = self->priv; GvPlayer *player = gv_core_player; /* Here we only handle the middle-click */ if (event->button != 2) return GDK_EVENT_PROPAGATE; switch (priv->middle_click_action) { case GV_STATUS_ICON_MIDDLE_CLICK_TOGGLE: gv_player_toggle(player); break; case GV_STATUS_ICON_MIDDLE_CLICK_MUTE: gv_player_toggle_mute(player); break; default: CRITICAL("Unhandled middle-click action: %d", priv->middle_click_action); break; } return GDK_EVENT_PROPAGATE; } static gboolean on_scroll_event(GtkStatusIcon *status_icon G_GNUC_UNUSED, GdkEventScroll *event, GvStatusIcon *self G_GNUC_UNUSED) { GvStatusIconPrivate *priv = self->priv; GvPlayer *player = gv_core_player; switch (priv->scroll_action) { case GV_STATUS_ICON_SCROLL_STATION: switch (event->direction) { case GDK_SCROLL_DOWN: gv_player_next(player); break; case GDK_SCROLL_UP: gv_player_prev(player); break; default: break; } break; case GV_STATUS_ICON_SCROLL_VOLUME: switch (event->direction) { case GDK_SCROLL_DOWN: gv_player_lower_volume(player); break; case GDK_SCROLL_UP: gv_player_raise_volume(player); break; default: break; } break; default: CRITICAL("Unhandled scroll action: %d", priv->scroll_action); break; } return GDK_EVENT_PROPAGATE; } static gboolean on_size_changed(GtkStatusIcon *status_icon G_GNUC_UNUSED, gint size, GvStatusIcon *self G_GNUC_UNUSED) { DEBUG("Status icon size is now %d", size); gv_status_icon_update_icon(self); return FALSE; } /* * Goodvibes signal handlers */ static void on_player_notify(GvPlayer *player, GParamSpec *pspec, GvStatusIcon *self) { const gchar *property_name = g_param_spec_get_name(pspec); TRACE("%p, %s, %p", player, property_name, self); if (!g_strcmp0(property_name, "state") || !g_strcmp0(property_name, "volume") || !g_strcmp0(property_name, "mute") || !g_strcmp0(property_name, "station") || !g_strcmp0(property_name, "metadata")) { gv_status_icon_update_icon(self); return; } } /* * Property accessors */ static void gv_status_icon_set_main_window(GvStatusIcon *self, GtkWindow *main_window) { GvStatusIconPrivate *priv = self->priv; /* Construct-only property */ g_assert_null(priv->main_window); priv->main_window = g_object_ref(main_window); } GvStatusIconMiddleClick gv_status_icon_get_middle_click_action(GvStatusIcon *self) { return self->priv->middle_click_action; } void gv_status_icon_set_middle_click_action(GvStatusIcon *self, GvStatusIconMiddleClick action) { GvStatusIconPrivate *priv = self->priv; if (priv->middle_click_action == action) return; priv->middle_click_action = action; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_MIDDLE_CLICK_ACTION]); } GvStatusIconScroll gv_status_icon_get_scroll_action(GvStatusIcon *self) { return self->priv->scroll_action; } void gv_status_icon_set_scroll_action(GvStatusIcon *self, GvStatusIconScroll action) { GvStatusIconPrivate *priv = self->priv; if (priv->scroll_action == action) return; priv->scroll_action = action; g_object_notify_by_pspec(G_OBJECT(self), properties[PROP_SCROLL_ACTION]); } static void gv_status_icon_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GvStatusIcon *self = GV_STATUS_ICON(object); TRACE_GET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_MIDDLE_CLICK_ACTION: g_value_set_enum(value, gv_status_icon_get_middle_click_action(self)); break; case PROP_SCROLL_ACTION: g_value_set_enum(value, gv_status_icon_get_scroll_action(self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void gv_status_icon_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GvStatusIcon *self = GV_STATUS_ICON(object); TRACE_SET_PROPERTY(object, property_id, value, pspec); switch (property_id) { case PROP_MAIN_WINDOW: gv_status_icon_set_main_window(self, g_value_get_object(value)); break; case PROP_MIDDLE_CLICK_ACTION: gv_status_icon_set_middle_click_action(self, g_value_get_enum(value)); break; case PROP_SCROLL_ACTION: gv_status_icon_set_scroll_action(self, g_value_get_enum(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } /* * Public methods */ GvStatusIcon * gv_status_icon_new(GtkWindow *main_window) { return g_object_new(GV_TYPE_STATUS_ICON, "main-window", main_window, NULL); } /* * GvConfigurable interface */ static void gv_status_icon_configure(GvConfigurable *configurable) { GvStatusIcon *self = GV_STATUS_ICON(configurable); TRACE("%p", self); g_assert(gv_ui_settings); g_settings_bind(gv_ui_settings, "middle-click-action", self, "middle-click-action", G_SETTINGS_BIND_DEFAULT); g_settings_bind(gv_ui_settings, "scroll-action", self, "scroll-action", G_SETTINGS_BIND_DEFAULT); } static void gv_status_icon_configurable_interface_init(GvConfigurableInterface *iface) { iface->configure = gv_status_icon_configure; } /* * GObject methods */ static void gv_status_icon_finalize(GObject *object) { GvStatusIcon *self = GV_STATUS_ICON(object); GvStatusIconPrivate *priv = self->priv; TRACE("%p", object); /* Release main window */ g_object_unref(priv->main_window); /* Destroy popup menu */ g_object_unref(priv->popup_menu); /* Unref the status icon */ g_object_unref(priv->status_icon); /* Chain up */ G_OBJECT_CHAINUP_FINALIZE(gv_status_icon, object); } static void gv_status_icon_setup_menu(GvStatusIcon *self) { GvStatusIconPrivate *priv = self->priv; GtkBuilder *builder; GMenuModel *model; GtkWidget *menu; /* Load the menu model */ builder = gtk_builder_new_from_resource(UI_RESOURCE_PATH); /* Build the menu */ model = G_MENU_MODEL(gtk_builder_get_object(builder, "status-icon-menu")); menu = gtk_menu_new_from_model(model); /* Attach to main window */ gtk_menu_attach_to_widget(GTK_MENU(menu), GTK_WIDGET(priv->main_window), NULL); /* Save a pointer */ priv->popup_menu = g_object_ref_sink(menu); /* Cleanup */ g_object_unref(builder); } static void gv_status_icon_constructed(GObject *object) { GvStatusIcon *self = GV_STATUS_ICON(object); GvStatusIconPrivate *priv = self->priv; GvPlayer *player = gv_core_player; GtkStatusIcon *status_icon; /* Ensure construct-only properties have been set */ g_assert_nonnull(priv->main_window); /* Create the popup menu */ gv_status_icon_setup_menu(self); /* Create the status icon */ status_icon = gtk_status_icon_new(); /* Connect status icon signal handlers */ g_signal_connect_object(status_icon, "activate", /* Left click */ G_CALLBACK(on_activate), self, 0); g_signal_connect_object(status_icon, "popup-menu", /* Right click */ G_CALLBACK(on_popup_menu), self, 0); g_signal_connect_object(status_icon, "button-release-event", /* Middle click */ G_CALLBACK(on_button_release_event), self, 0); g_signal_connect_object(status_icon, "scroll_event", /* Mouse scroll */ G_CALLBACK(on_scroll_event), self, 0); g_signal_connect_object(status_icon, "size-changed", /* Change of size */ G_CALLBACK(on_size_changed), self, 0); /* Save to private data */ priv->status_icon = status_icon; priv->status_icon_size = ICON_MIN_SIZE; /* Connect core signal handlers */ g_signal_connect_object(player, "notify", G_CALLBACK(on_player_notify), self, 0); /* Chain up */ G_OBJECT_CHAINUP_CONSTRUCTED(gv_status_icon, object); } static void gv_status_icon_init(GvStatusIcon *self) { TRACE("%p", self); /* Initialize private pointer */ self->priv = gv_status_icon_get_instance_private(self); } static void gv_status_icon_class_init(GvStatusIconClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); TRACE("%p", class); /* Override GObject methods */ object_class->finalize = gv_status_icon_finalize; object_class->constructed = gv_status_icon_constructed; /* Properties */ object_class->get_property = gv_status_icon_get_property; object_class->set_property = gv_status_icon_set_property; properties[PROP_MAIN_WINDOW] = g_param_spec_object("main-window", "Main window", NULL, GTK_TYPE_WINDOW, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); properties[PROP_MIDDLE_CLICK_ACTION] = g_param_spec_enum("middle-click-action", "Middle click action", NULL, GV_TYPE_STATUS_ICON_MIDDLE_CLICK, GV_STATUS_ICON_MIDDLE_CLICK_TOGGLE, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); properties[PROP_SCROLL_ACTION] = g_param_spec_enum("scroll-action", "Scroll action", NULL, GV_TYPE_STATUS_ICON_SCROLL, GV_STATUS_ICON_SCROLL_STATION, GV_PARAM_DEFAULT_FLAGS | G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_N, properties); /* Register transform function */ g_value_register_transform_func(GV_TYPE_STATUS_ICON_MIDDLE_CLICK, G_TYPE_STRING, gv_value_transform_enum_string); g_value_register_transform_func(G_TYPE_STRING, GV_TYPE_STATUS_ICON_MIDDLE_CLICK, gv_value_transform_string_enum); g_value_register_transform_func(GV_TYPE_STATUS_ICON_SCROLL, G_TYPE_STRING, gv_value_transform_enum_string); g_value_register_transform_func(G_TYPE_STRING, GV_TYPE_STATUS_ICON_SCROLL, gv_value_transform_string_enum); } goodvibes-v0.4.2/src/ui/gv-status-icon.h000066400000000000000000000037771337147241200201460ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_STATUS_ICON_H__ #define __GOODVIBES_UI_GV_STATUS_ICON_H__ #include #include /* GObject declarations */ #define GV_TYPE_STATUS_ICON gv_status_icon_get_type() G_DECLARE_FINAL_TYPE(GvStatusIcon, gv_status_icon, GV, STATUS_ICON, GObject) /* Data types */ typedef enum { GV_STATUS_ICON_MIDDLE_CLICK_UNDEFINED, GV_STATUS_ICON_MIDDLE_CLICK_TOGGLE, GV_STATUS_ICON_MIDDLE_CLICK_MUTE, } GvStatusIconMiddleClick; typedef enum { GV_STATUS_ICON_SCROLL_UNDEFINED, GV_STATUS_ICON_SCROLL_STATION, GV_STATUS_ICON_SCROLL_VOLUME, } GvStatusIconScroll; /* Methods */ GvStatusIcon *gv_status_icon_new(GtkWindow *main_window); /* Property accessors */ GvStatusIconMiddleClick gv_status_icon_get_middle_click_action(GvStatusIcon *self); void gv_status_icon_set_middle_click_action(GvStatusIcon *self, GvStatusIconMiddleClick action); GvStatusIconScroll gv_status_icon_get_scroll_action (GvStatusIcon *self); void gv_status_icon_set_scroll_action (GvStatusIcon *self, GvStatusIconScroll action); #endif /* __GOODVIBES_UI_GV_STATUS_ICON_H__ */ goodvibes-v0.4.2/src/ui/gv-ui-helpers.c000066400000000000000000000034341337147241200177330ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include void gv_value_transform_enum_string(const GValue *src_value, GValue *dest_value) { GEnumClass *enum_class; GEnumValue *enum_value; enum_class = g_type_class_ref(G_VALUE_TYPE(src_value)); enum_value = g_enum_get_value(enum_class, g_value_get_enum(src_value)); if (enum_value) g_value_set_static_string(dest_value, enum_value->value_nick); else { /* Assume zero holds the invalid value */ enum_value = g_enum_get_value(enum_class, 0); g_value_set_static_string(dest_value, enum_value->value_nick); } g_type_class_unref(enum_class); } void gv_value_transform_string_enum(const GValue *src_value, GValue *dest_value) { GEnumClass *enum_class; GEnumValue *enum_value; enum_class = g_type_class_ref(G_VALUE_TYPE(dest_value)); enum_value = g_enum_get_value_by_nick(enum_class, g_value_get_string(src_value)); if (enum_value) g_value_set_enum(dest_value, enum_value->value); else /* Assume zero holds the invalid value */ g_value_set_enum(dest_value, 0); g_type_class_unref(enum_class); } goodvibes-v0.4.2/src/ui/gv-ui-helpers.h000066400000000000000000000021111337147241200177270ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #ifndef __GOODVIBES_UI_GV_UI_HELPERS_H__ #define __GOODVIBES_UI_GV_UI_HELPERS_H__ #include void gv_value_transform_enum_string(const GValue *src_value, GValue *dest_value); void gv_value_transform_string_enum(const GValue *src_value, GValue *dest_value); #endif /* __GOODVIBES_UI_GV_UI_HELPERS_H__ */ goodvibes-v0.4.2/src/ui/gv-ui-internal.h000066400000000000000000000027561337147241200201200ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This header contains definitions to be used internally by ui files */ #ifndef __GOODVIBES_UI_GV_UI_INTERNAL_H__ #define __GOODVIBES_UI_GV_UI_INTERNAL_H__ #include #include "ui/gv-status-icon.h" #include "ui/gv-main-window-manager.h" /* Global variables */ extern GSettings *gv_ui_settings; extern GvStatusIcon *gv_ui_status_icon; extern GtkWidget *gv_ui_main_window; extern GvMainWindowManager *gv_ui_main_window_manager; /* * Visual layout, according to: * https://developer.gnome.org/hig/stable/visual-layout.html */ #define GV_UI_WINDOW_BORDER 18 #define GV_UI_GROUP_SPACING 18 #define GV_UI_ELEM_SPACING 6 #define GV_UI_COLUMN_SPACING 12 #endif /* __GOODVIBES_UI_GV_UI_INTERNAL_H__ */ goodvibes-v0.4.2/src/ui/gv-ui.c000066400000000000000000000120421337147241200162660ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ #include #include "framework/gv-framework.h" #include "core/gv-core.h" #include "ui/gtk-additions.h" #include "ui/gv-about-dialog.h" #include "ui/gv-main-window.h" #include "ui/gv-main-window-manager.h" #include "ui/gv-prefs-window.h" #include "ui/gv-station-dialog.h" #include "ui/gv-status-icon.h" #include "ui/resources/gv-ui-resources.h" #define UI_SCHEMA_ID_SUFFIX "Ui" /* * Public variables */ GSettings *gv_ui_settings; GvStatusIcon *gv_ui_status_icon; GtkWidget *gv_ui_main_window; GvMainWindowManager *gv_ui_main_window_manager; /* * Private variables */ static GList *ui_objects; /* * Underlying graphical toolkit */ GOptionGroup * gv_ui_toolkit_init_get_option_group(void) { /* Very not sure about the argument to pass here. From my experience: * - if we don't use gtk_init(), we should pass TRUE. * - if we use gtk_init(), passing FALSE is ok. * Since we use GtkApplication which calls gtk_init() internally, * let's pass FALSE and pray that it works. */ return gtk_get_option_group(FALSE); } const gchar * gv_ui_toolkit_runtime_version_string(void) { return gtk_get_runtime_version_string(); } const gchar * gv_ui_toolkit_compile_version_string(void) { return gtk_get_compile_version_string(); } /* * Ui public functions */ void gv_ui_hide(void) { /* In status icon mode, do nothing */ if (gv_ui_status_icon) return; gtk_widget_hide(gv_ui_main_window); } void gv_ui_present_about(void) { gv_show_about_dialog(GTK_WINDOW(gv_ui_main_window), gv_core_audio_backend_runtime_version_string(), gv_ui_toolkit_runtime_version_string()); } void gv_ui_present_preferences(void) { gv_show_prefs_window(GTK_WINDOW(gv_ui_main_window)); } void gv_ui_present_main(void) { /* In status icon mode, do nothing */ if (gv_ui_status_icon) return; gtk_window_present(GTK_WINDOW(gv_ui_main_window)); } void gv_ui_present_add_station(void) { GvStationList *station_list = gv_core_station_list; GvStation *station; station = gv_show_add_station_dialog(GTK_WINDOW(gv_ui_main_window)); if (station) gv_station_list_append(station_list, station); } void gv_ui_configure(void) { GList *item; /* Configure each object that is configurable */ for (item = ui_objects; item; item = item->next) { GObject *object = item->data; if (!GV_IS_CONFIGURABLE(object)) continue; gv_configurable_configure(GV_CONFIGURABLE(object)); } } void gv_ui_cleanup(void) { GList *item; /* Windows must be destroyed with gtk_widget_destroy(). * Forget about gtk_window_close() here, which seems to be asynchronous. * Read the doc: * https://developer.gnome.org/gtk3/stable/GtkWindow.html#gtk-window-new */ /* Destroy public ui objects */ ui_objects = g_list_reverse(ui_objects); for (item = ui_objects; item; item = item->next) { GObject *object = item->data; if (GTK_IS_WINDOW(object)) gtk_widget_destroy(GTK_WIDGET(object)); else g_object_unref(object); } g_list_free(ui_objects); } void gv_ui_init(GApplication *app, gboolean status_icon_mode) { GList *item; /* Create resource * * The resource object is a bit special, it's a static resource, * it lives in the data segment, which is read-only. So calling * g_object_ref() on it causes a segfault, for example. * * Actually, we don't even need to keep a reference to this object. * All we want is to initialize the resource, which is triggered * by the first access (lazy init). */ gv_ui_get_resource(); /* Create ui objects */ gv_ui_settings = gv_get_settings(UI_SCHEMA_ID_SUFFIX); ui_objects = g_list_append(ui_objects, gv_ui_settings); gv_ui_main_window = gv_main_window_new(app, status_icon_mode); ui_objects = g_list_append(ui_objects, gv_ui_main_window); gv_ui_main_window_manager = gv_main_window_manager_new (GV_MAIN_WINDOW(gv_ui_main_window), status_icon_mode); ui_objects = g_list_append(ui_objects, gv_ui_main_window_manager); if (status_icon_mode) { gv_ui_status_icon = gv_status_icon_new(GTK_WINDOW(gv_ui_main_window)); ui_objects = g_list_append(ui_objects, gv_ui_status_icon); } else { gv_ui_status_icon = NULL; } /* Register objects in the framework */ for (item = ui_objects; item; item = item->next) { GObject *object = G_OBJECT(item->data); gv_framework_register_object(object); } } goodvibes-v0.4.2/src/ui/gv-ui.h000066400000000000000000000027411337147241200163000ustar00rootroot00000000000000/* * Goodvibes Radio Player * * Copyright (C) 2015-2018 Arnaud Rebillout * * SPDX-License-Identifier: GPL-3.0-or-later * * 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 . */ /* * This header contains definitions to be used by ui users */ #ifndef __GOODVIBES_UI_GV_UI_H__ #define __GOODVIBES_UI_GV_UI_H__ #include /* Functions */ void gv_ui_init (GApplication *app, gboolean status_icon_mode); void gv_ui_cleanup (void); void gv_ui_configure(void); void gv_ui_present_add_station(void); void gv_ui_present_main (void); void gv_ui_present_preferences(void); void gv_ui_present_about (void); void gv_ui_hide (void); /* Underlying toolkit */ GOptionGroup *gv_ui_toolkit_init_get_option_group (void); const gchar *gv_ui_toolkit_runtime_version_string(void); const gchar *gv_ui_toolkit_compile_version_string(void); #endif /* __GOODVIBES_UI_GV_UI_H__ */ goodvibes-v0.4.2/src/ui/meson.build000066400000000000000000000016611337147241200172420ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Subdirectories subdir('resources') # Sources and dependencies ui_sources = [ 'gtk-additions.c', 'gv-about-dialog.c', 'gv-main-window.c', 'gv-main-window-manager.c', 'gv-prefs-window.c', 'gv-station-context-menu.c', 'gv-station-dialog.c', 'gv-stations-tree-view.c', 'gv-status-icon.c', 'gv-ui.c', 'gv-ui-helpers.c', ] ui_dependencies = [ glib_dep, gobject_dep, gio_dep, gtk_dep, gvframework_dep, gvcore_dep, ] ui_enum_headers = [ 'gv-main-window.h', 'gv-status-icon.h' ] ui_enums = gnome.mkenums_simple('gv-ui-enum-types', sources: ui_enum_headers ) ui_enum_h = ui_enums[1] # Library definition gvui = static_library('gvui', sources: [ ui_enums, ui_resources, ui_sources ], dependencies: ui_dependencies, include_directories: root_inc, ) gvui_dep = declare_dependency( dependencies: ui_dependencies, sources: [ ui_enum_h ], link_with: gvui, ) goodvibes-v0.4.2/src/ui/resources/000077500000000000000000000000001337147241200171065ustar00rootroot00000000000000goodvibes-v0.4.2/src/ui/resources/app-menu.glade000066400000000000000000000023441337147241200216310ustar00rootroot00000000000000
Add Station app.add-station <Primary>a
Preferences app.preferences
Online Help app.help F1 About app.about Close UI app.close-ui <Primary>c Quit app.quit <Primary>q
goodvibes-v0.4.2/src/ui/resources/io.gitlab.Goodvibes.Ui.gresource.xml.in000066400000000000000000000007641337147241200264050ustar00rootroot00000000000000 app-menu.glade main-window.glade menubar.glade prefs-window.glade station-dialog.glade status-icon-menu.glade goodvibes-v0.4.2/src/ui/resources/main-window.glade000066400000000000000000000226211337147241200223400ustar00rootroot00000000000000 True False media-skip-forward-symbolic True False media-playback-start-symbolic True False media-skip-backward-symbolic True False media-playlist-repeat-symbolic True False media-playlist-shuffle-symbolic True False 6 6 6 6 vertical True False vertical True False No station selected end False True 0 True False Stopped end False True 1 False True 0 True False start True True True play_image none True True True 0 True True True True prev_image none True True True 1 True True True True next_image none True True True 2 True True True True repeat_image none True True True 3 True True True True True shuffle_image none True True True 4 True True True True False True none vertical audio-volume-muted-symbolic audio-volume-high-symbolic audio-volume-low-symbolic audio-volume-medium-symbolic True True center center none True True center center none True True 5 True True False True 1 True True in True True 2 goodvibes-v0.4.2/src/ui/resources/menubar.glade000066400000000000000000000030131337147241200215320ustar00rootroot00000000000000 Menu
Add Station app.add-station <Primary>a
Preferences app.preferences
Close UI app.close-ui <Primary>c Quit app.quit <Primary>q
Help
Online Help app.help F1
About app.about
goodvibes-v0.4.2/src/ui/resources/meson.build000066400000000000000000000014711337147241200212530ustar00rootroot00000000000000# SPDX-License-Identifier: GPL-3.0-or-later # Resources ui_resources_conf = configuration_data() ui_resources_conf.set('path', gv_application_path) ui_resources_file = configure_file( input: gv_application_id + '.Ui.gresource.xml.in', output: gv_application_id + '.Ui.gresource.xml', configuration: ui_resources_conf, ) ui_resources = gnome.compile_resources('gv-ui-resources', ui_resources_file, c_name: 'gv_ui', export: true, ) # Validation ui_glade_files = [ 'app-menu.glade', 'main-window.glade', 'menubar.glade', 'prefs-window.glade', 'station-dialog.glade', 'status-icon-menu.glade', ] validator = find_program(join_paths(meson.source_root(), 'scripts/meson/validate-ui-file')) foreach file : ui_glade_files test('Validate ' + file, validator, args: [ files(file) ], ) endforeach goodvibes-v0.4.2/src/ui/resources/prefs-window.glade000066400000000000000000000717601337147241200225430ustar00rootroot00000000000000 100 1 10 True False vertical True True True False vertical True False 0 none True False Autoplay on Startup True True False True 0 0 Custom Output Pipeline True True False True True 0 1 True False 6 True True True False True 0 Apply True True True False True 1 0 2 True False Playback False True 0 True False 0 none True False end True True 1 0 True False end Prevent sleep while playing 0 0 True False System False True 1 True False 0 none True False end True True 1 0 True True 1 1 True False end Native D-Bus Server 0 0 True False end MPRIS2 D-Bus Server 0 1 True False D-Bus False True 2 True False Misc False True False vertical True False 0 none True False end Autoset Window Height True True False True 0 1 True False end Theme Variant 0 0 True False 0 System Default Prefer Dark Prefer Light 1 0 True False Window False True 0 True False 0 none True False end True False start Send Notifications 0 0 True True 1 0 True False Notifications False True 1 True False 0 none True False end True True 1 0 True False Console Output 0 0 True False Console False True 2 1 True False Display 1 False True False vertical True False 0 none True False end True True 1 0 True False end Multimedia Hotkeys 0 0 True False Keyboard False True 0 True False 0 none True False end True False end Middle Click 0 0 True False 0 Toggle Play/Pause Mute 1 0 True False end Scrolling 0 1 True False 0 Change Station Change Volume 1 1 True False Mouse (Status Icon Mode) False True 1 2 True False Controls 2 False True True 0 True False end Close True True True True True 0 False True 1 goodvibes-v0.4.2/src/ui/resources/station-dialog.glade000066400000000000000000000034111337147241200230210ustar00rootroot00000000000000 True False True False Name 0 0 True True True 1 0 True False URI 0 1 True True True 1 1 goodvibes-v0.4.2/src/ui/resources/status-icon-menu.glade000066400000000000000000000015671337147241200233300ustar00rootroot00000000000000
Add Station app.add-station
Preferences app.preferences
Online Help app.help About app.about Quit app.quit