diff --git a/COPYING b/COPYING deleted file mode 100644 index be3f7b2..0000000 --- a/COPYING +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 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 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. - - - Copyright (C) - - 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 . - -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 -. diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index d46c1a9..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,1858 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" - -[[package]] -name = "atomic_refcell" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857253367827bd9d0fd973f0ef15506a96e79e41b0ad7aa691203a4e3214f6c8" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "cairo-rs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8af54f5d48af1226928adc1f57edd22f5df1349e7da1fc96ae15cf43db0e871" -dependencies = [ - "bitflags", - "cairo-sys-rs", - "glib 0.17.5", - "libc", - "once_cell", - "thiserror", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55382a01d30e5e53f185eee269124f5e21ab526595b872751278dfbb463594e" -dependencies = [ - "glib-sys 0.17.4", - "libc", - "system-deps", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "cfg-expr" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35b255461940a32985c627ce82900867c61db1659764d3675ea81963f72a4c6" -dependencies = [ - "smallvec", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "cpufeatures" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cxx" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.11", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.11", -] - -[[package]] -name = "dbus" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48b5f0f36f1eebe901b0e6bee369a77ed3396334bf3f09abd46454a576f71819" -dependencies = [ - "libc", - "libdbus-sys", -] - -[[package]] -name = "diesel" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4391a22b19c916e50bec4d6140f29bdda3e3bb187223fe6e3ea0b6e4d1021c04" -dependencies = [ - "diesel_derives", - "libsqlite3-sys", -] - -[[package]] -name = "diesel_derives" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad74fdcf086be3d4fdd142f67937678fe60ed431c3b2f08599e7687269410c4" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "diesel_migrations" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "field-offset" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - -[[package]] -name = "futures-channel" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" - -[[package]] -name = "futures-executor" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" - -[[package]] -name = "futures-macro" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "futures-task" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" - -[[package]] -name = "futures-util" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b023fbe0c6b407bd3d9805d107d9800da3829dc5a676653210f1d5f16d7f59bf" -dependencies = [ - "bitflags", - "gdk-pixbuf-sys", - "gio", - "glib 0.17.5", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b41bd2b44ed49d99277d3925652a163038bd5ed943ec9809338ffb2f4391e3b" -dependencies = [ - "gio-sys", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "system-deps", -] - -[[package]] -name = "gdk4" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3abf96408a26e3eddf881a7f893a1e111767137136e347745e8ea6ed12731ff" -dependencies = [ - "bitflags", - "cairo-rs", - "gdk-pixbuf", - "gdk4-sys", - "gio", - "glib 0.17.5", - "libc", - "pango", -] - -[[package]] -name = "gdk4-sys" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc92aa1608c089c49393d014c38ac0390d01e4841e1fedaa75dbcef77aaed64" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "gettext-rs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" -dependencies = [ - "gettext-sys", - "locale_config", -] - -[[package]] -name = "gettext-sys" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" -dependencies = [ - "cc", - "temp-dir", -] - -[[package]] -name = "gio" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261a3b4e922ec676d1c27ac466218c38cf5dcb49a759129e54bb5046e442125" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib 0.17.5", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror", -] - -[[package]] -name = "gio-sys" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1d43b0d7968b48455244ecafe41192871257f5740aa6b095eb19db78e362a5" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.15.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.15.13", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "once_cell", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib" -version = "0.17.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb53061756195d76969292c2d2e329e01259276524a9bae6c9b73af62854773" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros 0.17.6", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib-macros" -version = "0.15.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" -dependencies = [ - "anyhow", - "heck", - "proc-macro-crate", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "glib-macros" -version = "0.17.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e73a9790e243f6d55d8e302426419f6084a1de7a84cd07f7268300408a19de" -dependencies = [ - "anyhow", - "heck", - "proc-macro-crate", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "glib-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glib-sys" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f00ad0a1bf548e61adfff15d83430941d9e1bb620e334f779edd1c745680a5" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" -dependencies = [ - "glib-sys 0.15.10", - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e75b0000a64632b2d8ca3cf856af9308e3a970844f6e9659bd197f026793d0" -dependencies = [ - "glib-sys 0.17.4", - "libc", - "system-deps", -] - -[[package]] -name = "graphene-rs" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21cf11565bb0e4dfc2f99d4775b6c329f0d40a2cff9c0066214d31a0e1b46256" -dependencies = [ - "glib 0.17.5", - "graphene-sys", - "libc", -] - -[[package]] -name = "graphene-sys" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf80a4849a8d9565410a8fec6fc3678e9c617f4ac7be182ca55ab75016e07af9" -dependencies = [ - "glib-sys 0.17.4", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gsk4" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f01ef44fa7cac15e2da9978529383e6bee03e570ba5bf7036b4c10a15cc3a3c" -dependencies = [ - "bitflags", - "cairo-rs", - "gdk4", - "glib 0.17.5", - "graphene-rs", - "gsk4-sys", - "libc", - "pango", -] - -[[package]] -name = "gsk4-sys" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07a84fb4dcf1323d29435aa85e2f5f58bef564342bef06775ec7bd0da1f01b0" -dependencies = [ - "cairo-sys-rs", - "gdk4-sys", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "graphene-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gstreamer" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c46cc10a7ab79329feb68bef54a242ced84c3147cc1b81bc5c6140346a1dbf9" -dependencies = [ - "bitflags", - "cfg-if", - "futures-channel", - "futures-core", - "futures-util", - "glib 0.17.5", - "gstreamer-sys", - "libc", - "muldiv", - "num-integer", - "num-rational", - "once_cell", - "option-operations", - "paste", - "pretty-hex", - "smallvec", - "thiserror", -] - -[[package]] -name = "gstreamer-audio" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6d26ab15835a268939e2367ed4ddb1e7157b03d0bb56ba4a0b036c1ac8393" -dependencies = [ - "bitflags", - "cfg-if", - "glib 0.17.5", - "gstreamer", - "gstreamer-audio-sys", - "gstreamer-base", - "libc", - "once_cell", -] - -[[package]] -name = "gstreamer-audio-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4001b779e4707b32acd6ec0960e327b926369c1a34f7c41d477ac42b2670e8" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-base" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5598bfedbff12675a6cddbe420b6a3ba5039c64aaf7df130db6339d09b634b0e" -dependencies = [ - "atomic_refcell", - "bitflags", - "cfg-if", - "glib 0.17.5", - "gstreamer", - "gstreamer-base-sys", - "libc", -] - -[[package]] -name = "gstreamer-base-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26114ed96f6668380f5a1554128159e98e06c3a7a8460f216d7cd6dce28f928c" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-pbutils" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573aa7032124a61ddebad128f85fac777f58907621d08f3309d9d97fad6d1131" -dependencies = [ - "bitflags", - "glib 0.17.5", - "gstreamer", - "gstreamer-audio", - "gstreamer-pbutils-sys", - "gstreamer-video", - "libc", - "thiserror", -] - -[[package]] -name = "gstreamer-pbutils-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb4493d59f28023656686c7a3581ddbd510b309a861776586afcf9a52ed222b" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gstreamer-audio-sys", - "gstreamer-sys", - "gstreamer-video-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-player" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76566698a39a2d80b17fa80b5bacb7f1ae728facb7f2b2090532380250b5cbfe" -dependencies = [ - "bitflags", - "glib 0.17.5", - "gstreamer", - "gstreamer-player-sys", - "gstreamer-video", - "libc", - "once_cell", -] - -[[package]] -name = "gstreamer-player-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15321aaaf3bb247b4af3e09456a62dc17f030515d6328377a34081d9ed5803c0" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gstreamer-sys", - "gstreamer-video-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56fe047adef7d47dbafa8bc1340fddb53c325e16574763063702fc94b5786d2" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "system-deps", -] - -[[package]] -name = "gstreamer-video" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467cddb6a4135e72fefb6ba21262b1cca5493e9928792e88fe672ec0a37b761c" -dependencies = [ - "bitflags", - "cfg-if", - "futures-channel", - "glib 0.17.5", - "gstreamer", - "gstreamer-base", - "gstreamer-video-sys", - "libc", - "once_cell", -] - -[[package]] -name = "gstreamer-video-sys" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ddb6112d438aac0004d2db6053a572f92b1c5e0e9d6ff6c71d9245f7f73e46" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5bf7748fd4cd0b2490df8debcc911809dbcbee4ece9531b96c29a9c729de5a" - -[[package]] -name = "gtk4" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e30e124b5a605f6f5513db13958bfcd51d746607b20bc7bb718b33e303274ed" -dependencies = [ - "bitflags", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk-pixbuf", - "gdk4", - "gio", - "glib 0.17.5", - "graphene-rs", - "gsk4", - "gtk4-macros", - "gtk4-sys", - "libc", - "once_cell", - "pango", -] - -[[package]] -name = "gtk4-macros" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f041a797fb098bfb06e432c61738133604bfa3af57f13f1da3b9d46271422ef0" -dependencies = [ - "anyhow", - "proc-macro-crate", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "gtk4-sys" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f8283f707b07e019e76c7f2934bdd4180c277e08aa93f4c0d8dd07b7a34e22f" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk4-sys", - "gio-sys", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "graphene-sys", - "gsk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "iana-time-zone" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libadwaita" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c4efd2020a4fcedbad2c4a97de97bf6045e5dc49d61d5a5d0cfd753db60700" -dependencies = [ - "bitflags", - "futures-channel", - "gdk-pixbuf", - "gdk4", - "gio", - "glib 0.17.5", - "gtk4", - "libadwaita-sys", - "libc", - "once_cell", - "pango", -] - -[[package]] -name = "libadwaita-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0727b85b4fe2b1bed5ac90df6343de15cbf8118bfb96d7c3cc1512681a4b34ac" -dependencies = [ - "gdk4-sys", - "gio-sys", - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "gtk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "libc" -version = "0.2.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" - -[[package]] -name = "libdbus-sys" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - -[[package]] -name = "locale_config" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" -dependencies = [ - "lazy_static", - "objc", - "objc-foundation", - "regex", - "winapi", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] - -[[package]] -name = "migrations_internals" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc" -dependencies = [ - "serde", - "toml 0.5.11", -] - -[[package]] -name = "migrations_macros" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - -[[package]] -name = "mpris-player" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be832ec9171fdaf43609d02bb552f4129ba6eacd184bb25186e2906dbd3cf098" -dependencies = [ - "dbus", - "glib 0.15.12", -] - -[[package]] -name = "muldiv" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" - -[[package]] -name = "musicus" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures-channel", - "gettext-rs", - "gio", - "glib 0.17.5", - "gstreamer", - "gtk-macros", - "gtk4", - "libadwaita", - "log", - "musicus_backend", - "once_cell", - "rand", - "sanitize-filename", -] - -[[package]] -name = "musicus_backend" -version = "0.1.0" -dependencies = [ - "chrono", - "fragile", - "gio", - "glib 0.17.5", - "gstreamer", - "gstreamer-player", - "log", - "mpris-player", - "musicus_database", - "musicus_import", - "thiserror", - "tokio", -] - -[[package]] -name = "musicus_database" -version = "0.1.0" -dependencies = [ - "chrono", - "diesel", - "diesel_migrations", - "log", - "rand", - "thiserror", - "uuid", -] - -[[package]] -name = "musicus_import" -version = "0.1.0" -dependencies = [ - "base64", - "glib 0.17.5", - "gstreamer", - "gstreamer-pbutils", - "log", - "once_cell", - "rand", - "sha2", - "thiserror", - "tokio", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "option-operations" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" -dependencies = [ - "paste", -] - -[[package]] -name = "pango" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c280b82a881e4208afb3359a8e7fde27a1b272280981f1f34610bed5770d37" -dependencies = [ - "bitflags", - "gio", - "glib 0.17.5", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4293d0f0b5525eb5c24734d30b0ed02cd02aa734f216883f376b54de49625de8" -dependencies = [ - "glib-sys 0.17.4", - "gobject-sys 0.17.4", - "libc", - "system-deps", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "pretty-hex" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "sanitize-filename" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "scratch" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.11", -] - -[[package]] -name = "serde_spanned" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" -dependencies = [ - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "6.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 0.7.3", - "version-compare", -] - -[[package]] -name = "temp-dir" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.11", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tokio" -version = "1.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" -dependencies = [ - "autocfg", - "pin-project-lite", - "windows-sys", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "uuid" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" -dependencies = [ - "getrandom", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "winnow" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" -dependencies = [ - "memchr", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 29005dd..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[workspace] -members = [ - "crates/backend", - "crates/database", - "crates/import", - "crates/musicus" -] diff --git a/README.md b/README.md deleted file mode 100644 index aa29edc..0000000 --- a/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Musicus - -This is a desktop app for Musicus. - -## Hacking - -### Building - -Musicus uses the [Meson build system](https://mesonbuild.com/). You can build -it using the following commands: - -``` -$ meson build -$ ninja -C build -``` - -Afterwards the resulting binary executable is under -`build/target/debug/musicus`. - -### Flatpak - -There is a Flatpak manifest file called `de.johrpan.musicus.json`. To build a -Flatpak you need the the latest Gnome SDK and the Freedesktop SDK with the Rust -extension. You can install those using the following commands: - -``` -$ flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo -$ flatpak remote-add --user --if-not-exists gnome-nightly https://nightly.gnome.org/gnome-nightly.flatpakrepo -$ flatpak install --user gnome-nightly org.gnome.Sdk org.gnome.Platform -$ flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//19.08 -``` - -Afterwards, the following commands will build, install and run the application: - -``` -$ rm -rf flatpak -$ flatpak-builder --user --install flatpak de.johrpan.musicus.json -$ flatpak run de.johrpan.musicus -``` - -### Special requirements - -This program uses [Diesel](https://diesel.rs) as its ORM. After installing -the Diesel command line utility, you will be able to create a new schema -migration using the following command: - -``` -$ diesel migration generate [change_description] -``` - -To update the `src/database/schema.rs` file, you should use the following -command: - -``` -$ diesel migration run --database-url test.sqlite -``` - -This file should never be edited manually. - -## License - -Musicus is free and open source 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. - -Musicus 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/. diff --git a/build-aux/cargo.sh b/build-aux/cargo.sh deleted file mode 100644 index 4a7236d..0000000 --- a/build-aux/cargo.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh - -export MESON_BUILD_ROOT="$1" -export MESON_SOURCE_ROOT="$2" -export CARGO_TARGET_DIR="$MESON_BUILD_ROOT"/target -export CARGO_HOME="$CARGO_TARGET_DIR"/cargo-home -export OUTPUT="$3" -export BUILDTYPE="$4" -export APP_BIN="$5" - -if [ -z ${CARGO_BUILD_TARGET+defined} ]; then - CARGO_OUTPUT_PATH="${CARGO_TARGET_DIR}" -else - CARGO_OUTPUT_PATH="${CARGO_TARGET_DIR}/${CARGO_BUILD_TARGET}" -fi - -if [ $BUILDTYPE = "release" ]; then - echo "RELEASE MODE" - cargo build --manifest-path \ - "$MESON_SOURCE_ROOT"/Cargo.toml --release && \ - cp "$CARGO_OUTPUT_PATH"/release/"$APP_BIN" "$OUTPUT" -else - echo "DEBUG MODE" - cargo build --manifest-path \ - "$MESON_SOURCE_ROOT"/Cargo.toml --verbose && \ - cp "$CARGO_OUTPUT_PATH"/debug/"$APP_BIN" "$OUTPUT" -fi - diff --git a/build-aux/postinstall.py b/build-aux/postinstall.py deleted file mode 100755 index 6a3ea97..0000000 --- a/build-aux/postinstall.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -from os import environ, path -from subprocess import call - -prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') -datadir = path.join(prefix, 'share') -destdir = environ.get('DESTDIR', '') - -# Package managers set this so we don't need to run -if not destdir: - print('Updating icon cache...') - call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) - - print('Updating desktop database...') - call(['update-desktop-database', '-q', path.join(datadir, 'applications')]) - - print('Compiling GSettings schemas...') - call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')]) - - diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index 2b47d11..0000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -type-complexity-threshold = 500 \ No newline at end of file diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml deleted file mode 100644 index 22fe818..0000000 --- a/crates/backend/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "musicus_backend" -version = "0.1.0" -edition = "2021" - -[dependencies] -chrono = "0.4" -fragile = "2" -gio = "0.17" -glib = "0.17" -gstreamer = "0.20" -gstreamer-player = "0.20" -log = { version = "0.4", features = ["std"] } -musicus_database = { version = "0.1.0", path = "../database" } -musicus_import = { version = "0.1.0", path = "../import" } -thiserror = "1" -tokio = { version = "1", features = ["sync"] } - -[target.'cfg(target_os = "linux")'.dependencies] -mpris-player = "0.6" diff --git a/crates/backend/src/error.rs b/crates/backend/src/error.rs deleted file mode 100644 index 4e64332..0000000 --- a/crates/backend/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// An error that happened within the backend. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - DatabaseError(#[from] musicus_database::Error), - - #[error("An error happened while decoding to UTF-8.")] - Utf8Error(#[from] std::str::Utf8Error), - - #[error("Failed to receive an event.")] - RecvError(#[from] tokio::sync::broadcast::error::RecvError), - - #[error("An error happened: {0}")] - Other(String), -} - -pub type Result = std::result::Result; diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs deleted file mode 100644 index 42e61f0..0000000 --- a/crates/backend/src/lib.rs +++ /dev/null @@ -1,191 +0,0 @@ -use db::SqliteConnection; -use gio::traits::SettingsExt; -use log::warn; -use std::{ - cell::{Cell, RefCell}, - path::PathBuf, - rc::Rc, - sync::{Arc, Mutex}, -}; -use tokio::sync::{broadcast, broadcast::Sender}; - -pub use musicus_database as db; -pub use musicus_import as import; - -pub mod error; -pub use error::*; - -pub mod library; -pub use library::*; - -mod logger; -pub use logger::{LogMessage, Logger}; - -pub mod player; -pub use player::*; - -/// General states the application can be in. -#[derive(Debug, Copy, Clone)] -pub enum BackendState { - /// The backend is not set up yet. This means that no backend methods except for setting the - /// music library path should be called. The user interface should adapt and only present this - /// option. - NoMusicLibrary, - - /// The backend is loading the music library. No methods should be called. The user interface - /// should represent that state by prohibiting all interaction. - Loading, - - /// The backend is ready and all methods may be called. - Ready, -} - -/// A collection of all backend state and functionality. -pub struct Backend { - /// Registered instance of [Logger]. - logger: Arc, - - /// A closure that will be called whenever the backend state changes. - state_cb: RefCell>>, - - /// Access to GSettings. - settings: gio::Settings, - - /// The current path to the music library, which is used by the player and the database. This - /// is guaranteed to be Some, when the state is set to BackendState::Ready. - music_library_path: RefCell>, - - /// The sender for sending library update notifications. - library_updated_sender: Sender<()>, - - /// The database. This can be assumed to exist, when the state is set to BackendState::Ready. - database: RefCell>>>, - - /// The player handling playlist and playback. This can be assumed to exist, when the state is - /// set to BackendState::Ready. - player: RefCell>>, - - /// Whether to keep playing random tracks after the playlist ends. - keep_playing: Cell, - - /// Whether to choose full recordings for random playback. - play_full_recordings: Cell, -} - -impl Backend { - /// Create a new backend initerface. The user interface should subscribe to the state stream - /// and call init() afterwards. There may be only one backend for a process and this method - /// may only be called exactly once. Otherwise it will panic. - pub fn new() -> Self { - let logger = logger::register(); - let (library_updated_sender, _) = broadcast::channel(1024); - - Backend { - logger, - state_cb: RefCell::new(None), - settings: gio::Settings::new("de.johrpan.musicus"), - music_library_path: RefCell::new(None), - library_updated_sender, - database: RefCell::new(None), - player: RefCell::new(None), - keep_playing: Cell::new(false), - play_full_recordings: Cell::new(true), - } - } - - /// Get the registered instance of [Logger]. - pub fn logger(&self) -> Arc { - Arc::clone(&self.logger) - } - - /// Set the closure to be called whenever the backend state changes. - pub fn set_state_cb(&self, cb: F) { - self.state_cb.replace(Some(Box::new(cb))); - } - - /// Initialize the backend. A state callback should already have been registered using - /// [`set_state_cb()`] to react to the result. - pub fn init(self: Rc) -> Result<()> { - self.keep_playing.set(self.settings.boolean("keep-playing")); - self.play_full_recordings - .set(self.settings.boolean("play-full-recordings")); - - Rc::clone(&self).init_library()?; - - match self.get_music_library_path() { - None => self.set_state(BackendState::NoMusicLibrary), - Some(_) => self.set_state(BackendState::Ready), - }; - - Ok(()) - } - - /// Whether to keep playing random tracks after the playlist ends. - pub fn keep_playing(&self) -> bool { - self.keep_playing.get() - } - - /// Set whether to keep playing random tracks after the playlist ends. - pub fn set_keep_playing(self: Rc, keep_playing: bool) { - if let Err(err) = self.settings.set_boolean("keep-playing", keep_playing) { - warn!( - "The preference \"keep-playing\" could not be saved using GSettings. It will most \ - likely not be available at the next startup. Error message: {}", - err - ); - } - - self.keep_playing.set(keep_playing); - self.update_track_generator(); - } - - /// Whether to choose full recordings for random playback. - pub fn play_full_recordings(&self) -> bool { - self.play_full_recordings.get() - } - - /// Set whether to choose full recordings for random playback. - pub fn set_play_full_recordings(self: Rc, play_full_recordings: bool) { - if let Err(err) = self - .settings - .set_boolean("play-full-recordings", play_full_recordings) - { - warn!( - "The preference \"play-full-recordings\" could not be saved using GSettings. It \ - will most likely not be available at the next startup. Error message: {}", - err - ); - } - - self.play_full_recordings.set(play_full_recordings); - self.update_track_generator(); - } - - /// Set the current state and notify the user interface. - fn set_state(&self, state: BackendState) { - if let Some(cb) = &*self.state_cb.borrow() { - cb(state); - } - } - - /// Apply the current track generation settings. - fn update_track_generator(self: Rc) { - if let Some(player) = self.get_player() { - if self.keep_playing() { - if self.play_full_recordings() { - player.set_track_generator(Some(RandomRecordingGenerator::new(self))); - } else { - player.set_track_generator(Some(RandomTrackGenerator::new(self))); - } - } else { - player.set_track_generator(None::); - } - } - } -} - -impl Default for Backend { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/backend/src/library.rs b/crates/backend/src/library.rs deleted file mode 100644 index fc74e34..0000000 --- a/crates/backend/src/library.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{Backend, BackendState, Player, Result}; -use gio::prelude::*; -use log::warn; -use musicus_database::SqliteConnection; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::{Arc, Mutex}; - -impl Backend { - /// Initialize the music library if it is set in the settings. - pub(super) fn init_library(self: Rc) -> Result<()> { - let path = self.settings.string("music-library-path"); - if !path.is_empty() { - self.set_music_library_path_priv(PathBuf::from(path.to_string()))?; - } - - Ok(()) - } - - /// Set the path to the music library folder and connect to the database. - pub fn set_music_library_path(self: Rc, path: PathBuf) -> Result<()> { - if let Err(err) = self - .settings - .set_string("music-library-path", path.to_str().unwrap()) - { - warn!( - "The music library path could not be saved using GSettings. It will most likely \ - not be available at the next startup. Error message: {}", - err - ); - } - - self.set_music_library_path_priv(path) - } - - /// Set the path to the music library folder and and connect to the database. - pub fn set_music_library_path_priv(self: Rc, path: PathBuf) -> Result<()> { - self.set_state(BackendState::Loading); - - self.music_library_path.replace(Some(path.clone())); - - let mut db_path = path.clone(); - db_path.push("musicus.db"); - - let database = Arc::new(Mutex::new(musicus_database::connect( - db_path.to_str().unwrap(), - )?)); - - self.database.replace(Some(database)); - - let player = Player::new(path); - self.player.replace(Some(player)); - - Rc::clone(&self).update_track_generator(); - - self.set_state(BackendState::Ready); - - Ok(()) - } - - /// Get the currently set music library path. - pub fn get_music_library_path(&self) -> Option { - self.music_library_path.borrow().clone() - } - - /// Get an interface to the database and panic if there is none. - pub fn db(&self) -> Arc> { - self.database.borrow().clone().unwrap() - } - - /// Get an interface to the playback service. - pub fn get_player(&self) -> Option> { - self.player.borrow().clone() - } - - /// Wait for the next library update. - pub async fn library_update(&self) -> Result<()> { - Ok(self.library_updated_sender.subscribe().recv().await?) - } - - /// Notify the frontend that the library was changed. - pub fn library_changed(&self) { - self.library_updated_sender.send(()).unwrap(); - } - - /// Get an interface to the player and panic if there is none. - pub fn pl(&self) -> Rc { - self.get_player().unwrap() - } -} diff --git a/crates/backend/src/logger.rs b/crates/backend/src/logger.rs deleted file mode 100644 index 5f4c15a..0000000 --- a/crates/backend/src/logger.rs +++ /dev/null @@ -1,84 +0,0 @@ -use chrono::{DateTime, Local}; -use log::{Level, LevelFilter, Log, Metadata, Record}; -use std::{ - fmt::Display, - sync::{Arc, Mutex}, -}; - -/// Register the custom logger. This will panic if called more than once. -pub fn register() -> Arc { - let logger = Arc::new(Logger::default()); - - log::set_boxed_logger(Box::new(Arc::clone(&logger))) - .map(|()| log::set_max_level(LevelFilter::Info)) - .unwrap(); - - logger -} - -/// A simple logging handler that prints out all messages and caches them for -/// later access by the user interface. -pub struct Logger { - /// All messages since the start of the program. - messages: Mutex>, -} - -impl Logger { - pub fn messages(&self) -> Vec { - self.messages.lock().unwrap().clone() - } -} - -impl Default for Logger { - fn default() -> Self { - Self { - messages: Mutex::new(Vec::new()), - } - } -} - -impl Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info - } - - fn log(&self, record: &Record) { - if record.level() <= Level::Info { - let message = record.into(); - println!("{}", message); - self.messages.lock().unwrap().push(message); - } - } - - fn flush(&self) {} -} - -/// A simplified representation of a [`Record`]. -#[derive(Clone)] -pub struct LogMessage { - pub time: DateTime, - pub level: String, - pub module: String, - pub message: String, -} - -impl<'a> From<&Record<'a>> for LogMessage { - fn from(record: &Record<'a>) -> Self { - Self { - time: Local::now(), - level: record.level().to_string(), - module: String::from(record.module_path().unwrap_or_else(|| record.target())), - message: format!("{}", record.args()), - } - } -} - -impl Display for LogMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} ({}): {}", - self.time, self.module, self.level, self.message - ) - } -} diff --git a/crates/backend/src/player.rs b/crates/backend/src/player.rs deleted file mode 100644 index e18bd31..0000000 --- a/crates/backend/src/player.rs +++ /dev/null @@ -1,484 +0,0 @@ -use crate::{Backend, Error, Result}; -use db::Track; -use glib::clone; -use gstreamer_player::PlayerVideoRenderer; -use musicus_database as db; -use std::cell::{Cell, RefCell}; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; - -#[cfg(target_os = "linux")] -use mpris_player::{Metadata, MprisPlayer, PlaybackStatus}; - -pub struct Player { - music_library_path: PathBuf, - player: gstreamer_player::Player, - playlist: RefCell>, - current_track: Cell>, - playing: Cell, - duration: Cell, - track_generator: RefCell>>, - playlist_cbs: RefCell)>>>, - track_cbs: RefCell>>, - duration_cbs: RefCell>>, - playing_cbs: RefCell>>, - position_cbs: RefCell>>, - raise_cb: RefCell>>, - - #[cfg(target_os = "linux")] - mpris: Arc, -} - -impl Player { - pub fn new(music_library_path: PathBuf) -> Rc { - let dispatcher = gstreamer_player::PlayerGMainContextSignalDispatcher::new(None); - let player = gstreamer_player::Player::new(None::, Some(dispatcher)); - let mut config = player.config(); - config.set_position_update_interval(250); - player.set_config(config).unwrap(); - player.set_video_track_enabled(false); - - let result = Rc::new(Self { - music_library_path, - player: player.clone(), - playlist: RefCell::new(Vec::new()), - current_track: Cell::new(None), - playing: Cell::new(false), - duration: Cell::new(0), - track_generator: RefCell::new(None), - playlist_cbs: RefCell::new(Vec::new()), - track_cbs: RefCell::new(Vec::new()), - duration_cbs: RefCell::new(Vec::new()), - playing_cbs: RefCell::new(Vec::new()), - position_cbs: RefCell::new(Vec::new()), - raise_cb: RefCell::new(None), - #[cfg(target_os = "linux")] - mpris: { - let mpris = MprisPlayer::new( - "de.johrpan.musicus".to_string(), - "Musicus".to_string(), - "de.johrpan.musicus.desktop".to_string(), - ); - - mpris.set_can_raise(true); - mpris.set_can_play(false); - mpris.set_can_go_previous(false); - mpris.set_can_go_next(false); - mpris.set_can_seek(false); - mpris.set_can_set_fullscreen(false); - - mpris - }, - }); - - let clone = fragile::Fragile::new(result.clone()); - player.connect_end_of_stream(move |_| { - let clone = clone.get(); - if clone.has_next() { - clone.next().unwrap(); - } else { - clone.player.stop(); - clone.playing.replace(false); - - for cb in &*clone.playing_cbs.borrow() { - cb(false); - } - - #[cfg(target_os = "linux")] - clone.mpris.set_playback_status(PlaybackStatus::Paused); - } - }); - - let clone = fragile::Fragile::new(result.clone()); - player.connect_position_updated(move |_, position| { - for cb in &*clone.get().position_cbs.borrow() { - cb(position.unwrap().mseconds()); - } - }); - - let clone = fragile::Fragile::new(result.clone()); - player.connect_duration_changed(move |_, duration| { - for cb in &*clone.get().duration_cbs.borrow() { - let duration = duration.unwrap().mseconds(); - clone.get().duration.set(duration); - cb(duration); - } - }); - - #[cfg(target_os = "linux")] - { - result - .mpris - .connect_play_pause(clone!(@weak result => move || { - result.play_pause().unwrap(); - })); - - result.mpris.connect_play(clone!(@weak result => move || { - if !result.is_playing() { - result.play_pause().unwrap(); - } - })); - - result.mpris.connect_pause(clone!(@weak result => move || { - if result.is_playing() { - result.play_pause().unwrap(); - } - })); - - result - .mpris - .connect_previous(clone!(@weak result => move || { - let _ = result.previous(); - })); - - result.mpris.connect_next(clone!(@weak result => move || { - let _ = result.next(); - })); - - result.mpris.connect_raise(clone!(@weak result => move || { - let cb = result.raise_cb.borrow(); - if let Some(cb) = &*cb { - cb() - } - })); - } - - result - } - - pub fn set_track_generator(&self, generator: Option) { - self.track_generator.replace(match generator { - Some(generator) => Some(Box::new(generator)), - None => None, - }); - } - - pub fn add_playlist_cb) + 'static>(&self, cb: F) { - self.playlist_cbs.borrow_mut().push(Box::new(cb)); - } - - pub fn add_track_cb(&self, cb: F) { - self.track_cbs.borrow_mut().push(Box::new(cb)); - } - - pub fn add_duration_cb(&self, cb: F) { - self.duration_cbs.borrow_mut().push(Box::new(cb)); - } - - pub fn add_playing_cb(&self, cb: F) { - self.playing_cbs.borrow_mut().push(Box::new(cb)); - } - - pub fn add_position_cb(&self, cb: F) { - self.position_cbs.borrow_mut().push(Box::new(cb)); - } - - pub fn set_raise_cb(&self, cb: F) { - self.raise_cb.replace(Some(Box::new(cb))); - } - - pub fn get_playlist(&self) -> Vec { - self.playlist.borrow().clone() - } - - pub fn get_current_track(&self) -> Option { - self.current_track.get() - } - - pub fn get_duration(&self) -> Option { - self.player.duration() - } - - pub fn is_playing(&self) -> bool { - self.playing.get() - } - - /// Add some items to the playlist. - pub fn add_items(&self, mut items: Vec) -> Result<()> { - if items.is_empty() { - return Ok(()); - } - - let was_empty = { - let mut playlist = self.playlist.borrow_mut(); - let was_empty = playlist.is_empty(); - - playlist.append(&mut items); - - was_empty - }; - - for cb in &*self.playlist_cbs.borrow() { - cb(self.playlist.borrow().clone()); - } - - if was_empty { - self.set_track(0)?; - self.player.play(); - self.playing.set(true); - - for cb in &*self.playing_cbs.borrow() { - cb(true); - } - - #[cfg(target_os = "linux")] - { - self.mpris.set_can_play(true); - self.mpris.set_playback_status(PlaybackStatus::Playing); - } - } - - Ok(()) - } - - pub fn play_pause(&self) -> Result<()> { - if self.is_playing() { - self.player.pause(); - self.playing.set(false); - - for cb in &*self.playing_cbs.borrow() { - cb(false); - } - - #[cfg(target_os = "linux")] - self.mpris.set_playback_status(PlaybackStatus::Paused); - } else { - if self.current_track.get().is_none() { - self.next()?; - } - - self.player.play(); - self.playing.set(true); - - for cb in &*self.playing_cbs.borrow() { - cb(true); - } - - #[cfg(target_os = "linux")] - self.mpris.set_playback_status(PlaybackStatus::Playing); - } - - Ok(()) - } - - pub fn seek(&self, ms: u64) { - self.player.seek(gstreamer::ClockTime::from_mseconds(ms)); - } - - pub fn has_previous(&self) -> bool { - if let Some(current_track) = self.current_track.get() { - current_track > 0 - } else { - false - } - } - - pub fn previous(&self) -> Result<()> { - let mut current_track = self.current_track.get().ok_or_else(|| { - Error::Other(String::from( - "Player tried to access non existant current track.", - )) - })?; - - if current_track > 0 { - current_track -= 1; - } else { - return Err(Error::Other(String::from("No existing previous track."))); - } - - self.set_track(current_track) - } - - pub fn has_next(&self) -> bool { - if let Some(generator) = &*self.track_generator.borrow() { - generator.has_next() - } else if let Some(current_track) = self.current_track.get() { - let playlist = self.playlist.borrow(); - current_track + 1 < playlist.len() - } else { - false - } - } - - pub fn next(&self) -> Result<()> { - let current_track = self.current_track.get(); - let generator = self.track_generator.borrow(); - - if let Some(current_track) = current_track { - if current_track + 1 >= self.playlist.borrow().len() { - if let Some(generator) = &*generator { - let items = generator.next(); - if !items.is_empty() { - self.add_items(items)?; - } else { - return Err(Error::Other(String::from( - "Track generator failed to generate next track.", - ))); - } - } else { - return Err(Error::Other(String::from("No existing next track."))); - } - } - - self.set_track(current_track + 1)?; - - Ok(()) - } else if let Some(generator) = &*generator { - let items = generator.next(); - if !items.is_empty() { - self.add_items(items)?; - } else { - return Err(Error::Other(String::from( - "Track generator failed to generate next track.", - ))); - } - - Ok(()) - } else { - Err(Error::Other(String::from("No existing next track."))) - } - } - - pub fn set_track(&self, current_track: usize) -> Result<()> { - let track = &self.playlist.borrow()[current_track]; - - let path = self - .music_library_path - .join(track.path.clone()) - .into_os_string() - .into_string() - .unwrap(); - - let uri = glib::filename_to_uri(&path, None) - .map_err(|_| Error::Other(format!("Failed to create URI from path: {}", path)))?; - - self.player.set_uri(Some(&uri)); - - if self.is_playing() { - self.player.play(); - } - - self.current_track.set(Some(current_track)); - - for cb in &*self.track_cbs.borrow() { - cb(current_track); - } - - #[cfg(target_os = "linux")] - { - let mut parts = Vec::::new(); - for part in &track.work_parts { - parts.push(track.recording.work.parts[*part].title.clone()); - } - - let mut title = track.recording.work.get_title(); - if !parts.is_empty() { - title = format!("{}: {}", title, parts.join(", ")); - } - - let subtitle = track.recording.get_performers(); - - let mut metadata = Metadata::new(); - metadata.artist = Some(vec![title]); - metadata.title = Some(subtitle); - - self.mpris.set_metadata(metadata); - self.mpris.set_can_go_previous(self.has_previous()); - self.mpris.set_can_go_next(self.has_next()); - } - - Ok(()) - } - - pub fn send_data(&self) { - for cb in &*self.playlist_cbs.borrow() { - cb(self.playlist.borrow().clone()); - } - - for cb in &*self.track_cbs.borrow() { - cb(self.current_track.get().unwrap()); - } - - for cb in &*self.duration_cbs.borrow() { - cb(self.duration.get()); - } - - for cb in &*self.playing_cbs.borrow() { - cb(self.is_playing()); - } - } - - pub fn clear(&self) { - self.player.stop(); - self.playing.set(false); - self.current_track.set(None); - self.playlist.replace(Vec::new()); - - for cb in &*self.playing_cbs.borrow() { - cb(false); - } - - for cb in &*self.playlist_cbs.borrow() { - cb(Vec::new()); - } - - #[cfg(target_os = "linux")] - self.mpris.set_can_play(false); - } -} - -/// Generator for new tracks to be appended to the playlist. -pub trait TrackGenerator { - /// Whether the generator will provide a next track if asked. - fn has_next(&self) -> bool; - - /// Provide the next track. - /// - /// This function should always return at least one track in a state where - /// `has_next()` returns `true`. - fn next(&self) -> Vec; -} - -/// A track generator that generates one random track per call. -pub struct RandomTrackGenerator { - backend: Rc, -} - -impl RandomTrackGenerator { - pub fn new(backend: Rc) -> Self { - Self { backend } - } -} - -impl TrackGenerator for RandomTrackGenerator { - fn has_next(&self) -> bool { - true - } - - fn next(&self) -> Vec { - vec![db::random_track(&mut self.backend.db().lock().unwrap()).unwrap()] - } -} - -/// A track generator that returns the tracks of one random recording per call. -pub struct RandomRecordingGenerator { - backend: Rc, -} - -impl RandomRecordingGenerator { - pub fn new(backend: Rc) -> Self { - Self { backend } - } -} - -impl TrackGenerator for RandomRecordingGenerator { - fn has_next(&self) -> bool { - true - } - - fn next(&self) -> Vec { - let recording = db::random_recording(&mut self.backend.db().lock().unwrap()).unwrap(); - db::get_tracks(&mut self.backend.db().lock().unwrap(), &recording.id).unwrap() - } -} diff --git a/crates/database/.gitignore b/crates/database/.gitignore deleted file mode 100644 index 141236d..0000000 --- a/crates/database/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test.sqlite diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml deleted file mode 100644 index 11a5d5c..0000000 --- a/crates/database/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "musicus_database" -version = "0.1.0" -edition = "2021" - -[dependencies] -diesel = { version = "2", features = ["sqlite"] } -diesel_migrations = "2" -chrono = "0.4" -log = "0.4" -rand = "0.8" -thiserror = "1" -uuid = { version = "1", features = ["v4"] } diff --git a/crates/database/diesel.toml b/crates/database/diesel.toml deleted file mode 100644 index f57985a..0000000 --- a/crates/database/diesel.toml +++ /dev/null @@ -1,2 +0,0 @@ -[print_schema] -file = "src/schema.rs" diff --git a/crates/database/migrations/2020-09-27-201047_initial_schema/down.sql b/crates/database/migrations/2020-09-27-201047_initial_schema/down.sql deleted file mode 100644 index ae0957e..0000000 --- a/crates/database/migrations/2020-09-27-201047_initial_schema/down.sql +++ /dev/null @@ -1,13 +0,0 @@ -PRAGMA defer_foreign_keys; - -DROP TABLE "persons"; -DROP TABLE "instruments"; -DROP TABLE "works"; -DROP TABLE "instrumentations"; -DROP TABLE "work_parts"; -DROP TABLE "ensembles"; -DROP TABLE "recordings"; -DROP TABLE "performances"; -DROP TABLE "mediums"; -DROP TABLE "tracks"; - diff --git a/crates/database/migrations/2020-09-27-201047_initial_schema/up.sql b/crates/database/migrations/2020-09-27-201047_initial_schema/up.sql deleted file mode 100644 index e103d96..0000000 --- a/crates/database/migrations/2020-09-27-201047_initial_schema/up.sql +++ /dev/null @@ -1,65 +0,0 @@ -CREATE TABLE "persons" ( - "id" TEXT NOT NULL PRIMARY KEY, - "first_name" TEXT NOT NULL, - "last_name" TEXT NOT NULL -); - -CREATE TABLE "instruments" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL -); - -CREATE TABLE "works" ( - "id" TEXT NOT NULL PRIMARY KEY, - "composer" TEXT NOT NULL REFERENCES "persons"("id"), - "title" TEXT NOT NULL -); - -CREATE TABLE "instrumentations" ( - "id" BIGINT NOT NULL PRIMARY KEY, - "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, - "instrument" TEXT NOT NULL REFERENCES "instruments"("id") ON DELETE CASCADE -); - -CREATE TABLE "work_parts" ( - "id" BIGINT NOT NULL PRIMARY KEY, - "work" TEXT NOT NULL REFERENCES "works"("id") ON DELETE CASCADE, - "part_index" BIGINT NOT NULL, - "title" TEXT NOT NULL -); - -CREATE TABLE "ensembles" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL -); - -CREATE TABLE "recordings" ( - "id" TEXT NOT NULL PRIMARY KEY, - "work" TEXT NOT NULL REFERENCES "works"("id"), - "comment" TEXT NOT NULL -); - -CREATE TABLE "performances" ( - "id" BIGINT NOT NULL PRIMARY KEY, - "recording" TEXT NOT NULL REFERENCES "recordings"("id") ON DELETE CASCADE, - "person" TEXT REFERENCES "persons"("id"), - "ensemble" TEXT REFERENCES "ensembles"("id"), - "role" TEXT REFERENCES "instruments"("id") -); - -CREATE TABLE "mediums" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL, - "discid" TEXT -); - -CREATE TABLE "tracks" ( - "id" TEXT NOT NULL PRIMARY KEY, - "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, - "index" INTEGER NOT NULL, - "recording" TEXT NOT NULL REFERENCES "recordings"("id"), - "work_parts" TEXT NOT NULL, - "source_index" INTEGER NOT NULL, - "path" TEXT NOT NULL -); - diff --git a/crates/database/migrations/2022-04-10-103835_access_history/down.sql b/crates/database/migrations/2022-04-10-103835_access_history/down.sql deleted file mode 100644 index 6b294d8..0000000 --- a/crates/database/migrations/2022-04-10-103835_access_history/down.sql +++ /dev/null @@ -1,20 +0,0 @@ -ALTER TABLE "persons" DROP COLUMN "last_used"; -ALTER TABLE "persons" DROP COLUMN "last_played"; - -ALTER TABLE "instruments" DROP COLUMN "last_used"; -ALTER TABLE "instruments" DROP COLUMN "last_played"; - -ALTER TABLE "works" DROP COLUMN "last_used"; -ALTER TABLE "works" DROP COLUMN "last_played"; - -ALTER TABLE "ensembles" DROP COLUMN "last_used"; -ALTER TABLE "ensembles" DROP COLUMN "last_played"; - -ALTER TABLE "recordings" DROP COLUMN "last_used"; -ALTER TABLE "recordings" DROP COLUMN "last_played"; - -ALTER TABLE "mediums" DROP COLUMN "last_used"; -ALTER TABLE "mediums" DROP COLUMN "last_played"; - -ALTER TABLE "tracks" DROP COLUMN "last_used"; -ALTER TABLE "tracks" DROP COLUMN "last_played"; diff --git a/crates/database/migrations/2022-04-10-103835_access_history/up.sql b/crates/database/migrations/2022-04-10-103835_access_history/up.sql deleted file mode 100644 index 27b609b..0000000 --- a/crates/database/migrations/2022-04-10-103835_access_history/up.sql +++ /dev/null @@ -1,21 +0,0 @@ -ALTER TABLE "persons" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "persons" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "instruments" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "instruments" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "works" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "works" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "ensembles" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "ensembles" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "recordings" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "recordings" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "mediums" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "mediums" ADD COLUMN "last_played" BIGINT; - -ALTER TABLE "tracks" ADD COLUMN "last_used" BIGINT; -ALTER TABLE "tracks" ADD COLUMN "last_played" BIGINT; - diff --git a/crates/database/migrations/2023-02-11-094238_tracks_without_medium/down.sql b/crates/database/migrations/2023-02-11-094238_tracks_without_medium/down.sql deleted file mode 100644 index 2a4d3c8..0000000 --- a/crates/database/migrations/2023-02-11-094238_tracks_without_medium/down.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE "old_tracks" ( - "id" TEXT NOT NULL PRIMARY KEY, - "medium" TEXT NOT NULL REFERENCES "mediums"("id") ON DELETE CASCADE, - "index" INTEGER NOT NULL, - "recording" TEXT NOT NULL REFERENCES "recordings"("id"), - "work_parts" TEXT NOT NULL, - "source_index" INTEGER NOT NULL, - "path" TEXT NOT NULL, - "last_used" BIGINT, - "last_played" BIGINT -); - -INSERT INTO "old_tracks" SELECT * FROM "tracks" WHERE "medium" IS NOT NULL; -DROP TABLE "tracks"; -ALTER TABLE "old_tracks" RENAME TO "tracks"; \ No newline at end of file diff --git a/crates/database/migrations/2023-02-11-094238_tracks_without_medium/up.sql b/crates/database/migrations/2023-02-11-094238_tracks_without_medium/up.sql deleted file mode 100644 index 671478b..0000000 --- a/crates/database/migrations/2023-02-11-094238_tracks_without_medium/up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE "new_tracks" ( - "id" TEXT NOT NULL PRIMARY KEY, - "medium" TEXT REFERENCES "mediums"("id") ON DELETE CASCADE, - "index" INTEGER NOT NULL, - "recording" TEXT NOT NULL REFERENCES "recordings"("id"), - "work_parts" TEXT NOT NULL, - "source_index" INTEGER NOT NULL, - "path" TEXT NOT NULL, - "last_used" BIGINT, - "last_played" BIGINT -); - -INSERT INTO "new_tracks" SELECT * FROM "tracks"; -DROP TABLE "tracks"; -ALTER TABLE "new_tracks" RENAME TO "tracks"; \ No newline at end of file diff --git a/crates/database/src/ensembles.rs b/crates/database/src/ensembles.rs deleted file mode 100644 index 13896be..0000000 --- a/crates/database/src/ensembles.rs +++ /dev/null @@ -1,74 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::{defer_foreign_keys, schema::ensembles, Result}; - -/// An ensemble that takes part in recordings. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Ensemble { - pub id: String, - pub name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Ensemble { - pub fn new(id: String, name: String) -> Self { - Self { - id, - name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } -} - -/// Update an existing ensemble or insert a new one. -pub fn update_ensemble(connection: &mut SqliteConnection, mut ensemble: Ensemble) -> Result<()> { - info!("Updating ensemble {:?}", ensemble); - defer_foreign_keys(connection)?; - - ensemble.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(ensembles::table) - .values(ensemble) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing ensemble. -pub fn get_ensemble(connection: &mut SqliteConnection, id: &str) -> Result> { - let ensemble = ensembles::table - .filter(ensembles::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(ensemble) -} - -/// Delete an existing ensemble. -pub fn delete_ensemble(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting ensemble {}", id); - diesel::delete(ensembles::table.filter(ensembles::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing ensembles. -pub fn get_ensembles(connection: &mut SqliteConnection) -> Result> { - let ensembles = ensembles::table.load::(connection)?; - Ok(ensembles) -} - -/// Get recently used ensembles. -pub fn get_recent_ensembles(connection: &mut SqliteConnection) -> Result> { - let ensembles = ensembles::table - .order(ensembles::last_used.desc()) - .load::(connection)?; - - Ok(ensembles) -} diff --git a/crates/database/src/error.rs b/crates/database/src/error.rs deleted file mode 100644 index c3b8524..0000000 --- a/crates/database/src/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Error that happens within the database module. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - ConnectionError(#[from] diesel::result::ConnectionError), - - #[error(transparent)] - Migrations(#[from] Box), - - #[error(transparent)] - QueryError(#[from] diesel::result::Error), - - #[error("Missing item dependency ({0} {1})")] - MissingItem(&'static str, String), - - #[error("Failed to parse {0} from '{1}'")] - ParsingError(&'static str, String), - - #[error("{0}")] - Other(&'static str), -} - -/// Return type for database methods. -pub type Result = std::result::Result; diff --git a/crates/database/src/instruments.rs b/crates/database/src/instruments.rs deleted file mode 100644 index 9aabce5..0000000 --- a/crates/database/src/instruments.rs +++ /dev/null @@ -1,79 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::{defer_foreign_keys, schema::instruments, Result}; - -/// An instrument or any other possible role within a recording. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Instrument { - pub id: String, - pub name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Instrument { - pub fn new(id: String, name: String) -> Self { - Self { - id, - name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } -} - -/// Update an existing instrument or insert a new one. -pub fn update_instrument( - connection: &mut SqliteConnection, - mut instrument: Instrument, -) -> Result<()> { - info!("Updating instrument {:?}", instrument); - defer_foreign_keys(connection)?; - - instrument.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(instruments::table) - .values(instrument) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing instrument. -pub fn get_instrument(connection: &mut SqliteConnection, id: &str) -> Result> { - let instrument = instruments::table - .filter(instruments::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(instrument) -} - -/// Delete an existing instrument. -pub fn delete_instrument(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting instrument {}", id); - diesel::delete(instruments::table.filter(instruments::id.eq(id))).execute(connection)?; - - Ok(()) -} - -/// Get all existing instruments. -pub fn get_instruments(connection: &mut SqliteConnection) -> Result> { - let instruments = instruments::table.load::(connection)?; - - Ok(instruments) -} - -/// Get recently used instruments. -pub fn get_recent_instruments(connection: &mut SqliteConnection) -> Result> { - let instruments = instruments::table - .order(instruments::last_used.desc()) - .load::(connection)?; - - Ok(instruments) -} diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs deleted file mode 100644 index 50aa92d..0000000 --- a/crates/database/src/lib.rs +++ /dev/null @@ -1,54 +0,0 @@ -use diesel::prelude::*; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use log::info; - -pub use diesel::SqliteConnection; - -pub mod ensembles; -pub use ensembles::*; - -pub mod error; -pub use error::*; - -pub mod instruments; -pub use instruments::*; - -pub mod medium; -pub use medium::*; - -pub mod persons; -pub use persons::*; - -pub mod recordings; -pub use recordings::*; - -pub mod works; -pub use works::*; - -mod schema; - -// This makes the SQL migration scripts accessible from the code. -const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); - -/// Connect to a Musicus database running migrations if necessary. -pub fn connect(file_name: &str) -> Result { - info!("Opening database file '{}'", file_name); - let mut connection = SqliteConnection::establish(file_name)?; - diesel::sql_query("PRAGMA foreign_keys = ON").execute(&mut connection)?; - - info!("Running migrations if necessary"); - connection.run_pending_migrations(MIGRATIONS)?; - - Ok(connection) -} - -/// Generate a random string suitable as an item ID. -pub fn generate_id() -> String { - uuid::Uuid::new_v4().simple().to_string() -} - -/// Defer all foreign keys for the next transaction. -fn defer_foreign_keys(connection: &mut SqliteConnection) -> Result<()> { - diesel::sql_query("PRAGMA defer_foreign_keys = ON").execute(connection)?; - Ok(()) -} diff --git a/crates/database/src/medium.rs b/crates/database/src/medium.rs deleted file mode 100644 index 8a4f32b..0000000 --- a/crates/database/src/medium.rs +++ /dev/null @@ -1,351 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::prelude::*; -use log::info; - -use crate::{ - defer_foreign_keys, generate_id, get_recording, - schema::{ensembles, mediums, performances, persons, recordings, tracks}, - update_recording, Error, Recording, Result, -}; - -/// Representation of someting like a physical audio disc or a folder with -/// audio files (i.e. a collection of tracks for one or more recordings). -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Medium { - /// An unique ID for the medium. - pub id: String, - - /// The human identifier for the medium. - pub name: String, - - /// If applicable, the MusicBrainz DiscID. - pub discid: Option, - - /// The tracks of the medium. - pub tracks: Vec, - - pub last_used: Option>, - pub last_played: Option>, -} - -impl Medium { - pub fn new(id: String, name: String, discid: Option, tracks: Vec) -> Self { - Self { - id, - name, - discid, - tracks, - last_used: Some(Utc::now()), - last_played: None, - } - } -} - -/// A track on a medium. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Track { - /// The recording on this track. - pub recording: Recording, - - /// The work parts that are played on this track. They are indices to the - /// work parts of the work that is associated with the recording. - pub work_parts: Vec, - - /// The index of the track within its source. This is used to associate - /// the metadata with the audio data from the source when importing. - pub source_index: usize, - - /// The path to the audio file containing this track. - pub path: String, - - pub last_used: Option>, - pub last_played: Option>, -} - -impl Track { - pub fn new( - recording: Recording, - work_parts: Vec, - source_index: usize, - path: String, - ) -> Self { - Self { - recording, - work_parts, - source_index, - path, - last_used: Some(Utc::now()), - last_played: None, - } - } -} - -/// Table data for a [`Medium`]. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = mediums)] -struct MediumRow { - pub id: String, - pub name: String, - pub discid: Option, - pub last_used: Option, - pub last_played: Option, -} - -/// Table data for a [`Track`]. -#[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] -#[diesel(table_name = tracks)] -struct TrackRow { - pub id: String, - pub medium: Option, - pub index: i32, - pub recording: String, - pub work_parts: String, - pub source_index: i32, - pub path: String, - pub last_used: Option, - pub last_played: Option, -} - -/// Update an existing medium or insert a new one. -pub fn update_medium(connection: &mut SqliteConnection, medium: Medium) -> Result<()> { - info!("Updating medium {:?}", medium); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let medium_id = &medium.id; - - // This will also delete the tracks. - delete_medium(connection, medium_id)?; - - // Add the new medium. - - let medium_row = MediumRow { - id: medium_id.to_owned(), - name: medium.name.clone(), - discid: medium.discid.clone(), - last_used: Some(Utc::now().timestamp()), - last_played: medium.last_played.map(|t| t.timestamp()), - }; - - diesel::insert_into(mediums::table) - .values(medium_row) - .execute(connection)?; - - for (index, track) in medium.tracks.iter().enumerate() { - // Add associated items from the server, if they don't already exist. - - if get_recording(connection, &track.recording.id)?.is_none() { - update_recording(connection, track.recording.clone())?; - } - - // Add the actual track data. - - let work_parts = track - .work_parts - .iter() - .map(|part_index| part_index.to_string()) - .collect::>() - .join(","); - - let track_row = TrackRow { - id: generate_id(), - medium: Some(medium_id.to_owned()), - index: index as i32, - recording: track.recording.id.clone(), - work_parts, - source_index: track.source_index as i32, - path: track.path.clone(), - last_used: Some(Utc::now().timestamp()), - last_played: track.last_played.map(|t| t.timestamp()), - }; - - diesel::insert_into(tracks::table) - .values(track_row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Get an existing medium. -pub fn get_medium(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = mediums::table - .filter(mediums::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - let medium = match row { - Some(row) => Some(get_medium_data(connection, row)?), - None => None, - }; - - Ok(medium) -} - -/// Get mediums that have a specific source ID. -pub fn get_mediums_by_source_id( - connection: &mut SqliteConnection, - source_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .filter(mediums::discid.nullable().eq(source_id)) - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Get mediums on which this person is performing. -pub fn get_mediums_for_person( - connection: &mut SqliteConnection, - person_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .inner_join(tracks::table.on(tracks::medium.eq(mediums::id.nullable()))) - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id)) - .select(mediums::table::all_columns()) - .distinct() - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Get mediums on which this ensemble is performing. -pub fn get_mediums_for_ensemble( - connection: &mut SqliteConnection, - ensemble_id: &str, -) -> Result> { - let mut mediums: Vec = Vec::new(); - - let rows = mediums::table - .inner_join(tracks::table.on(tracks::medium.eq(tracks::id.nullable()))) - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id)) - .select(mediums::table::all_columns()) - .distinct() - .load::(connection)?; - - for row in rows { - let medium = get_medium_data(connection, row)?; - mediums.push(medium); - } - - Ok(mediums) -} - -/// Delete a medium and all of its tracks. This will fail, if the music -/// library contains audio files referencing any of those tracks. -pub fn delete_medium(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting medium {}", id); - diesel::delete(mediums::table.filter(mediums::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all available tracks for a recording. -pub fn get_tracks(connection: &mut SqliteConnection, recording_id: &str) -> Result> { - let mut tracks: Vec = Vec::new(); - - let rows = tracks::table - .inner_join(recordings::table.on(recordings::id.eq(tracks::recording))) - .filter(recordings::id.eq(recording_id)) - .select(tracks::table::all_columns()) - .load::(connection)?; - - for row in rows { - let track = get_track_from_row(connection, row)?; - tracks.push(track); - } - - Ok(tracks) -} - -/// Get a random track from the database. -pub fn random_track(connection: &mut SqliteConnection) -> Result { - let row = diesel::sql_query("SELECT * FROM tracks ORDER BY RANDOM() LIMIT 1") - .load::(connection)? - .into_iter() - .next() - .ok_or(Error::Other("Failed to generate random track"))?; - - get_track_from_row(connection, row) -} - -/// Retrieve all available information on a medium from related tables. -fn get_medium_data(connection: &mut SqliteConnection, row: MediumRow) -> Result { - let track_rows = tracks::table - .filter(tracks::medium.eq(&row.id)) - .order_by(tracks::index) - .load::(connection)?; - - let mut tracks = Vec::new(); - - for track_row in track_rows { - let track = get_track_from_row(connection, track_row)?; - tracks.push(track); - } - - let medium = Medium { - id: row.id, - name: row.name, - discid: row.discid, - tracks, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(medium) -} - -/// Convert a track row from the database to an actual track. -fn get_track_from_row(connection: &mut SqliteConnection, row: TrackRow) -> Result { - let recording_id = row.recording; - - let recording = get_recording(connection, &recording_id)? - .ok_or(Error::MissingItem("recording", recording_id))?; - - let mut part_indices = Vec::new(); - - let work_parts = row.work_parts.split(','); - - for part_index in work_parts { - if !part_index.is_empty() { - let index = str::parse(part_index) - .map_err(|_| Error::ParsingError("part index", String::from(part_index)))?; - - part_indices.push(index); - } - } - - let track = Track { - recording, - work_parts: part_indices, - source_index: row.source_index as usize, - path: row.path, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(track) -} diff --git a/crates/database/src/persons.rs b/crates/database/src/persons.rs deleted file mode 100644 index 157bb1c..0000000 --- a/crates/database/src/persons.rs +++ /dev/null @@ -1,86 +0,0 @@ -use chrono::Utc; -use diesel::prelude::*; -use log::info; - -use crate::{defer_foreign_keys, schema::persons, Result}; - -/// A person that is a composer, an interpret or both. -#[derive(Insertable, Queryable, PartialEq, Eq, Hash, Debug, Clone)] -pub struct Person { - pub id: String, - pub first_name: String, - pub last_name: String, - pub last_used: Option, - pub last_played: Option, -} - -impl Person { - pub fn new(id: String, first_name: String, last_name: String) -> Self { - Self { - id, - first_name, - last_name, - last_used: Some(Utc::now().timestamp()), - last_played: None, - } - } - - /// Get the full name in the form "First Last". - pub fn name_fl(&self) -> String { - format!("{} {}", self.first_name, self.last_name) - } - - /// Get the full name in the form "Last, First". - pub fn name_lf(&self) -> String { - format!("{}, {}", self.last_name, self.first_name) - } -} -/// Update an existing person or insert a new one. -pub fn update_person(connection: &mut SqliteConnection, mut person: Person) -> Result<()> { - info!("Updating person {:?}", person); - defer_foreign_keys(connection)?; - - person.last_used = Some(Utc::now().timestamp()); - - connection.transaction(|connection| { - diesel::replace_into(persons::table) - .values(person) - .execute(connection) - })?; - - Ok(()) -} - -/// Get an existing person. -pub fn get_person(connection: &mut SqliteConnection, id: &str) -> Result> { - let person = persons::table - .filter(persons::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - Ok(person) -} - -/// Delete an existing person. -pub fn delete_person(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting person {}", id); - diesel::delete(persons::table.filter(persons::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing persons. -pub fn get_persons(connection: &mut SqliteConnection) -> Result> { - let persons = persons::table.load::(connection)?; - - Ok(persons) -} - -/// Get recently used persons. -pub fn get_recent_persons(connection: &mut SqliteConnection) -> Result> { - let persons = persons::table - .order(persons::last_used.desc()) - .load::(connection)?; - - Ok(persons) -} diff --git a/crates/database/src/recordings.rs b/crates/database/src/recordings.rs deleted file mode 100644 index 37bc72b..0000000 --- a/crates/database/src/recordings.rs +++ /dev/null @@ -1,350 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::prelude::*; -use log::info; - -use crate::{ - defer_foreign_keys, generate_id, get_ensemble, get_instrument, get_person, get_work, - schema::{ensembles, performances, persons, recordings}, - update_ensemble, update_instrument, update_person, update_work, Ensemble, Error, Instrument, - Person, Result, Work, -}; - -/// A specific recording of a work. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Recording { - pub id: String, - pub work: Work, - pub comment: String, - pub performances: Vec, - pub last_used: Option>, - pub last_played: Option>, -} - -impl Recording { - pub fn new(id: String, work: Work, comment: String, performances: Vec) -> Self { - Self { - id, - work, - comment, - performances, - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Initialize a new recording with a work. - pub fn from_work(work: Work) -> Self { - Self { - id: generate_id(), - work, - comment: String::new(), - performances: Vec::new(), - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Get a string representation of the performances in this recording. - // TODO: Maybe replace with impl Display? - pub fn get_performers(&self) -> String { - let texts: Vec = self - .performances - .iter() - .map(|performance| performance.get_title()) - .collect(); - - texts.join(", ") - } -} - -/// How a person or ensemble was involved in a recording. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Performance { - pub performer: PersonOrEnsemble, - pub role: Option, -} - -impl Performance { - /// Get a string representation of the performance. - // TODO: Replace with impl Display. - pub fn get_title(&self) -> String { - let performer_title = self.performer.get_title(); - - if let Some(role) = &self.role { - format!("{} ({})", performer_title, role.name) - } else { - performer_title - } - } -} - -/// Either a person or an ensemble. -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub enum PersonOrEnsemble { - Person(Person), - Ensemble(Ensemble), -} - -impl PersonOrEnsemble { - /// Get a short textual representation of the item. - pub fn get_title(&self) -> String { - match self { - PersonOrEnsemble::Person(person) => person.name_lf(), - PersonOrEnsemble::Ensemble(ensemble) => ensemble.name.clone(), - } - } -} - -/// Database table data for a recording. -#[derive(Insertable, Queryable, QueryableByName, Debug, Clone)] -#[diesel(table_name = recordings)] -struct RecordingRow { - pub id: String, - pub work: String, - pub comment: String, - pub last_used: Option, - pub last_played: Option, -} - -impl From for RecordingRow { - fn from(recording: Recording) -> Self { - RecordingRow { - id: recording.id, - work: recording.work.id, - comment: recording.comment, - last_used: Some(Utc::now().timestamp()), - last_played: recording.last_played.map(|t| t.timestamp()), - } - } -} - -/// Database table data for a performance. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = performances)] -struct PerformanceRow { - pub id: i64, - pub recording: String, - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// Update an existing recording or insert a new one. -// TODO: Think about whether to also insert the other items. -pub fn update_recording(connection: &mut SqliteConnection, recording: Recording) -> Result<()> { - info!("Updating recording {:?}", recording); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let recording_id = &recording.id; - delete_recording(connection, recording_id)?; - - // Add associated items from the server, if they don't already exist. - - if get_work(connection, &recording.work.id)?.is_none() { - update_work(connection, recording.work.clone())?; - } - - for performance in &recording.performances { - match &performance.performer { - PersonOrEnsemble::Person(person) => { - if get_person(connection, &person.id)?.is_none() { - update_person(connection, person.clone())?; - } - } - PersonOrEnsemble::Ensemble(ensemble) => { - if get_ensemble(connection, &ensemble.id)?.is_none() { - update_ensemble(connection, ensemble.clone())?; - } - } - } - - if let Some(role) = &performance.role { - if get_instrument(connection, &role.id)?.is_none() { - update_instrument(connection, role.clone())?; - } - } - } - - // Add the actual recording. - - let row: RecordingRow = recording.clone().into(); - diesel::insert_into(recordings::table) - .values(row) - .execute(connection)?; - - for performance in recording.performances { - let (person, ensemble) = match performance.performer { - PersonOrEnsemble::Person(person) => (Some(person.id), None), - PersonOrEnsemble::Ensemble(ensemble) => (None, Some(ensemble.id)), - }; - - let row = PerformanceRow { - id: rand::random(), - recording: recording_id.to_string(), - person, - ensemble, - role: performance.role.map(|role| role.id), - }; - - diesel::insert_into(performances::table) - .values(row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Check whether the database contains a recording. -pub fn recording_exists(connection: &mut SqliteConnection, id: &str) -> Result { - let exists = recordings::table - .filter(recordings::id.eq(id)) - .load::(connection)? - .first() - .is_some(); - - Ok(exists) -} - -/// Get an existing recording. -pub fn get_recording(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = recordings::table - .filter(recordings::id.eq(id)) - .load::(connection)? - .into_iter() - .next(); - - let recording = match row { - Some(row) => Some(get_recording_data(connection, row)?), - None => None, - }; - - Ok(recording) -} - -/// Get a random recording from the database. -pub fn random_recording(connection: &mut SqliteConnection) -> Result { - let row = diesel::sql_query("SELECT * FROM recordings ORDER BY RANDOM() LIMIT 1") - .load::(connection)? - .into_iter() - .next() - .ok_or(Error::Other("Failed to find random recording."))?; - - get_recording_data(connection, row) -} - -/// Retrieve all available information on a recording from related tables. -fn get_recording_data(connection: &mut SqliteConnection, row: RecordingRow) -> Result { - let mut performance_descriptions: Vec = Vec::new(); - - let performance_rows = performances::table - .filter(performances::recording.eq(&row.id)) - .load::(connection)?; - - for row in performance_rows { - performance_descriptions.push(Performance { - performer: if let Some(id) = row.person { - PersonOrEnsemble::Person( - get_person(connection, &id)?.ok_or(Error::MissingItem("person", id))?, - ) - } else if let Some(id) = row.ensemble { - PersonOrEnsemble::Ensemble( - get_ensemble(connection, &id)?.ok_or(Error::MissingItem("ensemble", id))?, - ) - } else { - return Err(Error::Other("Performance without performer")); - }, - role: match row.role { - Some(id) => Some( - get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?, - ), - None => None, - }, - }); - } - - let work_id = row.work; - let work = get_work(connection, &work_id)?.ok_or(Error::MissingItem("work", work_id))?; - - let recording_description = Recording { - id: row.id, - work, - comment: row.comment, - performances: performance_descriptions, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }; - - Ok(recording_description) -} - -/// Get all available information on all recordings where a person is performing. -pub fn get_recordings_for_person( - connection: &mut SqliteConnection, - person_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id)) - .select(recordings::table::all_columns()) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Get all available information on all recordings where an ensemble is performing. -pub fn get_recordings_for_ensemble( - connection: &mut SqliteConnection, - ensemble_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id)) - .select(recordings::table::all_columns()) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Get allavailable information on all recordings of a work. -pub fn get_recordings_for_work( - connection: &mut SqliteConnection, - work_id: &str, -) -> Result> { - let mut recordings: Vec = Vec::new(); - - let rows = recordings::table - .filter(recordings::work.eq(work_id)) - .load::(connection)?; - - for row in rows { - recordings.push(get_recording_data(connection, row)?); - } - - Ok(recordings) -} - -/// Delete an existing recording. This will fail if there are still references to this -/// recording from other tables that are not directly part of the recording data. -pub fn delete_recording(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting recording {}", id); - diesel::delete(recordings::table.filter(recordings::id.eq(id))).execute(connection)?; - Ok(()) -} diff --git a/crates/database/src/schema.rs b/crates/database/src/schema.rs deleted file mode 100644 index fe72d06..0000000 --- a/crates/database/src/schema.rs +++ /dev/null @@ -1,125 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - ensembles (id) { - id -> Text, - name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - instrumentations (id) { - id -> BigInt, - work -> Text, - instrument -> Text, - } -} - -diesel::table! { - instruments (id) { - id -> Text, - name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - mediums (id) { - id -> Text, - name -> Text, - discid -> Nullable, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - performances (id) { - id -> BigInt, - recording -> Text, - person -> Nullable, - ensemble -> Nullable, - role -> Nullable, - } -} - -diesel::table! { - persons (id) { - id -> Text, - first_name -> Text, - last_name -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - recordings (id) { - id -> Text, - work -> Text, - comment -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - tracks (id) { - id -> Text, - medium -> Nullable, - index -> Integer, - recording -> Text, - work_parts -> Text, - source_index -> Integer, - path -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::table! { - work_parts (id) { - id -> BigInt, - work -> Text, - part_index -> BigInt, - title -> Text, - } -} - -diesel::table! { - works (id) { - id -> Text, - composer -> Text, - title -> Text, - last_used -> Nullable, - last_played -> Nullable, - } -} - -diesel::joinable!(instrumentations -> instruments (instrument)); -diesel::joinable!(instrumentations -> works (work)); -diesel::joinable!(performances -> ensembles (ensemble)); -diesel::joinable!(performances -> instruments (role)); -diesel::joinable!(performances -> persons (person)); -diesel::joinable!(performances -> recordings (recording)); -diesel::joinable!(recordings -> works (work)); -diesel::joinable!(tracks -> mediums (medium)); -diesel::joinable!(tracks -> recordings (recording)); -diesel::joinable!(work_parts -> works (work)); -diesel::joinable!(works -> persons (composer)); - -diesel::allow_tables_to_appear_in_same_query!( - ensembles, - instrumentations, - instruments, - mediums, - performances, - persons, - recordings, - tracks, - work_parts, - works, -); diff --git a/crates/database/src/works.rs b/crates/database/src/works.rs deleted file mode 100644 index 1fb4245..0000000 --- a/crates/database/src/works.rs +++ /dev/null @@ -1,252 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; -use diesel::{prelude::*, Insertable, Queryable}; -use log::info; - -use crate::{ - defer_foreign_keys, generate_id, get_instrument, get_person, - schema::{instrumentations, work_parts, works}, - update_instrument, update_person, Error, Instrument, Person, Result, -}; - -/// Table row data for a work. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = works)] -struct WorkRow { - pub id: String, - pub composer: String, - pub title: String, - pub last_used: Option, - pub last_played: Option, -} - -impl From for WorkRow { - fn from(work: Work) -> Self { - WorkRow { - id: work.id, - composer: work.composer.id, - title: work.title, - last_used: Some(Utc::now().timestamp()), - last_played: work.last_played.map(|t| t.timestamp()), - } - } -} - -/// Definition that a work uses an instrument. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = instrumentations)] -struct InstrumentationRow { - pub id: i64, - pub work: String, - pub instrument: String, -} - -/// Table row data for a work part. -#[derive(Insertable, Queryable, Debug, Clone)] -#[diesel(table_name = work_parts)] -struct WorkPartRow { - pub id: i64, - pub work: String, - pub part_index: i64, - pub title: String, -} - -/// A concrete work part that can be recorded. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct WorkPart { - pub title: String, -} - -/// A specific work by a composer. -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct Work { - pub id: String, - pub title: String, - pub composer: Person, - pub instruments: Vec, - pub parts: Vec, - pub last_used: Option>, - pub last_played: Option>, -} - -impl Work { - pub fn new( - id: String, - title: String, - composer: Person, - instruments: Vec, - parts: Vec, - ) -> Self { - Self { - id, - title, - composer, - instruments, - parts, - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Initialize a new work with a composer. - pub fn from_composer(composer: Person) -> Self { - Self { - id: generate_id(), - title: String::new(), - composer, - instruments: Vec::new(), - parts: Vec::new(), - last_used: Some(Utc::now()), - last_played: None, - } - } - - /// Get a string including the composer and title of the work. - // TODO: Replace with impl Display. - pub fn get_title(&self) -> String { - format!("{}: {}", self.composer.name_fl(), self.title) - } -} - -/// Update an existing work or insert a new one. -// TODO: Think about also inserting related items. -pub fn update_work(connection: &mut SqliteConnection, work: Work) -> Result<()> { - info!("Updating work {:?}", work); - defer_foreign_keys(connection)?; - - connection.transaction::<(), Error, _>(|connection| { - let work_id = &work.id; - delete_work(connection, work_id)?; - - // Add associated items from the server, if they don't already exist. - - if get_person(connection, &work.composer.id)?.is_none() { - update_person(connection, work.composer.clone())?; - } - - for instrument in &work.instruments { - if get_instrument(connection, &instrument.id)?.is_none() { - update_instrument(connection, instrument.clone())?; - } - } - - // Add the actual work. - - let row: WorkRow = work.clone().into(); - diesel::insert_into(works::table) - .values(row) - .execute(connection)?; - - let Work { - instruments, parts, .. - } = work; - - for instrument in instruments { - let row = InstrumentationRow { - id: rand::random(), - work: work_id.to_string(), - instrument: instrument.id, - }; - - diesel::insert_into(instrumentations::table) - .values(row) - .execute(connection)?; - } - - for (index, part) in parts.into_iter().enumerate() { - let row = WorkPartRow { - id: rand::random(), - work: work_id.to_string(), - part_index: index as i64, - title: part.title, - }; - - diesel::insert_into(work_parts::table) - .values(row) - .execute(connection)?; - } - - Ok(()) - })?; - - Ok(()) -} - -/// Get an existing work. -pub fn get_work(connection: &mut SqliteConnection, id: &str) -> Result> { - let row = works::table - .filter(works::id.eq(id)) - .load::(connection)? - .first() - .cloned(); - - let work = match row { - Some(row) => Some(get_work_data(connection, row)?), - None => None, - }; - - Ok(work) -} - -/// Retrieve all available information on a work from related tables. -fn get_work_data(connection: &mut SqliteConnection, row: WorkRow) -> Result { - let mut instruments: Vec = Vec::new(); - - let instrumentations = instrumentations::table - .filter(instrumentations::work.eq(&row.id)) - .load::(connection)?; - - for instrumentation in instrumentations { - let id = instrumentation.instrument; - instruments - .push(get_instrument(connection, &id)?.ok_or(Error::MissingItem("instrument", id))?); - } - - let mut parts: Vec = Vec::new(); - - let part_rows = work_parts::table - .filter(work_parts::work.eq(&row.id)) - .load::(connection)?; - - for part_row in part_rows { - parts.push(WorkPart { - title: part_row.title, - }); - } - - let person_id = row.composer; - let person = - get_person(connection, &person_id)?.ok_or(Error::MissingItem("person", person_id))?; - - Ok(Work { - id: row.id, - composer: person, - title: row.title, - instruments, - parts, - last_used: row.last_used.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - last_played: row.last_played.map(|t| Utc.timestamp_opt(t, 0).unwrap()), - }) -} - -/// Delete an existing work. This will fail if there are still other tables that relate to -/// this work except for the things that are part of the information on the work it -pub fn delete_work(connection: &mut SqliteConnection, id: &str) -> Result<()> { - info!("Deleting work {}", id); - diesel::delete(works::table.filter(works::id.eq(id))).execute(connection)?; - Ok(()) -} - -/// Get all existing works by a composer and related information from other tables. -pub fn get_works(connection: &mut SqliteConnection, composer_id: &str) -> Result> { - let mut works: Vec = Vec::new(); - - let rows = works::table - .filter(works::composer.eq(composer_id)) - .load::(connection)?; - - for row in rows { - works.push(get_work_data(connection, row)?); - } - - Ok(works) -} diff --git a/crates/import/Cargo.toml b/crates/import/Cargo.toml deleted file mode 100644 index 2935658..0000000 --- a/crates/import/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "musicus_import" -version = "0.1.0" -edition = "2021" - -[dependencies] -base64 = "0.21" -glib = "0.17" -gstreamer = "0.20" -gstreamer-pbutils = "0.20" -log = "0.4" -once_cell = "1" -rand = "0.8" -thiserror = "1" -sha2 = "0.10" -tokio = { version = "1", features = ["sync"] } diff --git a/crates/import/src/disc.rs b/crates/import/src/disc.rs deleted file mode 100644 index 84d7e4c..0000000 --- a/crates/import/src/disc.rs +++ /dev/null @@ -1,175 +0,0 @@ -use crate::error::{Error, Result}; -use crate::session::{ImportSession, ImportTrack, State}; -use base64::Engine; -use gstreamer::prelude::*; -use gstreamer::tags::{Duration, TrackNumber}; -use gstreamer::{ClockTime, ElementFactory, MessageType, MessageView, TocEntryType}; -use log::info; -use sha2::{Digest, Sha256}; -use std::path::PathBuf; -use tokio::sync::watch; - -/// Create a new import session for the default disc drive. -pub(super) fn new() -> Result { - let (state_sender, state_receiver) = watch::channel(State::Waiting); - - let mut tracks = Vec::new(); - let mut hasher = Sha256::new(); - - // Build the GStreamer pipeline. It will contain a fakesink initially to be able to run it - // forward to the paused state without specifying a file name before knowing the tracks. - - let cdparanoiasrc = ElementFactory::make("cdparanoiasrc").build()?; - let queue = ElementFactory::make("queue").build()?; - let audioconvert = ElementFactory::make("audioconvert").build()?; - let flacenc = ElementFactory::make("flacenc").build()?; - let fakesink = gstreamer::ElementFactory::make("fakesink").build()?; - - let pipeline = gstreamer::Pipeline::new(None); - pipeline.add_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &fakesink])?; - gstreamer::Element::link_many(&[&cdparanoiasrc, &queue, &audioconvert, &flacenc, &fakesink])?; - - let bus = pipeline - .bus() - .ok_or_else(|| Error::u(String::from("Failed to get bus from pipeline.")))?; - - // Run the pipeline into the paused state and wait for the resulting TOC message on the bus. - - pipeline.set_state(gstreamer::State::Paused)?; - - let msg = bus.timed_pop_filtered( - ClockTime::from_seconds(5), - &[MessageType::Toc, MessageType::Error], - ); - - let toc = match msg { - Some(msg) => match msg.view() { - MessageView::Error(err) => Err(Error::os(err.error())), - MessageView::Toc(toc) => Ok(toc.toc().0), - _ => Err(Error::u(format!( - "Unexpected message from GStreamer: {:?}", - msg - ))), - }, - None => Err(Error::Timeout( - "Timeout while waiting for first message from GStreamer.".to_string(), - )), - }?; - - pipeline.set_state(gstreamer::State::Ready)?; - - // Replace the fakesink with the real filesink. This won't need to be synced to the pipeline - // state, because we will set the whole pipeline's state to playing later. - - gstreamer::Element::unlink(&flacenc, &fakesink); - fakesink.set_state(gstreamer::State::Null)?; - pipeline.remove(&fakesink)?; - - let filesink = gstreamer::ElementFactory::make("filesink").build()?; - pipeline.add(&filesink)?; - gstreamer::Element::link(&flacenc, &filesink)?; - - // Get track data from the toc message that was received above. - - let tmp_dir = create_tmp_dir()?; - - for entry in toc.entries() { - if entry.entry_type() == TocEntryType::Track { - let duration = entry - .tags() - .ok_or_else(|| Error::u(String::from("No tags in TOC entry.")))? - .get::() - .ok_or_else(|| Error::u(String::from("No duration tag found in TOC entry.")))? - .get() - .mseconds(); - - let number = entry - .tags() - .ok_or_else(|| Error::u(String::from("No tags in TOC entry.")))? - .get::() - .ok_or_else(|| Error::u(String::from("No track number tag found in TOC entry.")))? - .get(); - - hasher.update(duration.to_le_bytes()); - - let name = format!("Track {}", number); - - let file_name = format!("track_{:02}.flac", number); - let mut path = tmp_dir.clone(); - path.push(file_name); - - let track = ImportTrack { - number, - name, - path, - duration, - }; - - tracks.push(track); - } - } - - let source_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hasher.finalize()); - - info!("Successfully loaded audio CD with {} tracks.", tracks.len()); - info!("Source ID: {}", source_id); - - let tracks_clone = tracks.clone(); - let copy = move || { - for track in &tracks_clone { - info!("Starting to rip track {}.", track.number); - - cdparanoiasrc.set_property("track", &track.number); - - // The filesink needs to be reset to be able to change the file location. - filesink.set_state(gstreamer::State::Null)?; - - let path = track.path.to_str().unwrap(); - filesink.set_property("location", &path); - - // This will also affect the filesink as expected. - pipeline.set_state(gstreamer::State::Playing)?; - - for msg in bus.iter_timed(None) { - match msg.view() { - MessageView::Eos(..) => { - info!("Finished ripping track {}.", track.number); - pipeline.set_state(gstreamer::State::Ready)?; - break; - } - MessageView::Error(err) => { - pipeline.set_state(gstreamer::State::Null)?; - return Err(Error::os(err.error())); - } - _ => (), - } - } - } - - pipeline.set_state(gstreamer::State::Null)?; - - Ok(()) - }; - - let session = ImportSession { - source_id, - tracks, - copy: Some(Box::new(copy)), - state_sender, - state_receiver, - }; - - Ok(session) -} - -/// Create a new temporary directory and return its path. -fn create_tmp_dir() -> Result { - let mut tmp_dir = glib::tmp_dir(); - - let dir_name = format!("musicus-{}", rand::random::()); - tmp_dir.push(dir_name); - - std::fs::create_dir(&tmp_dir)?; - - Ok(tmp_dir) -} diff --git a/crates/import/src/error.rs b/crates/import/src/error.rs deleted file mode 100644 index 127b65c..0000000 --- a/crates/import/src/error.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::error; - -/// An error within an import session. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// A timeout was reached. - #[error("{0}")] - Timeout(String), - - /// Some common error. - #[error("{msg}")] - Other { - /// The error message. - msg: String, - - #[source] - source: Option>, - }, - - /// Something unexpected happened. - #[error("{msg}")] - Unexpected { - /// The error message. - msg: String, - - #[source] - source: Option>, - }, -} - -impl Error { - /// Create a new error with an explicit source. - pub(super) fn os(source: impl error::Error + Send + Sync + 'static) -> Self { - Self::Unexpected { - msg: format!("An error has happened: {}", source), - source: Some(Box::new(source)), - } - } - - /// Create a new unexpected error without an explicit source. - pub(super) fn u(msg: String) -> Self { - Self::Unexpected { msg, source: None } - } - - /// Create a new unexpected error with an explicit source. - pub(super) fn us(source: impl error::Error + Send + Sync + 'static) -> Self { - Self::Unexpected { - msg: format!("An unexpected error has happened: {}", source), - source: Some(Box::new(source)), - } - } -} - -impl From for Error { - fn from(err: tokio::sync::oneshot::error::RecvError) -> Self { - Self::us(err) - } -} - -impl From for Error { - fn from(err: gstreamer::glib::Error) -> Self { - Self::us(err) - } -} - -impl From for Error { - fn from(err: gstreamer::glib::BoolError) -> Self { - Self::us(err) - } -} - -impl From for Error { - fn from(err: gstreamer::StateChangeError) -> Self { - Self::us(err) - } -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { - Self::us(err) - } -} - -pub type Result = std::result::Result; diff --git a/crates/import/src/folder.rs b/crates/import/src/folder.rs deleted file mode 100644 index dabde90..0000000 --- a/crates/import/src/folder.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::error::{Error, Result}; -use crate::session::{ImportSession, ImportTrack, State}; -use base64::Engine; -use gstreamer::ClockTime; -use gstreamer_pbutils::Discoverer; -use log::{info, warn}; -use sha2::{Digest, Sha256}; -use std::fs::DirEntry; -use std::path::PathBuf; -use tokio::sync::watch; - -/// Create a new import session for the specified folder. -pub(super) fn new(path: PathBuf) -> Result { - let (state_sender, state_receiver) = watch::channel(State::Ready); - - let mut tracks = Vec::new(); - let mut number: u32 = 1; - let mut hasher = Sha256::new(); - let discoverer = Discoverer::new(ClockTime::from_seconds(1))?; - - let mut entries = - std::fs::read_dir(path)?.collect::, std::io::Error>>()?; - entries.sort_by_key(|entry| entry.file_name()); - - for entry in entries { - if entry.file_type()?.is_file() { - let path = entry.path(); - - let uri = glib::filename_to_uri(&path, None) - .map_err(|_| Error::u(format!("Failed to create URI from path: {:?}", path)))?; - - let info = discoverer.discover_uri(&uri)?; - - if !info.audio_streams().is_empty() { - let duration = info - .duration() - .ok_or_else(|| Error::u(format!("Failed to get duration for {}.", uri)))? - .mseconds(); - - let file_name = entry.file_name(); - let name = file_name.into_string().map_err(|_| { - Error::u(format!( - "Failed to convert OsString to String: {:?}", - entry.file_name() - )) - })?; - - hasher.update(duration.to_le_bytes()); - - let track = ImportTrack { - number, - name, - path, - duration, - }; - - tracks.push(track); - number += 1; - } else { - warn!( - "File {} skipped, because it doesn't contain any audio streams.", - uri - ); - } - } - } - - let source_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hasher.finalize()); - - info!("Source ID: {}", source_id); - - let session = ImportSession { - source_id, - tracks, - copy: None, - state_sender, - state_receiver, - }; - - Ok(session) -} diff --git a/crates/import/src/lib.rs b/crates/import/src/lib.rs deleted file mode 100644 index 4912a13..0000000 --- a/crates/import/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub use error::{Error, Result}; -pub use session::{ImportSession, ImportTrack, State}; - -pub mod error; -pub mod session; - -mod disc; -mod folder; diff --git a/crates/import/src/session.rs b/crates/import/src/session.rs deleted file mode 100644 index 5a009da..0000000 --- a/crates/import/src/session.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::error::Result; -use crate::{disc, folder}; -use std::path::PathBuf; -use std::sync::Arc; -use std::thread; -use tokio::sync::{oneshot, watch}; - -/// The current state of the import process. -#[derive(Clone, Debug)] -pub enum State { - /// The import process has not been started yet. - Waiting, - - /// The audio is copied from the source. - Copying, - - /// The audio files are ready to be imported into the music library. - Ready, - - /// An error has happened. - Error, -} - -/// Interface for importing audio tracks from a medium or folder. -pub struct ImportSession { - /// A string identifying the source as specific as possible across platforms and formats. - pub(super) source_id: String, - - /// The tracks that are available on the source. - pub(super) tracks: Vec, - - /// A closure that has to be called to copy the tracks if set. - pub(super) copy: Option Result<()> + Send + Sync>>, - - /// Sender through which listeners are notified of state changes. - pub(super) state_sender: watch::Sender, - - /// Receiver for state changes. - pub(super) state_receiver: watch::Receiver, -} - -impl ImportSession { - /// Create a new import session for an audio CD. - pub async fn audio_cd() -> Result> { - let (sender, receiver) = oneshot::channel(); - - thread::spawn(move || { - let result = disc::new(); - let _ = sender.send(result); - }); - - Ok(Arc::new(receiver.await??)) - } - - /// Create a new import session for a folder. - pub async fn folder(path: PathBuf) -> Result> { - let (sender, receiver) = oneshot::channel(); - - thread::spawn(move || { - let result = folder::new(path); - let _ = sender.send(result); - }); - - Ok(Arc::new(receiver.await??)) - } - - /// Get a string identifying the source as specific as possible across platforms and mediums. - pub fn source_id(&self) -> &str { - &self.source_id - } - - /// Get the tracks that are available on the source. - pub fn tracks(&self) -> &[ImportTrack] { - &self.tracks - } - - /// Retrieve the current state of the import process. - pub fn state(&self) -> State { - self.state_receiver.borrow().clone() - } - - /// Wait for the next state change and get the new state. - pub async fn state_change(&self) -> State { - let mut receiver = self.state_receiver.clone(); - match receiver.changed().await { - Ok(()) => self.state(), - Err(_) => State::Error, - } - } - - /// Copy the tracks to their advertised locations in the background, if neccessary. The state - /// will be updated as the import is done. - pub fn copy(self: &Arc) { - if self.copy.is_some() { - let clone = Arc::clone(self); - - thread::spawn(move || { - let copy = clone.copy.as_ref().unwrap(); - - match copy() { - Ok(()) => clone.state_sender.send(State::Ready).unwrap(), - Err(_) => clone.state_sender.send(State::Error).unwrap(), - } - }); - } - } -} - -/// A track on an import source. -#[derive(Clone, Debug)] -pub struct ImportTrack { - /// The track number. - pub number: u32, - - /// A human readable identifier for the track. This will be used to present the track for - /// selection. - pub name: String, - - /// The path to the file where the corresponding audio file is. This file is only required to - /// exist, once the import was successfully completed. This will not be the actual file within - /// the user's music library, but the temporary location from which it can be copied to the - /// music library. - pub path: PathBuf, - - /// The track's duration in milliseconds. - pub duration: u64, -} diff --git a/crates/musicus/.gitignore b/crates/musicus/.gitignore deleted file mode 100644 index bbc2957..0000000 --- a/crates/musicus/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/src/config.rs -/src/resources.rs diff --git a/crates/musicus/Cargo.toml b/crates/musicus/Cargo.toml deleted file mode 100644 index 2ede063..0000000 --- a/crates/musicus/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "musicus" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = "1" -adw = { package = "libadwaita", version = "0.3", features = ["v1_2"] } -futures-channel = "0.3" -gettext-rs = { version = "0.7", features = ["gettext-system"] } -gio = "0.17" -glib = "0.17" -gstreamer = "0.20" -gtk = { package = "gtk4", version = "0.6" } -gtk-macros = "0.3" -log = "0.4" -musicus_backend = { version = "0.1.0", path = "../backend" } -once_cell = "1" -rand = "0.8" -sanitize-filename = "0.4" diff --git a/crates/musicus/res/icons/copy-symbolic.svg b/crates/musicus/res/icons/copy-symbolic.svg deleted file mode 100644 index e633938..0000000 --- a/crates/musicus/res/icons/copy-symbolic.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/crates/musicus/res/meson.build b/crates/musicus/res/meson.build deleted file mode 100644 index 13d8d92..0000000 --- a/crates/musicus/res/meson.build +++ /dev/null @@ -1,9 +0,0 @@ -pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) -gnome = import('gnome') - -resources = gnome.compile_resources('musicus', - 'musicus.gresource.xml', - gresource_bundle: true, - install: true, - install_dir: pkgdatadir, -) diff --git a/crates/musicus/res/musicus.gresource.xml b/crates/musicus/res/musicus.gresource.xml deleted file mode 100644 index 16da56b..0000000 --- a/crates/musicus/res/musicus.gresource.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - icons/copy-symbolic.svg - ui/editor.ui - ui/import_screen.ui - ui/main_screen.ui - ui/medium_editor.ui - ui/medium_preview.ui - ui/performance_editor.ui - ui/player_bar.ui - ui/player_screen.ui - ui/preferences.ui - ui/recording_editor.ui - ui/screen.ui - ui/section.ui - ui/selector.ui - ui/source_selector.ui - ui/track_editor.ui - ui/track_row.ui - ui/track_selector.ui - ui/track_set_editor.ui - ui/work_editor.ui - ui/work_part_editor.ui - - diff --git a/crates/musicus/res/ui/editor.ui b/crates/musicus/res/ui/editor.ui deleted file mode 100644 index 768fbec..0000000 --- a/crates/musicus/res/ui/editor.ui +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - crossfade - - - content - - - vertical - - - false - false - - - - - - Cancel - - - - - Save - - - - - - - - true - - - - - vertical - 12 - 12 - 36 - - - - - - - - - - - - - error - - - vertical - - - false - false - - - Error - - - - - - - network-error-symbolic - Error - true - - - Try again - true - true - center - center - - - - - - - - - - diff --git a/crates/musicus/res/ui/import_screen.ui b/crates/musicus/res/ui/import_screen.ui deleted file mode 100644 index 69c3d33..0000000 --- a/crates/musicus/res/ui/import_screen.ui +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - vertical - - - false - false - - - Import music - - - - - go-previous-symbolic - - - - - - - True - - - - - 6 - 6 - 6 - vertical - - - 12 - 6 - - - true - start - Matching metadata - - - - - - - - - - crossfade - false - true - - - loading - - - none - - - False - Loading… - - - True - - - - - - - - - - - - error - - - none - - - False - Error while searching for matching metadata - try_again_button - - - view-refresh-symbolic - center - - - - - - - - - - - - empty - - - none - - - False - No matching metadata found - - - - - - - - - - content - - - none - - - - - - - - - - start - 12 - 6 - Manually add metadata - - - - - - - - none - - - False - Select existing medium - select_button - - - Select - center - - - - - - - False - Add a new medium - add_button - - - Add - center - - - - - - - - - - - - - - - diff --git a/crates/musicus/res/ui/main_screen.ui b/crates/musicus/res/ui/main_screen.ui deleted file mode 100644 index 8cc4100..0000000 --- a/crates/musicus/res/ui/main_screen.ui +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - vertical - - - - - - - - - - folder-music-symbolic - Welcome to Musicus! - Get startet by selecting something from the sidebar or adding new things to your library using the button in the top left corner. - true - - - true - crossfade - - - center - Play something - - - - - - - - - vertical - - - true - - - sidebar - - - False - vertical - - - false - false - - - Musicus - - - - - - True - - - list-add-symbolic - - - - - - - True - open-menu-symbolic - menu - - - - - - - True - - - 400 - 300 - true - - - Search persons and ensembles … - - - - - - - - - True - crossfade - - - loading - - - True - True - True - center - center - - - - - - - content - - - - - - - - - - - - - - - - - - False - - - vertical - - - - - - - - - -
- - Preferences - widget.preferences - - - Debug log - widget.log - - - About Musicus - widget.about - -
-
- diff --git a/crates/musicus/res/ui/medium_editor.ui b/crates/musicus/res/ui/medium_editor.ui deleted file mode 100644 index 99535c1..0000000 --- a/crates/musicus/res/ui/medium_editor.ui +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - crossfade - - - content - - - vertical - - - false - false - - - Import music - - - - - - go-previous-symbolic - - - - - object-select-symbolic - - - - - - - - False - - - - - True - - - - - 6 - 6 - 6 - vertical - - - start - 12 - 6 - Medium - - - - - - - - none - - - Name of the medium - - - - - - - - horizontal - 12 - 6 - - - start - end - True - Recordings - - - - - - - - false - list-add-symbolic - - - - - - - - - - - - - - - - - - - - loading - - - true - true - true - center - center - - - - - - - error - - - vertical - - - false - false - - - Error - - - - - - - dialog-error-symbolic - Error - true - - - Try again - true - true - center - center - - - - - - - - - - - disc_error - - - vertical - - - false - false - - - Error - - - - - - - action-unavailable-symbolic - Error - true - - - Cancel - true - true - center - center - - - - - - - - - - diff --git a/crates/musicus/res/ui/medium_preview.ui b/crates/musicus/res/ui/medium_preview.ui deleted file mode 100644 index 3aa6620..0000000 --- a/crates/musicus/res/ui/medium_preview.ui +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - crossfade - - - content - - - vertical - - - false - false - - - Preview - - - - - go-previous-symbolic - - - - - False - - - crossfade - true - false - - - loading - - - True - - - - - - - ready - - - Import - - - - - - - - - - - - document-edit-symbolic - - - - - - - True - - - - - 6 - 6 - 6 - vertical - - - start - 12 - 6 - - - - - - - - vertical - - - - - - - - - - - - - - - loading - - - vertical - - - false - false - - - Loading - - - - - - - true - true - center - center - 32 - 32 - true - - - - - - - - - error - - - vertical - - - false - false - - - Error - - - - - - - dialog-error-symbolic - Error - true - - - Try again - true - true - center - center - - - - - - - - - - diff --git a/crates/musicus/res/ui/performance_editor.ui b/crates/musicus/res/ui/performance_editor.ui deleted file mode 100644 index 61c015e..0000000 --- a/crates/musicus/res/ui/performance_editor.ui +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - vertical - - - false - false - - - Performance - - - - - - go-previous-symbolic - - - - - False - object-select-symbolic - - - - - - - - true - - - 12 - 12 - 18 - 12 - 500 - 300 - - - none - - - False - Select a person - person_button - - - Select - center - - - - - - - False - Select an ensemble - ensemble_button - - - Select - center - - - - - - - False - Select a role - role_button - - - false - user-trash-symbolic - center - - - - - Select - center - - - - - - - - - - - - - diff --git a/crates/musicus/res/ui/player_bar.ui b/crates/musicus/res/ui/player_bar.ui deleted file mode 100644 index cd9b1a8..0000000 --- a/crates/musicus/res/ui/player_bar.ui +++ /dev/null @@ -1,116 +0,0 @@ - - - - - media-playback-start-symbolic - - - slide-up - - - vertical - - - - - - 6 - 6 - 6 - 6 - 12 - - - center - 6 - - - False - - - media-skip-backward-symbolic - - - - - - - - - media-playback-pause-symbolic - - - - - - - False - True - - - media-skip-forward-symbolic - - - - - - - - - vertical - True - - - start - Title - end - - - - - - - - start - Subtitle - end - - - - - - - 2 - - - 0:00 - - - - - / - - - - - 0:00 - - - - - - - center - - - view-list-bullet-symbolic - - - - - - - - - - diff --git a/crates/musicus/res/ui/player_screen.ui b/crates/musicus/res/ui/player_screen.ui deleted file mode 100644 index b0d2903..0000000 --- a/crates/musicus/res/ui/player_screen.ui +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - media-playback-start-symbolic - - - 1 - 0.01 - 0.05 - - - vertical - - - - - Player - - - - - - - - go-previous-symbolic - - - - - - - - - true - - - 12 - 12 - 18 - 12 - 800 - - - vertical - 12 - - - 12 - - - center - 6 - - - False - - - media-skip-backward-symbolic - - - - - - - True - - - media-playback-pause-symbolic - - - - - - - False - True - - - media-skip-forward-symbolic - - - - - - - - - vertical - True - - - start - Title - end - - - - - - - - start - Subtitle - end - - - - - - - True - center - - - media-playback-stop-symbolic - - - - - - - - - 6 - - - 0:00 - - - - - position - True - - - - - 0:00 - - - - - - - - - - - - diff --git a/crates/musicus/res/ui/preferences.ui b/crates/musicus/res/ui/preferences.ui deleted file mode 100644 index b1ae579..0000000 --- a/crates/musicus/res/ui/preferences.ui +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - True - 400 - 400 - - - General - - - Music library - - - False - Music library folder - select_music_library_path_button - None selected - - - Select - True - center - - - - - - - - - Playlist - - - False - Keep playing - keep_playing_switch - Whether to keep playing random tracks after the playlist ends. - - - center - - - - - - - False - Choose full recordings - play_full_recordings_switch - Whether to choose full recordings instead of single tracks for random playback. - - - center - - - - - - - - - - diff --git a/crates/musicus/res/ui/recording_editor.ui b/crates/musicus/res/ui/recording_editor.ui deleted file mode 100644 index 09291ca..0000000 --- a/crates/musicus/res/ui/recording_editor.ui +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - content - - - vertical - - - false - false - - - Recording - - - - - - go-previous-symbolic - - - - - False - object-select-symbolic - - - - - - - - False - - - - - true - - - 12 - 12 - 18 - 12 - - - 6 - 6 - 6 - vertical - - - start - 12 - 6 - Overview - - - - - - - - none - - - False - Select a work - work_button - - - Select - center - - - - - - - Comment - - - - - - - - horizontal - 12 - 6 - - - start - end - True - Performers - - - - - - - - false - list-add-symbolic - - - - - - - - - - - - - - - - - - - - loading - - - vertical - - - false - false - - - Recording - - - - - - - - true - true - center - center - true - - - - - - - - diff --git a/crates/musicus/res/ui/screen.ui b/crates/musicus/res/ui/screen.ui deleted file mode 100644 index 2027a6c..0000000 --- a/crates/musicus/res/ui/screen.ui +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - vertical - - - - - - - - go-previous-symbolic - - - - - menu - view-more-symbolic - - - - - edit-find-symbolic - - - - - - - False - - - true - - - - - - - - - - - - loading - - - true - true - center - center - 32 - 32 - true - - - - - - - content - - - - - - - vertical - 12 - 12 - 36 - - - - - - - - - - - - - diff --git a/crates/musicus/res/ui/section.ui b/crates/musicus/res/ui/section.ui deleted file mode 100644 index fe71908..0000000 --- a/crates/musicus/res/ui/section.ui +++ /dev/null @@ -1,38 +0,0 @@ - - - - - vertical - 6 - - - 12 - - - vertical - 18 - end - true - - - end - 0.0 - - - - - - - - true - 0.0 - false - 6 - - - - - - - - diff --git a/crates/musicus/res/ui/selector.ui b/crates/musicus/res/ui/selector.ui deleted file mode 100644 index fd5e845..0000000 --- a/crates/musicus/res/ui/selector.ui +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - 250 - False - vertical - - - false - false - - - vertical - center - - - - - - - - false - - - - - - - - go-previous-symbolic - - - - - list-add-symbolic - - - - - - - True - - - 500 - 300 - true - - - vertical - 6 - - - Search … - - - - - - - - - - - False - False - crossfade - True - - - loading - - - 12 - center - start - True - - - - - - - content - - - 200 - true - - - 500 - 300 - 6 - 6 - 12 - 6 - - - - - - - - - - diff --git a/crates/musicus/res/ui/source_selector.ui b/crates/musicus/res/ui/source_selector.ui deleted file mode 100644 index 201cdf5..0000000 --- a/crates/musicus/res/ui/source_selector.ui +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - crossfade - - - content - - - vertical - - - false - false - - - - - - go-previous-symbolic - - - - - - - true - folder-music-symbolic - Import music - Select the source which contains the new audio files below. - - - horizontal - true - 6 - center - - - Select folder - - - - - Copy audio CD - - - - - - - - - - - - - loading - - - vertical - - - false - false - - - Loading - - - - - - - true - true - center - center - 32 - 32 - true - - - - - - - - - error - - - vertical - - - false - false - - - Error - - - - - - - dialog-error-symbolic - Error - true - - - Try again - true - true - center - center - - - - - - - - - - diff --git a/crates/musicus/res/ui/track_editor.ui b/crates/musicus/res/ui/track_editor.ui deleted file mode 100644 index ff6aba6..0000000 --- a/crates/musicus/res/ui/track_editor.ui +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - vertical - - - false - false - - - Track - - - - - - go-previous-symbolic - - - - - object-select-symbolic - - - - - - - - True - - - 12 - 6 - 6 - 6 - - - - - - diff --git a/crates/musicus/res/ui/track_row.ui b/crates/musicus/res/ui/track_row.ui deleted file mode 100644 index 1ca859b..0000000 --- a/crates/musicus/res/ui/track_row.ui +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - horizontal - 12 - 6 - 6 - - - - - media-playback-start-symbolic - 6 - 12 - 18 - start - - - - - - - vertical - true - - - vertical - false - 12 - - - true - 0.0 - - - - - - - - true - 0.0 - - - - - - - - true - 0.0 - 12 - - - - - - - - diff --git a/crates/musicus/res/ui/track_selector.ui b/crates/musicus/res/ui/track_selector.ui deleted file mode 100644 index f50c062..0000000 --- a/crates/musicus/res/ui/track_selector.ui +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - vertical - - - false - false - - - Select tracks - - - - - - go-previous-symbolic - - - - - False - object-select-symbolic - - - - - - - - True - - - 12 - 6 - 6 - 6 - - - - - - diff --git a/crates/musicus/res/ui/track_set_editor.ui b/crates/musicus/res/ui/track_set_editor.ui deleted file mode 100644 index 9d96127..0000000 --- a/crates/musicus/res/ui/track_set_editor.ui +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - vertical - - - false - false - - - Import music - - - - - - go-previous-symbolic - - - - - object-select-symbolic - False - - - - - - - - True - - - - - 6 - 6 - 6 - vertical - - - start - 12 - 6 - Recording - - - - - - - - none - - - False - Select a recording - select_recording_button - - - Select - center - - - - - - - - - - horizontal - 12 - 6 - - - start - end - True - Tracks - - - - - - - - false - document-edit-symbolic - - - - - - - - - - - - - - - diff --git a/crates/musicus/res/ui/work_editor.ui b/crates/musicus/res/ui/work_editor.ui deleted file mode 100644 index bfa6de3..0000000 --- a/crates/musicus/res/ui/work_editor.ui +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - content - - - vertical - - - false - false - - - Work - - - - - - go-previous-symbolic - - - - - False - object-select-symbolic - - - - - - - - False - - - - - true - - - 12 - 12 - 18 - 12 - - - 6 - 6 - 6 - vertical - - - start - 12 - 6 - Overview - - - - - - - - none - - - False - Select a composer - composer_button - - - Select - center - - - - - - - Title - - - - - - - - horizontal - 12 - 6 - - - start - end - True - Instruments - - - - - - - - false - list-add-symbolic - - - - - - - - - - horizontal - 12 - 6 - - - start - end - True - Structure - - - - - - - - false - list-add-symbolic - - - - - - - - - - - - - - - - - - - - loading - - - vertical - - - false - false - - - Work - - - - - - - - true - true - center - center - True - - - - - - - - diff --git a/crates/musicus/res/ui/work_part_editor.ui b/crates/musicus/res/ui/work_part_editor.ui deleted file mode 100644 index 83ec73b..0000000 --- a/crates/musicus/res/ui/work_part_editor.ui +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - vertical - - - false - false - - - Work part - - - - - - go-previous-symbolic - - - - - object-select-symbolic - - - - - - - - False - - - - - true - - - 12 - 12 - 18 - 12 - 500 - 300 - - - none - start - - - Title - - - - - - - - - - - diff --git a/crates/musicus/src/config.rs.in b/crates/musicus/src/config.rs.in deleted file mode 100644 index be48dba..0000000 --- a/crates/musicus/src/config.rs.in +++ /dev/null @@ -1,2 +0,0 @@ -pub static VERSION: &str = @VERSION@; -pub static LOCALEDIR: &str = @LOCALEDIR@; \ No newline at end of file diff --git a/crates/musicus/src/editors/ensemble.rs b/crates/musicus/src/editors/ensemble.rs deleted file mode 100644 index 7e92652..0000000 --- a/crates/musicus/src/editors/ensemble.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::{Editor, Section, Widget}; -use anyhow::Result; -use gettextrs::gettext; -use gtk::{glib::clone, prelude::*}; -use musicus_backend::db::{self, generate_id, Ensemble}; -use std::rc::Rc; - -/// A dialog for creating or editing a ensemble. -pub struct EnsembleEditor { - handle: NavigationHandle, - - /// The ID of the ensemble that is edited or a newly generated one. - id: String, - - editor: Editor, - name: adw::EntryRow, -} - -impl Screen, Ensemble> for EnsembleEditor { - /// Create a new ensemble editor and optionally initialize it. - fn new(ensemble: Option, handle: NavigationHandle) -> Rc { - let editor = Editor::new(); - editor.set_title("Ensemble"); - - let list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let name = adw::EntryRow::builder().title(gettext("Name")).build(); - list.append(&name); - - let section = Section::new(&gettext("General"), &list); - editor.add_content(§ion.widget); - - let id = match ensemble { - Some(ensemble) => { - name.set_text(&ensemble.name); - ensemble.id - } - None => generate_id(), - }; - - let this = Rc::new(Self { - handle, - id, - editor, - name, - }); - - // Connect signals and callbacks - - this.editor.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.editor.set_save_cb(clone!(@weak this => move || { - match this.save() { - Ok(ensemble) => { - this.handle.pop(Some(ensemble)); - } - Err(err) => { - let description = gettext!("Cause: {}", err); - this.editor.error(&gettext("Failed to save ensemble!"), &description); - } - } - })); - - this.name - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.validate(); - - this - } -} - -impl EnsembleEditor { - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.editor.set_may_save(!self.name.text().is_empty()); - } - - /// Save the ensemble. - fn save(&self) -> Result { - let name = self.name.text(); - - let ensemble = Ensemble::new(self.id.clone(), name.to_string()); - - db::update_ensemble( - &mut self.handle.backend.db().lock().unwrap(), - ensemble.clone(), - )?; - self.handle.backend.library_changed(); - - Ok(ensemble) - } -} - -impl Widget for EnsembleEditor { - fn get_widget(&self) -> gtk::Widget { - self.editor.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/instrument.rs b/crates/musicus/src/editors/instrument.rs deleted file mode 100644 index a1350fc..0000000 --- a/crates/musicus/src/editors/instrument.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::{Editor, Section, Widget}; -use anyhow::Result; -use gettextrs::gettext; -use gtk::{glib::clone, prelude::*}; -use musicus_backend::db::{self, generate_id, Instrument}; -use std::rc::Rc; - -/// A dialog for creating or editing a instrument. -pub struct InstrumentEditor { - handle: NavigationHandle, - - /// The ID of the instrument that is edited or a newly generated one. - id: String, - - editor: Editor, - name: adw::EntryRow, -} - -impl Screen, Instrument> for InstrumentEditor { - /// Create a new instrument editor and optionally initialize it. - fn new(instrument: Option, handle: NavigationHandle) -> Rc { - let editor = Editor::new(); - editor.set_title("Instrument/Role"); - - let list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let name = adw::EntryRow::builder().title(gettext("Name")).build(); - list.append(&name); - - let section = Section::new(&gettext("General"), &list); - editor.add_content(§ion.widget); - - let id = match instrument { - Some(instrument) => { - name.set_text(&instrument.name); - instrument.id - } - None => generate_id(), - }; - - let this = Rc::new(Self { - handle, - id, - editor, - name, - }); - - // Connect signals and callbacks - - this.editor.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.editor.set_save_cb(clone!(@weak this => move || { - match this.save() { - Ok(instrument) => { - this.handle.pop(Some(instrument)); - } - Err(err) => { - let description = gettext!("Cause: {}", err); - this.editor.error(&gettext("Failed to save instrument!"), &description); - } - } - })); - - this.name - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.validate(); - - this - } -} - -impl InstrumentEditor { - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.editor.set_may_save(!self.name.text().is_empty()); - } - - /// Save the instrument. - fn save(&self) -> Result { - let name = self.name.text(); - - let instrument = Instrument::new(self.id.clone(), name.to_string()); - - db::update_instrument( - &mut self.handle.backend.db().lock().unwrap(), - instrument.clone(), - )?; - - self.handle.backend.library_changed(); - - Ok(instrument) - } -} - -impl Widget for InstrumentEditor { - fn get_widget(&self) -> gtk::Widget { - self.editor.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/mod.rs b/crates/musicus/src/editors/mod.rs deleted file mode 100644 index e558374..0000000 --- a/crates/musicus/src/editors/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub mod ensemble; -pub use ensemble::*; - -pub mod instrument; -pub use instrument::*; - -pub mod person; -pub use person::*; - -pub mod recording; -pub use recording::*; - -pub mod work; -pub use work::*; - -mod performance; -mod work_part; diff --git a/crates/musicus/src/editors/performance.rs b/crates/musicus/src/editors/performance.rs deleted file mode 100644 index 5b1cee9..0000000 --- a/crates/musicus/src/editors/performance.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::{EnsembleSelector, InstrumentSelector, PersonSelector}; -use crate::widgets::{ButtonRow, Editor, Section, Widget}; -use adw::prelude::*; -use gettextrs::gettext; - -use gtk::glib::clone; -use log::error; -use musicus_backend::db::{Ensemble, Instrument, Performance, Person, PersonOrEnsemble}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for editing a performance within a recording. -pub struct PerformanceEditor { - handle: NavigationHandle, - editor: Editor, - person_row: ButtonRow, - ensemble_row: ButtonRow, - role_row: ButtonRow, - reset_role_button: gtk::Button, - person: RefCell>, - ensemble: RefCell>, - role: RefCell>, -} - -impl Screen, Performance> for PerformanceEditor { - /// Create a new performance editor. - fn new(performance: Option, handle: NavigationHandle) -> Rc { - let editor = Editor::new(); - editor.set_title("Performance"); - editor.set_may_save(false); - - let performer_list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let person_row = ButtonRow::new("Person", "Select"); - let ensemble_row = ButtonRow::new("Ensemble", "Select"); - - performer_list.append(&person_row.get_widget()); - performer_list.append(&ensemble_row.get_widget()); - - let performer_section = Section::new(&gettext("Performer"), &performer_list); - performer_section.set_subtitle(&gettext( - "Select either a person or an ensemble as a performer.", - )); - - let role_list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let reset_role_button = gtk::Button::builder() - .icon_name("user-trash-symbolic") - .valign(gtk::Align::Center) - .visible(false) - .build(); - - let role_row = ButtonRow::new("Role", "Select"); - role_row.widget.add_suffix(&reset_role_button); - - role_list.append(&role_row.get_widget()); - - let role_section = Section::new(&gettext("Role"), &role_list); - role_section.set_subtitle(&gettext( - "Optionally, choose a role to specify what the performer does.", - )); - - editor.add_content(&performer_section); - editor.add_content(&role_section); - - let this = Rc::new(PerformanceEditor { - handle, - editor, - person_row, - ensemble_row, - role_row, - reset_role_button, - person: RefCell::new(None), - ensemble: RefCell::new(None), - role: RefCell::new(None), - }); - - this.editor.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.editor.set_save_cb(clone!(@weak this => move || { - let performance = Performance { - performer: if let Some(person) = this.person.borrow().clone() { - PersonOrEnsemble::Person(person) - } else if let Some(ensemble) = this.ensemble.borrow().clone() { - PersonOrEnsemble::Ensemble(ensemble) - } else { - error!("Tried to save performance without performer"); - return; - }, - role: this.role.borrow().clone(), - }; - - this.handle.pop(Some(performance)); - })); - - this.person_row.set_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(person) = push!(this.handle, PersonSelector).await { - this.show_person(Some(&person)); - this.person.replace(Some(person)); - this.show_ensemble(None); - this.ensemble.replace(None); - } - }); - })); - - this.ensemble_row.set_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(ensemble) = push!(this.handle, EnsembleSelector).await { - this.show_person(None); - this.person.replace(None); - this.show_ensemble(Some(&ensemble)); - this.ensemble.replace(Some(ensemble)); - } - }); - })); - - this.role_row.set_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(role) = push!(this.handle, InstrumentSelector).await { - this.show_role(Some(&role)); - this.role.replace(Some(role)); - } - }); - })); - - this.reset_role_button - .connect_clicked(clone!(@weak this => move |_| { - this.show_role(None); - this.role.replace(None); - })); - - // Initialize - - if let Some(performance) = performance { - match performance.performer { - PersonOrEnsemble::Person(person) => { - this.show_person(Some(&person)); - this.person.replace(Some(person)); - } - PersonOrEnsemble::Ensemble(ensemble) => { - this.show_ensemble(Some(&ensemble)); - this.ensemble.replace(Some(ensemble)); - } - }; - - if let Some(role) = performance.role { - this.show_role(Some(&role)); - this.role.replace(Some(role)); - } - } - - this - } -} - -impl PerformanceEditor { - /// Update the UI according to person. - fn show_person(&self, person: Option<&Person>) { - if let Some(person) = person { - self.person_row.set_subtitle(&person.name_fl()); - self.editor.set_may_save(true); - } else { - self.person_row.set_subtitle(""); - } - } - - /// Update the UI according to ensemble. - fn show_ensemble(&self, ensemble: Option<&Ensemble>) { - if let Some(ensemble) = ensemble { - self.ensemble_row.set_subtitle(&ensemble.name); - self.editor.set_may_save(true); - } else { - self.ensemble_row.set_subtitle(""); - } - } - - /// Update the UI according to role. - fn show_role(&self, role: Option<&Instrument>) { - if let Some(role) = role { - self.role_row.set_subtitle(&role.name); - self.reset_role_button.show(); - } else { - self.role_row.set_subtitle(""); - self.reset_role_button.hide(); - } - } -} - -impl Widget for PerformanceEditor { - fn get_widget(&self) -> gtk::Widget { - self.editor.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/person.rs b/crates/musicus/src/editors/person.rs deleted file mode 100644 index 0add45d..0000000 --- a/crates/musicus/src/editors/person.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::{Editor, Section, Widget}; -use anyhow::Result; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use musicus_backend::db::{self, generate_id, Person}; -use std::rc::Rc; - -/// A dialog for creating or editing a person. -pub struct PersonEditor { - handle: NavigationHandle, - - /// The ID of the person that is edited or a newly generated one. - id: String, - - editor: Editor, - first_name: adw::EntryRow, - last_name: adw::EntryRow, -} - -impl Screen, Person> for PersonEditor { - /// Create a new person editor and optionally initialize it. - fn new(person: Option, handle: NavigationHandle) -> Rc { - let editor = Editor::new(); - editor.set_title("Person"); - - let list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let first_name = adw::EntryRow::builder() - .title(gettext("First name")) - .build(); - - let last_name = adw::EntryRow::builder().title(gettext("Last name")).build(); - - list.append(&first_name); - list.append(&last_name); - - let section = Section::new(&gettext("General"), &list); - editor.add_content(§ion.widget); - - let id = match person { - Some(person) => { - first_name.set_text(&person.first_name); - last_name.set_text(&person.last_name); - - person.id - } - None => generate_id(), - }; - - let this = Rc::new(Self { - handle, - id, - editor, - first_name, - last_name, - }); - - // Connect signals and callbacks - - this.editor.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.editor.set_save_cb(clone!(@strong this => move || { - match this.save() { - Ok(person) => { - this.handle.pop(Some(person)); - } - Err(err) => { - let description = gettext!("Cause: {}", err); - this.editor.error(&gettext("Failed to save person!"), &description); - } - } - })); - - this.first_name - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.last_name - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.validate(); - - this - } -} - -impl PersonEditor { - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.editor - .set_may_save(!self.first_name.text().is_empty() && !self.last_name.text().is_empty()); - } - - /// Save the person. - fn save(self: &Rc) -> Result { - let first_name = self.first_name.text(); - let last_name = self.last_name.text(); - - let person = Person::new( - self.id.clone(), - first_name.to_string(), - last_name.to_string(), - ); - - db::update_person( - &mut self.handle.backend.db().lock().unwrap(), - person.clone(), - )?; - - self.handle.backend.library_changed(); - - Ok(person) - } -} - -impl Widget for PersonEditor { - fn get_widget(&self) -> gtk::Widget { - self.editor.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/recording.rs b/crates/musicus/src/editors/recording.rs deleted file mode 100644 index b47cf34..0000000 --- a/crates/musicus/src/editors/recording.rs +++ /dev/null @@ -1,206 +0,0 @@ -use super::performance::PerformanceEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::WorkSelector; -use crate::widgets::{List, Widget}; - -use adw::prelude::*; -use anyhow::Result; -use gettextrs::gettext; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::{self, generate_id, Performance, Recording, Work}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A widget for creating or editing a recording. -pub struct RecordingEditor { - handle: NavigationHandle, - widget: gtk::Stack, - save_button: gtk::Button, - info_bar: gtk::InfoBar, - work_row: adw::ActionRow, - comment_row: adw::EntryRow, - performance_list: Rc, - id: String, - work: RefCell>, - performances: RefCell>, -} - -impl Screen, Recording> for RecordingEditor { - /// Create a new recording editor widget and optionally initialize it. - fn new(recording: Option, handle: NavigationHandle) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::InfoBar, info_bar); - get_widget!(builder, adw::ActionRow, work_row); - get_widget!(builder, gtk::Button, work_button); - get_widget!(builder, adw::EntryRow, comment_row); - get_widget!(builder, gtk::Frame, performance_frame); - get_widget!(builder, gtk::Button, add_performer_button); - - let performance_list = List::new(); - performance_frame.set_child(Some(&performance_list.widget)); - - let (id, work, performances) = match recording { - Some(recording) => { - comment_row.set_text(&recording.comment); - (recording.id, Some(recording.work), recording.performances) - } - None => (generate_id(), None, Vec::new()), - }; - - let this = Rc::new(RecordingEditor { - handle, - widget, - save_button, - info_bar, - work_row, - comment_row, - performance_list, - id, - work: RefCell::new(work), - performances: RefCell::new(performances), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.save_button - .connect_clicked(clone!(@weak this => move |_| { - match this.save() { - Ok(recording) => { - this.handle.pop(Some(recording)); - } - Err(_) => { - this.info_bar.set_revealed(true); - this.widget.set_visible_child_name("content"); - } - } - })); - - work_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(work) = push!(this.handle, WorkSelector).await { - this.work_selected(&work); - this.work.replace(Some(work)); - } - }); - })); - - this.performance_list.set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let performance = &this.performances.borrow()[index]; - - let delete_button = gtk::Button::from_icon_name("user-trash-symbolic"); - delete_button.set_valign(gtk::Align::Center); - - delete_button.connect_clicked(clone!(@weak this => move |_| { - let length = { - let mut performances = this.performances.borrow_mut(); - performances.remove(index); - performances.len() - }; - - this.performance_list.update(length); - })); - - let edit_button = gtk::Button::from_icon_name("document-edit-symbolic"); - edit_button.set_valign(gtk::Align::Center); - - edit_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - let performance = this.performances.borrow()[index].clone(); - if let Some(performance) = push!(this.handle, PerformanceEditor, Some(performance)).await { - let length = { - let mut performances = this.performances.borrow_mut(); - performances[index] = performance; - performances.len() - }; - - this.performance_list.update(length); - } - }); - })); - - let row = adw::ActionRow::builder() - .focusable(false) - .activatable_widget(&edit_button) - .title(performance.get_title()) - .build(); - - row.add_suffix(&delete_button); - row.add_suffix(&edit_button); - - row.upcast() - })); - - add_performer_button.connect_clicked(clone!(@strong this => move |_| { - spawn!(@clone this, async move { - if let Some(performance) = push!(this.handle, PerformanceEditor, None).await { - let length = { - let mut performances = this.performances.borrow_mut(); - performances.push(performance); - performances.len() - }; - - this.performance_list.update(length); - } - }); - })); - - // Initialize - - if let Some(work) = &*this.work.borrow() { - this.work_selected(work); - } - - let length = this.performances.borrow().len(); - this.performance_list.update(length); - - this - } -} - -impl RecordingEditor { - /// Update the UI according to work. - fn work_selected(&self, work: &Work) { - self.work_row.set_title(&gettext("Work")); - self.work_row.set_subtitle(&work.get_title()); - self.save_button.set_sensitive(true); - } - - /// Save the recording. - fn save(self: &Rc) -> Result { - let recording = Recording::new( - self.id.clone(), - self.work - .borrow() - .clone() - .expect("Tried to create recording without work!"), - self.comment_row.text().to_string(), - self.performances.borrow().clone(), - ); - - db::update_recording( - &mut self.handle.backend.db().lock().unwrap(), - recording.clone(), - )?; - - self.handle.backend.library_changed(); - - Ok(recording) - } -} - -impl Widget for RecordingEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/work.rs b/crates/musicus/src/editors/work.rs deleted file mode 100644 index 37fbc13..0000000 --- a/crates/musicus/src/editors/work.rs +++ /dev/null @@ -1,275 +0,0 @@ -use super::work_part::WorkPartEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::{InstrumentSelector, PersonSelector}; -use crate::widgets::{List, Widget}; - -use adw::prelude::*; -use anyhow::Result; -use gettextrs::gettext; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::{self, generate_id, Instrument, Person, Work, WorkPart}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A widget for editing and creating works. -pub struct WorkEditor { - handle: NavigationHandle, - widget: gtk::Stack, - save_button: gtk::Button, - title_row: adw::EntryRow, - info_bar: gtk::InfoBar, - composer_row: adw::ActionRow, - instrument_list: Rc, - part_list: Rc, - id: String, - composer: RefCell>, - instruments: RefCell>, - parts: RefCell>, -} - -impl Screen, Work> for WorkEditor { - /// Create a new work editor widget and optionally initialize it. - fn new(work: Option, handle: NavigationHandle) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_editor.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::InfoBar, info_bar); - get_widget!(builder, adw::EntryRow, title_row); - get_widget!(builder, gtk::Button, composer_button); - get_widget!(builder, adw::ActionRow, composer_row); - get_widget!(builder, gtk::Frame, instrument_frame); - get_widget!(builder, gtk::Button, add_instrument_button); - get_widget!(builder, gtk::Frame, structure_frame); - get_widget!(builder, gtk::Button, add_part_button); - - let instrument_list = List::new(); - instrument_frame.set_child(Some(&instrument_list.widget)); - - let part_list = List::new(); - part_list.set_enable_dnd(true); - structure_frame.set_child(Some(&part_list.widget)); - - let (id, composer, instruments, structure) = match work { - Some(work) => { - title_row.set_text(&work.title); - (work.id, Some(work.composer), work.instruments, work.parts) - } - None => (generate_id(), None, Vec::new(), Vec::new()), - }; - - let this = Rc::new(Self { - handle, - widget, - save_button, - id, - info_bar, - title_row, - composer_row, - instrument_list, - part_list, - composer: RefCell::new(composer), - instruments: RefCell::new(instruments), - parts: RefCell::new(structure), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.save_button - .connect_clicked(clone!(@weak this => move |_| { - match this.save() { - Ok(work) => { - this.handle.pop(Some(work)); - } - Err(_) => { - this.info_bar.set_revealed(true); - this.widget.set_visible_child_name("content"); - } - } - })); - - composer_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(person) = push!(this.handle, PersonSelector).await { - this.show_composer(&person); - this.composer.replace(Some(person)); - } - }); - })); - - this.title_row - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.instrument_list.set_make_widget_cb( - clone!(@weak this => @default-panic, move |index| { - let instrument = &this.instruments.borrow()[index]; - - let delete_button = gtk::Button::from_icon_name("user-trash-symbolic"); - delete_button.set_valign(gtk::Align::Center); - - delete_button.connect_clicked(clone!(@strong this => move |_| { - let length = { - let mut instruments = this.instruments.borrow_mut(); - instruments.remove(index); - instruments.len() - }; - - this.instrument_list.update(length); - })); - - let row = adw::ActionRow::builder() - .title(&instrument.name) - .build(); - - row.add_suffix(&delete_button); - - row.upcast() - }), - ); - - add_instrument_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(instrument) = push!(this.handle, InstrumentSelector).await { - let length = { - let mut instruments = this.instruments.borrow_mut(); - instruments.push(instrument); - instruments.len() - }; - - this.instrument_list.update(length); - } - }); - })); - - this.part_list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let part = &this.parts.borrow()[index]; - - let delete_button = gtk::Button::from_icon_name("user-trash-symbolic"); - delete_button.set_valign(gtk::Align::Center); - - delete_button.connect_clicked(clone!(@weak this => move |_| { - let length = { - let mut structure = this.parts.borrow_mut(); - structure.remove(index); - structure.len() - }; - - this.part_list.update(length); - })); - - let edit_button = gtk::Button::from_icon_name("document-edit-symbolic"); - edit_button.set_valign(gtk::Align::Center); - - edit_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - let part = this.parts.borrow()[index].clone(); - if let Some(part) = push!(this.handle, WorkPartEditor, Some(part)).await { - let length = { - let mut structure = this.parts.borrow_mut(); - structure[index] = part; - structure.len() - }; - - this.part_list.update(length); - } - }); - })); - - let row = adw::ActionRow::builder() - .focusable(false) - .title(&part.title) - .activatable_widget(&edit_button) - .build(); - - row.add_suffix(&delete_button); - row.add_suffix(&edit_button); - - row.upcast() - })); - - this.part_list - .set_move_cb(clone!(@weak this => move |old_index, new_index| { - let length = { - let mut parts = this.parts.borrow_mut(); - parts.swap(old_index, new_index); - parts.len() - }; - - this.part_list.update(length); - })); - - add_part_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(part) = push!(this.handle, WorkPartEditor, None).await { - let length = { - let mut parts = this.parts.borrow_mut(); - parts.push(part); - parts.len() - }; - - this.part_list.update(length); - } - }); - })); - - // Initialization - - if let Some(composer) = &*this.composer.borrow() { - this.show_composer(composer); - } - - this.instrument_list.update(this.instruments.borrow().len()); - this.part_list.update(this.parts.borrow().len()); - - this - } -} - -impl WorkEditor { - /// Update the UI according to person. - fn show_composer(&self, person: &Person) { - self.composer_row.set_title(&gettext("Composer")); - self.composer_row.set_subtitle(&person.name_fl()); - self.validate(); - } - - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.save_button - .set_sensitive(!self.title_row.text().is_empty() && self.composer.borrow().is_some()); - } - - /// Save the work. - fn save(self: &Rc) -> Result { - let work = Work::new( - self.id.clone(), - self.title_row.text().to_string(), - self.composer - .borrow() - .clone() - .expect("Tried to create work without composer!"), - self.instruments.borrow().clone(), - self.parts.borrow().clone(), - ); - - db::update_work(&mut self.handle.backend.db().lock().unwrap(), work.clone())?; - self.handle.backend.library_changed(); - - Ok(work) - } -} - -impl Widget for WorkEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/editors/work_part.rs b/crates/musicus/src/editors/work_part.rs deleted file mode 100644 index e6131ed..0000000 --- a/crates/musicus/src/editors/work_part.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use musicus_backend::db::WorkPart; -use std::rc::Rc; - -/// A dialog for creating or editing a work section. -pub struct WorkPartEditor { - handle: NavigationHandle, - widget: gtk::Box, - save_button: gtk::Button, - title_row: adw::EntryRow, -} - -impl Screen, WorkPart> for WorkPartEditor { - /// Create a new part editor and optionally initialize it. - fn new(section: Option, handle: NavigationHandle) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_part_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, adw::EntryRow, title_row); - - if let Some(section) = section { - title_row.set_text(§ion.title); - } - - let this = Rc::new(Self { - handle, - widget, - save_button, - title_row, - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.save_button - .connect_clicked(clone!(@weak this => move |_| { - let section = WorkPart { - title: this.title_row.text().to_string(), - }; - - this.handle.pop(Some(section)); - })); - - this.title_row - .connect_changed(clone!(@weak this => move |_| this.validate())); - - this.validate(); - - this - } -} - -impl WorkPartEditor { - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.save_button - .set_sensitive(!self.title_row.text().is_empty()); - } -} - -impl Widget for WorkPartEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/import_screen.rs b/crates/musicus/src/import/import_screen.rs deleted file mode 100644 index 3bbbf55..0000000 --- a/crates/musicus/src/import/import_screen.rs +++ /dev/null @@ -1,168 +0,0 @@ -use super::medium_editor::MediumEditor; -use super::medium_preview::MediumPreview; -use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::MediumSelector; -use crate::widgets::Widget; - -use adw::prelude::*; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::{self, Medium}; -use musicus_backend::import::ImportSession; -use std::rc::Rc; -use std::sync::Arc; - -/// A dialog for selecting metadata when importing music. -pub struct ImportScreen { - handle: NavigationHandle<()>, - session: Arc, - widget: gtk::Box, - matching_stack: gtk::Stack, - error_row: adw::ActionRow, - matching_list: gtk::ListBox, -} - -impl ImportScreen { - /// Find matching mediums in the library. - fn load_matches(self: &Rc) { - self.matching_stack.set_visible_child_name("loading"); - - let this = self; - spawn!(@clone this, async move { - let mediums = db::get_mediums_by_source_id( - &mut this.handle.backend.db().lock().unwrap(), - this.session.source_id() - ); - - match mediums { - Ok(mediums) => { - if !mediums.is_empty() { - this.show_matches(mediums); - this.matching_stack.set_visible_child_name("content"); - } else { - this.matching_stack.set_visible_child_name("empty"); - } - } - Err(err) => { - this.error_row.set_subtitle(&err.to_string()); - this.matching_stack.set_visible_child_name("error"); - } - } - }); - } - - /// Populate the list of matches - fn show_matches(self: &Rc, mediums: Vec) { - if let Some(mut child) = self.matching_list.first_child() { - loop { - let next_child = child.next_sibling(); - self.matching_list.remove(&child); - - match next_child { - Some(next_child) => child = next_child, - None => break, - } - } - } - - let this = self; - - for medium in mediums { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&medium.name) - .subtitle(format!("{} Tracks", medium.tracks.len())) - .build(); - - row.connect_activated(clone!(@weak this => move |_| { - let medium = medium.clone(); - spawn!(@clone this, async move { - if let Some(()) = push!(this.handle, MediumPreview, (this.session.clone(), medium.clone())).await { - this.handle.pop(Some(())); - } - }); - })); - - this.matching_list.append(&row); - } - } - - /// Select a medium from somewhere and present a preview. - fn select_medium(self: &Rc, medium: Medium) { - let this = self; - - spawn!(@clone this, async move { - if let Some(()) = push!(this.handle, MediumPreview, (this.session.clone(), medium)).await { - this.handle.pop(Some(())); - } - }); - } -} - -impl Screen, ()> for ImportScreen { - /// Create a new import screen. - fn new(session: Arc, handle: NavigationHandle<()>) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/import_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Stack, matching_stack); - get_widget!(builder, gtk::Button, try_again_button); - get_widget!(builder, adw::ActionRow, error_row); - get_widget!(builder, gtk::ListBox, matching_list); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, gtk::Button, add_button); - - let this = Rc::new(Self { - handle, - session, - widget, - matching_stack, - error_row, - matching_list, - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - try_again_button.connect_clicked(clone!(@weak this => move |_| { - this.load_matches(); - })); - - select_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(medium) = push!(this.handle, MediumSelector).await { - this.select_medium(medium); - } - }); - })); - - add_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(medium) = push!(this.handle, MediumEditor, (Arc::clone(&this.session), None)).await { - this.select_medium(medium); - } - }); - })); - - // Initialize the view - - this.load_matches(); - - // Copy the tracks in the background, if necessary. - this.session.copy(); - - this - } -} - -impl Widget for ImportScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/medium_editor.rs b/crates/musicus/src/import/medium_editor.rs deleted file mode 100644 index 8ca10a1..0000000 --- a/crates/musicus/src/import/medium_editor.rs +++ /dev/null @@ -1,220 +0,0 @@ -use super::track_set_editor::{TrackData, TrackSetData, TrackSetEditor}; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::{List, Widget}; - -use adw::prelude::*; -use anyhow::Result; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::{generate_id, Medium, Track}; -use musicus_backend::import::ImportSession; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::Arc; - -/// A dialog for editing metadata while importing music into the music library. -pub struct MediumEditor { - handle: NavigationHandle, - session: Arc, - widget: gtk::Stack, - done_button: gtk::Button, - name_row: adw::EntryRow, - status_page: adw::StatusPage, - track_set_list: Rc, - track_sets: RefCell>, -} - -impl Screen<(Arc, Option), Medium> for MediumEditor { - /// Create a new medium editor. - fn new( - (session, medium): (Arc, Option), - handle: NavigationHandle, - ) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_editor.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, done_button); - get_widget!(builder, adw::EntryRow, name_row); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::Frame, frame); - get_widget!(builder, adw::StatusPage, status_page); - get_widget!(builder, gtk::Button, try_again_button); - get_widget!(builder, gtk::Button, cancel_button); - - let list = List::new(); - frame.set_child(Some(&list.widget)); - - let this = Rc::new(Self { - handle, - session, - widget, - done_button, - name_row, - status_page, - track_set_list: list, - track_sets: RefCell::new(Vec::new()), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.done_button - .connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("loading"); - spawn!(@clone this, async move { - match this.save().await { - Ok(medium) => this.handle.pop(Some(medium)), - Err(err) => { - this.status_page.set_description(Some(&err.to_string())); - this.widget.set_visible_child_name("error"); - } - } - }); - })); - - this.name_row - .connect_changed(clone!(@weak this => move |_| this.validate())); - - add_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(track_set) = push!(this.handle, TrackSetEditor, Arc::clone(&this.session)).await { - let length = { - let mut track_sets = this.track_sets.borrow_mut(); - track_sets.push(track_set); - track_sets.len() - }; - - this.track_set_list.update(length); - this.validate(); - } - }); - })); - - this.track_set_list.set_make_widget_cb( - clone!(@weak this => @default-panic, move |index| { - let track_set = &this.track_sets.borrow()[index]; - - let title = track_set.recording.work.get_title(); - let subtitle = track_set.recording.get_performers(); - - let edit_image = gtk::Image::from_icon_name("document-edit-symbolic"); - let edit_button = gtk::Button::new(); - edit_button.set_has_frame(false); - edit_button.set_valign(gtk::Align::Center); - edit_button.set_child(Some(&edit_image)); - - let row = adw::ActionRow::builder() - .focusable(false) - .title(title) - .subtitle(subtitle) - .activatable_widget(&edit_button) - .build(); - - row.add_suffix(&edit_button); - - edit_button.connect_clicked(clone!(@weak this => move |_| { - // TODO: Implement editing. - })); - - row.upcast() - }), - ); - - try_again_button.connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("content"); - })); - - cancel_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - // Initialize, if necessary. - - if let Some(medium) = medium { - this.name_row.set_text(&medium.name); - - let mut track_sets: Vec = Vec::new(); - - for track in medium.tracks { - let track_data = TrackData { - track_source: track.source_index, - work_parts: track.work_parts, - }; - - if let Some(track_set) = track_sets.last_mut() { - if track.recording.id == track_set.recording.id { - track_set.tracks.push(track_data); - continue; - } - } - - track_sets.push(TrackSetData { - recording: track.recording, - tracks: vec![track_data], - }); - } - - let length = track_sets.len(); - this.track_sets.replace(track_sets); - this.track_set_list.update(length); - } - - this.validate(); - - this - } -} - -impl MediumEditor { - /// Validate inputs and enable/disable saving. - fn validate(&self) { - self.done_button.set_sensitive( - !self.name_row.text().is_empty() && !self.track_sets.borrow().is_empty(), - ); - } - - /// Create the medium. - async fn save(&self) -> Result { - // Convert the track set data to real track sets. - - let mut tracks = Vec::new(); - - for track_set_data in &*self.track_sets.borrow() { - for track_data in &track_set_data.tracks { - let track = Track::new( - track_set_data.recording.clone(), - track_data.work_parts.clone(), - track_data.track_source, - String::new(), - ); - - tracks.push(track); - } - } - - let medium = Medium::new( - generate_id(), - self.name_row.text().to_string(), - Some(self.session.source_id().to_owned()), - tracks, - ); - - // The medium is not added to the database, because the track paths are not known until the - // medium is actually imported into the music library. This step will be handled by the - // medium preview dialog. - - Ok(medium) - } -} - -impl Widget for MediumEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/medium_preview.rs b/crates/musicus/src/import/medium_preview.rs deleted file mode 100644 index a2591ab..0000000 --- a/crates/musicus/src/import/medium_preview.rs +++ /dev/null @@ -1,271 +0,0 @@ -use super::medium_editor::MediumEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use anyhow::{anyhow, Result}; -use gettextrs::gettext; -use glib::clone; - -use gtk::prelude::*; -use gtk_macros::get_widget; -use musicus_backend::db::{self, Medium}; -use musicus_backend::import::{ImportSession, State}; -use std::cell::RefCell; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; - -/// A dialog for presenting the selected medium when importing music. -pub struct MediumPreview { - handle: NavigationHandle<()>, - session: Arc, - medium: RefCell>, - widget: gtk::Stack, - import_button: gtk::Button, - done_stack: gtk::Stack, - name_label: gtk::Label, - medium_box: gtk::Box, - status_page: adw::StatusPage, -} - -impl Screen<(Arc, Medium), ()> for MediumPreview { - /// Create a new medium preview screen. - fn new( - (session, medium): (Arc, Medium), - handle: NavigationHandle<()>, - ) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/medium_preview.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, edit_button); - get_widget!(builder, gtk::Button, import_button); - get_widget!(builder, gtk::Stack, done_stack); - get_widget!(builder, gtk::Box, medium_box); - get_widget!(builder, gtk::Label, name_label); - get_widget!(builder, adw::StatusPage, status_page); - get_widget!(builder, gtk::Button, try_again_button); - - let this = Rc::new(Self { - handle, - session, - medium: RefCell::new(None), - widget, - import_button, - done_stack, - name_label, - medium_box, - status_page, - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - edit_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - let old_medium = this.medium.borrow().clone().unwrap(); - if let Some(medium) = push!(this.handle, MediumEditor, (this.session.clone(), Some(old_medium))).await { - this.set_medium(medium); - } - }); - })); - - this.import_button - .connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("loading"); - - spawn!(@clone this, async move { - match this.import().await { - Ok(()) => this.handle.pop(Some(())), - Err(err) => { - this.widget.set_visible_child_name("error"); - this.status_page.set_description(Some(&err.to_string())); - } - } - }); - })); - - try_again_button.connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("content"); - })); - - this.set_medium(medium); - - this.handle_state(&this.session.state()); - spawn!(@clone this, async move { - loop { - let state = this.session.state_change().await; - this.handle_state(&state); - - match state { - State::Ready | State::Error => break, - _ => (), - } - } - }); - - this - } -} - -impl MediumPreview { - /// Set a new medium and update the view accordingly. - fn set_medium(&self, medium: Medium) { - self.name_label.set_text(&medium.name); - - if let Some(widget) = self.medium_box.first_child() { - let mut child = widget; - - loop { - let next_child = child.next_sibling(); - self.medium_box.remove(&child); - - match next_child { - Some(widget) => child = widget, - None => break, - } - } - } - - let mut last_recording_id = ""; - let mut last_list = None::; - - let import_tracks = self.session.tracks(); - - for track in &medium.tracks { - if track.recording.id != last_recording_id { - last_recording_id = &track.recording.id; - - let list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .margin_bottom(12) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let header = adw::ActionRow::builder() - .activatable(false) - .title(track.recording.work.get_title()) - .subtitle(track.recording.get_performers()) - .build(); - - list.append(&header); - - if let Some(list) = &last_list { - self.medium_box.append(list); - } - - last_list = Some(list); - } - - if let Some(list) = &last_list { - let mut parts = Vec::::new(); - for part in &track.work_parts { - parts.push(track.recording.work.parts[*part].title.clone()); - } - - let title = if parts.is_empty() { - gettext("Unknown") - } else { - parts.join(", ") - }; - - let row = adw::ActionRow::builder() - .activatable(false) - .title(title) - .subtitle(&import_tracks[track.source_index].name) - .margin_start(12) - .build(); - - list.append(&row); - } - } - - if let Some(list) = &last_list { - let frame = gtk::Frame::builder().margin_bottom(12).build(); - - frame.set_child(Some(list)); - self.medium_box.append(&frame); - } - - self.medium.replace(Some(medium)); - } - - /// Handle a state change of the import process. - fn handle_state(&self, state: &State) { - match state { - State::Waiting | State::Copying => self.done_stack.set_visible_child_name("loading"), - State::Ready => { - self.done_stack.set_visible_child_name("ready"); - self.import_button.set_sensitive(true); - } - State::Error => todo!("Import error!"), - } - } - - /// Copy the tracks to the music library and add the medium to the database. - async fn import(&self) -> Result<()> { - let medium = self.medium.borrow(); - let medium = medium.as_ref().ok_or_else(|| anyhow!("No medium set!"))?; - - // Create a new directory in the music library path for the imported medium. - - let music_library_path = self.handle.backend.get_music_library_path().unwrap(); - - let directory_name = sanitize_filename::sanitize_with_options( - &medium.name, - sanitize_filename::Options { - windows: true, - truncate: true, - replacement: "", - }, - ); - - let directory = PathBuf::from(&directory_name); - std::fs::create_dir(music_library_path.join(&directory))?; - - // Copy the tracks to the music library. - - let mut tracks = Vec::new(); - let import_tracks = self.session.tracks(); - - for track in &medium.tracks { - let mut track = track.clone(); - - // Set the track path to the new audio file location. - - let import_track = &import_tracks[track.source_index]; - let track_path = directory.join(import_track.path.file_name().unwrap()); - track.path = track_path.to_str().unwrap().to_owned(); - - // Copy the corresponding audio file to the music library. - std::fs::copy(&import_track.path, music_library_path.join(&track_path))?; - - tracks.push(track); - } - - // Add the modified medium to the database. - - let medium = Medium::new( - medium.id.clone(), - medium.name.clone(), - medium.discid.clone(), - tracks, - ); - - db::update_medium(&mut self.handle.backend.db().lock().unwrap(), medium)?; - self.handle.backend.library_changed(); - - Ok(()) - } -} - -impl Widget for MediumPreview { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/mod.rs b/crates/musicus/src/import/mod.rs deleted file mode 100644 index 780eb5a..0000000 --- a/crates/musicus/src/import/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod import_screen; -mod medium_editor; -mod medium_preview; -mod source_selector; -mod track_editor; -mod track_selector; -mod track_set_editor; - -pub use source_selector::SourceSelector; diff --git a/crates/musicus/src/import/source_selector.rs b/crates/musicus/src/import/source_selector.rs deleted file mode 100644 index f2bac41..0000000 --- a/crates/musicus/src/import/source_selector.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::import_screen::ImportScreen; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use musicus_backend::import::ImportSession; -use std::rc::Rc; - -/// A dialog for starting to import music. -pub struct SourceSelector { - handle: NavigationHandle<()>, - widget: gtk::Stack, - status_page: adw::StatusPage, -} - -impl Screen<(), ()> for SourceSelector { - /// Create a new source selector. - fn new(_: (), handle: NavigationHandle<()>) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/source_selector.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, folder_button); - get_widget!(builder, gtk::Button, disc_button); - get_widget!(builder, adw::StatusPage, status_page); - get_widget!(builder, gtk::Button, try_again_button); - - let this = Rc::new(Self { - handle, - widget, - status_page, - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - folder_button.connect_clicked(clone!(@weak this => move |_| { - let dialog = gtk::FileChooserDialog::new( - Some(&gettext("Select folder")), - Some(&this.handle.window), - gtk::FileChooserAction::SelectFolder, - &[ - (&gettext("Cancel"), gtk::ResponseType::Cancel), - (&gettext("Select"), gtk::ResponseType::Accept), - ]); - - dialog.set_modal(true); - - dialog.connect_response(clone!(@weak this => move |dialog, response| { - dialog.hide(); - - if let gtk::ResponseType::Accept = response { - if let Some(file) = dialog.file() { - if let Some(path) = file.path() { - this.widget.set_visible_child_name("loading"); - - spawn!(@clone this, async move { - match ImportSession::folder(path).await { - Ok(session) => { - let result = push!(this.handle, ImportScreen, session).await; - this.handle.pop(result); - } - Err(err) => { - this.status_page.set_description(Some(&err.to_string())); - this.widget.set_visible_child_name("error"); - } - } - }); - } - } - } - })); - - dialog.show(); - })); - - disc_button.connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("loading"); - - spawn!(@clone this, async move { - match ImportSession::audio_cd().await { - Ok(session) => { - let result = push!(this.handle, ImportScreen, session).await; - this.handle.pop(result); - } - Err(err) => { - this.status_page.set_description(Some(&err.to_string())); - this.widget.set_visible_child_name("error"); - } - } - }); - })); - - try_again_button.connect_clicked(clone!(@weak this => move |_| { - this.widget.set_visible_child_name("content"); - })); - - this - } -} - -impl Widget for SourceSelector { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/track_editor.rs b/crates/musicus/src/import/track_editor.rs deleted file mode 100644 index 678a243..0000000 --- a/crates/musicus/src/import/track_editor.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use glib::clone; - -use gtk_macros::get_widget; -use musicus_backend::db::Recording; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen for editing a single track. -pub struct TrackEditor { - handle: NavigationHandle>, - widget: gtk::Box, - selection: RefCell>, -} - -impl Screen<(Recording, Vec), Vec> for TrackEditor { - /// Create a new track editor. - fn new( - (recording, selection): (Recording, Vec), - handle: NavigationHandle>, - ) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, adw::Clamp, clamp); - - let parts_list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - clamp.set_child(Some(&parts_list)); - - let this = Rc::new(Self { - handle, - widget, - selection: RefCell::new(selection), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - select_button.connect_clicked(clone!(@weak this => move |_| { - let selection = this.selection.borrow().clone(); - this.handle.pop(Some(selection)); - })); - - for (index, part) in recording.work.parts.iter().enumerate() { - let check = gtk::CheckButton::new(); - check.set_active(this.selection.borrow().contains(&index)); - - check.connect_toggled(clone!(@weak this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.is_active() { - selection.push(index); - } else if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - })); - - let row = adw::ActionRow::builder() - .focusable(false) - .title(&part.title) - .activatable_widget(&check) - .build(); - - row.add_prefix(&check); - parts_list.append(&row); - } - - this - } -} - -impl Widget for TrackEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/track_selector.rs b/crates/musicus/src/import/track_selector.rs deleted file mode 100644 index 8a3c4fd..0000000 --- a/crates/musicus/src/import/track_selector.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use glib::clone; - -use gtk_macros::get_widget; -use musicus_backend::import::ImportSession; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::Arc; - -/// A screen for selecting tracks from a source. -pub struct TrackSelector { - handle: NavigationHandle>, - session: Arc, - widget: gtk::Box, - select_button: gtk::Button, - selection: RefCell>, -} - -impl Screen, Vec> for TrackSelector { - /// Create a new track selector. - fn new(session: Arc, handle: NavigationHandle>) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, adw::Clamp, clamp); - - let track_list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - clamp.set_child(Some(&track_list)); - - let this = Rc::new(Self { - handle, - session, - widget, - select_button, - selection: RefCell::new(Vec::new()), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.select_button - .connect_clicked(clone!(@weak this => move |_| { - let selection = this.selection.borrow().clone(); - this.handle.pop(Some(selection)); - })); - - let tracks = this.session.tracks(); - - for (index, track) in tracks.iter().enumerate() { - let check = gtk::CheckButton::new(); - - check.connect_toggled(clone!(@weak this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.is_active() { - selection.push(index); - } else if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - - if selection.is_empty() { - this.select_button.set_sensitive(false); - } else { - this.select_button.set_sensitive(true); - } - })); - - let row = adw::ActionRow::builder() - .focusable(false) - .title(&track.name) - .activatable_widget(&check) - .build(); - - row.add_prefix(&check); - - track_list.append(&row); - } - - this - } -} - -impl Widget for TrackSelector { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/import/track_set_editor.rs b/crates/musicus/src/import/track_set_editor.rs deleted file mode 100644 index 526d626..0000000 --- a/crates/musicus/src/import/track_set_editor.rs +++ /dev/null @@ -1,233 +0,0 @@ -use super::track_editor::TrackEditor; -use super::track_selector::TrackSelector; -use crate::navigator::{NavigationHandle, Screen}; -use crate::selectors::RecordingSelector; -use crate::widgets::{List, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::Recording; -use musicus_backend::import::ImportSession; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::Arc; - -/// A track set before being imported. -#[derive(Clone, Debug)] -pub struct TrackSetData { - pub recording: Recording, - pub tracks: Vec, -} - -/// A track before being imported. -#[derive(Clone, Debug)] -pub struct TrackData { - /// Index of the track source within the medium source's tracks. - pub track_source: usize, - - /// Actual track data. - pub work_parts: Vec, -} - -/// A screen for editing a set of tracks for one recording. -pub struct TrackSetEditor { - handle: NavigationHandle, - session: Arc, - widget: gtk::Box, - save_button: gtk::Button, - recording_row: adw::ActionRow, - track_list: Rc, - recording: RefCell>, - tracks: RefCell>, -} - -impl Screen, TrackSetData> for TrackSetEditor { - /// Create a new track set editor. - fn new(session: Arc, handle: NavigationHandle) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, adw::ActionRow, recording_row); - get_widget!(builder, gtk::Button, select_recording_button); - get_widget!(builder, gtk::Button, edit_tracks_button); - get_widget!(builder, gtk::Frame, tracks_frame); - - let track_list = List::new(); - tracks_frame.set_child(Some(&track_list.widget)); - - let this = Rc::new(Self { - handle, - session, - widget, - save_button, - recording_row, - track_list, - recording: RefCell::new(None), - tracks: RefCell::new(Vec::new()), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.save_button - .connect_clicked(clone!(@weak this => move |_| { - let data = TrackSetData { - recording: this.recording.borrow().clone().unwrap(), - tracks: this.tracks.borrow().clone(), - }; - - this.handle.pop(Some(data)); - })); - - select_recording_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(recording) = push!(this.handle, RecordingSelector).await { - this.recording.replace(Some(recording)); - this.recording_selected(); - } - }); - })); - - edit_tracks_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - if let Some(selection) = push!(this.handle, TrackSelector, Arc::clone(&this.session)).await { - let mut tracks = Vec::new(); - - for index in selection { - let data = TrackData { - track_source: index, - work_parts: Vec::new(), - }; - - tracks.push(data); - } - - let length = tracks.len(); - this.tracks.replace(tracks); - this.track_list.update(length); - this.autofill_parts(); - } - }); - })); - - this.track_list.set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let track = &this.tracks.borrow()[index]; - - let mut title_parts = Vec::::new(); - - if let Some(recording) = &*this.recording.borrow() { - for part in &track.work_parts { - title_parts.push(recording.work.parts[*part].title.clone()); - } - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let tracks = this.session.tracks(); - let track_name = &tracks[track.track_source].name; - - let edit_image = gtk::Image::from_icon_name("document-edit-symbolic"); - let edit_button = gtk::Button::new(); - edit_button.set_has_frame(false); - edit_button.set_valign(gtk::Align::Center); - edit_button.set_child(Some(&edit_image)); - - let row = adw::ActionRow::builder() - .focusable(false) - .title(title) - .subtitle(track_name) - .activatable_widget(&edit_button) - .build(); - - row.add_suffix(&edit_button); - - edit_button.connect_clicked(clone!(@weak this => move |_| { - let recording = this.recording.borrow().clone(); - if let Some(recording) = recording { - spawn!(@clone this, async move { - let work_parts = this.tracks.borrow()[index].work_parts.clone(); - if let Some(selection) = push!(this.handle, TrackEditor, (recording, work_parts)).await { - { - let mut tracks = this.tracks.borrow_mut(); - let mut track = &mut tracks[index]; - track.work_parts = selection; - }; - - this.update_tracks(); - } - }); - } - })); - - row.upcast() - })); - - this.validate(); - - this - } -} - -impl TrackSetEditor { - /// Set everything up after selecting a recording. - fn recording_selected(&self) { - if let Some(recording) = &*self.recording.borrow() { - self.recording_row.set_title(&recording.work.get_title()); - self.recording_row.set_subtitle(&recording.get_performers()); - self.save_button.set_sensitive(true); - } - - // This will also call validate(). - self.autofill_parts(); - } - - /// Automatically try to put work part information from the selected recording into the - /// selected tracks. - fn autofill_parts(&self) { - if let Some(recording) = &*self.recording.borrow() { - let mut tracks = self.tracks.borrow_mut(); - - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut track) = tracks.get_mut(index) { - track.work_parts = vec![index]; - } else { - break; - } - } - } - - self.update_tracks(); - } - - /// Update the track list. - fn update_tracks(&self) { - let length = self.tracks.borrow().len(); - self.track_list.update(length); - self.validate(); - } - - /// Validate data and allow saving if possible. - fn validate(&self) { - self.save_button - .set_sensitive(self.recording.borrow().is_some() && !self.tracks.borrow().is_empty()); - } -} - -impl Widget for TrackSetEditor { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/macros.rs b/crates/musicus/src/macros.rs deleted file mode 100644 index 3a1af6c..0000000 --- a/crates/musicus/src/macros.rs +++ /dev/null @@ -1,80 +0,0 @@ -/// Simplification for pushing new screens. -/// -/// This macro can be invoked in two forms. -/// -/// 1. To push screens without an input value: -/// -/// ``` -/// let result = push!(handle, ScreenType).await; -/// ``` -/// -/// 2. To push screens with an input value: -/// -/// ``` -/// let result = push!(handle, ScreenType, input).await; -/// ``` -#[macro_export] -macro_rules! push { - ($handle:expr, $screen:ty) => { - $handle.push::<_, _, $screen>(()) - }; - ($handle:expr, $screen:ty, $input:expr) => { - $handle.push::<_, _, $screen>($input) - }; -} - -/// Simplification for replacing the current navigator screen. -/// -/// This macro can be invoked in two forms. -/// -/// 1. To replace with screens without an input value: -/// -/// ``` -/// let result = replace!(navigator, ScreenType).await; -/// ``` -/// -/// 2. To replace with screens with an input value: -/// -/// ``` -/// let result = replace!(navigator, ScreenType, input).await; -/// ``` -#[macro_export] -macro_rules! replace { - ($navigator:expr, $screen:ty) => { - $navigator.replace::<_, _, $screen>(()) - }; - ($navigator:expr, $screen:ty, $input:expr) => { - $navigator.replace::<_, _, $screen>($input) - }; -} - -/// Spawn a future on the GLib MainContext. -/// -/// This can be invoked in the following forms: -/// -/// 1. For spawning a future and nothing more: -/// -/// ``` -/// spawn!(async { -/// // Some code -/// }); -/// -/// 2. For spawning a future and cloning some data, that will be accessible -/// from the async code: -/// -/// ``` -/// spawn!(@clone data: Rc<_>, async move { -/// // Some code -/// }); -#[macro_export] -macro_rules! spawn { - ($future:expr) => {{ - let context = glib::MainContext::default(); - context.spawn_local($future); - }}; - (@clone $data:ident, $future:expr) => {{ - let context = glib::MainContext::default(); - let $data = Rc::clone(&$data); - context.spawn_local($future); - }}; -} diff --git a/crates/musicus/src/main.rs b/crates/musicus/src/main.rs deleted file mode 100644 index 4cdcaa5..0000000 --- a/crates/musicus/src/main.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gio::prelude::*; -use glib::clone; -use std::cell::RefCell; -use std::rc::Rc; - -#[macro_use] -mod macros; - -mod config; -mod editors; -mod import; -mod navigator; -mod preferences; -mod screens; -mod selectors; -mod widgets; - -mod window; -use window::Window; - -mod resources; - -fn main() { - gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, ""); - gettextrs::bindtextdomain("musicus", config::LOCALEDIR).unwrap(); - gettextrs::textdomain("musicus").unwrap(); - - gstreamer::init().expect("Failed to initialize GStreamer!"); - adw::init().expect("Failed to initialize libadwaita!"); - resources::init().expect("Failed to initialize resources!"); - - let app = gtk::Application::new(Some("de.johrpan.musicus"), gio::ApplicationFlags::empty()); - let window: RefCell>> = RefCell::new(None); - - app.connect_activate(clone!(@strong app => move |_| { - let mut window = window.borrow_mut(); - if window.is_none() { - window.replace(Window::new(&app)); - } - window.as_ref().unwrap().present(); - })); - - app.run(); -} diff --git a/crates/musicus/src/meson.build b/crates/musicus/src/meson.build deleted file mode 100644 index 62d84f7..0000000 --- a/crates/musicus/src/meson.build +++ /dev/null @@ -1,66 +0,0 @@ -prefix = get_option('prefix') -localedir = join_paths(prefix, get_option('localedir')) - -global_conf = configuration_data() -global_conf.set_quoted('LOCALEDIR', localedir) -global_conf.set_quoted('VERSION', meson.project_version()) -config_rs = configure_file( - input: 'config.rs.in', - output: 'config.rs', - configuration: global_conf -) - -run_command( - 'cp', - config_rs, - meson.current_source_dir(), - check: true -) - -resource_conf = configuration_data() -resource_conf.set_quoted('RESOURCEFILE', resources.full_path()) -resource_rs = configure_file( - input: 'resources.rs.in', - output: 'resources.rs', - configuration: resource_conf -) - -run_command( - 'cp', - resource_rs, - meson.current_source_dir(), - check: true -) - -sources = files( - 'config.rs', - 'resources.rs', -) - -system = host_machine.system() -if system == 'windows' - output = meson.project_name() + '.exe' -else - output = meson.project_name() -endif - -cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.sh')) -cargo_release = custom_target( - 'cargo-build', - build_by_default: true, - input: sources, - build_always_stale: true, - depends: resources, - output: output, - console: true, - install: true, - install_dir: get_option('bindir'), - command: [ - cargo_script, - meson.build_root(), - meson.source_root(), - '@OUTPUT@', - get_option('buildtype'), - output, - ] -) diff --git a/crates/musicus/src/navigator/mod.rs b/crates/musicus/src/navigator/mod.rs deleted file mode 100644 index b59b45d..0000000 --- a/crates/musicus/src/navigator/mod.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::widgets::Widget; -use futures_channel::oneshot; -use futures_channel::oneshot::{Receiver, Sender}; -use glib::clone; - -use gtk::prelude::*; -use musicus_backend::Backend; -use std::cell::{Cell, RefCell}; -use std::rc::{Rc, Weak}; - -pub mod window; -pub use window::*; - -/// A widget that represents a logical unit of transient user interaction and -/// that optionally resolves to a specific return value. -pub trait Screen: Widget { - /// Create a new screen and initialize it with the provided input value. - fn new(input: I, navigation_handle: NavigationHandle) -> Rc - where - Self: Sized; -} - -/// An accessor to navigation functionality for screens. -pub struct NavigationHandle { - /// The backend, in case the screen needs it. - pub backend: Rc, - - /// The toplevel window, in case the screen needs it. - pub window: gtk::Window, - - /// The navigator that created this navigation handle. - navigator: Weak, - - /// The sender through which the result should be sent. - sender: Cell>>>, -} - -impl NavigationHandle { - /// Switch to another screen and wait for that screen's result. - pub async fn push + 'static>(&self, input: I) -> Option { - let navigator = self.unwrap_navigator(); - let receiver = navigator.push::(input); - - // If the sender is dropped, return None. - receiver.await.unwrap_or(None) - } - - /// Go back to the previous screen optionally returning something. - pub fn pop(&self, output: Option) { - self.unwrap_navigator().pop(); - - let sender = self - .sender - .take() - .expect("Tried to send result from screen through a dropped sender."); - - if sender.send(output).is_err() { - panic!("Tried to send result from screen to non-existing previous screen."); - } - } - - /// Get the navigator and panic if it doesn't exist. - fn unwrap_navigator(&self) -> Rc { - Weak::upgrade(&self.navigator) - .expect("Tried to access non-existing navigator from a screen.") - } -} - -/// A toplevel widget for managing screens. -pub struct Navigator { - /// The underlying GTK widget. - pub widget: gtk::Stack, - - /// The backend, in case screens need it. - backend: Rc, - - /// The toplevel window of the navigator, in case screens need it. - window: gtk::Window, - - /// The currently active screens. The last screen in this vector is the one - /// that is currently visible. - screens: RefCell>>, - - /// A vector holding the widgets of the old screens that are waiting to be - /// removed after the animation has finished. - old_widgets: RefCell>, - - /// A closure that will be called when the last screen is popped. - back_cb: RefCell>>, -} - -impl Navigator { - /// Create a new navigator which will display the provided widget - /// initially. - pub fn new(backend: Rc, window: &W, empty_screen: &E) -> Rc - where - W: IsA, - E: IsA, - { - let widget = gtk::Stack::builder() - .hhomogeneous(false) - .vhomogeneous(false) - .interpolate_size(true) - .transition_type(gtk::StackTransitionType::Crossfade) - .hexpand(true) - .vexpand(true) - .build(); - - widget.add_named(empty_screen, Some("empty_screen")); - - let this = Rc::new(Self { - widget, - backend, - window: window.to_owned().upcast(), - screens: RefCell::new(Vec::new()), - old_widgets: RefCell::new(Vec::new()), - back_cb: RefCell::new(None), - }); - - this.widget - .connect_transition_running_notify(clone!(@strong this => move |_| { - if !this.widget.is_transition_running() { - this.clear_old_widgets(); - } - })); - - this - } - - /// Set the closure to be called when the last screen is popped so that - /// the navigator shows its empty state. - pub fn set_back_cb(&self, cb: F) { - self.back_cb.replace(Some(Box::new(cb))); - } - - /// Drop all screens and show the provided screen instead. - pub async fn replace + 'static>(self: &Rc, input: I) -> Option { - for screen in self.screens.replace(Vec::new()) { - self.old_widgets.borrow_mut().push(screen.get_widget()); - } - - let receiver = self.push::(input); - - if !self.widget.is_transition_running() { - self.clear_old_widgets(); - } - - // We ignore the case, if a sender is dropped. - receiver.await.unwrap_or(None) - } - - /// Drop all screens and go back to the initial screen. The back callback - /// will not be called. - pub fn reset(&self) { - self.widget.set_visible_child_name("empty_screen"); - - for screen in self.screens.replace(Vec::new()) { - self.old_widgets.borrow_mut().push(screen.get_widget()); - } - - if !self.widget.is_transition_running() { - self.clear_old_widgets(); - } - } - - /// Show a screen with the provided input. This should only be called from - /// within a navigation handle. - fn push + 'static>(self: &Rc, input: I) -> Receiver> { - let (sender, receiver) = oneshot::channel(); - - let handle = NavigationHandle { - backend: Rc::clone(&self.backend), - window: self.window.clone(), - navigator: Rc::downgrade(self), - sender: Cell::new(Some(sender)), - }; - - let screen = S::new(input, handle); - - let widget = screen.get_widget(); - self.widget.add_child(&widget); - self.widget.set_visible_child(&widget); - - self.screens.borrow_mut().push(screen); - - receiver - } - - /// Pop the last screen from the list of screens. - fn pop(&self) { - let popped = if let Some(screen) = self.screens.borrow_mut().pop() { - let widget = screen.get_widget(); - self.old_widgets.borrow_mut().push(widget); - true - } else { - false - }; - - if popped { - if let Some(screen) = self.screens.borrow().last() { - let widget = screen.get_widget(); - self.widget.set_visible_child(&widget); - } else { - self.widget.set_visible_child_name("empty_screen"); - - if let Some(cb) = &*self.back_cb.borrow() { - cb() - } - } - - if !self.widget.is_transition_running() { - self.clear_old_widgets(); - } - } - } - - /// Drop the old widgets. - fn clear_old_widgets(&self) { - for widget in self.old_widgets.borrow().iter() { - self.widget.remove(widget); - } - - self.old_widgets.borrow_mut().clear(); - } -} diff --git a/crates/musicus/src/navigator/window.rs b/crates/musicus/src/navigator/window.rs deleted file mode 100644 index 83a13ce..0000000 --- a/crates/musicus/src/navigator/window.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::Navigator; -use adw::prelude::*; -use glib::clone; -use musicus_backend::Backend; -use std::rc::Rc; - -/// A window hosting a navigator. -pub struct NavigatorWindow { - pub navigator: Rc, - window: adw::Window, -} - -impl NavigatorWindow { - /// Create a new navigator window and show it. - pub fn new(backend: Rc) -> Rc { - let window = adw::Window::new(); - window.set_default_size(600, 424); - let placeholder = gtk::Label::new(None); - let navigator = Navigator::new(backend, &window, &placeholder); - window.set_content(Some(&navigator.widget)); - - let this = Rc::new(Self { navigator, window }); - - this.navigator.set_back_cb(clone!(@strong this => move || { - this.window.close(); - })); - - this.window.show(); - - this - } -} diff --git a/crates/musicus/src/preferences.rs b/crates/musicus/src/preferences.rs deleted file mode 100644 index 2fed422..0000000 --- a/crates/musicus/src/preferences.rs +++ /dev/null @@ -1,90 +0,0 @@ -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::Backend; -use std::rc::Rc; - -/// A dialog for configuring the app. -pub struct Preferences { - backend: Rc, - window: adw::Window, - music_library_path_row: adw::ActionRow, -} - -impl Preferences { - /// Create a new preferences dialog. - pub fn new>(backend: Rc, parent: &P) -> Rc { - // Create UI - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/preferences.ui"); - - get_widget!(builder, adw::Window, window); - get_widget!(builder, adw::ActionRow, music_library_path_row); - get_widget!(builder, gtk::Button, select_music_library_path_button); - get_widget!(builder, gtk::Switch, keep_playing_switch); - get_widget!(builder, gtk::Switch, play_full_recordings_switch); - - window.set_transient_for(Some(parent)); - - let this = Rc::new(Self { - backend, - window, - music_library_path_row, - }); - - // Connect signals and callbacks - - select_music_library_path_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = gtk::FileChooserDialog::new( - Some(&gettext("Select music library folder")), - Some(&this.window), - gtk::FileChooserAction::SelectFolder, - &[ - (&gettext("Cancel"), gtk::ResponseType::Cancel), - (&gettext("Select"), gtk::ResponseType::Accept), - ]); - - dialog.set_modal(true); - - dialog.connect_response(clone!(@strong this => move |dialog, response| { - if let gtk::ResponseType::Accept = response { - if let Some(file) = dialog.file() { - if let Some(path) = file.path() { - Rc::clone(&this.backend).set_music_library_path(path.clone()).unwrap(); - this.music_library_path_row.set_subtitle(path.to_str().unwrap()); - } - } - } - - dialog.hide(); - })); - - dialog.show(); - })); - - keep_playing_switch.connect_active_notify(clone!(@weak this => move |switch| { - Rc::clone(&this.backend).set_keep_playing(switch.is_active()); - })); - - play_full_recordings_switch.connect_active_notify(clone!(@weak this => move |switch| { - Rc::clone(&this.backend).set_play_full_recordings(switch.is_active()); - })); - - // Initialize - - if let Some(path) = this.backend.get_music_library_path() { - this.music_library_path_row - .set_subtitle(path.to_str().unwrap()); - } - - keep_playing_switch.set_active(this.backend.keep_playing()); - play_full_recordings_switch.set_active(this.backend.play_full_recordings()); - - this - } - - /// Show the preferences dialog. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/crates/musicus/src/resources.rs.in b/crates/musicus/src/resources.rs.in deleted file mode 100644 index 6ece05e..0000000 --- a/crates/musicus/src/resources.rs.in +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::Result; - -pub fn init() -> Result<()> { - let bytes = glib::Bytes::from(include_bytes!(@RESOURCEFILE@).as_ref()); - let resource = gio::Resource::from_data(&bytes)?; - gio::resources_register(&resource); - - Ok(()) -} diff --git a/crates/musicus/src/screens/ensemble.rs b/crates/musicus/src/screens/ensemble.rs deleted file mode 100644 index f8df788..0000000 --- a/crates/musicus/src/screens/ensemble.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::{MediumScreen, RecordingScreen}; -use crate::editors::EnsembleEditor; -use crate::navigator::{NavigationHandle, NavigatorWindow, Screen}; -use crate::widgets; -use crate::widgets::{List, Section, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use musicus_backend::db::{self, Ensemble, Medium, Recording}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen for showing recordings with a ensemble. -pub struct EnsembleScreen { - handle: NavigationHandle<()>, - ensemble: Ensemble, - widget: widgets::Screen, - recording_list: Rc, - medium_list: Rc, - recordings: RefCell>, - mediums: RefCell>, -} - -impl Screen for EnsembleScreen { - /// Create a new ensemble screen for the specified ensemble and load the - /// contents asynchronously. - fn new(ensemble: Ensemble, handle: NavigationHandle<()>) -> Rc { - let widget = widgets::Screen::new(); - widget.set_title(&ensemble.name); - - let recording_list = List::new(); - let medium_list = List::new(); - - let this = Rc::new(Self { - handle, - ensemble, - widget, - recording_list, - medium_list, - recordings: RefCell::new(Vec::new()), - mediums: RefCell::new(Vec::new()), - }); - - this.widget.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.widget.add_action( - &gettext("Edit ensemble"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - let window = NavigatorWindow::new(this.handle.backend.clone()); - replace!(window.navigator, EnsembleEditor, Some(this.ensemble.clone())).await; - }); - }), - ); - - this.widget.add_action( - &gettext("Delete ensemble"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - db::delete_ensemble(&mut this.handle.backend.db().lock().unwrap(), &this.ensemble.id).unwrap(); - this.handle.backend.library_changed(); - }); - }), - ); - - this.widget.set_search_cb(clone!(@weak this => move || { - this.recording_list.invalidate_filter(); - this.medium_list.invalidate_filter(); - })); - - this.recording_list.set_make_widget_cb( - clone!(@weak this => @default-panic, move |index| { - let recording = &this.recordings.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(recording.work.get_title()) - .subtitle(recording.get_performers()) - .build(); - - let recording = recording.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let recording = recording.clone(); - spawn!(@clone this, async move { - push!(this.handle, RecordingScreen, recording.clone()).await; - }); - })); - - row.upcast() - }), - ); - - this.recording_list - .set_filter_cb(clone!(@weak this => @default-panic, move |index| { - let recording = &this.recordings.borrow()[index]; - let search = this.widget.get_search(); - let text = recording.work.get_title() + &recording.get_performers(); - search.is_empty() || text.to_lowercase().contains(&search) - })); - - this.medium_list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let medium = &this.mediums.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(&medium.name) - .build(); - - let medium = medium.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let medium = medium.clone(); - spawn!(@clone this, async move { - push!(this.handle, MediumScreen, medium.clone()).await; - }); - })); - - row.upcast() - })); - - this.medium_list - .set_filter_cb(clone!(@weak this => @default-panic, move |index| { - let medium = &this.mediums.borrow()[index]; - let search = this.widget.get_search(); - let name = medium.name.to_lowercase(); - search.is_empty() || name.contains(&search) - })); - - // Load the content. - - let recordings = db::get_recordings_for_ensemble( - &mut this.handle.backend.db().lock().unwrap(), - &this.ensemble.id, - ) - .unwrap(); - - let mediums = db::get_mediums_for_ensemble( - &mut this.handle.backend.db().lock().unwrap(), - &this.ensemble.id, - ) - .unwrap(); - - if !recordings.is_empty() { - let length = recordings.len(); - this.recordings.replace(recordings); - this.recording_list.update(length); - - let section = Section::new("Recordings", &this.recording_list.widget); - this.widget.add_content(§ion.widget); - } - - if !mediums.is_empty() { - let length = mediums.len(); - this.mediums.replace(mediums); - this.medium_list.update(length); - - let section = Section::new("Mediums", &this.medium_list.widget); - this.widget.add_content(§ion.widget); - } - - this.widget.ready(); - - this - } -} - -impl Widget for EnsembleScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/main.rs b/crates/musicus/src/screens/main.rs deleted file mode 100644 index c8dbd70..0000000 --- a/crates/musicus/src/screens/main.rs +++ /dev/null @@ -1,284 +0,0 @@ -use super::{EnsembleScreen, PersonScreen, PlayerScreen}; -use crate::config; -use crate::import::SourceSelector; -use crate::navigator::{NavigationHandle, Navigator, NavigatorWindow, Screen}; -use crate::preferences::Preferences; -use crate::widgets::{List, PlayerBar, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use gtk_macros::get_widget; -use musicus_backend::db::{self, PersonOrEnsemble}; -use std::cell::RefCell; -use std::rc::Rc; - -/// The main screen of the app, once it's set up and finished loading. The screen assumes that the -/// music library and the player are available and initialized. -pub struct MainScreen { - handle: NavigationHandle<()>, - widget: gtk::Box, - leaflet: adw::Leaflet, - search_entry: gtk::SearchEntry, - stack: gtk::Stack, - poe_list: Rc, - navigator: Rc, - poes: RefCell>, -} - -impl Screen<(), ()> for MainScreen { - /// Create a new main screen. - fn new(_: (), handle: NavigationHandle<()>) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/main_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, adw::Leaflet, leaflet); - get_widget!(builder, gtk::Revealer, play_button_revealer); - get_widget!(builder, gtk::Button, play_button); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Box, empty_screen); - - let actions = gio::SimpleActionGroup::new(); - let preferences_action = gio::SimpleAction::new("preferences", None); - let log_action = gio::SimpleAction::new("log", None); - let about_action = gio::SimpleAction::new("about", None); - actions.add_action(&preferences_action); - actions.add_action(&log_action); - actions.add_action(&about_action); - widget.insert_action_group("widget", Some(&actions)); - - let poe_list = List::new(); - poe_list.widget.set_css_classes(&["navigation-sidebar"]); - poe_list.enable_selection(); - - let navigator = Navigator::new(Rc::clone(&handle.backend), &handle.window, &empty_screen); - - scroll.set_child(Some(&poe_list.widget)); - leaflet.append(&navigator.widget); - - let player_bar = PlayerBar::new(); - widget.append(&player_bar.widget); - player_bar.set_player(Some(Rc::clone(&handle.backend.pl()))); - - let this = Rc::new(Self { - handle, - widget, - leaflet, - search_entry, - stack, - poe_list, - navigator, - poes: RefCell::new(Vec::new()), - }); - - preferences_action.connect_activate(clone!(@weak this => move |_, _| { - Preferences::new(Rc::clone(&this.handle.backend), &this.handle.window).show(); - })); - - log_action.connect_activate(clone!(@weak this => move |_, _| { - this.show_log_window(); - })); - - about_action.connect_activate(clone!(@weak this => move |_, _| { - this.show_about_dialog(); - })); - - add_button.connect_clicked(clone!(@weak this => move |_| { - spawn!(@clone this, async move { - let window = NavigatorWindow::new(Rc::clone(&this.handle.backend)); - replace!(window.navigator, SourceSelector).await; - }); - })); - - this.search_entry - .connect_search_changed(clone!(@weak this => move |_| { - this.poe_list.invalidate_filter(); - })); - - this.poe_list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let poe = &this.poes.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(poe.get_title()) - .build(); - - let poe = poe.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let poe = poe.clone(); - spawn!(@clone this, async move { - this.leaflet.set_visible_child(&this.navigator.widget); - - match poe { - PersonOrEnsemble::Person(person) => { - replace!(this.navigator, PersonScreen, person).await; - } - PersonOrEnsemble::Ensemble(ensemble) => { - replace!(this.navigator, EnsembleScreen, ensemble).await; - } - } - }); - })); - - row.upcast() - })); - - this.poe_list - .set_filter_cb(clone!(@weak this => @default-panic, move |index| { - let poe = &this.poes.borrow()[index]; - let search = this.search_entry.text().to_string().to_lowercase(); - let title = poe.get_title().to_lowercase(); - search.is_empty() || title.contains(&search) - })); - - this.handle.backend.pl().add_playlist_cb( - clone!(@weak play_button_revealer => move |new_playlist| { - play_button_revealer.set_reveal_child(new_playlist.is_empty()); - }), - ); - - play_button.connect_clicked(clone!(@weak this => move |_| { - let recording = db::random_recording(&mut this.handle.backend.db().lock().unwrap()); - if let Ok(recording) = recording { - this.handle.backend.pl().add_items(db::get_tracks(&mut this.handle.backend.db().lock().unwrap(), &recording.id).unwrap()).unwrap(); - } - })); - - this.navigator.set_back_cb(clone!(@weak this => move || { - this.leaflet.set_visible_child_name("sidebar"); - })); - - player_bar.set_playlist_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - push!(this.handle, PlayerScreen).await; - }); - })); - - // Load the content whenever there is a new library update. - spawn!(@clone this, async move { - loop { - this.navigator.reset(); - - let mut poes = Vec::new(); - - let persons = db::get_persons(&mut this.handle.backend.db().lock().unwrap(), ).unwrap(); - let ensembles = db::get_ensembles(&mut this.handle.backend.db().lock().unwrap(), ).unwrap(); - - for person in persons { - poes.push(PersonOrEnsemble::Person(person)); - } - - for ensemble in ensembles { - poes.push(PersonOrEnsemble::Ensemble(ensemble)); - } - - let length = poes.len(); - this.poes.replace(poes); - this.poe_list.update(length); - - this.stack.set_visible_child_name("content"); - - if this.handle.backend.library_update().await.is_err() { - break; - } - } - }); - - this - } -} - -impl Widget for MainScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} - -impl MainScreen { - /// Show a window displaying all currently cached log messages. - fn show_log_window(&self) { - let copy_button = gtk::Button::builder().icon_name("copy-symbolic").build(); - let logger = self.handle.backend.logger(); - let toast_overlay = adw::ToastOverlay::new(); - - copy_button.connect_clicked(clone!(@weak logger, @weak toast_overlay => move |widget| { - widget.clipboard().set_text(&logger.messages().into_iter().map(|m| m.to_string()).collect::>().join("\n")); - toast_overlay.add_toast(adw::Toast::builder().title(gettext("Copied to clipboard")).build()); - })); - - let header = adw::HeaderBar::builder() - .title_widget( - &adw::WindowTitle::builder() - .title(gettext("Debug log")) - .build(), - ) - .build(); - - header.pack_end(©_button); - - let log_list = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .build(); - - for message in logger.messages() { - log_list.append( - &adw::ActionRow::builder() - .title(format!( - "{} {} {}", - message.level, - message.time.format("%Y-%m-%d %H:%M:%S"), - message.module - )) - .subtitle(&message.message) - .build(), - ); - } - - let content = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - content.append(&header); - content.append( - >k::ScrolledWindow::builder() - .vexpand(true) - .child(&log_list) - .build(), - ); - - toast_overlay.set_child(Some(&content)); - - adw::Window::builder() - .transient_for(&self.handle.window) - .modal(true) - .title(gettext("Debug log")) - .default_width(640) - .default_height(480) - .content(&toast_overlay) - .build() - .show(); - } - - /// Show a dialog with information on this application. - fn show_about_dialog(&self) { - let dialog = adw::AboutWindow::builder() - .transient_for(&self.handle.window) - .modal(true) - .application_icon("de.johrpan.musicus") - .application_name(gettext("Musicus")) - .developer_name("Elias Projahn") - .version(config::VERSION) - .comments(gettext("The classical music player and organizer.")) - .website("https://code.johrpan.de/johrpan/musicus") - .developers(vec![String::from("Elias Projahn ")]) - .copyright("© 2022 Elias Projahn") - .license_type(gtk::License::Agpl30) - .build(); - - dialog.show(); - } -} diff --git a/crates/musicus/src/screens/medium.rs b/crates/musicus/src/screens/medium.rs deleted file mode 100644 index 98567de..0000000 --- a/crates/musicus/src/screens/medium.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets; -use crate::widgets::{List, Section, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use musicus_backend::db::Medium; -use std::rc::Rc; - -/// A screen for showing the contents of a medium. -pub struct MediumScreen { - handle: NavigationHandle<()>, - medium: Medium, - widget: widgets::Screen, - list: Rc, -} - -impl Screen for MediumScreen { - /// Create a new medium screen for the specified medium and load the - /// contents asynchronously. - fn new(medium: Medium, handle: NavigationHandle<()>) -> Rc { - let widget = widgets::Screen::new(); - widget.set_title(&medium.name); - - let list = List::new(); - let section = Section::new("Recordings", &list.widget); - widget.add_content(§ion.widget); - widget.ready(); - - let this = Rc::new(Self { - handle, - medium, - widget, - list, - }); - - this.widget.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.widget.add_action( - &gettext("Edit medium"), - clone!(@weak this => move || { - // TODO: Show medium editor. - }), - ); - - this.widget.add_action( - &gettext("Delete medium"), - clone!(@weak this => move || { - // TODO: Delete medium and maybe also the tracks? - }), - ); - - section.add_action( - "media-playback-start-symbolic", - clone!(@weak this => move || { - this.handle.backend.pl().add_items(this.medium.tracks.clone()).unwrap(); - }), - ); - - this.list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let track = &this.medium.tracks[index]; - - let mut parts = Vec::::new(); - for part in &track.work_parts { - parts.push(track.recording.work.parts[*part].title.clone()); - } - - let title = if parts.is_empty() { - gettext("Unknown") - } else { - parts.join(", ") - }; - - let row = adw::ActionRow::builder() - .margin_start(12) - .selectable(false) - .title(title) - .build(); - - row.upcast() - })); - - this.list.update(this.medium.tracks.len()); - - this - } -} - -impl Widget for MediumScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/mod.rs b/crates/musicus/src/screens/mod.rs deleted file mode 100644 index f3922ab..0000000 --- a/crates/musicus/src/screens/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod ensemble; -pub use ensemble::*; - -pub mod main; -pub use main::*; - -pub mod medium; -pub use medium::*; - -pub mod person; -pub use person::*; - -pub mod player; -pub use player::*; - -pub mod work; -pub use work::*; - -pub mod welcome; -pub use welcome::*; - -pub mod recording; -pub use recording::*; diff --git a/crates/musicus/src/screens/person.rs b/crates/musicus/src/screens/person.rs deleted file mode 100644 index 664abb2..0000000 --- a/crates/musicus/src/screens/person.rs +++ /dev/null @@ -1,223 +0,0 @@ -use super::{MediumScreen, RecordingScreen, WorkScreen}; -use crate::editors::PersonEditor; -use crate::navigator::{NavigationHandle, NavigatorWindow, Screen}; -use crate::widgets; -use crate::widgets::{List, Section, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use musicus_backend::db::{self, Medium, Person, Recording, Work}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen for showing works by and recordings with a person. -pub struct PersonScreen { - handle: NavigationHandle<()>, - person: Person, - widget: widgets::Screen, - work_list: Rc, - recording_list: Rc, - medium_list: Rc, - works: RefCell>, - recordings: RefCell>, - mediums: RefCell>, -} - -impl Screen for PersonScreen { - /// Create a new person screen for the specified person and load the - /// contents asynchronously. - fn new(person: Person, handle: NavigationHandle<()>) -> Rc { - let widget = widgets::Screen::new(); - widget.set_title(&person.name_fl()); - - let work_list = List::new(); - let recording_list = List::new(); - let medium_list = List::new(); - - let this = Rc::new(Self { - handle, - person, - widget, - work_list, - recording_list, - medium_list, - works: RefCell::new(Vec::new()), - recordings: RefCell::new(Vec::new()), - mediums: RefCell::new(Vec::new()), - }); - - this.widget.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.widget.add_action( - &gettext("Edit person"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - let window = NavigatorWindow::new(this.handle.backend.clone()); - replace!(window.navigator, PersonEditor, Some(this.person.clone())).await; - }); - }), - ); - - this.widget.add_action( - &gettext("Delete person"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - db::delete_person(&mut this.handle.backend.db().lock().unwrap(), &this.person.id).unwrap(); - this.handle.backend.library_changed(); - }); - }), - ); - - this.widget.set_search_cb(clone!(@weak this => move || { - this.work_list.invalidate_filter(); - this.recording_list.invalidate_filter(); - this.medium_list.invalidate_filter(); - })); - - this.work_list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let work = &this.works.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(&work.title) - .build(); - - let work = work.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let work = work.clone(); - spawn!(@clone this, async move { - push!(this.handle, WorkScreen, work.clone()).await; - }); - })); - - row.upcast() - })); - - this.work_list - .set_filter_cb(clone!(@weak this => @default-panic, move|index| { - let work = &this.works.borrow()[index]; - let search = this.widget.get_search(); - let title = work.title.to_lowercase(); - search.is_empty() || title.contains(&search) - })); - - this.recording_list.set_make_widget_cb( - clone!(@weak this => @default-panic, move |index| { - let recording = &this.recordings.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(recording.work.get_title()) - .subtitle(recording.get_performers()) - .build(); - - let recording = recording.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let recording = recording.clone(); - spawn!(@clone this, async move { - push!(this.handle, RecordingScreen, recording.clone()).await; - }); - })); - - row.upcast() - }), - ); - - this.recording_list - .set_filter_cb(clone!(@weak this => @default-panic,move |index| { - let recording = &this.recordings.borrow()[index]; - let search = this.widget.get_search(); - let text = recording.work.get_title() + &recording.get_performers(); - search.is_empty() || text.to_lowercase().contains(&search) - })); - - this.medium_list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let medium = &this.mediums.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(&medium.name) - .build(); - - let medium = medium.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let medium = medium.clone(); - spawn!(@clone this, async move { - push!(this.handle, MediumScreen, medium.clone()).await; - }); - })); - - row.upcast() - })); - - this.medium_list - .set_filter_cb(clone!(@weak this => @default-panic, move |index| { - let medium = &this.mediums.borrow()[index]; - let search = this.widget.get_search(); - let name = medium.name.to_lowercase(); - search.is_empty() || name.contains(&search) - })); - - // Load the content. - - let works = db::get_works( - &mut this.handle.backend.db().lock().unwrap(), - &this.person.id, - ) - .unwrap(); - - let recordings = db::get_recordings_for_person( - &mut this.handle.backend.db().lock().unwrap(), - &this.person.id, - ) - .unwrap(); - - let mediums = db::get_mediums_for_person( - &mut this.handle.backend.db().lock().unwrap(), - &this.person.id, - ) - .unwrap(); - - if !works.is_empty() { - let length = works.len(); - this.works.replace(works); - this.work_list.update(length); - - let section = Section::new("Works", &this.work_list.widget); - this.widget.add_content(§ion.widget); - } - - if !recordings.is_empty() { - let length = recordings.len(); - this.recordings.replace(recordings); - this.recording_list.update(length); - - let section = Section::new("Recordings", &this.recording_list.widget); - this.widget.add_content(§ion.widget); - } - - if !mediums.is_empty() { - let length = mediums.len(); - this.mediums.replace(mediums); - this.medium_list.update(length); - - let section = Section::new("Mediums", &this.medium_list.widget); - this.widget.add_content(§ion.widget); - } - - this.widget.ready(); - - this - } -} - -impl Widget for PersonScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/player.rs b/crates/musicus/src/screens/player.rs deleted file mode 100644 index ca8a92c..0000000 --- a/crates/musicus/src/screens/player.rs +++ /dev/null @@ -1,260 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::{List, TrackRow, Widget}; -use adw::prelude::*; -use glib::clone; -use gtk::gdk; -use gtk_macros::get_widget; -use musicus_backend::db::Track; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; - -/// A playable track within the playlist. -#[derive(Clone)] -struct ListItem { - /// Index within the playlist. - index: usize, - - /// Whether this is the first track of the recording. - first: bool, - - /// Whether this is the currently played track. - playing: bool, -} - -pub struct PlayerScreen { - handle: NavigationHandle<()>, - widget: gtk::Box, - title_label: gtk::Label, - subtitle_label: gtk::Label, - previous_button: gtk::Button, - play_button: gtk::Button, - next_button: gtk::Button, - position_label: gtk::Label, - position: gtk::Adjustment, - duration_label: gtk::Label, - play_image: gtk::Image, - pause_image: gtk::Image, - list: Rc, - playlist: RefCell>, - items: RefCell>, - seeking: Cell, - current_track: Cell, -} - -impl Screen<(), ()> for PlayerScreen { - fn new(_: (), handle: NavigationHandle<()>) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Box, content); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - get_widget!(builder, gtk::Button, previous_button); - get_widget!(builder, gtk::Button, play_button); - get_widget!(builder, gtk::Button, next_button); - get_widget!(builder, gtk::Button, stop_button); - get_widget!(builder, gtk::Label, position_label); - get_widget!(builder, gtk::Scale, position_scale); - get_widget!(builder, gtk::Adjustment, position); - get_widget!(builder, gtk::Label, duration_label); - get_widget!(builder, gtk::Image, play_image); - get_widget!(builder, gtk::Image, pause_image); - - let list = List::new(); - content.append(&list.widget); - - let event_controller = gtk::EventControllerLegacy::new(); - position_scale.add_controller(event_controller.clone()); - - let this = Rc::new(Self { - handle, - widget, - title_label, - subtitle_label, - previous_button, - play_button, - next_button, - position_label, - position, - duration_label, - play_image, - pause_image, - list, - items: RefCell::new(Vec::new()), - playlist: RefCell::new(Vec::new()), - seeking: Cell::new(false), - current_track: Cell::new(0), - }); - - let player = &this.handle.backend.pl(); - - player.add_playlist_cb(clone!(@weak this => move |playlist| { - if playlist.is_empty() { - this.handle.pop(None); - } - - this.playlist.replace(playlist); - this.show_playlist(); - })); - - player.add_track_cb(clone!(@weak this, @weak player => move |current_track| { - this.previous_button.set_sensitive(this.handle.backend.pl().has_previous()); - this.next_button.set_sensitive(this.handle.backend.pl().has_next()); - - let track = &this.playlist.borrow()[current_track]; - - let mut parts = Vec::::new(); - for part in &track.work_parts { - parts.push(track.recording.work.parts[*part].title.clone()); - } - - let mut title = track.recording.work.get_title(); - if !parts.is_empty() { - title = format!("{}: {}", title, parts.join(", ")); - } - - this.title_label.set_text(&title); - this.subtitle_label.set_text(&track.recording.get_performers()); - this.position_label.set_text("0:00"); - - this.current_track.set(current_track); - - this.show_playlist(); - })); - - player.add_duration_cb(clone!(@weak this => move |ms| { - let min = ms / 60000; - let sec = (ms % 60000) / 1000; - this.duration_label.set_text(&format!("{}:{:02}", min, sec)); - this.position.set_upper(ms as f64); - })); - - player.add_playing_cb(clone!(@weak this => move |playing| { - this.play_button.set_child(Some(if playing { - &this.pause_image - } else { - &this.play_image - })); - })); - - player.add_position_cb(clone!(@weak this => move |ms| { - if !this.seeking.get() { - let min = ms / 60000; - let sec = (ms % 60000) / 1000; - this.position_label.set_text(&format!("{}:{:02}", min, sec)); - this.position.set_value(ms as f64); - } - })); - - back_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.pop(None); - })); - - this.previous_button - .connect_clicked(clone!(@weak this => move |_| { - this.handle.backend.pl().previous().unwrap(); - })); - - this.play_button - .connect_clicked(clone!(@weak this => move |_| { - this.handle.backend.pl().play_pause().unwrap(); - })); - - this.next_button - .connect_clicked(clone!(@weak this => move |_| { - this.handle.backend.pl().next().unwrap(); - })); - - stop_button.connect_clicked(clone!(@weak this => move |_| { - this.handle.backend.pl().clear(); - })); - - event_controller.connect_event( - clone!(@weak this => @default-return glib::signal::Inhibit(false), move |_, event| { - if let Some(event) = event.downcast_ref::() { - if event.button() == gdk::BUTTON_PRIMARY { - match event.event_type() { - gdk::EventType::ButtonPress => { - this.seeking.replace(true); - } - gdk::EventType::ButtonRelease => { - this.handle.backend.pl().seek(this.position.value() as u64); - this.seeking.replace(false); - } - _ => (), - } - } - - } - - glib::signal::Inhibit(false) - }), - ); - - position_scale.connect_value_changed(clone!(@weak this => move |_| { - if this.seeking.get() { - let ms = this.position.value() as u64; - let min = ms / 60000; - let sec = (ms % 60000) / 1000; - - this.position_label.set_text(&format!("{}:{:02}", min, sec)); - } - })); - - this.list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let item = &this.items.borrow()[index]; - let track = &this.playlist.borrow()[item.index]; - TrackRow::new(track, item.first, item.playing).get_widget() - })); - - this.list - .widget - .connect_row_activated(clone!(@weak this => move |_, row| { - let list_index = row.index(); - let list_item = this.items.borrow()[list_index as usize].clone(); - this.handle.backend.pl().set_track(list_item.index).unwrap(); - })); - - player.send_data(); - - this - } -} - -impl PlayerScreen { - /// Update the user interface according to the playlist. - fn show_playlist(&self) { - let playlist = self.playlist.borrow(); - let current_track = self.current_track.get(); - - let mut items = Vec::new(); - let mut last_recording_id = ""; - - for (index, track) in playlist.iter().enumerate() { - let first_track = if track.recording.id != last_recording_id { - last_recording_id = &track.recording.id; - true - } else { - false - }; - - items.push(ListItem { - index, - first: first_track, - playing: index == current_track, - }); - } - - let length = items.len(); - self.items.replace(items); - self.list.update(length); - } -} - -impl Widget for PlayerScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/recording.rs b/crates/musicus/src/screens/recording.rs deleted file mode 100644 index 49d272a..0000000 --- a/crates/musicus/src/screens/recording.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::editors::RecordingEditor; -use crate::navigator::{NavigationHandle, NavigatorWindow, Screen}; -use crate::widgets; -use crate::widgets::{List, Section, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use musicus_backend::db::{self, Recording, Track}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen for showing a recording. -pub struct RecordingScreen { - handle: NavigationHandle<()>, - recording: Recording, - widget: widgets::Screen, - list: Rc, - tracks: RefCell>, -} - -impl Screen for RecordingScreen { - /// Create a new recording screen for the specified recording and load the - /// contents asynchronously. - fn new(recording: Recording, handle: NavigationHandle<()>) -> Rc { - let widget = widgets::Screen::new(); - widget.set_title(&recording.work.get_title()); - widget.set_subtitle(&recording.get_performers()); - - let list = List::new(); - let section = Section::new(&gettext("Tracks"), &list.widget); - widget.add_content(§ion.widget); - - let this = Rc::new(Self { - handle, - recording, - widget, - list, - tracks: RefCell::new(Vec::new()), - }); - - section.add_action( - "media-playback-start-symbolic", - clone!(@weak this => move || { - this.handle.backend.pl().add_items(this.tracks.borrow().clone()).unwrap(); - }), - ); - - this.widget.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.widget.add_action( - &gettext("Edit recording"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - let window = NavigatorWindow::new(this.handle.backend.clone()); - replace!(window.navigator, RecordingEditor, Some(this.recording.clone())).await; - }); - }), - ); - - this.widget.add_action( - &gettext("Delete recording"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - db::delete_recording(&mut this.handle.backend.db().lock().unwrap(), &this.recording.id).unwrap(); - this.handle.backend.library_changed(); - }); - }), - ); - - this.list - .set_make_widget_cb(clone!(@weak this => @default-panic, move |index| { - let track = &this.tracks.borrow()[index]; - - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - title_parts.push(this.recording.work.parts[*part].title.clone()); - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let row = adw::ActionRow::builder() - .title(title) - .build(); - - row.upcast() - })); - - // Load the content. - - let tracks = db::get_tracks( - &mut this.handle.backend.db().lock().unwrap(), - &this.recording.id, - ) - .unwrap(); - - this.show_tracks(tracks); - this.widget.ready(); - - this - } -} - -impl RecordingScreen { - /// Update the tracks variable as well as the user interface. - fn show_tracks(&self, tracks: Vec) { - let length = tracks.len(); - self.tracks.replace(tracks); - self.list.update(length); - } -} - -impl Widget for RecordingScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/welcome.rs b/crates/musicus/src/screens/welcome.rs deleted file mode 100644 index 3f41826..0000000 --- a/crates/musicus/src/screens/welcome.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use std::rc::Rc; - -/// A screen displaying a welcome message and the necessary means to set up the application. This -/// screen doesn't access the backend except for setting the initial values and is safe to be used -/// while the backend is loading. -pub struct WelcomeScreen { - handle: NavigationHandle<()>, - widget: gtk::Box, -} - -impl Screen<(), ()> for WelcomeScreen { - fn new(_: (), handle: NavigationHandle<()>) -> Rc { - let widget = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - let header = gtk::HeaderBar::builder() - .title_widget(&adw::WindowTitle::new("Musicus", "")) - .build(); - - let button = gtk::Button::builder() - .halign(gtk::Align::Center) - .label(gettext("Select folder")) - .build(); - - let welcome = adw::StatusPage::builder() - .icon_name("folder-music-symbolic") - .title(gettext("Welcome to Musicus!")) - .description(gettext( - "Get startet by selecting the folder containing your music \ - files! Musicus will create a new database there or open one that already exists.", - )) - .child(&button) - .vexpand(true) - .build(); - - button.add_css_class("suggested-action"); - - widget.append(&header); - widget.append(&welcome); - - let this = Rc::new(Self { handle, widget }); - - button.connect_clicked(clone!(@weak this => move |_| { - let dialog = gtk::FileChooserDialog::new( - Some(&gettext("Select music library folder")), - Some(&this.handle.window), - gtk::FileChooserAction::SelectFolder, - &[ - (&gettext("Cancel"), gtk::ResponseType::Cancel), - (&gettext("Select"), gtk::ResponseType::Accept), - ]); - - dialog.set_modal(true); - - dialog.connect_response(clone!(@weak this => move |dialog, response| { - if let gtk::ResponseType::Accept = response { - if let Some(file) = dialog.file() { - if let Some(path) = file.path() { - Rc::clone(&this.handle.backend).set_music_library_path(path).unwrap(); - } - } - } - - dialog.hide(); - })); - - dialog.show(); - })); - - this - } -} - -impl Widget for WelcomeScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/screens/work.rs b/crates/musicus/src/screens/work.rs deleted file mode 100644 index e8bf5bc..0000000 --- a/crates/musicus/src/screens/work.rs +++ /dev/null @@ -1,126 +0,0 @@ -use super::RecordingScreen; -use crate::editors::WorkEditor; -use crate::navigator::{NavigationHandle, NavigatorWindow, Screen}; -use crate::widgets; -use crate::widgets::{List, Section, Widget}; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use musicus_backend::db::{self, Recording, Work}; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen for showing recordings of a work. -pub struct WorkScreen { - handle: NavigationHandle<()>, - work: Work, - widget: widgets::Screen, - recording_list: Rc, - recordings: RefCell>, -} - -impl Screen for WorkScreen { - /// Create a new work screen for the specified work and load the - /// contents asynchronously. - fn new(work: Work, handle: NavigationHandle<()>) -> Rc { - let widget = widgets::Screen::new(); - widget.set_title(&work.title); - widget.set_subtitle(&work.composer.name_fl()); - - let recording_list = List::new(); - - let this = Rc::new(Self { - handle, - work, - widget, - recording_list, - recordings: RefCell::new(Vec::new()), - }); - - this.widget.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.widget.add_action( - &gettext("Edit work"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - let window = NavigatorWindow::new(this.handle.backend.clone()); - replace!(window.navigator, WorkEditor, Some(this.work.clone())).await; - }); - }), - ); - - this.widget.add_action( - &gettext("Delete work"), - clone!(@weak this => move || { - spawn!(@clone this, async move { - db::delete_work(&mut this.handle.backend.db().lock().unwrap(), &this.work.id).unwrap(); - this.handle.backend.library_changed(); - }); - }), - ); - - this.widget.set_search_cb(clone!(@weak this => move || { - this.recording_list.invalidate_filter(); - })); - - this.recording_list.set_make_widget_cb( - clone!(@weak this => @default-panic, move |index| { - let recording = &this.recordings.borrow()[index]; - - let row = adw::ActionRow::builder() - .activatable(true) - .title(recording.work.get_title()) - .subtitle(recording.get_performers()) - .build(); - - let recording = recording.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let recording = recording.clone(); - spawn!(@clone this, async move { - push!(this.handle, RecordingScreen, recording.clone()).await; - }); - })); - - row.upcast() - }), - ); - - this.recording_list - .set_filter_cb(clone!(@weak this => @default-panic, move |index| { - let recording = &this.recordings.borrow()[index]; - let search = this.widget.get_search(); - let text = recording.work.get_title() + &recording.get_performers(); - search.is_empty() || text.to_lowercase().contains(&search) - })); - - // Load the content. - - let recordings = db::get_recordings_for_work( - &mut this.handle.backend.db().lock().unwrap(), - &this.work.id, - ) - .unwrap(); - - if !recordings.is_empty() { - let length = recordings.len(); - this.recordings.replace(recordings); - this.recording_list.update(length); - - let section = Section::new("Recordings", &this.recording_list.widget); - this.widget.add_content(§ion.widget); - } - - this.widget.ready(); - - this - } -} - -impl Widget for WorkScreen { - fn get_widget(&self) -> gtk::Widget { - self.widget.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/ensemble.rs b/crates/musicus/src/selectors/ensemble.rs deleted file mode 100644 index dd45a64..0000000 --- a/crates/musicus/src/selectors/ensemble.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::selector::Selector; -use crate::editors::EnsembleEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Ensemble}; -use std::rc::Rc; - -/// A screen for selecting a ensemble. -pub struct EnsembleSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Ensemble> for EnsembleSelector { - /// Create a new ensemble selector. - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select ensemble")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(ensemble) = push!(this.handle, EnsembleEditor, None).await { - this.handle.pop(Some(ensemble)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |ensemble| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&ensemble.name) - .build(); - - let ensemble = ensemble.to_owned(); - - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_ensemble(&mut this.handle.backend.db().lock().unwrap(), ensemble.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(ensemble.clone())) - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search)); - - this.selector.set_items( - db::get_recent_ensembles(&mut this.handle.backend.db().lock().unwrap()).unwrap(), - ); - - this - } -} - -impl Widget for EnsembleSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/instrument.rs b/crates/musicus/src/selectors/instrument.rs deleted file mode 100644 index 2e42312..0000000 --- a/crates/musicus/src/selectors/instrument.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::selector::Selector; -use crate::editors::InstrumentEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Instrument}; -use std::rc::Rc; - -/// A screen for selecting a instrument. -pub struct InstrumentSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Instrument> for InstrumentSelector { - /// Create a new instrument selector. - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select instrument")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(instrument) = push!(this.handle, InstrumentEditor, None).await { - this.handle.pop(Some(instrument)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |instrument| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&instrument.name) - .build(); - - let instrument = instrument.to_owned(); - - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_instrument(&mut this.handle.backend.db().lock().unwrap(), instrument.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(instrument.clone())) - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, instrument| instrument.name.to_lowercase().contains(search)); - - this.selector.set_items( - db::get_recent_instruments(&mut this.handle.backend.db().lock().unwrap()).unwrap(), - ); - - this - } -} - -impl Widget for InstrumentSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/medium.rs b/crates/musicus/src/selectors/medium.rs deleted file mode 100644 index c3b1833..0000000 --- a/crates/musicus/src/selectors/medium.rs +++ /dev/null @@ -1,159 +0,0 @@ -use super::selector::Selector; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Medium, PersonOrEnsemble}; -use std::rc::Rc; - -/// A screen for selecting a medium. -pub struct MediumSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Medium> for MediumSelector { - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select performer")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_make_widget(clone!(@weak this => @default-panic, move |poe| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(poe.get_title()) - .build(); - - let poe = poe.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - let poe = poe.clone(); - spawn!(@clone this, async move { - if let Some(medium) = push!(this.handle, MediumSelectorMediumScreen, poe).await { - this.handle.pop(Some(medium)); - } - }); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, poe| poe.get_title().to_lowercase().contains(search)); - - // Initialize items. - - let mut poes = Vec::new(); - - let persons = - db::get_recent_persons(&mut this.handle.backend.db().lock().unwrap()).unwrap(); - let ensembles = - db::get_recent_ensembles(&mut this.handle.backend.db().lock().unwrap()).unwrap(); - - for person in persons { - poes.push(PersonOrEnsemble::Person(person)); - } - - for ensemble in ensembles { - poes.push(PersonOrEnsemble::Ensemble(ensemble)); - } - - this.selector.set_items(poes); - - this - } -} - -impl Widget for MediumSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} - -/// The actual medium selector that is displayed after the user has selected a person or ensemble. -struct MediumSelectorMediumScreen { - handle: NavigationHandle, - poe: PersonOrEnsemble, - selector: Rc>, -} - -impl Screen for MediumSelectorMediumScreen { - fn new(poe: PersonOrEnsemble, handle: NavigationHandle) -> Rc { - let selector = Selector::::new(); - selector.set_title(&gettext("Select medium")); - selector.set_subtitle(&poe.get_title()); - - let this = Rc::new(Self { - handle, - poe, - selector, - }); - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |medium| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&medium.name) - .build(); - - let medium = medium.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_medium(&mut this.handle.backend.db().lock().unwrap(), medium.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(medium.clone())); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, medium| medium.name.to_lowercase().contains(search)); - - // Initialize items. - match this.poe.clone() { - PersonOrEnsemble::Person(person) => { - this.selector.set_items( - db::get_mediums_for_person( - &mut this.handle.backend.db().lock().unwrap(), - &person.id, - ) - .unwrap(), - ); - } - PersonOrEnsemble::Ensemble(ensemble) => { - this.selector.set_items( - db::get_mediums_for_ensemble( - &mut this.handle.backend.db().lock().unwrap(), - &ensemble.id, - ) - .unwrap(), - ); - } - } - - this - } -} - -impl Widget for MediumSelectorMediumScreen { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/mod.rs b/crates/musicus/src/selectors/mod.rs deleted file mode 100644 index ac73bbc..0000000 --- a/crates/musicus/src/selectors/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub mod ensemble; -pub use ensemble::*; - -pub mod instrument; -pub use instrument::*; - -pub mod medium; -pub use medium::*; - -pub mod person; -pub use person::*; - -pub mod recording; -pub use recording::*; - -pub mod work; -pub use work::*; - -mod selector; diff --git a/crates/musicus/src/selectors/person.rs b/crates/musicus/src/selectors/person.rs deleted file mode 100644 index 3a27714..0000000 --- a/crates/musicus/src/selectors/person.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::selector::Selector; -use crate::editors::PersonEditor; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Person}; -use std::rc::Rc; - -/// A screen for selecting a person. -pub struct PersonSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Person> for PersonSelector { - /// Create a new person selector. - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select person")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(person) = push!(this.handle, PersonEditor, None).await { - this.handle.pop(Some(person)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |person| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(person.name_lf()) - .build(); - - let person = person.to_owned(); - - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_person(&mut this.handle.backend.db().lock().unwrap(), person.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(person.clone())); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); - - this.selector.set_items( - db::get_recent_persons(&mut this.handle.backend.db().lock().unwrap()).unwrap(), - ); - - this - } -} - -impl Widget for PersonSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/recording.rs b/crates/musicus/src/selectors/recording.rs deleted file mode 100644 index 7557ad6..0000000 --- a/crates/musicus/src/selectors/recording.rs +++ /dev/null @@ -1,237 +0,0 @@ -use super::selector::Selector; -use crate::editors::{PersonEditor, RecordingEditor, WorkEditor}; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Person, Recording, Work}; -use std::rc::Rc; - -/// A screen for selecting a recording. -pub struct RecordingSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Recording> for RecordingSelector { - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select composer")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(person) = push!(this.handle, PersonEditor, None).await { - // We can assume that there are no existing works of this composer and - // immediately show the work editor. Going back from the work editor will - // correctly show the person selector again. - - let work = Work::from_composer(person); - if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { - // There will also be no existing recordings, so we show the recording - // editor next. - - let recording = Recording::from_work(work); - if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await { - this.handle.pop(Some(recording)); - } - } - } - }); - })); - - this.selector.set_make_widget(clone!(@weak this => @default-panic, move |person| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(person.name_lf()) - .build(); - - let person = person.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - // Instead of returning the person from here, like the person selector does, we - // show a second selector for choosing the work. - - let person = person.clone(); - spawn!(@clone this, async move { - if let Some(work) = push!(this.handle, RecordingSelectorWorkScreen, person).await { - // Now the user can select a recording for that work. - - if let Some(recording) = push!(this.handle, RecordingSelectorRecordingScreen, work).await { - this.handle.pop(Some(recording)); - } - } - }); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); - - this.selector.set_items( - db::get_recent_persons(&mut this.handle.backend.db().lock().unwrap()).unwrap(), - ); - - this - } -} - -impl Widget for RecordingSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} - -/// The work selector within the recording selector. -struct RecordingSelectorWorkScreen { - handle: NavigationHandle, - person: Person, - selector: Rc>, -} - -impl Screen for RecordingSelectorWorkScreen { - fn new(person: Person, handle: NavigationHandle) -> Rc { - let selector = Selector::::new(); - selector.set_title(&gettext("Select work")); - selector.set_subtitle(&person.name_fl()); - - let this = Rc::new(Self { - handle, - person, - selector, - }); - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - let work = Work::from_composer(this.person.clone()); - if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { - this.handle.pop(Some(work)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |work| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&work.title) - .build(); - - let work = work.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - this.handle.pop(Some(work.clone())); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, work| work.title.to_lowercase().contains(search)); - - this.selector.set_items( - db::get_works( - &mut this.handle.backend.db().lock().unwrap(), - &this.person.id, - ) - .unwrap(), - ); - - this - } -} - -impl Widget for RecordingSelectorWorkScreen { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} - -/// The actual recording selector within the recording selector. -struct RecordingSelectorRecordingScreen { - handle: NavigationHandle, - work: Work, - selector: Rc>, -} - -impl Screen for RecordingSelectorRecordingScreen { - fn new(work: Work, handle: NavigationHandle) -> Rc { - let selector = Selector::::new(); - selector.set_title(&gettext("Select recording")); - selector.set_subtitle(&work.get_title()); - - let this = Rc::new(Self { - handle, - work, - selector, - }); - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - let recording = Recording::from_work(this.work.clone()); - if let Some(recording) = push!(this.handle, RecordingEditor, Some(recording)).await { - this.handle.pop(Some(recording)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |recording| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(recording.get_performers()) - .build(); - - let recording = recording.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_recording(&mut this.handle.backend.db().lock().unwrap(), recording.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(recording.clone())); - })); - - row.upcast() - })); - - this.selector.set_filter(|search, recording| { - recording.get_performers().to_lowercase().contains(search) - }); - - this.selector.set_items( - db::get_recordings_for_work( - &mut this.handle.backend.db().lock().unwrap(), - &this.work.id, - ) - .unwrap(), - ); - - this - } -} - -impl Widget for RecordingSelectorRecordingScreen { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/selectors/selector.rs b/crates/musicus/src/selectors/selector.rs deleted file mode 100644 index 1e35add..0000000 --- a/crates/musicus/src/selectors/selector.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::widgets::List; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen that presents a list of items from the library. -pub struct Selector { - pub widget: gtk::Box, - title_label: gtk::Label, - subtitle_label: gtk::Label, - search_entry: gtk::SearchEntry, - stack: gtk::Stack, - list: Rc, - items: RefCell>, - back_cb: RefCell>>, - add_cb: RefCell>>, - make_widget: RefCell gtk::Widget>>>, - filter: RefCell bool>>>, -} - -impl Selector { - /// Create a new selector. - pub fn new() -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, adw::Clamp, clamp); - - let list = List::new(); - clamp.set_child(Some(&list.widget)); - - let this = Rc::new(Self { - widget, - title_label, - subtitle_label, - search_entry, - stack, - list, - items: RefCell::new(Vec::new()), - back_cb: RefCell::new(None), - add_cb: RefCell::new(None), - make_widget: RefCell::new(None), - filter: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.back_cb.borrow() { - cb(); - } - })); - - add_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.add_cb.borrow() { - cb(); - } - })); - - this.search_entry - .connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - this.list - .set_make_widget_cb(clone!(@strong this => move |index| { - if let Some(cb) = &*this.make_widget.borrow() { - let item = &this.items.borrow()[index]; - cb(item) - } else { - gtk::Label::new(None).upcast() - } - })); - - this.list - .set_filter_cb(clone!(@strong this => move |index| { - match &*this.filter.borrow() { - Some(filter) => { - let item = &this.items.borrow()[index]; - let search = this.search_entry.text().to_string().to_lowercase(); - search.is_empty() || filter(&search, item) - } - None => true, - } - })); - - this - } - - /// Set the title to be shown in the header. - pub fn set_title(&self, title: &str) { - self.title_label.set_label(title); - } - - /// Set the subtitle to be shown in the header. - pub fn set_subtitle(&self, subtitle: &str) { - self.subtitle_label.set_label(subtitle); - self.subtitle_label.show(); - } - - /// Set the closure to be called when the user wants to go back. - pub fn set_back_cb(&self, cb: F) { - self.back_cb.replace(Some(Box::new(cb))); - } - - /// Set the closure to be called when the user wants to add an item. - pub fn set_add_cb(&self, cb: F) { - self.add_cb.replace(Some(Box::new(cb))); - } - - /// Set the closure to be called for creating a new list row. - pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { - self.make_widget.replace(Some(Box::new(make_widget))); - } - - /// Set a closure to call when deciding whether to show an item based on a search string. The - /// search string will be converted to lowercase. - pub fn set_filter bool + 'static>(&self, filter: F) { - self.filter.replace(Some(Box::new(filter))); - } - - /// Set the list items the user may select from. - pub fn set_items(&self, items: Vec) { - let length = items.len(); - self.items.replace(items); - self.list.update(length); - self.stack.set_visible_child_name("content"); - } -} diff --git a/crates/musicus/src/selectors/work.rs b/crates/musicus/src/selectors/work.rs deleted file mode 100644 index a388423..0000000 --- a/crates/musicus/src/selectors/work.rs +++ /dev/null @@ -1,158 +0,0 @@ -use super::selector::Selector; -use crate::editors::{PersonEditor, WorkEditor}; -use crate::navigator::{NavigationHandle, Screen}; -use crate::widgets::Widget; - -use adw::prelude::*; -use gettextrs::gettext; -use glib::clone; -use log::warn; -use musicus_backend::db::{self, Person, Work}; -use std::rc::Rc; - -/// A screen for selecting a work. -pub struct WorkSelector { - handle: NavigationHandle, - selector: Rc>, -} - -impl Screen<(), Work> for WorkSelector { - fn new(_: (), handle: NavigationHandle) -> Rc { - // Create UI - - let selector = Selector::::new(); - selector.set_title(&gettext("Select composer")); - - let this = Rc::new(Self { handle, selector }); - - // Connect signals and callbacks - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - if let Some(person) = push!(this.handle, PersonEditor, None).await { - // We can assume that there are no existing works of this composer and - // immediately show the work editor. Going back from the work editor will - // correctly show the person selector again. - - let work = Work::from_composer(person); - if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { - this.handle.pop(Some(work)); - } - } - }); - })); - - this.selector.set_make_widget(clone!(@weak this => @default-panic, move |person| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(person.name_lf()) - .build(); - - let person = person.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - // Instead of returning the person from here, like the person selector does, we - // show a second selector for choosing the work. - - let person = person.clone(); - spawn!(@clone this, async move { - if let Some(work) = push!(this.handle, WorkSelectorWorkScreen, person).await { - this.handle.pop(Some(work)); - } - }); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); - - this.selector.set_items( - db::get_recent_persons(&mut this.handle.backend.db().lock().unwrap()).unwrap(), - ); - - this - } -} - -impl Widget for WorkSelector { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} - -/// The actual work selector that is displayed after the user has selected a composer. -struct WorkSelectorWorkScreen { - handle: NavigationHandle, - person: Person, - selector: Rc>, -} - -impl Screen for WorkSelectorWorkScreen { - fn new(person: Person, handle: NavigationHandle) -> Rc { - let selector = Selector::::new(); - selector.set_title(&gettext("Select work")); - selector.set_subtitle(&person.name_fl()); - - let this = Rc::new(Self { - handle, - person, - selector, - }); - - this.selector.set_back_cb(clone!(@weak this => move || { - this.handle.pop(None); - })); - - this.selector.set_add_cb(clone!(@weak this => move || { - spawn!(@clone this, async move { - let work = Work::from_composer(this.person.clone()); - if let Some(work) = push!(this.handle, WorkEditor, Some(work)).await { - this.handle.pop(Some(work)); - } - }); - })); - - this.selector - .set_make_widget(clone!(@weak this => @default-panic, move |work| { - let row = adw::ActionRow::builder() - .activatable(true) - .title(&work.title) - .build(); - - let work = work.to_owned(); - row.connect_activated(clone!(@weak this => move |_| { - if let Err(err) = db::update_work(&mut this.handle.backend.db().lock().unwrap(), work.clone()) { - warn!("Failed to update access time. {err}"); - } - - this.handle.pop(Some(work.clone())); - })); - - row.upcast() - })); - - this.selector - .set_filter(|search, work| work.title.to_lowercase().contains(search)); - - this.selector.set_items( - db::get_works( - &mut this.handle.backend.db().lock().unwrap(), - &this.person.id, - ) - .unwrap(), - ); - - this - } -} - -impl Widget for WorkSelectorWorkScreen { - fn get_widget(&self) -> gtk::Widget { - self.selector.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/widgets/button_row.rs b/crates/musicus/src/widgets/button_row.rs deleted file mode 100644 index 1d577f8..0000000 --- a/crates/musicus/src/widgets/button_row.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::Widget; -use adw::prelude::*; - -/// A list box row with a single button. -pub struct ButtonRow { - /// The actual GTK widget. - pub widget: adw::ActionRow, - - /// The managed button. - button: gtk::Button, -} - -impl ButtonRow { - /// Create a new button row. - pub fn new(title: &str, label: &str) -> Self { - let button = gtk::Button::builder() - .valign(gtk::Align::Center) - .label(label) - .build(); - - let widget = adw::ActionRow::builder() - .focusable(false) - .activatable_widget(&button) - .title(title) - .build(); - - widget.add_suffix(&button); - - Self { widget, button } - } - - /// Set the subtitle of the row. - pub fn set_subtitle(&self, subtitle: &str) { - self.widget.set_subtitle(subtitle); - } - - /// Set the closure to be called on activation - pub fn set_cb(&self, cb: F) { - self.button.connect_clicked(move |_| cb()); - } -} - -impl Widget for ButtonRow { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/widgets/editor.rs b/crates/musicus/src/widgets/editor.rs deleted file mode 100644 index 1b4693c..0000000 --- a/crates/musicus/src/widgets/editor.rs +++ /dev/null @@ -1,85 +0,0 @@ -use super::Widget; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; - -/// Common UI elements for an editor. -pub struct Editor { - /// The actual GTK widget. - pub widget: gtk::Stack, - - /// The button to switch to the previous screen. - back_button: gtk::Button, - - /// The title widget within the header bar. - window_title: adw::WindowTitle, - - /// The button to save the edited item. - save_button: gtk::Button, - - /// The box containing the content. - content_box: gtk::Box, - - /// The status page for the error screen. - status_page: adw::StatusPage, -} - -impl Editor { - /// Create a new screen. - pub fn new() -> Self { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/editor.ui"); - - get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, adw::WindowTitle, window_title); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Box, content_box); - get_widget!(builder, adw::StatusPage, status_page); - get_widget!(builder, gtk::Button, try_again_button); - - try_again_button.connect_clicked(clone!(@strong widget => move |_| { - widget.set_visible_child_name("content"); - })); - - Self { - widget, - back_button, - window_title, - save_button, - content_box, - status_page, - } - } - - /// Set a closure to be called when the back button is pressed. - pub fn set_back_cb(&self, cb: F) { - self.back_button.connect_clicked(move |_| cb()); - } - - /// Show a title in the header bar. - pub fn set_title(&self, title: &str) { - self.window_title.set_title(title); - } - - /// Set whether the user should be able to click the save button. - pub fn set_may_save(&self, save: bool) { - self.save_button.set_sensitive(save); - } - - pub fn set_save_cb(&self, cb: F) { - self.save_button.connect_clicked(move |_| cb()); - } - - /// Show an error page. The page contains a button to get back to the - /// actual editor. - pub fn error(&self, title: &str, description: &str) { - self.status_page.set_title(title); - self.status_page.set_description(Some(description)); - self.widget.set_visible_child_name("error"); - } - - /// Add content to the bottom of the content area. - pub fn add_content(&self, content: &W) { - self.content_box.append(&content.get_widget()); - } -} diff --git a/crates/musicus/src/widgets/indexed_list_model.rs b/crates/musicus/src/widgets/indexed_list_model.rs deleted file mode 100644 index 924fb81..0000000 --- a/crates/musicus/src/widgets/indexed_list_model.rs +++ /dev/null @@ -1,128 +0,0 @@ -use gio::prelude::*; -use gio::subclass::prelude::*; - -use std::cell::Cell; - -glib::wrapper! { - /// A thin list model managing only indices to an external data source. - pub struct IndexedListModel(ObjectSubclass) - @implements gio::ListModel; -} - -impl IndexedListModel { - /// Set the length of the list model. - pub fn set_length(&self, length: u32) { - let old_length = self.n_items(); - self.set_n_items(length); - self.items_changed(0, old_length, length); - } -} - -impl Default for IndexedListModel { - fn default() -> Self { - glib::Object::new() - } -} - -mod indexed_list_model_imp { - use glib::Properties; - - use super::*; - - #[derive(Properties, Default)] - #[properties(wrapper_type = super::IndexedListModel)] - pub struct IndexedListModel { - #[property(get, set)] - n_items: Cell, - } - - #[glib::object_subclass] - impl ObjectSubclass for IndexedListModel { - const NAME: &'static str = "IndexedListModel"; - type Type = super::IndexedListModel; - type ParentType = glib::Object; - type Interfaces = (gio::ListModel,); - } - - impl ObjectImpl for IndexedListModel { - fn properties() -> &'static [glib::ParamSpec] { - Self::derived_properties() - } - - fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - self.derived_set_property(id, value, pspec) - } - - fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { - self.derived_property(id, pspec) - } - } - - impl ListModelImpl for IndexedListModel { - fn item_type(&self) -> glib::Type { - ItemIndex::static_type() - } - - fn n_items(&self) -> u32 { - self.n_items.get() - } - - fn item(&self, position: u32) -> Option { - Some(ItemIndex::new(position).upcast()) - } - } -} - -glib::wrapper! { - /// A simple GObject holding just one integer. - pub struct ItemIndex(ObjectSubclass); -} - -impl ItemIndex { - /// Create a new item index. - pub fn new(value: u32) -> Self { - let object = glib::Object::new::(); - object.set_value(value); - object - } - - /// Get the value of the item index.. - pub fn get(&self) -> u32 { - self.property("value") - } -} - -mod item_index_imp { - use glib::Properties; - - use super::*; - - #[derive(Properties, Default)] - #[properties(wrapper_type = super::ItemIndex)] - pub struct ItemIndex { - #[property(get, set)] - value: Cell, - } - - #[glib::object_subclass] - impl ObjectSubclass for ItemIndex { - const NAME: &'static str = "ItemIndex"; - type Type = super::ItemIndex; - type ParentType = glib::Object; - type Interfaces = (); - } - - impl ObjectImpl for ItemIndex { - fn properties() -> &'static [glib::ParamSpec] { - Self::derived_properties() - } - - fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - self.derived_set_property(id, value, pspec) - } - - fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { - self.derived_property(id, pspec) - } - } -} diff --git a/crates/musicus/src/widgets/list.rs b/crates/musicus/src/widgets/list.rs deleted file mode 100644 index c34db4c..0000000 --- a/crates/musicus/src/widgets/list.rs +++ /dev/null @@ -1,136 +0,0 @@ -use super::indexed_list_model::{IndexedListModel, ItemIndex}; -use glib::clone; -use gtk::{gdk, prelude::*}; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; - -/// A simple list of widgets. -pub struct List { - pub widget: gtk::ListBox, - model: IndexedListModel, - filter: gtk::CustomFilter, - enable_dnd: Cell, - make_widget_cb: RefCell gtk::Widget>>>, - filter_cb: RefCell bool>>>, - move_cb: RefCell>>, -} - -impl List { - /// Create a new list. The list will be empty initially. - pub fn new() -> Rc { - let model = IndexedListModel::default(); - let filter = gtk::CustomFilter::new(|_| true); - let filter_model = gtk::FilterListModel::new(Some(model.clone()), Some(filter.clone())); - - // TODO: Switch to gtk::ListView. - // let selection = gtk::NoSelection::new(Some(&model)); - // let factory = gtk::SignalListItemFactory::new(); - // let widget = gtk::ListView::new(Some(&selection), Some(&factory)); - - let widget = gtk::ListBox::builder() - .selection_mode(gtk::SelectionMode::None) - .css_classes(vec![String::from("boxed-list")]) - .build(); - - let this = Rc::new(Self { - widget, - model, - filter, - enable_dnd: Cell::new(false), - make_widget_cb: RefCell::new(None), - filter_cb: RefCell::new(None), - move_cb: RefCell::new(None), - }); - - this.filter - .set_filter_func(clone!(@strong this => move |index| { - if let Some(cb) = &*this.filter_cb.borrow() { - let index = index.downcast_ref::().unwrap().value() as usize; - cb(index) - } else { - true - } - })); - - this.widget.bind_model(Some(&filter_model), clone!(@strong this => move |index| { - let index = index.downcast_ref::().unwrap().get() as usize; - if let Some(cb) = &*this.make_widget_cb.borrow() { - let widget = cb(index); - - if this.enable_dnd.get() { - let drag_source = gtk::DragSource::new(); - - drag_source.connect_drag_begin(clone!(@strong widget => move |_, drag| { - // TODO: Replace with a better solution. - let paintable = gtk::WidgetPaintable::new(Some(&widget)); - gtk::DragIcon::set_from_paintable(drag, &paintable, 0, 0); - })); - - let drag_value = (index as u32).to_value(); - drag_source.set_content(Some(&gdk::ContentProvider::for_value(&drag_value))); - - let drop_target = gtk::DropTarget::new(glib::Type::U32, gdk::DragAction::COPY); - - drop_target.connect_drop(clone!(@strong this => move |_, value, _, _| { - if let Some(cb) = &*this.move_cb.borrow() { - let old_index: u32 = value.get().unwrap(); - cb(old_index as usize, index); - true - } else { - false - } - })); - - widget.add_controller(drag_source); - widget.add_controller(drop_target); - } - - widget - } else { - // This shouldn't be reachable under normal circumstances. - gtk::Label::new(None).upcast() - } - })); - - this - } - - /// Whether the list should support drag and drop. - pub fn set_enable_dnd(&self, enable: bool) { - self.enable_dnd.set(enable); - } - - /// Set the closure to be called to construct widgets for the items. - pub fn set_make_widget_cb gtk::Widget + 'static>(&self, cb: F) { - self.make_widget_cb.replace(Some(Box::new(cb))); - } - - /// Set the closure to be called to filter the items. If this returns - /// false, the item will not be shown. - pub fn set_filter_cb bool + 'static>(&self, cb: F) { - self.filter_cb.replace(Some(Box::new(cb))); - self.invalidate_filter(); - } - - /// Set the closure to be called to when the use has dragged an item to a - /// new position. - pub fn set_move_cb(&self, cb: F) { - self.move_cb.replace(Some(Box::new(cb))); - } - - /// Set the lists selection mode to single. - pub fn enable_selection(&self) { - self.widget.set_selection_mode(gtk::SelectionMode::Single); - } - - /// Refilter the list based on the filter callback. - pub fn invalidate_filter(&self) { - self.filter.changed(gtk::FilterChange::Different); - } - - /// Call the make_widget function for each item. This will automatically - /// show all children by indices 0..length. - pub fn update(&self, length: usize) { - self.model.set_length(length as u32); - } -} diff --git a/crates/musicus/src/widgets/mod.rs b/crates/musicus/src/widgets/mod.rs deleted file mode 100644 index 9c8f8d9..0000000 --- a/crates/musicus/src/widgets/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gtk::prelude::*; - -pub mod button_row; -pub use button_row::*; - -pub mod editor; -pub use editor::*; - -pub mod list; -pub use list::*; - -pub mod player_bar; -pub use player_bar::*; - -pub mod screen; -pub use screen::*; - -pub mod section; -pub use section::*; - -pub mod track_row; -pub use track_row::TrackRow; - -mod indexed_list_model; - -/// Something that can be represented as a GTK widget. -pub trait Widget { - /// Get the widget. - fn get_widget(&self) -> gtk::Widget; -} - -impl> Widget for W { - fn get_widget(&self) -> gtk::Widget { - self.clone().upcast() - } -} diff --git a/crates/musicus/src/widgets/player_bar.rs b/crates/musicus/src/widgets/player_bar.rs deleted file mode 100644 index 3447256..0000000 --- a/crates/musicus/src/widgets/player_bar.rs +++ /dev/null @@ -1,170 +0,0 @@ -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use musicus_backend::Player; -use std::cell::RefCell; -use std::rc::Rc; - -pub struct PlayerBar { - pub widget: gtk::Revealer, - title_label: gtk::Label, - subtitle_label: gtk::Label, - previous_button: gtk::Button, - play_button: gtk::Button, - next_button: gtk::Button, - position_label: gtk::Label, - duration_label: gtk::Label, - play_image: gtk::Image, - pause_image: gtk::Image, - player: Rc>>>, - playlist_cb: Rc>>>, -} - -impl PlayerBar { - pub fn new() -> Self { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/player_bar.ui"); - - get_widget!(builder, gtk::Revealer, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - get_widget!(builder, gtk::Button, previous_button); - get_widget!(builder, gtk::Button, play_button); - get_widget!(builder, gtk::Button, next_button); - get_widget!(builder, gtk::Label, position_label); - get_widget!(builder, gtk::Label, duration_label); - get_widget!(builder, gtk::Button, playlist_button); - get_widget!(builder, gtk::Image, play_image); - get_widget!(builder, gtk::Image, pause_image); - - let player = Rc::new(RefCell::new(None::>)); - let playlist_cb = Rc::new(RefCell::new(None::>)); - - previous_button.connect_clicked(clone!(@strong player => move |_| { - if let Some(player) = &*player.borrow() { - player.previous().unwrap(); - } - })); - - play_button.connect_clicked(clone!(@strong player => move |_| { - if let Some(player) = &*player.borrow() { - player.play_pause().unwrap(); - } - })); - - next_button.connect_clicked(clone!(@strong player => move |_| { - if let Some(player) = &*player.borrow() { - player.next().unwrap(); - } - })); - - playlist_button.connect_clicked(clone!(@strong playlist_cb => move |_| { - if let Some(cb) = &*playlist_cb.borrow() { - cb(); - } - })); - - Self { - widget, - title_label, - subtitle_label, - previous_button, - play_button, - next_button, - position_label, - duration_label, - play_image, - pause_image, - player, - playlist_cb, - } - } - - pub fn set_player(&self, player: Option>) { - self.player.replace(player.clone()); - - if let Some(player) = player { - let playlist = Rc::new(RefCell::new(Vec::new())); - - player.add_playlist_cb(clone!( - @strong player, - @strong self.widget as widget, - @strong self.previous_button as previous_button, - @strong self.next_button as next_button, - @strong playlist - => move |new_playlist| { - widget.set_reveal_child(!new_playlist.is_empty()); - playlist.replace(new_playlist); - previous_button.set_sensitive(player.has_previous()); - next_button.set_sensitive(player.has_next()); - } - )); - - player.add_track_cb(clone!( - @strong player, - @strong playlist, - @strong self.previous_button as previous_button, - @strong self.next_button as next_button, - @strong self.title_label as title_label, - @strong self.subtitle_label as subtitle_label, - @strong self.position_label as position_label - => move |current_track| { - previous_button.set_sensitive(player.has_previous()); - next_button.set_sensitive(player.has_next()); - - let track = &playlist.borrow()[current_track]; - - let mut parts = Vec::::new(); - for part in &track.work_parts { - parts.push(track.recording.work.parts[*part].title.clone()); - } - - let mut title = track.recording.work.get_title(); - if !parts.is_empty() { - title = format!("{}: {}", title, parts.join(", ")); - } - - title_label.set_text(&title); - subtitle_label.set_text(&track.recording.get_performers()); - position_label.set_text("0:00"); - } - )); - - player.add_duration_cb(clone!( - @strong self.duration_label as duration_label - => move |ms| { - let min = ms / 60000; - let sec = (ms % 60000) / 1000; - duration_label.set_text(&format!("{}:{:02}", min, sec)); - } - )); - - player.add_playing_cb(clone!( - @strong self.play_button as play_button, - @strong self.play_image as play_image, - @strong self.pause_image as pause_image - => move |playing| { - play_button.set_child(Some(if playing { - &pause_image - } else { - &play_image - })); - } - )); - - player.add_position_cb(clone!( - @strong self.position_label as position_label - => move |ms| { - let min = ms / 60000; - let sec = (ms % 60000) / 1000; - position_label.set_text(&format!("{}:{:02}", min, sec)); - } - )); - } else { - self.widget.set_reveal_child(false); - } - } - - pub fn set_playlist_cb(&self, cb: F) { - self.playlist_cb.replace(Some(Box::new(cb))); - } -} diff --git a/crates/musicus/src/widgets/screen.rs b/crates/musicus/src/widgets/screen.rs deleted file mode 100644 index 5bea5ab..0000000 --- a/crates/musicus/src/widgets/screen.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; - -/// A general framework for screens. Screens have a header bar with at least -/// a button to go back and a scrollable content area that clamps its content. -pub struct Screen { - /// The actual GTK widget. - pub widget: gtk::Box, - - /// The button to switch to the previous screen. - back_button: gtk::Button, - - /// The title widget within the header bar. - window_title: adw::WindowTitle, - - /// The action menu. - menu: gio::Menu, - - /// The entry for searching. - search_entry: gtk::SearchEntry, - - /// The stack to switch to the loading page. - stack: gtk::Stack, - - /// The box containing the content. - content_box: gtk::Box, - - /// The actions for the menu. - actions: gio::SimpleActionGroup, -} - -impl Screen { - /// Create a new screen. - pub fn new() -> Self { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, adw::WindowTitle, window_title); - get_widget!(builder, gio::Menu, menu); - get_widget!(builder, gtk::ToggleButton, search_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::Box, content_box); - - let actions = gio::SimpleActionGroup::new(); - widget.insert_action_group("widget", Some(&actions)); - - search_button.connect_toggled(clone!(@strong search_entry => move |search_button| { - if search_button.is_active() { - search_entry.grab_focus(); - } - })); - - Self { - widget, - back_button, - window_title, - menu, - search_entry, - stack, - content_box, - actions, - } - } - - /// Set a closure to be called when the back button is pressed. - pub fn set_back_cb(&self, cb: F) { - self.back_button.connect_clicked(move |_| cb()); - } - - /// Show a title in the header bar. - pub fn set_title(&self, title: &str) { - self.window_title.set_title(title); - } - - /// Show a subtitle in the header bar. - pub fn set_subtitle(&self, subtitle: &str) { - self.window_title.set_subtitle(subtitle); - } - - /// Add a new item to the action menu and register a callback for it. - pub fn add_action(&self, label: &str, cb: F) { - let name = rand::random::().to_string(); - let action = gio::SimpleAction::new(&name, None); - action.connect_activate(move |_, _| cb()); - - self.actions.add_action(&action); - self.menu - .append(Some(label), Some(&format!("widget.{}", name))); - } - - /// Set the closure to be called when the search string has changed. - pub fn set_search_cb(&self, cb: F) { - self.search_entry.connect_search_changed(move |_| cb()); - } - - /// Get the current search string. - pub fn get_search(&self) -> String { - self.search_entry.text().to_string().to_lowercase() - } - - /// Hide the loading page and switch to the content. - pub fn ready(&self) { - self.stack.set_visible_child_name("content"); - } - - /// Add content to the bottom of the content area. - pub fn add_content>(&self, content: &W) { - self.content_box.append(content); - } -} diff --git a/crates/musicus/src/widgets/section.rs b/crates/musicus/src/widgets/section.rs deleted file mode 100644 index 0e78319..0000000 --- a/crates/musicus/src/widgets/section.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::Widget; -use gtk::prelude::*; -use gtk_macros::get_widget; - -/// A widget displaying a title, a framed child widget and, if needed, some -/// actions. -pub struct Section { - /// The actual GTK widget. - pub widget: gtk::Box, - - /// The box containing the title and action buttons. - title_box: gtk::Box, - - /// An optional subtitle below the title. - subtitle_label: gtk::Label, -} - -impl Section { - /// Create a new section. - pub fn new(title: &str, content: &W) -> Self { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Box, title_box); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - - title_label.set_label(title); - widget.append(&content.get_widget()); - - Self { - widget, - title_box, - subtitle_label, - } - } - - /// Add a subtitle below the title. - pub fn set_subtitle(&self, subtitle: &str) { - self.subtitle_label.set_label(subtitle); - self.subtitle_label.show(); - } - - /// Add an action button. This should by definition be something that is - /// doing something with the child widget that is applicable in all - /// situations where the widget is visible. The new button will be packed - /// to the end of the title box. - pub fn add_action(&self, icon_name: &str, cb: F) { - let button = gtk::Button::builder() - .has_frame(false) - .valign(gtk::Align::Center) - .margin_top(12) - .icon_name(icon_name) - .build(); - - button.connect_clicked(move |_| cb()); - - self.title_box.append(&button); - } -} - -impl Widget for Section { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/widgets/track_row.rs b/crates/musicus/src/widgets/track_row.rs deleted file mode 100644 index 3df9b6b..0000000 --- a/crates/musicus/src/widgets/track_row.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::Widget; -use gtk::prelude::*; -use gtk_macros::get_widget; -use musicus_backend::db::Track; - -/// A widget for showing a single track in a list. -pub struct TrackRow { - /// The actual GTK widget. - pub widget: gtk::ListBoxRow, -} - -impl TrackRow { - /// Create a new track row. - pub fn new(track: &Track, show_header: bool, playing: bool) -> Self { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_row.ui"); - - get_widget!(builder, gtk::ListBoxRow, widget); - get_widget!(builder, gtk::Revealer, playing_revealer); - get_widget!(builder, gtk::Image, playing_image); - get_widget!(builder, gtk::Box, header_box); - get_widget!(builder, gtk::Label, work_title_label); - get_widget!(builder, gtk::Label, performances_label); - get_widget!(builder, gtk::Label, track_title_label); - - playing_revealer.set_reveal_child(playing); - - let mut parts = Vec::<&str>::new(); - for part in &track.work_parts { - parts.push(&track.recording.work.parts[*part].title); - } - - if parts.is_empty() || show_header { - work_title_label.set_text(&track.recording.work.get_title()); - performances_label.set_text(&track.recording.get_performers()); - header_box.show(); - } else { - playing_image.set_margin_top(0); - } - - if !parts.is_empty() { - track_title_label.set_text(&parts.join(", ")); - } else { - track_title_label.hide(); - } - - Self { widget } - } -} - -impl Widget for TrackRow { - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } -} diff --git a/crates/musicus/src/window.rs b/crates/musicus/src/window.rs deleted file mode 100644 index 35a48f7..0000000 --- a/crates/musicus/src/window.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::navigator::Navigator; -use crate::screens::{MainScreen, WelcomeScreen}; - -use adw::prelude::*; -use glib::clone; - -use musicus_backend::{Backend, BackendState}; -use std::rc::Rc; - -/// The main window of this application. This will also handle initializing and managing the -/// backend. -pub struct Window { - window: adw::ApplicationWindow, - backend: Rc, - navigator: Rc, -} - -impl Window { - pub fn new(app: >k::Application) -> Rc { - let backend = Rc::new(Backend::new()); - - let window = adw::ApplicationWindow::new(app); - window.set_title(Some("Musicus")); - window.set_default_size(1000, 707); - - let loading_screen = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - - let header = gtk::HeaderBar::builder() - .title_widget(&adw::WindowTitle::new("Musicus", "")) - .build(); - - let spinner = gtk::Spinner::builder() - .hexpand(true) - .vexpand(true) - .halign(gtk::Align::Center) - .valign(gtk::Align::Center) - .width_request(32) - .height_request(32) - .spinning(true) - .build(); - - loading_screen.append(&header); - loading_screen.append(&spinner); - - let navigator = Navigator::new(Rc::clone(&backend), &window, &loading_screen); - window.set_content(Some(&navigator.widget)); - - let this = Rc::new(Self { - backend, - window, - navigator, - }); - - // Listen for backend state changes. - this.backend - .set_state_cb(clone!(@weak this => move |state| { - match state { - BackendState::Loading => this.navigator.reset(), - BackendState::NoMusicLibrary => this.show_welcome_screen(), - BackendState::Ready => this.show_main_screen(), - } - })); - - // Initialize the backend. - Rc::clone(&this.backend).init().unwrap(); - - this - } - - /// Present this window to the user. - pub fn present(&self) { - self.window.present(); - } - - /// Replace the current screen with the welcome screen. - fn show_welcome_screen(self: &Rc) { - let this = self; - spawn!(@clone this, async move { - replace!(this.navigator, WelcomeScreen).await; - }); - } - - /// Replace the current screen with the main screen. - fn show_main_screen(self: &Rc) { - let this = self; - spawn!(@clone this, async move { - replace!(this.navigator, MainScreen).await; - }); - } -} diff --git a/data/de.johrpan.musicus.desktop.in b/data/de.johrpan.musicus.desktop.in deleted file mode 100644 index bf2ce63..0000000 --- a/data/de.johrpan.musicus.desktop.in +++ /dev/null @@ -1,8 +0,0 @@ -[Desktop Entry] -Name=Musicus -Icon=de.johrpan.musicus -Exec=musicus -Terminal=false -Type=Application -Categories=GTK; -StartupNotify=true diff --git a/data/de.johrpan.musicus.gschema.xml b/data/de.johrpan.musicus.gschema.xml deleted file mode 100644 index 39e6bce..0000000 --- a/data/de.johrpan.musicus.gschema.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - "" - Path to the music library folder - - - false - Keep playing after the playlist ends - - - true - Choose full recordings for random playback - - - diff --git a/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg b/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg deleted file mode 100644 index e1785e1..0000000 --- a/data/icons/hicolor/scalable/apps/de.johrpan.musicus.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg b/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg deleted file mode 100644 index c59702a..0000000 --- a/data/icons/hicolor/symbolic/apps/de.johrpan.musicus-symbolic.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/data/meson.build b/data/meson.build deleted file mode 100644 index bfab2e7..0000000 --- a/data/meson.build +++ /dev/null @@ -1,40 +0,0 @@ -datadir = get_option('datadir') - -scalable_dir = join_paths('icons', 'hicolor', 'scalable', 'apps') -install_data( - join_paths(scalable_dir, 'de.johrpan.musicus.svg'), - install_dir: join_paths(datadir, scalable_dir), -) - -symbolic_dir = join_paths('icons', 'hicolor', 'symbolic', 'apps') -install_data( - join_paths(symbolic_dir, 'de.johrpan.musicus-symbolic.svg'), - install_dir: join_paths(datadir, symbolic_dir), -) - -desktop_file = i18n.merge_file( - input: 'de.johrpan.musicus.desktop.in', - output: 'de.johrpan.musicus.desktop', - type: 'desktop', - po_dir: '../po', - install: true, - install_dir: join_paths(datadir, 'applications') -) - -desktop_utils = find_program('desktop-file-validate', required: false) -if desktop_utils.found() - test('Validate desktop file', desktop_utils, - args: [desktop_file] - ) -endif - -install_data('de.johrpan.musicus.gschema.xml', - install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') -) - -compile_schemas = find_program('glib-compile-schemas', required: false) -if compile_schemas.found() - test('Validate schema file', compile_schemas, - args: ['--strict', '--dry-run', meson.current_source_dir()] - ) -endif diff --git a/de.johrpan.musicus.json b/de.johrpan.musicus.json deleted file mode 100644 index 4d8b3fe..0000000 --- a/de.johrpan.musicus.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "app-id": "de.johrpan.musicus", - "runtime": "org.gnome.Platform", - "runtime-version": "master", - "sdk": "org.gnome.Sdk", - "sdk-extensions": [ - "org.freedesktop.Sdk.Extension.rust-stable" - ], - "command": "musicus", - "finish-args": [ - "--share=network", - "--share=ipc", - "--socket=x11", - "--socket=wayland", - "--socket=pulseaudio", - "--filesystem=host", - "--talk-name=org.mpris.MediaPlayer2.Player", - "--own-name=org.mpris.MediaPlayer2.de.johrpan.musicus", - "--device=all" - ], - "build-options": { - "append-path": "/usr/lib/sdk/rust-stable/bin", - "build-args": [ - "--share=network" - ], - "env": { - "RUSTFLAGS": "-L=/app/lib", - "CARGO_HOME": "/run/build/musicus/cargo", - "RUST_BACKTRACE": "1", - "RUST_LOG": "musicus=debug" - } - }, - "cleanup": [ - "/include", - "/lib/pkgconfig", - "/man", - "/share/doc", - "/share/gtk-doc", - "/share/man", - "/share/pkgconfig", - "*.la", - "*.a" - ], - "modules": [{ - "name": "cdparanoia", - "buildsystem": "simple", - "build-commands": [ - "cp /usr/share/automake-*/config.{sub,guess} .", - "./configure --prefix=/app", - "make all slib", - "make install" - ], - "sources": [{ - "type": "archive", - "url": "http://downloads.xiph.org/releases/cdparanoia/cdparanoia-III-10.2.src.tgz", - "sha256": "005db45ef4ee017f5c32ec124f913a0546e77014266c6a1c50df902a55fe64df" - }] - }, - { - "name": "gst-plugins-base", - "buildsystem": "meson", - "config-opts": [ - "--prefix=/app", - "-Dauto_features=disabled", - "-Dcdparanoia=enabled" - ], - "cleanup": ["*.la", "/share/gtk-doc"], - "sources": [{ - "type": "git", - "url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git", - "branch": "1.16.2", - "commit": "9d3581b2e6f12f0b7e790d1ebb63b90cf5b1ef4e" - }] - }, - { - "name": "musicus", - "builddir": true, - "buildsystem": "meson", - "sources": [{ - "type": "git", - "url": "." - }] - } - ] -} \ No newline at end of file diff --git a/meson.build b/meson.build deleted file mode 100644 index 0e757aa..0000000 --- a/meson.build +++ /dev/null @@ -1,24 +0,0 @@ -project('musicus', - version: '0.1.0', - meson_version: '>= 0.50.0', - license: 'AGPLv3+', -) - -dependency('dbus-1', version: '>= 1.3') -dependency('glib-2.0', version: '>= 2.56') -dependency('gio-2.0', version: '>= 2.56') -dependency('gstreamer-1.0', version: '>= 1.12') -dependency('gtk4', version: '>= 4.0') -dependency('libadwaita-1', version: '>= 1.2') -dependency('pango', version: '>= 1.0') -dependency('sqlite3', version: '>= 3.20') - -i18n = import('i18n') - -subdir('data') -subdir('po') - -subdir('crates/musicus/res') -subdir('crates/musicus/src') - -meson.add_install_script('build-aux/postinstall.py') diff --git a/po/LINGUAS b/po/LINGUAS deleted file mode 100644 index c42e816..0000000 --- a/po/LINGUAS +++ /dev/null @@ -1 +0,0 @@ -de \ No newline at end of file diff --git a/po/NOTES.md b/po/NOTES.md deleted file mode 100644 index 6ac3519..0000000 --- a/po/NOTES.md +++ /dev/null @@ -1,21 +0,0 @@ -All commands should be executed from this directory! - -Regenerate `POTFILES.in` using: - -```bash -find ../crates \( -name \*.rs -o -name \*.ui \) -print > POTFILES.in -``` - -Update `musicus.pot` using: - -```bash -xgettext -f POTFILES.in -o musicus.pot -``` - -Update the translation files using e.g.: - -```bash -msgmerge de.po musicus.pot > tmp.po -# Inspect tmp.po for errors. -mv tmp.po de.po -``` diff --git a/po/POTFILES.in b/po/POTFILES.in deleted file mode 100644 index fc12dbc..0000000 --- a/po/POTFILES.in +++ /dev/null @@ -1,89 +0,0 @@ -../crates/musicus/src/editors/ensemble.rs -../crates/musicus/src/editors/instrument.rs -../crates/musicus/src/editors/mod.rs -../crates/musicus/src/editors/performance.rs -../crates/musicus/src/editors/person.rs -../crates/musicus/src/editors/recording.rs -../crates/musicus/src/editors/work.rs -../crates/musicus/src/editors/work_part.rs -../crates/musicus/src/import/import_screen.rs -../crates/musicus/src/import/medium_editor.rs -../crates/musicus/src/import/medium_preview.rs -../crates/musicus/src/import/mod.rs -../crates/musicus/src/import/source_selector.rs -../crates/musicus/src/import/track_editor.rs -../crates/musicus/src/import/track_selector.rs -../crates/musicus/src/import/track_set_editor.rs -../crates/musicus/src/macros.rs -../crates/musicus/src/main.rs -../crates/musicus/src/navigator/mod.rs -../crates/musicus/src/navigator/window.rs -../crates/musicus/src/preferences.rs -../crates/musicus/src/screens/ensemble.rs -../crates/musicus/src/screens/main.rs -../crates/musicus/src/screens/medium.rs -../crates/musicus/src/screens/mod.rs -../crates/musicus/src/screens/person.rs -../crates/musicus/src/screens/player.rs -../crates/musicus/src/screens/recording.rs -../crates/musicus/src/screens/welcome.rs -../crates/musicus/src/screens/work.rs -../crates/musicus/src/selectors/ensemble.rs -../crates/musicus/src/selectors/instrument.rs -../crates/musicus/src/selectors/medium.rs -../crates/musicus/src/selectors/mod.rs -../crates/musicus/src/selectors/person.rs -../crates/musicus/src/selectors/recording.rs -../crates/musicus/src/selectors/selector.rs -../crates/musicus/src/selectors/work.rs -../crates/musicus/src/widgets/button_row.rs -../crates/musicus/src/widgets/editor.rs -../crates/musicus/src/widgets/indexed_list_model.rs -../crates/musicus/src/widgets/list.rs -../crates/musicus/src/widgets/mod.rs -../crates/musicus/src/widgets/player_bar.rs -../crates/musicus/src/widgets/screen.rs -../crates/musicus/src/widgets/section.rs -../crates/musicus/src/widgets/track_row.rs -../crates/musicus/src/window.rs -../crates/musicus/src/config.rs -../crates/musicus/src/resources.rs -../crates/musicus/res/ui/editor.ui -../crates/musicus/res/ui/import_screen.ui -../crates/musicus/res/ui/main_screen.ui -../crates/musicus/res/ui/medium_editor.ui -../crates/musicus/res/ui/medium_preview.ui -../crates/musicus/res/ui/performance_editor.ui -../crates/musicus/res/ui/player_bar.ui -../crates/musicus/res/ui/player_screen.ui -../crates/musicus/res/ui/preferences.ui -../crates/musicus/res/ui/recording_editor.ui -../crates/musicus/res/ui/screen.ui -../crates/musicus/res/ui/section.ui -../crates/musicus/res/ui/selector.ui -../crates/musicus/res/ui/source_selector.ui -../crates/musicus/res/ui/track_editor.ui -../crates/musicus/res/ui/track_row.ui -../crates/musicus/res/ui/track_selector.ui -../crates/musicus/res/ui/track_set_editor.ui -../crates/musicus/res/ui/work_editor.ui -../crates/musicus/res/ui/work_part_editor.ui -../crates/backend/src/error.rs -../crates/backend/src/lib.rs -../crates/backend/src/library.rs -../crates/backend/src/logger.rs -../crates/backend/src/player.rs -../crates/database/src/ensembles.rs -../crates/database/src/error.rs -../crates/database/src/instruments.rs -../crates/database/src/lib.rs -../crates/database/src/medium.rs -../crates/database/src/persons.rs -../crates/database/src/recordings.rs -../crates/database/src/schema.rs -../crates/database/src/works.rs -../crates/import/src/disc.rs -../crates/import/src/error.rs -../crates/import/src/folder.rs -../crates/import/src/lib.rs -../crates/import/src/session.rs diff --git a/po/de.po b/po/de.po deleted file mode 100644 index be2b9f4..0000000 --- a/po/de.po +++ /dev/null @@ -1,549 +0,0 @@ -# -# <>, YEAR-2022. -# -msgid "" -msgstr "" -"Project-Id-Version: unnamed project\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-26 14:49+0200\n" -"PO-Revision-Date: 2022-02-10 12:34+0100\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: de\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: Gtranslator 41.0\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: ../crates/musicus/src/editors/ensemble.rs:31 -#: ../crates/musicus/src/editors/instrument.rs:31 -msgid "Name" -msgstr "Name" - -#: ../crates/musicus/src/editors/ensemble.rs:34 -#: ../crates/musicus/src/editors/instrument.rs:34 -#: ../crates/musicus/src/editors/person.rs:44 -#: ../crates/musicus/res/ui/preferences.ui:11 -msgid "General" -msgstr "Allgemein" - -#: ../crates/musicus/src/editors/ensemble.rs:65 -msgid "Failed to save ensemble!" -msgstr "Ensemble konnte nicht gespeichert werden!" - -#: ../crates/musicus/src/editors/instrument.rs:65 -msgid "Failed to save instrument!" -msgstr "Instrument konnte nicht gespeichert werden!" - -#: ../crates/musicus/src/editors/performance.rs:44 -msgid "Performer" -msgstr "Interpret" - -#: ../crates/musicus/src/editors/performance.rs:46 -msgid "Select either a person or an ensemble as a performer." -msgstr "Wählen Sie entweder eine Person oder ein Ensemble als Interpreten aus." - -#: ../crates/musicus/src/editors/performance.rs:65 -msgid "Role" -msgstr "Rolle" - -#: ../crates/musicus/src/editors/performance.rs:67 -msgid "Optionally, choose a role to specify what the performer does." -msgstr "" -"Wählen Sie optional eine Rolle aus, die angibt, was der Interpret macht." - -#: ../crates/musicus/src/editors/person.rs:34 -msgid "First name" -msgstr "Vorname" - -#: ../crates/musicus/src/editors/person.rs:38 -msgid "Last name" -msgstr "Nachname" - -#: ../crates/musicus/src/editors/person.rs:78 -msgid "Failed to save person!" -msgstr "Person konnte nicht gespeichert werden!" - -#: ../crates/musicus/src/editors/recording.rs:174 -#: ../crates/musicus/res/ui/work_editor.ui:18 -#: ../crates/musicus/res/ui/work_editor.ui:173 -msgid "Work" -msgstr "Werk" - -#: ../crates/musicus/src/editors/work.rs:240 -msgid "Composer" -msgstr "Komponist" - -#: ../crates/musicus/src/import/medium_preview.rs:172 -#: ../crates/musicus/src/import/track_set_editor.rs:134 -#: ../crates/musicus/src/screens/medium.rs:73 -#: ../crates/musicus/src/screens/recording.rs:83 -msgid "Unknown" -msgstr "Unbekannt" - -#: ../crates/musicus/src/import/source_selector.rs:46 -#: ../crates/musicus/src/screens/welcome.rs:30 -#: ../crates/musicus/res/ui/source_selector.ui:41 -msgid "Select folder" -msgstr "Ordner auswählen" - -#: ../crates/musicus/src/import/source_selector.rs:50 -#: ../crates/musicus/src/preferences.rs:43 -#: ../crates/musicus/src/screens/welcome.rs:57 -#: ../crates/musicus/res/ui/editor.ui:22 -#: ../crates/musicus/res/ui/medium_editor.ui:192 -msgid "Cancel" -msgstr "Abbrechen" - -#: ../crates/musicus/src/import/source_selector.rs:51 -#: ../crates/musicus/src/preferences.rs:44 -#: ../crates/musicus/src/screens/welcome.rs:58 -#: ../crates/musicus/res/ui/import_screen.ui:160 -#: ../crates/musicus/res/ui/performance_editor.ui:56 -#: ../crates/musicus/res/ui/performance_editor.ui:69 -#: ../crates/musicus/res/ui/performance_editor.ui:89 -#: ../crates/musicus/res/ui/preferences.ui:23 -#: ../crates/musicus/res/ui/recording_editor.ui:81 -#: ../crates/musicus/res/ui/track_set_editor.ui:67 -#: ../crates/musicus/res/ui/work_editor.ui:81 -msgid "Select" -msgstr "Auswählen" - -#: ../crates/musicus/src/preferences.rs:39 -#: ../crates/musicus/src/screens/welcome.rs:53 -msgid "Select music library folder" -msgstr "Ordner der Musikbibliothek auswählen" - -#: ../crates/musicus/src/screens/ensemble.rs:50 -msgid "Edit ensemble" -msgstr "Ensemble bearbeiten" - -#: ../crates/musicus/src/screens/ensemble.rs:60 -msgid "Delete ensemble" -msgstr "Ensemble löschen" - -#: ../crates/musicus/src/screens/main.rs:202 -msgid "Musicus" -msgstr "Musicus" - -#: ../crates/musicus/src/screens/main.rs:204 -msgid "The classical music player and organizer." -msgstr "Das Programm zum Abspielen und Organisieren von Klassik." - -#: ../crates/musicus/src/screens/main.rs:206 -msgid "Further information and source code" -msgstr "Weitere Informationen und Quellcode" - -#: ../crates/musicus/src/screens/medium.rs:43 -msgid "Edit medium" -msgstr "Medium bearbeiten" - -#: ../crates/musicus/src/screens/medium.rs:50 -msgid "Delete medium" -msgstr "Medium löschen" - -#: ../crates/musicus/src/screens/person.rs:55 -msgid "Edit person" -msgstr "Person bearbeiten" - -#: ../crates/musicus/src/screens/person.rs:65 -msgid "Delete person" -msgstr "Person löschen" - -#: ../crates/musicus/src/screens/recording.rs:31 -#: ../crates/musicus/res/ui/track_set_editor.ui:88 -msgid "Tracks" -msgstr "Tracks" - -#: ../crates/musicus/src/screens/recording.rs:54 -msgid "Edit recording" -msgstr "Aufnahme bearbeiten" - -#: ../crates/musicus/src/screens/recording.rs:64 -msgid "Delete recording" -msgstr "Aufnahme löschen" - -#: ../crates/musicus/src/screens/welcome.rs:35 -#: ../crates/musicus/res/ui/main_screen.ui:17 -msgid "Welcome to Musicus!" -msgstr "Willkommen bei Musicus!" - -#: ../crates/musicus/src/screens/welcome.rs:37 -msgid "" -"Get startet by selecting the folder containing your music " -"files! Musicus will create a new database there or open one that already " -"exists." -msgstr "" -"Wählen Sie als Erstes den Ordner aus, worin sich Ihre Musik befindet. " -"Musicus wird dort eine neue Datenbank anlegen oder eine bereits existierende " -"öffnen." - -#: ../crates/musicus/src/screens/work.rs:46 -msgid "Edit work" -msgstr "Werk bearbeiten" - -#: ../crates/musicus/src/screens/work.rs:56 -msgid "Delete work" -msgstr "Werk löschen" - -#: ../crates/musicus/src/selectors/ensemble.rs:25 -msgid "Select ensemble" -msgstr "Ensemble auswählen" - -#: ../crates/musicus/src/selectors/instrument.rs:25 -msgid "Select instrument" -msgstr "Instrument auswählen" - -#: ../crates/musicus/src/selectors/medium.rs:23 -msgid "Select performer" -msgstr "Interpreten auswählen" - -#: ../crates/musicus/src/selectors/medium.rs:92 -msgid "Select medium" -msgstr "Medium auswählen" - -#: ../crates/musicus/src/selectors/person.rs:25 -msgid "Select person" -msgstr "Person auswählen" - -#: ../crates/musicus/src/selectors/recording.rs:24 -#: ../crates/musicus/src/selectors/work.rs:24 -msgid "Select composer" -msgstr "Komponisten auswählen" - -#: ../crates/musicus/src/selectors/recording.rs:107 -#: ../crates/musicus/src/selectors/work.rs:97 -msgid "Select work" -msgstr "Werk auswählen" - -#: ../crates/musicus/src/selectors/recording.rs:170 -msgid "Select recording" -msgstr "Aufnahme auswählen" - -#: ../crates/musicus/res/ui/editor.ui:27 -msgid "Save" -msgstr "Speichern" - -#: ../crates/musicus/res/ui/editor.ui:68 -#: ../crates/musicus/res/ui/medium_editor.ui:143 -#: ../crates/musicus/res/ui/medium_editor.ui:180 -#: ../crates/musicus/res/ui/medium_preview.ui:149 -#: ../crates/musicus/res/ui/source_selector.ui:101 -msgid "Error" -msgstr "Fehler" - -#: ../crates/musicus/res/ui/editor.ui:80 -#: ../crates/musicus/res/ui/medium_editor.ui:155 -#: ../crates/musicus/res/ui/medium_preview.ui:161 -#: ../crates/musicus/res/ui/source_selector.ui:113 -msgid "Try again" -msgstr "Nochmal versuchen" - -#: ../crates/musicus/res/ui/import_screen.ui:13 -#: ../crates/musicus/res/ui/medium_editor.ui:19 -#: ../crates/musicus/res/ui/source_selector.ui:31 -#: ../crates/musicus/res/ui/track_set_editor.ui:13 -msgid "Import music" -msgstr "Musik importieren" - -#: ../crates/musicus/res/ui/import_screen.ui:42 -msgid "Matching metadata" -msgstr "Passende Metadaten" - -#: ../crates/musicus/res/ui/import_screen.ui:64 -msgid "Loading…" -msgstr "Lade…" - -#: ../crates/musicus/res/ui/import_screen.ui:88 -msgid "Error while searching for matching metadata" -msgstr "Fehler bei der Suche nach passenden Metadaten" - -#: ../crates/musicus/res/ui/import_screen.ui:114 -msgid "No matching metadata found" -msgstr "Keine passenden Metadaten gefunden" - -#: ../crates/musicus/res/ui/import_screen.ui:144 -msgid "Manually add metadata" -msgstr "Metadaten manuell hinzufügen" - -#: ../crates/musicus/res/ui/import_screen.ui:156 -msgid "Select existing medium" -msgstr "Existierendes Medium auswählen" - -#: ../crates/musicus/res/ui/import_screen.ui:169 -msgid "Add a new medium" -msgstr "Neues Medium hinzufügen" - -#: ../crates/musicus/res/ui/import_screen.ui:173 -msgid "Add" -msgstr "Hinzufügen" - -#: ../crates/musicus/res/ui/main_screen.ui:18 -msgid "" -"Get startet by selecting something from the sidebar or adding new things to " -"your library using the button in the top left corner." -msgstr "" -"Legen Sie los, indem Sie etwas in der Seitenleiste auswählen oder fügen Sie " -"mit dem Knopf oben links neue Aufnahmen zu Ihrer Musikbibliothek hinzu." - -#: ../crates/musicus/res/ui/main_screen.ui:27 -msgid "Play something" -msgstr "Musik abspielen" - -#: ../crates/musicus/res/ui/main_screen.ui:88 -msgid "Search persons and ensembles …" -msgstr "Personen und Ensembles durchsuchen …" - -#: ../crates/musicus/res/ui/main_screen.ui:150 -msgid "Preferences" -msgstr "Einstellungen" - -#: ../crates/musicus/res/ui/main_screen.ui:154 -msgid "About Musicus" -msgstr "Über Musicus" - -#: ../crates/musicus/res/ui/medium_editor.ui:61 -msgid "Medium" -msgstr "Medium" - -#: ../crates/musicus/res/ui/medium_editor.ui:72 -msgid "Name of the medium" -msgstr "Name des Mediums" - -#: ../crates/musicus/res/ui/medium_editor.ui:90 -msgid "Recordings" -msgstr "Aufnahmen" - -#: ../crates/musicus/res/ui/medium_preview.ui:19 -msgid "Preview" -msgstr "Vorschau" - -#: ../crates/musicus/res/ui/medium_preview.ui:50 -msgid "Import" -msgstr "Importieren" - -#: ../crates/musicus/res/ui/medium_preview.ui:117 -#: ../crates/musicus/res/ui/source_selector.ui:69 -msgid "Loading" -msgstr "Lade…" - -#: ../crates/musicus/res/ui/performance_editor.ui:13 -msgid "Performance" -msgstr "Auftritt" - -#: ../crates/musicus/res/ui/performance_editor.ui:52 -msgid "Select a person" -msgstr "Person auswählen" - -#: ../crates/musicus/res/ui/performance_editor.ui:65 -msgid "Select an ensemble" -msgstr "Ensemble auswählen" - -#: ../crates/musicus/res/ui/performance_editor.ui:78 -msgid "Select a role" -msgstr "Rolle auswählen" - -#: ../crates/musicus/res/ui/player_bar.ui:65 -#: ../crates/musicus/res/ui/player_screen.ui:97 -#: ../crates/musicus/res/ui/work_editor.ui:89 -#: ../crates/musicus/res/ui/work_part_editor.ui:56 -msgid "Title" -msgstr "Titel" - -#: ../crates/musicus/res/ui/player_bar.ui:75 -#: ../crates/musicus/res/ui/player_screen.ui:107 -msgid "Subtitle" -msgstr "Untertitel" - -#: ../crates/musicus/res/ui/player_bar.ui:86 -#: ../crates/musicus/res/ui/player_bar.ui:96 -#: ../crates/musicus/res/ui/player_screen.ui:131 -#: ../crates/musicus/res/ui/player_screen.ui:142 -msgid "0:00" -msgstr "0:00" - -#: ../crates/musicus/res/ui/player_bar.ui:91 -msgid "/" -msgstr "/" - -#: ../crates/musicus/res/ui/player_screen.ui:19 -msgid "Player" -msgstr "Wiedergabe" - -#: ../crates/musicus/res/ui/preferences.ui:14 -msgid "Music library" -msgstr "Musikbibliothek" - -#: ../crates/musicus/res/ui/preferences.ui:18 -msgid "Music library folder" -msgstr "Ordner der Musikbibliothek" - -#: ../crates/musicus/res/ui/preferences.ui:20 -msgid "None selected" -msgstr "Keiner ausgewählt" - -#: ../crates/musicus/res/ui/preferences.ui:34 -msgid "Playlist" -msgstr "Wiedergabeliste" - -#: ../crates/musicus/res/ui/preferences.ui:38 -msgid "Keep playing" -msgstr "Weiter abspielen" - -#: ../crates/musicus/res/ui/preferences.ui:40 -msgid "Whether to keep playing random tracks after the playlist ends." -msgstr "Nach dem Ende der Wiedergabeliste weiter im Zufallsmodus abspielen." - -#: ../crates/musicus/res/ui/preferences.ui:51 -msgid "Choose full recordings" -msgstr "Komplette Aufnahmen abspielen" - -#: ../crates/musicus/res/ui/preferences.ui:53 -msgid "" -"Whether to choose full recordings instead of single tracks for random " -"playback." -msgstr "" -"Im Zufallsmodus vollständige Aufnahmen anstatt einzelner Tracks verwenden." - -#: ../crates/musicus/res/ui/recording_editor.ui:18 -#: ../crates/musicus/res/ui/recording_editor.ui:146 -#: ../crates/musicus/res/ui/track_set_editor.ui:51 -msgid "Recording" -msgstr "Aufnahme" - -#: ../crates/musicus/res/ui/recording_editor.ui:65 -#: ../crates/musicus/res/ui/work_editor.ui:65 -msgid "Overview" -msgstr "Überblick" - -#: ../crates/musicus/res/ui/recording_editor.ui:77 -msgid "Select a work" -msgstr "Werk auswählen" - -#: ../crates/musicus/res/ui/recording_editor.ui:89 -msgid "Comment" -msgstr "Kommentar" - -#: ../crates/musicus/res/ui/recording_editor.ui:107 -msgid "Performers" -msgstr "Interpreten" - -#: ../crates/musicus/res/ui/selector.ui:60 -msgid "Search …" -msgstr "Suchen…" - -#: ../crates/musicus/res/ui/source_selector.ui:32 -msgid "Select the source which contains the new audio files below." -msgstr "Wählen Sie die Quelle mit den Audiodateien unten aus." - -#: ../crates/musicus/res/ui/source_selector.ui:46 -msgid "Copy audio CD" -msgstr "Audio-CD kopieren" - -#: ../crates/musicus/res/ui/track_editor.ui:13 -msgid "Track" -msgstr "Track" - -#: ../crates/musicus/res/ui/track_selector.ui:13 -msgid "Select tracks" -msgstr "Tracks auswählen" - -#: ../crates/musicus/res/ui/track_set_editor.ui:63 -msgid "Select a recording" -msgstr "Aufnahme auswählen" - -#: ../crates/musicus/res/ui/work_editor.ui:77 -msgid "Select a composer" -msgstr "Komponisten auswählen" - -#: ../crates/musicus/res/ui/work_editor.ui:107 -msgid "Instruments" -msgstr "Instrumente" - -#: ../crates/musicus/res/ui/work_editor.ui:134 -msgid "Structure" -msgstr "Struktur" - -#: ../crates/musicus/res/ui/work_part_editor.ui:13 -msgid "Work part" -msgstr "Werkabschnitt" - -#, fuzzy -#~ msgid "Personal data" -#~ msgstr "Person" - -#~ msgid "Ensemble" -#~ msgstr "Ensemble" - -#~ msgid "Search recordings …" -#~ msgstr "Aufnahmen durchsuchen …" - -#~ msgid "No recordings found." -#~ msgstr "Keine Aufnahmen gefunden." - -#~ msgid "No ensembles found." -#~ msgstr "Keine Ensembles gefunden." - -#~ msgid "Instrument" -#~ msgstr "Instrument" - -#~ msgid "No instruments found." -#~ msgstr "Keine Instrumente gefunden." - -#~ msgid "Select …" -#~ msgstr "Auswählen …" - -#~ msgid "Type" -#~ msgstr "Typ" - -#~ msgid "Search persons …" -#~ msgstr "Personen durchsuchen …" - -#~ msgid "Search works and recordings …" -#~ msgstr "Werke und Aufnahmen durchsuchen …" - -#~ msgid "Works" -#~ msgstr "Werke" - -#~ msgid "No works or recordings found." -#~ msgstr "Keine Werke oder Aufnahmen gefunden." - -#~ msgid "Select a composer on the left." -#~ msgstr "Wählen Sie einen Komponisten aus." - -#~ msgid "Work section" -#~ msgstr "Werkteil" - -#~ msgid "Select a recording of a work with multiple parts." -#~ msgstr "Wählen Sie eine Aufnahme eines mehrteiligen Werks aus." - -#~ msgid "No performers added." -#~ msgstr "Keine Interpreten hinzugefügt." - -#~ msgid "No works found." -#~ msgstr "Keine Werke gefunden." - -#~ msgid "Add some tracks." -#~ msgstr "Fügen Sie Tracks hinzu." - -#~ msgid "Select audio files" -#~ msgstr "Audiodateien auswählen" - -#~ msgid "No instruments added." -#~ msgstr "Keine Instrumente hinzugefügt." - -#~ msgid "No work parts added." -#~ msgstr "Keine Werkabschnitte hinzugefügt." - -#~ msgid "Edit tracks" -#~ msgstr "Tracks bearbeiten" - -#~ msgid "No tracks found." -#~ msgstr "Keine Tracks gefunden." - -#~ msgid "No persons found." -#~ msgstr "Keine Personen gefunden." - -#~ msgid "No persons or ensembles found." -#~ msgstr "Keine Personen oder Ensembles gefunden." diff --git a/po/meson.build b/po/meson.build deleted file mode 100644 index 38f16f1..0000000 --- a/po/meson.build +++ /dev/null @@ -1 +0,0 @@ -i18n.gettext('musicus', preset: 'glib') diff --git a/po/musicus.pot b/po/musicus.pot deleted file mode 100644 index 096818c..0000000 --- a/po/musicus.pot +++ /dev/null @@ -1,464 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-26 14:49+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../crates/musicus/src/editors/ensemble.rs:31 -#: ../crates/musicus/src/editors/instrument.rs:31 -msgid "Name" -msgstr "" - -#: ../crates/musicus/src/editors/ensemble.rs:34 -#: ../crates/musicus/src/editors/instrument.rs:34 -#: ../crates/musicus/src/editors/person.rs:44 -#: ../crates/musicus/res/ui/preferences.ui:11 -msgid "General" -msgstr "" - -#: ../crates/musicus/src/editors/ensemble.rs:65 -msgid "Failed to save ensemble!" -msgstr "" - -#: ../crates/musicus/src/editors/instrument.rs:65 -msgid "Failed to save instrument!" -msgstr "" - -#: ../crates/musicus/src/editors/performance.rs:44 -msgid "Performer" -msgstr "" - -#: ../crates/musicus/src/editors/performance.rs:46 -msgid "Select either a person or an ensemble as a performer." -msgstr "" - -#: ../crates/musicus/src/editors/performance.rs:65 -msgid "Role" -msgstr "" - -#: ../crates/musicus/src/editors/performance.rs:67 -msgid "Optionally, choose a role to specify what the performer does." -msgstr "" - -#: ../crates/musicus/src/editors/person.rs:34 -msgid "First name" -msgstr "" - -#: ../crates/musicus/src/editors/person.rs:38 -msgid "Last name" -msgstr "" - -#: ../crates/musicus/src/editors/person.rs:78 -msgid "Failed to save person!" -msgstr "" - -#: ../crates/musicus/src/editors/recording.rs:174 -#: ../crates/musicus/res/ui/work_editor.ui:18 -#: ../crates/musicus/res/ui/work_editor.ui:173 -msgid "Work" -msgstr "" - -#: ../crates/musicus/src/editors/work.rs:240 -msgid "Composer" -msgstr "" - -#: ../crates/musicus/src/import/medium_preview.rs:172 -#: ../crates/musicus/src/import/track_set_editor.rs:134 -#: ../crates/musicus/src/screens/medium.rs:73 -#: ../crates/musicus/src/screens/recording.rs:83 -msgid "Unknown" -msgstr "" - -#: ../crates/musicus/src/import/source_selector.rs:46 -#: ../crates/musicus/src/screens/welcome.rs:30 -#: ../crates/musicus/res/ui/source_selector.ui:41 -msgid "Select folder" -msgstr "" - -#: ../crates/musicus/src/import/source_selector.rs:50 -#: ../crates/musicus/src/preferences.rs:43 -#: ../crates/musicus/src/screens/welcome.rs:57 -#: ../crates/musicus/res/ui/editor.ui:22 -#: ../crates/musicus/res/ui/medium_editor.ui:192 -msgid "Cancel" -msgstr "" - -#: ../crates/musicus/src/import/source_selector.rs:51 -#: ../crates/musicus/src/preferences.rs:44 -#: ../crates/musicus/src/screens/welcome.rs:58 -#: ../crates/musicus/res/ui/import_screen.ui:160 -#: ../crates/musicus/res/ui/performance_editor.ui:56 -#: ../crates/musicus/res/ui/performance_editor.ui:69 -#: ../crates/musicus/res/ui/performance_editor.ui:89 -#: ../crates/musicus/res/ui/preferences.ui:23 -#: ../crates/musicus/res/ui/recording_editor.ui:81 -#: ../crates/musicus/res/ui/track_set_editor.ui:67 -#: ../crates/musicus/res/ui/work_editor.ui:81 -msgid "Select" -msgstr "" - -#: ../crates/musicus/src/preferences.rs:39 -#: ../crates/musicus/src/screens/welcome.rs:53 -msgid "Select music library folder" -msgstr "" - -#: ../crates/musicus/src/screens/ensemble.rs:50 -msgid "Edit ensemble" -msgstr "" - -#: ../crates/musicus/src/screens/ensemble.rs:60 -msgid "Delete ensemble" -msgstr "" - -#: ../crates/musicus/src/screens/main.rs:202 -msgid "Musicus" -msgstr "" - -#: ../crates/musicus/src/screens/main.rs:204 -msgid "The classical music player and organizer." -msgstr "" - -#: ../crates/musicus/src/screens/main.rs:206 -msgid "Further information and source code" -msgstr "" - -#: ../crates/musicus/src/screens/medium.rs:43 -msgid "Edit medium" -msgstr "" - -#: ../crates/musicus/src/screens/medium.rs:50 -msgid "Delete medium" -msgstr "" - -#: ../crates/musicus/src/screens/person.rs:55 -msgid "Edit person" -msgstr "" - -#: ../crates/musicus/src/screens/person.rs:65 -msgid "Delete person" -msgstr "" - -#: ../crates/musicus/src/screens/recording.rs:31 -#: ../crates/musicus/res/ui/track_set_editor.ui:88 -msgid "Tracks" -msgstr "" - -#: ../crates/musicus/src/screens/recording.rs:54 -msgid "Edit recording" -msgstr "" - -#: ../crates/musicus/src/screens/recording.rs:64 -msgid "Delete recording" -msgstr "" - -#: ../crates/musicus/src/screens/welcome.rs:35 -#: ../crates/musicus/res/ui/main_screen.ui:17 -msgid "Welcome to Musicus!" -msgstr "" - -#: ../crates/musicus/src/screens/welcome.rs:37 -msgid "" -"Get startet by selecting the folder containing your music " -"files! Musicus will create a new database there or open one that already " -"exists." -msgstr "" - -#: ../crates/musicus/src/screens/work.rs:46 -msgid "Edit work" -msgstr "" - -#: ../crates/musicus/src/screens/work.rs:56 -msgid "Delete work" -msgstr "" - -#: ../crates/musicus/src/selectors/ensemble.rs:25 -msgid "Select ensemble" -msgstr "" - -#: ../crates/musicus/src/selectors/instrument.rs:25 -msgid "Select instrument" -msgstr "" - -#: ../crates/musicus/src/selectors/medium.rs:23 -msgid "Select performer" -msgstr "" - -#: ../crates/musicus/src/selectors/medium.rs:92 -msgid "Select medium" -msgstr "" - -#: ../crates/musicus/src/selectors/person.rs:25 -msgid "Select person" -msgstr "" - -#: ../crates/musicus/src/selectors/recording.rs:24 -#: ../crates/musicus/src/selectors/work.rs:24 -msgid "Select composer" -msgstr "" - -#: ../crates/musicus/src/selectors/recording.rs:107 -#: ../crates/musicus/src/selectors/work.rs:97 -msgid "Select work" -msgstr "" - -#: ../crates/musicus/src/selectors/recording.rs:170 -msgid "Select recording" -msgstr "" - -#: ../crates/musicus/res/ui/editor.ui:27 -msgid "Save" -msgstr "" - -#: ../crates/musicus/res/ui/editor.ui:68 -#: ../crates/musicus/res/ui/medium_editor.ui:143 -#: ../crates/musicus/res/ui/medium_editor.ui:180 -#: ../crates/musicus/res/ui/medium_preview.ui:149 -#: ../crates/musicus/res/ui/source_selector.ui:101 -msgid "Error" -msgstr "" - -#: ../crates/musicus/res/ui/editor.ui:80 -#: ../crates/musicus/res/ui/medium_editor.ui:155 -#: ../crates/musicus/res/ui/medium_preview.ui:161 -#: ../crates/musicus/res/ui/source_selector.ui:113 -msgid "Try again" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:13 -#: ../crates/musicus/res/ui/medium_editor.ui:19 -#: ../crates/musicus/res/ui/source_selector.ui:31 -#: ../crates/musicus/res/ui/track_set_editor.ui:13 -msgid "Import music" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:42 -msgid "Matching metadata" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:64 -msgid "Loading…" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:88 -msgid "Error while searching for matching metadata" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:114 -msgid "No matching metadata found" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:144 -msgid "Manually add metadata" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:156 -msgid "Select existing medium" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:169 -msgid "Add a new medium" -msgstr "" - -#: ../crates/musicus/res/ui/import_screen.ui:173 -msgid "Add" -msgstr "" - -#: ../crates/musicus/res/ui/main_screen.ui:18 -msgid "" -"Get startet by selecting something from the sidebar or adding new things to " -"your library using the button in the top left corner." -msgstr "" - -#: ../crates/musicus/res/ui/main_screen.ui:27 -msgid "Play something" -msgstr "" - -#: ../crates/musicus/res/ui/main_screen.ui:88 -msgid "Search persons and ensembles …" -msgstr "" - -#: ../crates/musicus/res/ui/main_screen.ui:150 -msgid "Preferences" -msgstr "" - -#: ../crates/musicus/res/ui/main_screen.ui:154 -msgid "About Musicus" -msgstr "" - -#: ../crates/musicus/res/ui/medium_editor.ui:61 -msgid "Medium" -msgstr "" - -#: ../crates/musicus/res/ui/medium_editor.ui:72 -msgid "Name of the medium" -msgstr "" - -#: ../crates/musicus/res/ui/medium_editor.ui:90 -msgid "Recordings" -msgstr "" - -#: ../crates/musicus/res/ui/medium_preview.ui:19 -msgid "Preview" -msgstr "" - -#: ../crates/musicus/res/ui/medium_preview.ui:50 -msgid "Import" -msgstr "" - -#: ../crates/musicus/res/ui/medium_preview.ui:117 -#: ../crates/musicus/res/ui/source_selector.ui:69 -msgid "Loading" -msgstr "" - -#: ../crates/musicus/res/ui/performance_editor.ui:13 -msgid "Performance" -msgstr "" - -#: ../crates/musicus/res/ui/performance_editor.ui:52 -msgid "Select a person" -msgstr "" - -#: ../crates/musicus/res/ui/performance_editor.ui:65 -msgid "Select an ensemble" -msgstr "" - -#: ../crates/musicus/res/ui/performance_editor.ui:78 -msgid "Select a role" -msgstr "" - -#: ../crates/musicus/res/ui/player_bar.ui:65 -#: ../crates/musicus/res/ui/player_screen.ui:97 -#: ../crates/musicus/res/ui/work_editor.ui:89 -#: ../crates/musicus/res/ui/work_part_editor.ui:56 -msgid "Title" -msgstr "" - -#: ../crates/musicus/res/ui/player_bar.ui:75 -#: ../crates/musicus/res/ui/player_screen.ui:107 -msgid "Subtitle" -msgstr "" - -#: ../crates/musicus/res/ui/player_bar.ui:86 -#: ../crates/musicus/res/ui/player_bar.ui:96 -#: ../crates/musicus/res/ui/player_screen.ui:131 -#: ../crates/musicus/res/ui/player_screen.ui:142 -msgid "0:00" -msgstr "" - -#: ../crates/musicus/res/ui/player_bar.ui:91 -msgid "/" -msgstr "" - -#: ../crates/musicus/res/ui/player_screen.ui:19 -msgid "Player" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:14 -msgid "Music library" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:18 -msgid "Music library folder" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:20 -msgid "None selected" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:34 -msgid "Playlist" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:38 -msgid "Keep playing" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:40 -msgid "Whether to keep playing random tracks after the playlist ends." -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:51 -msgid "Choose full recordings" -msgstr "" - -#: ../crates/musicus/res/ui/preferences.ui:53 -msgid "" -"Whether to choose full recordings instead of single tracks for random " -"playback." -msgstr "" - -#: ../crates/musicus/res/ui/recording_editor.ui:18 -#: ../crates/musicus/res/ui/recording_editor.ui:146 -#: ../crates/musicus/res/ui/track_set_editor.ui:51 -msgid "Recording" -msgstr "" - -#: ../crates/musicus/res/ui/recording_editor.ui:65 -#: ../crates/musicus/res/ui/work_editor.ui:65 -msgid "Overview" -msgstr "" - -#: ../crates/musicus/res/ui/recording_editor.ui:77 -msgid "Select a work" -msgstr "" - -#: ../crates/musicus/res/ui/recording_editor.ui:89 -msgid "Comment" -msgstr "" - -#: ../crates/musicus/res/ui/recording_editor.ui:107 -msgid "Performers" -msgstr "" - -#: ../crates/musicus/res/ui/selector.ui:60 -msgid "Search …" -msgstr "" - -#: ../crates/musicus/res/ui/source_selector.ui:32 -msgid "Select the source which contains the new audio files below." -msgstr "" - -#: ../crates/musicus/res/ui/source_selector.ui:46 -msgid "Copy audio CD" -msgstr "" - -#: ../crates/musicus/res/ui/track_editor.ui:13 -msgid "Track" -msgstr "" - -#: ../crates/musicus/res/ui/track_selector.ui:13 -msgid "Select tracks" -msgstr "" - -#: ../crates/musicus/res/ui/track_set_editor.ui:63 -msgid "Select a recording" -msgstr "" - -#: ../crates/musicus/res/ui/work_editor.ui:77 -msgid "Select a composer" -msgstr "" - -#: ../crates/musicus/res/ui/work_editor.ui:107 -msgid "Instruments" -msgstr "" - -#: ../crates/musicus/res/ui/work_editor.ui:134 -msgid "Structure" -msgstr "" - -#: ../crates/musicus/res/ui/work_part_editor.ui:13 -msgid "Work part" -msgstr ""