diff options
| author | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2022-08-07 20:10:51 -0400 |
|---|---|---|
| committer | Jakob L. Kreuze <zerodaysfordays@sdf.org> | 2022-08-07 20:12:17 -0400 |
| commit | 0de7e9ac43f88aae3750ecfe25b5454fdd1ad015 (patch) | |
| tree | b3634b46b9979b707a5479e280d8fffb9c1aed14 | |
| parent | 73c086d37346ae80fa5ed8cc1aaab304db70d463 (diff) | |
Refactor and finalize
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | LICENSE | 661 | ||||
| -rw-r--r-- | src/main.rs | 650 | ||||
| -rw-r--r-- | tunes.desktop | 9 |
4 files changed, 1012 insertions, 310 deletions
@@ -8,5 +8,5 @@ anyhow = "1.0" futures = "0.3" gtk = "0.15" libhandy = "0.9" -mpd = { path = "../mpd" } +mpd = { path = "./vendored/mpd" } once_cell = "1.13" @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +<https://www.gnu.org/licenses/>. diff --git a/src/main.rs b/src/main.rs index 5c262bb..b363500 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,247 @@ +// Copyright © 2021-2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +// +// This file is part of Tunes. +// +// Tunes is free software; you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation; either version 3 of the +// License, or (at your option) any later version. +// +// Tunes 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 Affero General +// Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with Tunes. If not, see <http://www.gnu.org/licenses/>. + use futures::{channel::mpsc, StreamExt}; +use glib::clone; use gtk::prelude::*; -use gtk::{gdk_pixbuf, gio, glib}; +use gtk::subclass::prelude::ObjectSubclassExt; +use gtk::{gdk_pixbuf, gio, glib, pango}; use libhandy::prelude::*; use libhandy::{ApplicationWindow, HeaderBar}; use mpd::idle::Idle; use mpd::Client; -struct TunesUI { - header_bar: HeaderBar, - // album_art: Image, - // queue_switcher: Notebook, +const MPD_HOST: &str = "127.0.0.1:6600"; + +fn main() { + let application = gtk::Application::builder() + .application_id("space.jakob.Tunes") + .build(); + + // We have to wait until the `activate` signal is fired before we can do our + // setup. + application.connect_activate(|app| { + // Our event-handling code will look a bit like what's common in SDL + // with their `SDLPollEvent` interface, in the sense that we'll have all + // of the different sub-systems of this application notify the main + // event loop by way of a channel. + let (sender, mut receiver) = mpsc::channel(1024); + + // Load all of the mobile UI support code from `libhandy`. + libhandy::init(); + + // `mpd` will notify us of events. Let's spin up a thread to listen for + // those notifications, and shuttle them through a channel as they + // arrive. + std::thread::spawn(clone!(@strong sender => move || { + let mut conn = Client::connect(MPD_HOST).unwrap(); + while let Ok(_subsystems) = conn.wait(&[mpd::idle::Subsystem::Player]) { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); + } + })); + + // We'll connect to the MPD daemon here so we can populate the UI with + // some information from the current state. + let mut conn = Client::connect(MPD_HOST).unwrap(); + + // We'll have two "views" in our application: one for viewing and + // manipulating the current `mpd` queue, and another for searching for + // songs to add to the queue. In GTK, we can handle switching between + // these different views using a Stack. + let stack = gtk::Stack::new(); + stack.set_expand(true); + + let song_info = SongInfo::new(sender.clone()); + stack.add_named(song_info.as_ref(), "current_song"); + stack.set_child_title(song_info.as_ref(), Some("Now Playing")); + stack.set_child_icon_name(song_info.as_ref(), Some("audio-speakers-symbolic")); + + let query_info = QueryInfo::new(sender.clone()); + stack.add_named(query_info.as_ref(), "query_songs"); + stack.set_child_title(query_info.as_ref(), Some("Search Database")); + stack.set_child_icon_name(query_info.as_ref(), Some("system-search-symbolic")); + + // The `HeaderBar` is a GTK concept that libhandy plays nicely with. On + // desktop, the elements for switching stack views will show up there. + // On mobile, it will show up in a `ViewSwitcherBar` at the bottom. + let header_bar = HeaderBar::builder() + .show_close_button(true) + .title(&header_title(&mut conn).unwrap()) + .build(); + let view_switcher_title = libhandy::ViewSwitcherTitle::builder() + .title("Tunes") + .stack(&stack) + .build(); + header_bar.add(&view_switcher_title); + let view_switcher_bar = libhandy::ViewSwitcherBar::builder() + .visible(true) + .can_focus(false) + .stack(&stack) + .reveal(true) + .build(); + + // The window needs a single child, so we'll join the header bar, the + // stack, and the view switcher into a single box. + let content = gtk::Box::new(gtk::Orientation::Vertical, 0); + content.set_vexpand(true); + content.add(&header_bar); + content.add(&stack); + content.add(&view_switcher_bar); + + // Finally, the window. It's tied to a child, which we made above, and + // the GtkApplication that we declared at the beginning of `main`. + let window = ApplicationWindow::builder() + .default_width(350) + .default_height(70) + .modal(true) + .child(&content) + .build(); + window.set_application(Some(app)); + window.show_all(); + + // This isn't perfect (it won't run when the window gets its initial + // size), but this is how we notify that the album art display should be + // resized. + window.connect_configure_event(clone!(@strong sender => move |_, _| { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::WindowResizeEvent) + .expect("Couldn't notify thread"); + false + })); + + // Now that everything's been allocated a window, let's go ahead and + // update the widgets. + song_info + .update(&mut conn) + .expect("Couldn't update song info"); + + // The following code will fill the search view with every song in the + // database. If you have a music library as big as mine, it will + // negatively impact startup time. This could be done in, for example, a + // worker thread, but I've just omitted it because I don't want this + // example to be more complex than it has to be. + // + // let mut query = mpd::Query::new(); + // query.and(mpd::Term::Any, ""); + // let songs = conn.search(&query, (0, 65535)); + // for song in songs.unwrap() { + // query_info.model.insert(0, &SongObject::new(&song)); + // } + + // Finally, we'll start the "main event loop" we've been talking about + // in the main context of the application. + let main_context = gtk::glib::MainContext::default(); + main_context.spawn_local(async move { + let mut conn = Client::connect(MPD_HOST).unwrap(); + while let Some(event_type) = receiver.next().await { + match event_type { + StateUpdateKind::MpdEvent => { + if let Ok(title) = header_title(&mut conn) { + header_bar.set_title(Some(&title)); + song_info + .update(&mut conn) + .expect("Couldn't update song info"); + } + } + StateUpdateKind::WindowResizeEvent => { + song_info + .update_album_art(&mut conn) + .expect("Couldn't update album art"); + } + StateUpdateKind::QueryUpdateEvent(query_string) => { + // Let's not produce massive queries while the user is typing :) + if query_string.len() <= 2 { + continue; + } + + // Start from a blank slate. + query_info.model.remove_all(); + + // Query on all fields, case-insensitively, for the text + // that the user input. + let mut query = mpd::Query::new(); + query.and(mpd::Term::Any, &query_string); + let songs = conn.search(&query, (0, 65535)); + + // Insert them all into the model. This is reversed, + // which I don't consider to be a big deal. It's far + // less complex than adding it in order, which you will + // see below in the code that handles the queue. + for song in songs.unwrap() { + query_info.model.insert(0, &SongObject::new(&song)); + } + } + StateUpdateKind::QueueDeleteRequest(index) => { + conn.delete(index).expect("Couldn't dequeue song"); + } + StateUpdateKind::QueueAddRequest(filename) => { + conn.push_str(filename).expect("Couldn't queue song"); + } + StateUpdateKind::PlaybackStateChange(action) => { + dispatch_playback_state_change(&mut conn, action) + .expect("Couldn't queue action"); + } + } + } + }); + }); + + application.run(); +} + +/// Take action on `conn` based on a `PlaybackStateChange` notification +fn dispatch_playback_state_change( + conn: &mut mpd::Client, + action: PlaybackStateChange, +) -> anyhow::Result<()> { + use PlaybackStateChange::*; + match action { + SkipBackwards => conn.prev()?, + SkipForwards => conn.next()?, + Start => conn.play()?, + Stop => conn.stop()?, + Pause => conn.pause(true)?, + } + Ok(()) +} + +/// Kind of event we can notify the UI future about +#[derive(Debug)] +enum StateUpdateKind { + MpdEvent, + WindowResizeEvent, + QueryUpdateEvent(String), + QueueAddRequest(String), + QueueDeleteRequest(u32), + PlaybackStateChange(PlaybackStateChange), +} + +/// A simple action that affects playback state. +#[derive(Debug)] +enum PlaybackStateChange { + Start, + Stop, + Pause, + SkipBackwards, + SkipForwards, } /// Produce a short status line for the current state of `conn`. @@ -47,6 +279,7 @@ impl SongInfo { let song_text = gtk::Label::new(None); song_text.set_justify(gtk::Justification::Center); song_text.set_line_wrap(true); + song_text.set_line_wrap_mode(pango::WrapMode::WordChar); container.add(&album_art); container.add(&song_text); @@ -58,142 +291,132 @@ impl SongInfo { gtk::IconSize::SmallToolbar, ); action_bar.add(&control_previous_song); - let sender1 = sender.clone(); - control_previous_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_previous_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( PlaybackStateChange::SkipBackwards, )) .expect("Couldn't notify thread"); - }); + })); let control_start_song = gtk::Button::from_icon_name( Some("media-playback-start-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_start_song); - let sender1 = sender.clone(); - control_start_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_start_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::StartPlayback, + PlaybackStateChange::Start, )) .expect("Couldn't notify thread"); - }); + })); let control_pause_song = gtk::Button::from_icon_name( Some("media-playback-pause-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_pause_song); - let sender1 = sender.clone(); - control_pause_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_pause_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::PausePlayback, + PlaybackStateChange::Pause, )) .expect("Couldn't notify thread"); - }); + })); let control_stop_song = gtk::Button::from_icon_name( Some("media-playback-stop-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_stop_song); - let sender1 = sender.clone(); - control_stop_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_stop_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::StopPlayback, + PlaybackStateChange::Stop, )) .expect("Couldn't notify thread"); - }); + })); let control_next_song = gtk::Button::from_icon_name( Some("media-skip-forward-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_next_song); - let sender1 = sender.clone(); - control_next_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_next_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( PlaybackStateChange::SkipForwards, )) .expect("Couldn't notify thread"); - }); + })); let model = gio::ListStore::new(SongObject::static_type()); let listbox = gtk::ListBox::new(); - let sender1 = sender.clone(); - listbox.bind_model(Some(&model), move |item| { - let sender = sender1.clone(); + listbox.bind_model( + Some(&model), + clone!(@strong sender => move |item| { + let sender = sender.clone(); - let box_ = gtk::ListBoxRow::new(); - let item = item - .downcast_ref::<SongObject>() - .expect("Row data is of wrong type"); - - let grid = gtk::Grid::builder().column_homogeneous(true).build(); - - let remove_individual_song = gtk::Button::from_icon_name( - Some("list-remove-symbolic"), - gtk::IconSize::SmallToolbar, - ); - remove_individual_song.set_visible(true); - let index = item.property::<u32>("index"); - remove_individual_song.connect_clicked(move |_| { - let mut sender = sender.clone(); - sender - .try_send(StateUpdateKind::QueueDeleteRequest(index)) - .expect("Couldn't notify thread"); - - // We don't actually get notified by `mpd`, but we can pretend - // that we did! - sender - .try_send(StateUpdateKind::MpdEvent) - .expect("Couldn't notify thread"); - }); - grid.attach(&remove_individual_song, 0, 0, 1, 1); - - let title_label = gtk::Label::new(None); - title_label.set_line_wrap(true); - item.bind_property("title", &title_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&title_label, 1, 0, 1, 1); + let box_ = gtk::ListBoxRow::new(); + let item = item + .downcast_ref::<SongObject>() + .expect("Row data is of wrong type"); - title_label.set_visible(true); // why? + let grid = gtk::Grid::builder().column_homogeneous(true).build(); - let album_label = gtk::Label::new(None); - album_label.set_line_wrap(true); - item.bind_property("album", &album_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&album_label, 2, 0, 1, 1); + let remove_individual_song = gtk::Button::from_icon_name( + Some("list-remove-symbolic"), + gtk::IconSize::SmallToolbar, + ); + let index = item.property::<u32>("index"); + remove_individual_song.connect_clicked(move |_| { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::QueueDeleteRequest(index)) + .expect("Couldn't notify thread"); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); + }); + grid.attach(&remove_individual_song, 0, 0, 1, 1); - album_label.set_visible(true); // why? + let title_label = gtk::Label::new(None); + title_label.set_line_wrap(true); + title_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("title", &title_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&title_label, 1, 0, 1, 1); - let artist_label = gtk::Label::new(None); - artist_label.set_line_wrap(true); - item.bind_property("artist", &artist_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&artist_label, 3, 0, 1, 1); - artist_label.set_visible(true); // why? + let album_label = gtk::Label::new(None); + album_label.set_line_wrap(true); + album_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("album", &album_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&album_label, 2, 0, 1, 1); - grid.set_visible(true); // why? - box_.add(&grid); + let artist_label = gtk::Label::new(None); + artist_label.set_line_wrap(true); + artist_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("artist", &artist_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&artist_label, 3, 0, 1, 1); - box_.upcast::<gtk::Widget>() - }); + grid.show_all(); + box_.add(&grid); + box_.upcast::<gtk::Widget>() + }), + ); let scrolled_window = gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); @@ -202,7 +425,6 @@ impl SongInfo { container.add(&action_bar); container.add(&scrolled_window); - container.show_all(); SongInfo { @@ -215,11 +437,11 @@ impl SongInfo { fn update_album_art(&self, conn: &mut mpd::Client) -> anyhow::Result<()> { if let Some(song) = conn.currentsong()? { - // If we've been allocated a window, pick the greatest dimension - // (width or height) and divide that dimension by two to get the - // size (in pixels) that we'll scale the album art to. Otherwise, we - // default to 128. - let album_art_size = std::cmp::max( + // If we've been allocated a window, pick the least dimension (width + // or height) and divide that dimension by two to get the size (in + // pixels) that we'll scale the album art to. Otherwise, we default + // to 128. + let album_art_size = std::cmp::min( self.container .window() .map(|x| x.width() / 2) @@ -252,16 +474,8 @@ impl SongInfo { self.update_album_art(conn)?; if let Some(song) = conn.currentsong()? { - let title = song - .title - .as_ref() - .map(|x| x.as_str()) - .unwrap_or("[Unknown]"); - let artist = song - .artist - .as_ref() - .map(|x| x.as_str()) - .unwrap_or("[Unknown]"); + let title = song.title.as_deref().unwrap_or("[Unknown]"); + let artist = song.artist.as_deref().unwrap_or("[Unknown]"); let album = song .tags .get("Album") @@ -270,15 +484,14 @@ impl SongInfo { let text = format!("{}\n{} - {}", title, artist, album); self.song_text.set_text(&text); + // We'll use `pango` attributes to make the display look nice and + // pretty. Scale the title of the song the most, and still make the + // other info reasonably large. let attr_list = gtk::pango::AttrList::new(); - - // Scale the title of the song the most. let mut attr = gtk::pango::AttrFloat::new_scale(2.0); attr.set_start_index(0); attr.set_end_index(title.len() as u32); attr_list.insert(attr); - - // And still make the other info reasonably large. let mut attr = gtk::pango::AttrFloat::new_scale(1.5); attr.set_start_index(title.len() as u32 + 1); attr_list.insert(attr); @@ -288,9 +501,10 @@ impl SongInfo { self.model.remove_all(); for (i, song) in conn.queue()?.iter().enumerate() { - let object = SongObject::new(&song); - object.set_index(i.try_into().unwrap()); - self.model.insert(i.try_into().unwrap(), &object) + let index = i.try_into().unwrap(); + let object = SongObject::new(song); + object.set_index(index); + self.model.insert(index, &object) } Ok(()) @@ -314,20 +528,18 @@ impl QueryInfo { let container = gtk::Box::new(gtk::Orientation::Vertical, 2); let query_input = gtk::Entry::builder().visible(true).build(); - let sender1 = sender.clone(); - query_input.connect_key_press_event(move |widget, _| { - let mut sender = sender1.clone(); // Interesting ownership puzzle :D + query_input.connect_key_press_event(clone!(@strong sender => move |widget, _| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::QueryUpdateEvent(widget.text().into())) .expect("Couldn't notify thread"); gtk::Inhibit(false) - }); + })); let model = gio::ListStore::new(SongObject::static_type()); let listbox = gtk::ListBox::new(); - let sender1 = sender.clone(); - listbox.bind_model(Some(&model), move |item| { - let sender = sender1.clone(); + listbox.bind_model(Some(&model), clone!(@strong sender => move |item| { + let sender = sender.clone(); let box_ = gtk::ListBoxRow::new(); let item = item @@ -346,42 +558,40 @@ impl QueryInfo { sender .try_send(StateUpdateKind::QueueAddRequest(filename)) .expect("Couldn't notify thread"); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); }); grid.attach(&add_individual_song, 0, 0, 1, 1); let title_label = gtk::Label::new(None); title_label.set_line_wrap(true); + title_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("title", &title_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&title_label, 1, 0, 1, 1); - title_label.set_visible(true); // why? - let album_label = gtk::Label::new(None); album_label.set_line_wrap(true); + album_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("album", &album_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&album_label, 2, 0, 1, 1); - album_label.set_visible(true); // why? - let artist_label = gtk::Label::new(None); artist_label.set_line_wrap(true); + artist_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("artist", &artist_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&artist_label, 3, 0, 1, 1); - artist_label.set_visible(true); // why? - - grid.set_visible(true); // why? - + grid.show_all(); box_.add(&grid); - box_.upcast::<gtk::Widget>() - }); + })); let scrolled_window = gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); @@ -401,11 +611,13 @@ impl AsRef<gtk::Widget> for QueryInfo { } } +// Unfortunately, to use the `ListStore` interface, we'll need to represent our +// data as an actual `glib` object. This is a little hairy in Rust, involving a +// fair bit of boilerplate, but not too terrible. glib::wrapper! { pub struct SongObject(ObjectSubclass<imp::SongObject>); } -use gtk::subclass::prelude::ObjectSubclassExt; impl SongObject { pub fn new(song: &mpd::song::Song) -> Self { glib::Object::new(&[ @@ -415,7 +627,7 @@ impl SongObject { &song .title .as_ref() - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[Untitled]".into()), ), ( @@ -423,7 +635,7 @@ impl SongObject { &song .artist .as_ref() - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[No Artist]".into()), ), ( @@ -431,7 +643,7 @@ impl SongObject { &song .tags .get("Album") - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[Untitled]".into()), ), ]) @@ -444,6 +656,8 @@ impl SongObject { } } +// These class "implementations" are typically done in a separate +// file/directory. I wanted to keep the example self-contained. mod imp { use std::cell::{Cell, RefCell}; @@ -531,185 +745,3 @@ mod imp { } } } - -/// Kind of event we can notify the UI future about -enum StateUpdateKind { - MpdEvent, - WindowResizeEvent, - QueryUpdateEvent(String), - QueueAddRequest(String), - QueueDeleteRequest(u32), - PlaybackStateChange(PlaybackStateChange), -} - -#[derive(Debug)] -enum PlaybackStateChange { - SkipBackwards, - SkipForwards, - StartPlayback, - StopPlayback, - PausePlayback, -} - -fn main() { - let application = gtk::Application::builder() - .application_id("space.jakob.Tunes") - .build(); - - application.connect_activate(|app| { - libhandy::init(); - - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - let (sender, mut receiver) = mpsc::channel(1000); - - let mut sender1 = sender.clone(); - std::thread::spawn(move || loop { - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - if let Ok(_subsystems) = conn.wait(&[mpd::idle::Subsystem::Player]) { - sender1 - .try_send(StateUpdateKind::MpdEvent) - .expect("Couldn't notify thread"); - } else { - break; - } - }); - - // conn.volume(100).unwrap(); - // conn.load("My Lounge Playlist", ..).unwrap(); - // conn.play().unwrap(); - - let stack = gtk::Stack::new(); - stack.set_expand(true); - - let song_info = SongInfo::new(sender.clone()); - stack.add_named(song_info.as_ref(), "current_song"); - stack.set_child_title(song_info.as_ref(), Some("Now Playing")); - stack.set_child_icon_name(song_info.as_ref(), Some("audio-speakers-symbolic")); - - let query_info = QueryInfo::new(sender.clone()); - stack.add_named(query_info.as_ref(), "query_songs"); - stack.set_child_title(query_info.as_ref(), Some("Search Database")); - stack.set_child_icon_name(query_info.as_ref(), Some("system-search-symbolic")); - - let header_bar = HeaderBar::builder() - .show_close_button(true) - .title(&header_title(&mut conn).unwrap()) - .build(); - let view_switcher_title = libhandy::ViewSwitcherTitle::builder() - .title("Tunes") - .stack(&stack) - .build(); - header_bar.add(&view_switcher_title); - - let view_switcher_bar = libhandy::ViewSwitcherBar::builder() - .visible(true) - .can_focus(false) - .stack(&stack) - .reveal(true) - .build(); - - let ui = TunesUI { header_bar }; - - // Combine the content in a box - let content = gtk::Box::new(gtk::Orientation::Vertical, 0); - content.set_vexpand(true); - // Handy's ApplicationWindow does not include a HeaderBar - content.add(&ui.header_bar); - content.add(&stack); - content.add(&view_switcher_bar); - - let window = ApplicationWindow::builder() - .default_width(350) - .default_height(70) - .modal(true) - // add content to window - .child(&content) - .build(); - window.set_application(Some(app)); - window.show_all(); - - window.connect_size_allocate(move |_, _| { - let mut sender = sender.clone(); - sender - .try_send(StateUpdateKind::WindowResizeEvent) - .expect("Couldn't notify thread"); - }); - - // Now that everything's been allocated a window, let's go ahead and - // update the widgets. - song_info - .update(&mut conn) - .expect("Couldn't update song info"); - - let mut query = mpd::Query::new(); - query.and(mpd::Term::Any, ""); - let songs = conn.find(&mut query, (0, 65535)); - // println!("{:?}", songs); - for song in songs.unwrap() { - query_info.model.insert(0, &SongObject::new(&song)); - } - - let main_context = gtk::glib::MainContext::default(); - main_context.spawn_local(async move { - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - while let Some(event_type) = receiver.next().await { - match event_type { - StateUpdateKind::MpdEvent => { - if let Ok(title) = header_title(&mut conn) { - ui.header_bar.set_title(Some(&title)); - song_info - .update(&mut conn) - .expect("Couldn't update song info"); - } - } - StateUpdateKind::WindowResizeEvent => { - song_info - .update_album_art(&mut conn) - .expect("Couldn't update album art"); - } - StateUpdateKind::QueryUpdateEvent(query_string) => { - // Let's not produce massive queries while the user is typing :) - if query_string.len() <= 2 { - continue; - } - query_info.model.remove_all(); - let mut query = mpd::Query::new(); - query.and(mpd::Term::Any, &query_string); - let songs = conn.search(&mut query, (0, 65535)); - // println!("{:?}", songs); - for song in songs.unwrap() { - query_info.model.insert(0, &SongObject::new(&song)); - } - } - StateUpdateKind::QueueDeleteRequest(index) => { - conn.delete(index).expect("Couldn't dequeue song"); - } - StateUpdateKind::QueueAddRequest(filename) => { - conn.push_str(filename).expect("Couldn't queue song"); - } - StateUpdateKind::PlaybackStateChange(action) => { - dispatch_playback_state_change(&mut conn, action) - .expect("Couldn't queue action"); - } - } - } - }); - }); - - application.run(); -} - -fn dispatch_playback_state_change( - conn: &mut mpd::Client, - action: PlaybackStateChange, -) -> anyhow::Result<()> { - use PlaybackStateChange::*; - match action { - SkipBackwards => conn.prev()?, - SkipForwards => conn.next()?, - StartPlayback => conn.play()?, - StopPlayback => conn.stop()?, - PausePlayback => conn.pause(true)?, - } - Ok(()) -} diff --git a/tunes.desktop b/tunes.desktop new file mode 100644 index 0000000..b00744a --- /dev/null +++ b/tunes.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Tunes +Comment=Mobile-friendly MPD client +Icon=mpd +Terminal=false +Exec=/usr/bin/tunes +Categories=Multimedia |