diff --git a/.gitignore b/.gitignore index 567609b1..6be34d98 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,19 @@ build/ + +debian/* +!debian/source/ +!debian/changelog.in +!debian/compat +!debian/control.in +!debian/copyright +!debian/rules + +flatpak/com.github.tkashkin.gamehub.json +flatpak/.flatpak-builder/ + +*~ + +.buildconfig + +snap/* +!snap/snapcraft.yaml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..75336a76 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "flatpak/libs/shared"] + path = flatpak/libs/shared + url = https://github.com/flathub/shared-modules.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d81c0620 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +--- + +language: node_js + +node_js: + - lts/* + +sudo: required + +services: + - docker + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - libstdc++-5-dev + +cache: + directories: + - /tmp/liftoff + +install: + - npm install @elementaryos/houston + +script: + - houston ci diff --git a/COPYING b/COPYING index 8cffccc1..f288702d 100644 --- a/COPYING +++ b/COPYING @@ -1,13 +1,674 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 - Copyright (C) 2004 Sam Hocevar + 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. - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. + Preamble - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + The GNU General Public License is a free, copyleft license for +software and other kinds of works. - 0. You just DO WHAT THE FUCK YOU WANT TO. \ No newline at end of file + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 63eac847..222b0fa3 100644 --- a/README.md +++ b/README.md @@ -1 +1,93 @@ -# GameHub +# [GameHub](https://tkashkin.tk/projects/gamehub) [![Build status](https://ci.appveyor.com/api/projects/status/cgw5hc4kos4uvmy9/branch/master?svg=true)](https://ci.appveyor.com/project/tkashkin/gamehub/branch/master) [![Translation status](https://hosted.weblate.org/widgets/gamehub/-/translations/svg-badge.svg)](https://hosted.weblate.org/engage/gamehub/?utm_source=widget) +Unified library for all your games, written in Vala using GTK+3, designed for elementary OS. + +### Game sources +GameHub supports multiple game sources and services: +* Steam +* GOG +* Humble Bundle +* Humble Trove + +Locally installed games can also be added to GameHub. + +### Features +GameHub allows to view, download, install, run and uninstall games from supported sources. + +It also allows to download bonus content and DLCs for GOG games. + +### Games +GameHub supports non-native games as well as native games for Linux. + +It supports multiple [compatibility layers](https://github.com/tkashkin/GameHub/wiki/Compatibility-layers) for non-native games: +* Wine / Proton +* DOSBox +* ScummVM +* RetroArch + +It also allows to add custom emulators. + +## Installation +Prebuilt releases can be found on [releases page](https://github.com/tkashkin/GameHub/releases). + +### Ubuntu-based distros +Use prebuilt deb packages from [releases page](https://github.com/tkashkin/GameHub/releases) or add a [PPA](https://launchpad.net/~tkashkin/+archive/ubuntu/gamehub) and install with `apt`: +```bash +# install if `add-apt-repository` is not available +sudo apt install --no-install-recommends software-properties-common + +sudo add-apt-repository ppa:tkashkin/gamehub +sudo apt update +sudo apt install com.github.tkashkin.gamehub +``` + +### Arch Linux +[gamehub-git](https://aur.archlinux.org/packages/gamehub-git/) is available in AUR: +```bash +aurman -S gamehub-git +``` +Package is maintained by [@btd1337](https://github.com/btd1337). + +## Building + +### Debian/Ubuntu-based distros + +#### Build dependencies +* `meson` +* `valac` +* `libgranite-dev` +* `libgtk-3-dev` +* `libglib2.0-dev` +* `libwebkit2gtk-4.0-dev` +* `libjson-glib-dev` +* `libgee-0.8-dev` +* `libsoup2.4-dev` +* `libsqlite3-dev` +* `libxml2-dev` +* `libmanette-0.2-dev`, `libx11-dev`, `libxtst-dev` (optional for gamepad support) + +#### Building +```bash +git clone https://github.com/tkashkin/GameHub.git +cd GameHub +scripts/build.sh build_deb +``` + +### Any distro, without package manager +```bash +git clone https://github.com/tkashkin/GameHub.git +cd GameHub +CFLAGS="$CFLAGS -O0" meson build --prefix=/usr -Ddistro=generic --buildtype=debug +cd build +ninja +sudo ninja install +``` + +### flatpak +```bash +git clone https://github.com/tkashkin/GameHub.git +cd GameHub +scripts/build.sh build_flatpak +``` + +## [Screenshots](https://tkashkin.tk/projects/gamehub/#/screenshots) +

diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..f3498e90 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,54 @@ +version: 0.12.1-{build}-{branch} + +pull_requests: + do_not_increment_build_number: true + +skip_tags: true + +image: + - Ubuntu1804 + - Ubuntu1604 + +clone_folder: ~/build/GameHub +clone_depth: 1 + +environment: + keys_enc_secret: + secure: VBUP6GQXENGa7E+H90WDhA== + +build_script: + - sh: bash scripts/build.sh build_deb + - sh: bash scripts/build.sh build + - sh: bash scripts/build.sh appimage + - sh: bash scripts/build.sh appimage_tweak + - sh: bash scripts/build.sh appimage_bundle_libs + - sh: bash scripts/build.sh appimage_checkrt + - sh: bash scripts/build.sh appimage_pack + - sh: bash scripts/build.sh build_flatpak + +install: + - sh: bash scripts/build.sh import_keys + - sh: bash scripts/build.sh deps + +test: off + +artifacts: + - path: build/*/*.deb + name: deb + - path: build/appimage/GameHub*.AppImage* + name: AppImage + - path: build/flatpak/GameHub*.flatpak + name: flatpak + +deploy: + - provider: GitHub + description: | + CI build + + * Use `bionic` packages for recent distros (>= Ubuntu 18.04) + * Use `xenial` packages for older distros (>= Ubuntu 16.04) + auth_token: + secure: J2LCcNeVYvzbvHRa/LChp+SmN6UKbg1ELsA4jmxnObCbX+ZyZ9DFH+S2aQIoA3dG + artifact: deb,AppImage,flatpak + draft: false + prerelease: true diff --git a/data/GameHub.css b/data/GameHub.css old mode 100644 new mode 100755 index ce713e6d..4564443b --- a/data/GameHub.css +++ b/data/GameHub.css @@ -1,13 +1,478 @@ .gamecard { - border-radius: 2px; + opacity: 0.75; + transition: 100ms; + background-color: alpha(#999, 0.5); + color: white; + -gtk-icon-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.gamecard, .gamecard border +{ + border: none; + border-radius: 4px; +} + +.gamecard.installed, .gamecard.static +{ + opacity: 1; } -.gamecard GtkLabel +.gamecard.hover:not(.installed), +.games-grid flowboxchild:focus .gamecard:not(.installed), +.games-grid flowboxchild:selected .gamecard:not(.installed) +{ + opacity: 0.8; +} + +.gamecard GtkLabel, .gamecard label { color: rgba(255, 255, 255, 0.9); font-weight: bold; + margin-top: 16px; font-size: 1.3em; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); +} + +.gamecard GtkLabel.status, .gamecard label.status +{ + color: rgba(255, 255, 255, 0.6); + font-size: 0.9em; + margin-top: -8px; + margin-bottom: -16px; + opacity: 0; + transition: 100ms; +} + +.gamecard.installing GtkLabel.status, .gamecard.installing label.status, +.gamecard.hover GtkLabel.status, .gamecard.hover label.status, +.gamecard.running GtkLabel.status, .gamecard.running label.status, +.games-grid flowboxchild:focus .gamecard GtkLabel.status, .games-grid flowboxchild:focus .gamecard label.status, +.games-grid flowboxchild:selected .gamecard GtkLabel.status, .games-grid flowboxchild:selected .gamecard label.status +{ + margin-bottom: 0px; + opacity: 1; +} + +.gamecard.downloading GtkLabel.status, .gamecard.downloading label.status +{ + margin-bottom: 8px; + opacity: 1; +} + +.gamecard .info +{ + background-color: transparent; background-image: linear-gradient(to top, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0)); + border-radius: 0 0 4px 4px; + border: none; + outline: none; + box-shadow: none; +} + +.gamecard .actions +{ + background-color: rgba(0, 0, 0, 0.4); + border: 1px rgba(0, 0, 0, 0.6) solid; + border-radius: 4px; + transition: 100ms; +} + +.gamecard.installed .actions, .gamecard.static .actions +{ + background-color: rgba(0, 0, 0, 0); +} + +.gamecard.hover .actions, +.games-grid flowboxchild:focus .gamecard .actions, +.games-grid flowboxchild:selected .gamecard .actions +{ + background-color: rgba(0, 0, 0, 0.2); +} + +.gamecard.installed.hover .actions, +.games-grid flowboxchild:focus .gamecard.installed .actions, +.games-grid flowboxchild:selected .gamecard.installed .actions +{ + background-color: rgba(255, 255, 255, 0.2); +} + +.gamecard .progress +{ + background-color: rgba(255, 255, 255, 0.6); + border-radius: 4px; + transition: 100ms; + margin-bottom: -16px; + opacity: 0; +} + +.gamecard.downloading .progress +{ + margin-bottom: 0; + opacity: 1; +} + +.gamecard image +{ + opacity: 0.6; + transition: 100ms; +} + +.gamecard.hover image, +.games-grid flowboxchild:focus image, +.games-grid flowboxchild:selected image +{ + opacity: 0.8; +} + +.games-grid flowboxchild +{ + border-radius: 4px; + padding: 0; +} + +.games-grid flowboxchild:focus +{ + background: @theme_selected_bg_color; +} + +.gamecard.running .actions, +.gamecard.hover.running .actions, +.games-grid flowboxchild:focus .gamecard.running .actions, +.games-grid flowboxchild:selected .gamecard.running .actions +{ + background-color: rgba(0, 0, 0, 0.6); +} +.gamecard .running-indicator +{ + opacity: 1 !important; +} + +/* Some granite styles */ +.h1 +{ + font-size: 24pt; +} +.h2 +{ + font-weight: 300; + font-size: 18pt; +} +.h3 +{ + font-size: 11pt; +} +.h4, .category-label +{ + font-weight: bold; +} +.h4, .gameinfo-singleline-value +{ + padding-bottom: 6px; + padding-top: 6px; +} +.description-header +{ + padding-bottom: 0; +} +GtkList .h4, list .h4 +{ + padding-left: 6px; +} +GtkButton GtkLabel, button label +{ + padding: 0 6px; +} +.card +{ + border: none; + border-color: transparent; + box-shadow: 0 0 0 1px alpha(#000, 0.05), 0 3px 3px alpha(#000, 0.22); + border-radius: 4px; +} +GtkListBox:not(:backdrop) .list-row:selected:focus GtkLabel.category-label, +list:not(:backdrop) row:selected:focus label.category-label +{ + color: alpha(#ffffff, 0.8); +} + +GtkListBox.installers-list, +list.installers-list +{ + background: transparent; +} + + +GtkListBox.installers-list .list-row, +list.installers-list row +{ + border-radius: 2px; +} + +GtkListBox.downloads-list .list-row, +list.downloads-list row +{ + background: transparent; + border-bottom: 1px alpha(#333, 0.3) solid; + outline: none; +} +GtkListBox.downloads-list .list-row:last-child, +list.downloads-list row:last-child +{ + border-bottom: none; +} +GtkListBox.downloads-list .list-row:selected, +list.downloads-list row:selected +{ + background: transparent; + outline: none; +} +GtkListBox.downloads-list .list-row:focus, +list.downloads-list row:focus +{ + background: transparent; + outline: none; +} + +GtkLabel.games-list-header, +label.games-list-header +{ + background: alpha(#333, 0.1); + margin-top: -1px; + border-top: 1px alpha(#333, 0.3) solid; + border-bottom: 1px alpha(#333, 0.3) solid; +} + +.gameinfo-background +{ + background-color: rgb(245, 245, 245); + color: rgb(66, 66, 66); +} +.gameinfo-background.dark +{ + background-color: rgb(59, 63, 69); + color: white; +} + +.dl-progress-type-icon +{ + padding: 4px; + background-color: #ddd; + border-radius: 50%; + border: 1px #aaa solid; +} + +GtkListBox.gameinfo-content-list, +list.gameinfo-content-list +{ + background: transparent; +} +GtkListBox.gameinfo-content-list .list-row, +list.gameinfo-content-list row +{ + background: transparent; + border-bottom: 1px alpha(#888, 0.2) solid; + outline: none; + padding: 0; + margin: 0; +} +GtkListBox.gameinfo-content-list .list-row:last-child, +list.gameinfo-content-list row:last-child +{ + border-bottom: none; +} +GtkListBox.gameinfo-content-list .list-row:selected, +list.gameinfo-content-list row:selected +{ + outline: none; +} +GtkListBox.gameinfo-content-list .list-row:focus, +list.gameinfo-content-list row:focus +{ + background: transparent; + outline: none; +} +GtkListBox.gameinfo-content-list .list-row:hover, +list.gameinfo-content-list row:hover +{ + background: alpha(#888, 0.1); +} + +.gameinfo-content-list .progress +{ + border: none; + outline: none; + background: alpha(#888, 0.2); + opacity: 0; + transition: 100ms; +} +.gameinfo-content-list .progress.downloading +{ + opacity: 1; +} + +.gameinfo-toolbar +{ + border: none; + border-bottom: 1px alpha(#333, 0.3) solid; + box-shadow: none; + border-radius: 0; + padding: 8px 16px; + margin: 0; + background: alpha(#333, 0.1); +} + +.gameinfo-toolbar revealer, .gameinfo-toolbar box +{ + border: none; + box-shadow: none; + border-radius: 0; + padding: 0; + margin: 0; + background: transparent; +} + +.dark .gameinfo-toolbar +{ + border-bottom: 1px alpha(#333, 0.8) solid; + background: alpha(#333, 0.3); +} + +dialog .gameinfo-toolbar, dialog .dark .gameinfo-toolbar +{ + background: transparent; +} + +GtkListBox.tags-list, +list.tags-list +{ + background: transparent; +} +GtkListBox.tags-list .list-row, +list.tags-list row +{ + background: transparent; + outline: none; + padding: 0; + margin: 0; + border-radius: 2px; +} +GtkListBox.tags-list.not-rounded .list-row, +list.tags-list.not-rounded row +{ + border-radius: 0; +} +GtkListBox.tags-list .list-row:last-child, +list.tags-list row:last-child +{ + border-bottom: none; +} +GtkListBox.tags-list .list-row:selected, +list.tags-list row:selected +{ + outline: none; +} +GtkListBox.tags-list .list-row:focus, +list.tags-list row:focus +{ + background: @theme_selected_bg_color; + color: @theme_selected_fg_color; + outline: none; +} +GtkListBox.tags-list .list-row:hover, +list.tags-list row:hover +{ + background: alpha(#888, 0.1); +} +GtkListBox.tags-list .list-row:focus:hover, +list.tags-list row:focus:hover +{ + background: shade(@theme_selected_bg_color, 0.9); +} + +.tags-list-header.hover +{ + background: alpha(#888, 0.1); +} +.tags-list-header label +{ + padding: 2px 0; +} +.tags-list-header:focus +{ + background: @theme_selected_bg_color; + color: @theme_selected_fg_color; +} +.tags-list-header.hover:focus +{ + background: shade(@theme_selected_bg_color, 0.9); +} +.tags-list-header:focus label +{ + color: @theme_selected_fg_color; +} + +GtkListBox.overlays-list, +list.overlays-list +{ + background: transparent; +} +GtkListBox.overlays-list .list-row, +list.overlays-list row +{ + background: transparent; + outline: none; + padding: 0; + margin: 0; + border-radius: 2px; + border-bottom: 1px alpha(#888, 0.2) solid; +} +GtkListBox.overlays-list .list-row:last-child, +list.overlays-list row:last-child +{ + border-bottom: none; +} +GtkListBox.overlays-list .list-row:selected, +list.overlays-list row:selected +{ + outline: none; +} +GtkListBox.overlays-list .list-row:hover, +list.overlays-list row:hover +{ + background: alpha(#888, 0.1); +} + +.link, .link label +{ + padding: 0; +} + +button.file label +{ + padding: 0 2px; +} + +checkbutton:not(.default-padding), checkbutton:not(.default-padding) check +{ + padding: 0; + margin: 0; +} + +dialog .sidebar, +dialog .sidebar list +{ + background: transparent; +} + +.filters-sort-mode > * +{ + padding: 4px; + min-width: 16px; + min-height: 16px; +} +.filters-sort-mode > * > image +{ + padding: 0; + margin: 0; } diff --git a/data/com.github.tkashkin.gamehub-overlayfs-helper b/data/com.github.tkashkin.gamehub-overlayfs-helper new file mode 100755 index 00000000..ad3a14d4 --- /dev/null +++ b/data/com.github.tkashkin.gamehub-overlayfs-helper @@ -0,0 +1,26 @@ +#!/bin/bash + +if [[ $EUID -ne 0 ]]; then + echo "This script requires root permissions" + exit 1 +fi + +ACTION="$1" +OVERLAY_ID="$2" + +case "$ACTION" in + mount) + MOUNT_OPTIONS="$3" + TARGET="$4" + mount -t overlay "$OVERLAY_ID" -o "$MOUNT_OPTIONS" "$TARGET" + ;; + + umount) + umount "$OVERLAY_ID" + ;; + + *) + echo "This script only allows to (u)mount overlays" + exit 2 + ;; +esac diff --git a/data/com.github.tkashkin.gamehub.appdata.xml.in b/data/com.github.tkashkin.gamehub.appdata.xml.in index 9ce9ea03..94530b8c 100644 --- a/data/com.github.tkashkin.gamehub.appdata.xml.in +++ b/data/com.github.tkashkin.gamehub.appdata.xml.in @@ -1,30 +1,263 @@ - com.github.tkashkin.gamehub.desktop - + com.github.tkashkin.gamehub + CC0-1.0 - WTFPL - + GPL-3.0+ + GameHub All your games in one place - + -

Manage your Steam and GOG games in one place.

+

Manage your Steam, GOG and Humble Bundle games in one place.

- - tkashkin - https://github.com/tkashkin/GameHub + + Anatoliy Kashkin + ​tkashkin@gmail.com + https://tkashkin.tk/projects/gamehub https://github.com/tkashkin/GameHub/issues - + com.github.tkashkin.gamehub com.github.tkashkin.gamehub.desktop - - - 1 - - + + + + https://raw.githubusercontent.com/tkashkin/GameHub/8b321ac7991d2946b1d331e1a2e525ceb2ece324/data/screenshots/light/welcome%402x.png + + + https://raw.githubusercontent.com/tkashkin/GameHub/8b321ac7991d2946b1d331e1a2e525ceb2ece324/data/screenshots/dark/grid%402x.png + + + https://raw.githubusercontent.com/tkashkin/GameHub/8b321ac7991d2946b1d331e1a2e525ceb2ece324/data/screenshots/light/list%402x.png + + + https://raw.githubusercontent.com/tkashkin/GameHub/8b321ac7991d2946b1d331e1a2e525ceb2ece324/data/screenshots/dark/details%402x.png + + + https://raw.githubusercontent.com/tkashkin/GameHub/8b321ac7991d2946b1d331e1a2e525ceb2ece324/data/screenshots/light/properties%402x.png + + + + + + +
    +
  • Fixes and improvements
  • +
  • Gamepad support
  • +
  • Overlays
  • +
+
+
+ + +
    +
  • Overlays
  • +
+
+
+ + +
    +
  • Gamepad support
  • +
+
+
+ + +
    +
  • UI tweaks
  • +
  • Proton 3.16 support
  • +
  • Basic keyboard/gamepad navigation in grid view
  • +
+
+
+ + + +

User-added games

+
+
+ + +

Fixes for CompatTools

+
+
+ + +

Updated pt_BR localization

+
+
+ + +
    +
  • Humble Trove support
  • +
  • Additional compat tools (CustomScript, DOSBox)
  • +
  • AppImage version
  • +
+
+
+ + +

Compatibility tools support for non-native games:

+
    +
  • Innoextract
  • +
  • Proton
  • +
  • Wine
  • +
+

Database rewrite

+

Performance improvements

+

Bugfixes

+
+
+ + +
    +
  • Basic games collection management
  • +
  • GOG bonus content downloading
  • +
  • GOG multipart installer support
  • +
  • Improved performance and loading times
  • +
  • Non-native games can now be shown and installed
  • +
  • Bugfixes
  • +
+
+
+ + +

Option to merge games from different sources

+
+
+ + +

Settings dialog rework

+

Compact games list

+
+
+ + +

Trying to fix Humble Bundle access token extraction

+
+
+ + +

Search crash fix and UI improvements

+
+
+ + +

UI improvements

+
+
+ + +

Downloader rewrite: now it's possible to pause and cancel downloads

+

Downloads can be resumed after interruption

+

UI improvements

+
+
+ + +

Yet another locale fix

+

Installers cache management

+
+
+ + +

Fix crash for GOG "games" like Hotline Miami 2: Wrong Number - Digital Comics

+

Fix building with GTK+3 < 3.22

+
+
+ + +

Bugfixes

+

Design changes

+

Compatibility

+
+
+ + +

Icon fixes

+
+
+ + +

Runtime injection support

+
+
+ + +

Added localizations:

+
    +
  • pt_BR
  • +
  • pl
  • +
  • uk
  • +
  • de
  • +
+
+
+ + +

Fixes for flatpak packaging.

+
+
+ + +

New features:

+
    +
  • Games list view
  • +
  • Game details view/dialog
  • +
+
+
+ + +

Small features.

+
+
+ + +

Unneeded game sources now can be disabled.

+
+
+ + +

Humble Bundle authentication fix and icons fix.

+
+
+ + +

Humble Bundle support.

+
+
+ + +

Bug fixes.

+
+
+ + +

Settings dialog.

+
+
+ + +

GOG games installation and running.

+
+
+ + +

Steam config path fix.

+
+
+ + +

Initial release.

+

Steam and GOG linux-compatible games fetching.

+
+
+
+ none none diff --git a/data/com.github.tkashkin.gamehub.desktop.in b/data/com.github.tkashkin.gamehub.desktop.in index 9c5d2b20..779c8366 100644 --- a/data/com.github.tkashkin.gamehub.desktop.in +++ b/data/com.github.tkashkin.gamehub.desktop.in @@ -2,8 +2,8 @@ Name=GameHub GenericName=GameHub Comment=All your games in one place -Categories=Game;Utility; -Keywords=Game;Hub;Steam;GOG; +Categories=Game; +Keywords=Game;Hub;Steam;GOG;Humble;HumbleBundle; Exec=com.github.tkashkin.gamehub X-GNOME-Gettext-Domain=com.github.tkashkin.gamehub Icon=com.github.tkashkin.gamehub diff --git a/data/com.github.tkashkin.gamehub.gschema.xml b/data/com.github.tkashkin.gamehub.gschema.xml new file mode 100644 index 00000000..ea287a94 --- /dev/null +++ b/data/com.github.tkashkin.gamehub.gschema.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + "Normal" + + + -1 + + + -1 + + + + -1 + + + -1 + + + "Grid" + + + "Name" + + + + + + false + + + false + + + true + + + false + + + true + + + true + + + + + + true + + + false + + + '8B10B604CAC6AC90F57AACE025DD904C' + + + + + + true + + + false + + + '' + + + '' + + + + + + true + + + false + + + '' + + + true + + + + + + '~/.steam' + + + '~/Games/GOG' + + + '~/Games/HumbleBundle' + + + '/usr/lib/libretro' + + + '/usr/share/libretro/info' + + + + + + '~/Games/_Collection' + + + + + + '$root/GOG/$game' + + + '$game_dir' + + + '$game_dir/dlc' + + + '$game_dir/bonus' + + + + + + '$root/Humble Bundle/$game' + + + '$game_dir' + + + diff --git a/data/com.github.tkashkin.gamehub.policy.in b/data/com.github.tkashkin.gamehub.policy.in new file mode 100644 index 00000000..7e83c264 --- /dev/null +++ b/data/com.github.tkashkin.gamehub.policy.in @@ -0,0 +1,18 @@ + + + + Anatoliy Kashkin + https://tkashkin.tk/projects/gamehub + + + Manage overlays + Authentication is required to manage overlays + drive-removable-media + + yes + yes + yes + + @BINDIR@/@PROJECT_NAME@-overlayfs-helper + + diff --git a/data/icons/gog-white.svg b/data/icons/gog-white.svg deleted file mode 100644 index 7f997e80..00000000 --- a/data/icons/gog-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/data/icons/gog.svg b/data/icons/gog.svg deleted file mode 100644 index f439d5fc..00000000 --- a/data/icons/gog.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/data/icons/icons.gresource.xml b/data/icons/icons.gresource.xml index 5a0725f0..f2e196d6 100644 --- a/data/icons/icons.gresource.xml +++ b/data/icons/icons.gresource.xml @@ -1,11 +1,42 @@ - steam.svg - steam-symbolic.svg - gog.svg - - steam-symbolic-white.svg - gog-white.svg + symbolic/sources/sources-all.svg + symbolic/sources/steam.svg + symbolic/sources/gog.svg + symbolic/sources/humble.svg + symbolic/sources/humble-trove.svg + + symbolic/emu/retroarch.svg + + symbolic/platforms/linux.svg + symbolic/platforms/windows.svg + symbolic/platforms/macos.svg + + symbolic/tools/wine.svg + symbolic/tools/dosbox.svg + symbolic/tools/scummvm.svg + + normal/tags/tag.svg + + symbolic/tags/tag.svg + symbolic/tags/tag-add.svg + symbolic/tags/tag-remove.svg + symbolic/tags/tag-multiple.svg + symbolic/tags/tag-favorites.svg + symbolic/tags/tag-hidden.svg + + normal/controller/a.svg + normal/controller/b.svg + normal/controller/x.svg + normal/controller/y.svg + normal/controller/start.svg + normal/controller/select.svg + normal/controller/bumper-left.svg + normal/controller/bumper-right.svg + normal/controller/trigger-left.svg + normal/controller/trigger-right.svg + normal/controller/grip-left.svg + normal/controller/grip-right.svg diff --git a/data/icons/normal/controller/a.svg b/data/icons/normal/controller/a.svg new file mode 100644 index 00000000..d57790ad --- /dev/null +++ b/data/icons/normal/controller/a.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/b.svg b/data/icons/normal/controller/b.svg new file mode 100644 index 00000000..b5bd3255 --- /dev/null +++ b/data/icons/normal/controller/b.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/bumper-left.svg b/data/icons/normal/controller/bumper-left.svg new file mode 100644 index 00000000..c794ab3c --- /dev/null +++ b/data/icons/normal/controller/bumper-left.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/bumper-right.svg b/data/icons/normal/controller/bumper-right.svg new file mode 100644 index 00000000..2fdc1788 --- /dev/null +++ b/data/icons/normal/controller/bumper-right.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/grip-left.svg b/data/icons/normal/controller/grip-left.svg new file mode 100644 index 00000000..6adcb2d6 --- /dev/null +++ b/data/icons/normal/controller/grip-left.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/grip-right.svg b/data/icons/normal/controller/grip-right.svg new file mode 100644 index 00000000..4099368c --- /dev/null +++ b/data/icons/normal/controller/grip-right.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/select.svg b/data/icons/normal/controller/select.svg new file mode 100644 index 00000000..46664a25 --- /dev/null +++ b/data/icons/normal/controller/select.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/start.svg b/data/icons/normal/controller/start.svg new file mode 100644 index 00000000..fa3bf8e9 --- /dev/null +++ b/data/icons/normal/controller/start.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/trigger-left.svg b/data/icons/normal/controller/trigger-left.svg new file mode 100644 index 00000000..e9344b24 --- /dev/null +++ b/data/icons/normal/controller/trigger-left.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/trigger-right.svg b/data/icons/normal/controller/trigger-right.svg new file mode 100644 index 00000000..267530b7 --- /dev/null +++ b/data/icons/normal/controller/trigger-right.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/x.svg b/data/icons/normal/controller/x.svg new file mode 100644 index 00000000..550c2e46 --- /dev/null +++ b/data/icons/normal/controller/x.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/controller/y.svg b/data/icons/normal/controller/y.svg new file mode 100644 index 00000000..1b5fe9c8 --- /dev/null +++ b/data/icons/normal/controller/y.svg @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/normal/tags/tag.svg b/data/icons/normal/tags/tag.svg new file mode 100644 index 00000000..5b33820a --- /dev/null +++ b/data/icons/normal/tags/tag.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/steam-symbolic-white.svg b/data/icons/steam-symbolic-white.svg deleted file mode 100644 index b063e66e..00000000 --- a/data/icons/steam-symbolic-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/data/icons/steam-symbolic.svg b/data/icons/steam-symbolic.svg deleted file mode 100644 index a89e27f6..00000000 --- a/data/icons/steam-symbolic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/data/icons/steam.svg b/data/icons/steam.svg deleted file mode 100644 index 0c27cadc..00000000 --- a/data/icons/steam.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/icons/symbolic/emu/retroarch.svg b/data/icons/symbolic/emu/retroarch.svg new file mode 100644 index 00000000..7ce5bc72 --- /dev/null +++ b/data/icons/symbolic/emu/retroarch.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/symbolic/platforms/linux.svg b/data/icons/symbolic/platforms/linux.svg new file mode 100644 index 00000000..174b7348 --- /dev/null +++ b/data/icons/symbolic/platforms/linux.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/icons/symbolic/platforms/macos.svg b/data/icons/symbolic/platforms/macos.svg new file mode 100644 index 00000000..609f73b5 --- /dev/null +++ b/data/icons/symbolic/platforms/macos.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/data/icons/symbolic/platforms/windows.svg b/data/icons/symbolic/platforms/windows.svg new file mode 100644 index 00000000..831cb31f --- /dev/null +++ b/data/icons/symbolic/platforms/windows.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/icons/symbolic/sources/gog.svg b/data/icons/symbolic/sources/gog.svg new file mode 100644 index 00000000..83f27ee4 --- /dev/null +++ b/data/icons/symbolic/sources/gog.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/icons/symbolic/sources/humble-trove.svg b/data/icons/symbolic/sources/humble-trove.svg new file mode 100644 index 00000000..2f65244d --- /dev/null +++ b/data/icons/symbolic/sources/humble-trove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/sources/humble.svg b/data/icons/symbolic/sources/humble.svg new file mode 100644 index 00000000..71eee6e0 --- /dev/null +++ b/data/icons/symbolic/sources/humble.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/icons/symbolic/sources/sources-all.svg b/data/icons/symbolic/sources/sources-all.svg new file mode 100644 index 00000000..67a89712 --- /dev/null +++ b/data/icons/symbolic/sources/sources-all.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/data/icons/symbolic/sources/steam.svg b/data/icons/symbolic/sources/steam.svg new file mode 100644 index 00000000..e6abb6de --- /dev/null +++ b/data/icons/symbolic/sources/steam.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/data/icons/symbolic/tags/tag-add.svg b/data/icons/symbolic/tags/tag-add.svg new file mode 100644 index 00000000..98dcaf5b --- /dev/null +++ b/data/icons/symbolic/tags/tag-add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tags/tag-favorites.svg b/data/icons/symbolic/tags/tag-favorites.svg new file mode 100644 index 00000000..bb2a2d32 --- /dev/null +++ b/data/icons/symbolic/tags/tag-favorites.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tags/tag-hidden.svg b/data/icons/symbolic/tags/tag-hidden.svg new file mode 100644 index 00000000..8317d839 --- /dev/null +++ b/data/icons/symbolic/tags/tag-hidden.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tags/tag-multiple.svg b/data/icons/symbolic/tags/tag-multiple.svg new file mode 100644 index 00000000..165a4353 --- /dev/null +++ b/data/icons/symbolic/tags/tag-multiple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tags/tag-remove.svg b/data/icons/symbolic/tags/tag-remove.svg new file mode 100644 index 00000000..c129ae72 --- /dev/null +++ b/data/icons/symbolic/tags/tag-remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tags/tag.svg b/data/icons/symbolic/tags/tag.svg new file mode 100644 index 00000000..28661e25 --- /dev/null +++ b/data/icons/symbolic/tags/tag.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tools/dosbox.svg b/data/icons/symbolic/tools/dosbox.svg new file mode 100644 index 00000000..0d785564 --- /dev/null +++ b/data/icons/symbolic/tools/dosbox.svg @@ -0,0 +1,3 @@ + + + diff --git a/data/icons/symbolic/tools/scummvm.svg b/data/icons/symbolic/tools/scummvm.svg new file mode 100644 index 00000000..722844f0 --- /dev/null +++ b/data/icons/symbolic/tools/scummvm.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/symbolic/tools/wine.svg b/data/icons/symbolic/tools/wine.svg new file mode 100644 index 00000000..f0ebad6f --- /dev/null +++ b/data/icons/symbolic/tools/wine.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/meson.build b/data/meson.build index ceb91917..1e7eecbd 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,32 +1,55 @@ install_data( meson.project_name() + '.svg', - install_dir: join_paths(get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps') + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'icons', 'hicolor', 'scalable', 'apps') ) +install_data( + meson.project_name() + '.gschema.xml', + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'glib-2.0', 'schemas') +) + +install_subdir('share/compat', install_dir: join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())) + i18n.merge_file( - input: meson.project_name() + '.desktop.in', - output: meson.project_name() + '.desktop', - po_dir: join_paths(meson.source_root(), 'po'), - type: 'desktop', - install: true, - install_dir: join_paths(get_option('datadir'), 'applications') + input: meson.project_name() + '.desktop.in', + output: meson.project_name() + '.desktop', + po_dir: join_paths(meson.source_root(), 'po'), + type: 'desktop', + install: true, + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'applications') ) i18n.merge_file( - input: meson.project_name() + '.appdata.xml.in', - output: meson.project_name() + '.appdata.xml', - po_dir: join_paths(meson.source_root(), 'po'), - install: true, - install_dir: join_paths(get_option('datadir'), 'metainfo') + input: meson.project_name() + '.appdata.xml.in', + output: meson.project_name() + '.appdata.xml', + po_dir: join_paths(meson.source_root(), 'po'), + install: true, + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'metainfo') +) + +polkit_policy = configure_file( + input: meson.project_name() + '.policy.in', + output: meson.project_name() + '.policy', + configuration: conf_data +) + +install_data( + polkit_policy, + install_dir: join_paths(get_option('prefix'), get_option('datadir'), 'polkit-1', 'actions') +) + +install_data( + meson.project_name() + '-overlayfs-helper', + install_dir: join_paths(get_option('prefix'), get_option('bindir')) ) icons_gresource = gnome.compile_resources( - 'gresource_icons', - 'icons/icons.gresource.xml', - source_dir: 'icons' + 'gresource_icons', + 'icons/icons.gresource.xml', + source_dir: 'icons' ) css_gresource = gnome.compile_resources( - 'gresource_css', - meson.project_name() + '.gresource.xml' + 'gresource_css', + meson.project_name() + '.gresource.xml' ) diff --git a/data/screenshots/dark/details@2x.png b/data/screenshots/dark/details@2x.png new file mode 100644 index 00000000..9118f4af Binary files /dev/null and b/data/screenshots/dark/details@2x.png differ diff --git a/data/screenshots/dark/grid@2x.png b/data/screenshots/dark/grid@2x.png new file mode 100644 index 00000000..225f2a2d Binary files /dev/null and b/data/screenshots/dark/grid@2x.png differ diff --git a/data/screenshots/dark/grid_gamepad@2x.png b/data/screenshots/dark/grid_gamepad@2x.png new file mode 100644 index 00000000..6193469d Binary files /dev/null and b/data/screenshots/dark/grid_gamepad@2x.png differ diff --git a/data/screenshots/dark/list@2x.png b/data/screenshots/dark/list@2x.png new file mode 100644 index 00000000..d0234566 Binary files /dev/null and b/data/screenshots/dark/list@2x.png differ diff --git a/data/screenshots/dark/settings_collection@2x.png b/data/screenshots/dark/settings_collection@2x.png new file mode 100644 index 00000000..b6a536ed Binary files /dev/null and b/data/screenshots/dark/settings_collection@2x.png differ diff --git a/data/screenshots/dark/welcome@2x.png b/data/screenshots/dark/welcome@2x.png new file mode 100644 index 00000000..6ec3d2f4 Binary files /dev/null and b/data/screenshots/dark/welcome@2x.png differ diff --git a/data/screenshots/light/details@2x.png b/data/screenshots/light/details@2x.png new file mode 100644 index 00000000..bf21037e Binary files /dev/null and b/data/screenshots/light/details@2x.png differ diff --git a/data/screenshots/light/grid@2x.png b/data/screenshots/light/grid@2x.png new file mode 100644 index 00000000..df78c836 Binary files /dev/null and b/data/screenshots/light/grid@2x.png differ diff --git a/data/screenshots/light/grid_gamepad@2x.png b/data/screenshots/light/grid_gamepad@2x.png new file mode 100644 index 00000000..2b1fa7c3 Binary files /dev/null and b/data/screenshots/light/grid_gamepad@2x.png differ diff --git a/data/screenshots/light/install@2x.png b/data/screenshots/light/install@2x.png new file mode 100644 index 00000000..acd64584 Binary files /dev/null and b/data/screenshots/light/install@2x.png differ diff --git a/data/screenshots/light/install_compat@2x.png b/data/screenshots/light/install_compat@2x.png new file mode 100644 index 00000000..d994a227 Binary files /dev/null and b/data/screenshots/light/install_compat@2x.png differ diff --git a/data/screenshots/light/list@2x.png b/data/screenshots/light/list@2x.png new file mode 100644 index 00000000..ab8d3b42 Binary files /dev/null and b/data/screenshots/light/list@2x.png differ diff --git a/data/screenshots/light/overlays@2x.png b/data/screenshots/light/overlays@2x.png new file mode 100644 index 00000000..27713764 Binary files /dev/null and b/data/screenshots/light/overlays@2x.png differ diff --git a/data/screenshots/light/properties@2x.png b/data/screenshots/light/properties@2x.png new file mode 100644 index 00000000..23e015ff Binary files /dev/null and b/data/screenshots/light/properties@2x.png differ diff --git a/data/screenshots/light/settings_collection@2x.png b/data/screenshots/light/settings_collection@2x.png new file mode 100644 index 00000000..a91c3de4 Binary files /dev/null and b/data/screenshots/light/settings_collection@2x.png differ diff --git a/data/screenshots/light/welcome@2x.png b/data/screenshots/light/welcome@2x.png new file mode 100644 index 00000000..5f7372e2 Binary files /dev/null and b/data/screenshots/light/welcome@2x.png differ diff --git a/data/share/compat/dosbox/windowed.conf b/data/share/compat/dosbox/windowed.conf new file mode 100644 index 00000000..e5cc666c --- /dev/null +++ b/data/share/compat/dosbox/windowed.conf @@ -0,0 +1,2 @@ +[sdl] +fullscreen=false diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index c7a5d603..00000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -com.github.tkashkin.gamehub (0.1) loki; urgency=low - - * Initial Release. - - -- tkashkin Thu, 24 May 2018 07:56:30 +0300 diff --git a/debian/changelog.in b/debian/changelog.in new file mode 100644 index 00000000..d5738b58 --- /dev/null +++ b/debian/changelog.in @@ -0,0 +1,244 @@ +com.github.tkashkin.gamehub ($VERSION) $DISTRO; urgency=low + + * CI update + + -- Anatoliy Kashkin $DATE + +com.github.tkashkin.gamehub (0.12.0) bionic; urgency=low + + * Fixes and improvements + * Gamepad support + * Overlays + + -- Anatoliy Kashkin Fri, 09 Nov 2018 01:27:12 +0300 + +com.github.tkashkin.gamehub (0.11.7) bionic; urgency=low + + * Overlays + + -- Anatoliy Kashkin Mon, 05 Nov 2018 05:46:22 +0300 + +com.github.tkashkin.gamehub (0.11.6) bionic; urgency=low + + * Gamepad support + + -- Anatoliy Kashkin Tue, 23 Oct 2018 15:15:24 +0300 + +com.github.tkashkin.gamehub (0.11.5) bionic; urgency=low + + * UI tweaks + * Proton 3.16 support + * Basic keyboard/gamepad navigation in grid view (#75) + + -- Anatoliy Kashkin Sun, 14 Oct 2018 01:27:49 +0300 + +com.github.tkashkin.gamehub (0.11.4) bionic; urgency=low + + * 0.11.4 + + -- Anatoliy Kashkin Thu, 11 Oct 2018 23:57:59 +0300 + +com.github.tkashkin.gamehub (0.11.3) bionic; urgency=low + + * User-added games + + -- Anatoliy Kashkin Sat, 06 Oct 2018 00:28:26 +0300 + +com.github.tkashkin.gamehub (0.11.2) xenial; urgency=low + + * Fixes for CompatTools + + -- Anatoliy Kashkin Sun, 23 Sep 2018 08:55:24 +0300 + +com.github.tkashkin.gamehub (0.11.1) xenial; urgency=low + + * Updated pt_BR localization + + -- Anatoliy Kashkin Wed, 19 Sep 2018 03:19:47 +0300 + +com.github.tkashkin.gamehub (0.11.0) xenial; urgency=low + + * Humble Trove support + * Additional compat tools (CustomScript, DOSBox) + * AppImage version + + -- Anatoliy Kashkin Tue, 18 Sep 2018 09:18:25 +0300 + +com.github.tkashkin.gamehub (0.10.0) xenial; urgency=low + + * Compatibility tools support for non-native games: + * - Innoextract + * - Proton + * - Wine + * Database rewrite + * Performance improvements + * Bugfixes + + -- Anatoliy Kashkin Fri, 14 Sep 2018 13:37:12 +0300 + +com.github.tkashkin.gamehub (0.9.0) xenial; urgency=low + + * Basic games collection management + * GOG bonus content downloading + * GOG multipart installer support + * Improved performance and loading times + * Non-native games can now be shown and installed + * Bugfixes + + -- Anatoliy Kashkin Tue, 04 Sep 2018 09:47:25 +0300 + +com.github.tkashkin.gamehub (0.8.0) xenial; urgency=low + + * Option to merge games from different sources + + -- Anatoliy Kashkin Wed, 15 Aug 2018 16:25:54 +0300 + +com.github.tkashkin.gamehub (0.7.0) xenial; urgency=low + + * Settings dialog rework + * Compact games list + + -- Anatoliy Kashkin Thu, 09 Aug 2018 20:27:23 +0300 + +com.github.tkashkin.gamehub (0.6.3) xenial; urgency=low + + * Trying to fix Humble Bundle access token extraction + + -- Anatoliy Kashkin Mon, 06 Aug 2018 12:20:11 +0300 + +com.github.tkashkin.gamehub (0.6.2) xenial; urgency=low + + * Search crash fix and UI improvements + + -- Anatoliy Kashkin Wed, 25 Jul 2018 20:04:18 +0300 + +com.github.tkashkin.gamehub (0.6.1) xenial; urgency=low + + * UI improvements + + -- Anatoliy Kashkin Tue, 24 Jul 2018 08:01:37 +0300 + +com.github.tkashkin.gamehub (0.6.0) xenial; urgency=low + + * Downloader rewrite: now it's possible to pause and cancel downloads + * Downloads can be resumed after interruption + * UI improvements + + -- Anatoliy Kashkin Sun, 22 Jul 2018 16:18:24 +0300 + +com.github.tkashkin.gamehub (0.5.7) xenial; urgency=low + + * Yet another locale fix + * Installers cache management + + -- Anatoliy Kashkin Sat, 21 Jul 2018 02:39:34 +0300 + +com.github.tkashkin.gamehub (0.5.6) xenial; urgency=low + + * Fix crash for GOG "games" like Hotline Miami 2 Digital Comics + * Fix building with GTK+3 < 3.22 + + -- Anatoliy Kashkin Fri, 20 Jul 2018 19:01:45 +0300 + +com.github.tkashkin.gamehub (0.5.5) xenial; urgency=low + + * Bugfixes + * Design changes + * Compatibility + + -- Anatoliy Kashkin Fri, 20 Jul 2018 17:15:34 +0300 + +com.github.tkashkin.gamehub (0.5.4) xenial; urgency=low + + * Icon fixes + + -- Anatoliy Kashkin Mon, 16 Jul 2018 05:52:48 +0300 + +com.github.tkashkin.gamehub (0.5.3) xenial; urgency=low + + * Runtime injection support + + -- Anatoliy Kashkin Sun, 15 Jul 2018 11:55:24 +0300 + +com.github.tkashkin.gamehub (0.5.2) xenial; urgency=low + + * Added localizations: + * * pt_BR + * * pl + * * uk + * * de + + -- Anatoliy Kashkin Sat, 14 Jul 2018 02:56:19 +0300 + +com.github.tkashkin.gamehub (0.5.1) xenial; urgency=low + + * Fixes for flatpak packaging + + -- Anatoliy Kashkin Fri, 13 Jul 2018 05:28:15 +0300 + +com.github.tkashkin.gamehub (0.5.0) xenial; urgency=low + + * Games list view + * Game details view/dialog + * Games grid improvements + * Bug fixes + + -- Anatoliy Kashkin Mon, 09 Jul 2018 11:44:33 +0300 + +com.github.tkashkin.gamehub (0.4.1) xenial; urgency=low + + * Small features + + -- Anatoliy Kashkin Tue, 07 Jul 2018 09:40:55 +0300 + +com.github.tkashkin.gamehub (0.4.0) xenial; urgency=low + + * Unneeded game sources now can be disabled + * Bug fixes + + -- Anatoliy Kashkin Tue, 03 Jul 2018 04:51:10 +0300 + +com.github.tkashkin.gamehub (0.3.1) xenial; urgency=low + + * Humble Bundle authentication fix + * Humble Bundle game icons fix + + -- Anatoliy Kashkin Fri, 22 Jun 2018 18:03:16 +0300 + +com.github.tkashkin.gamehub (0.3.0) xenial; urgency=low + + * Humble Bundle support + + -- Anatoliy Kashkin Sun, 17 Jun 2018 04:57:12 +0300 + +com.github.tkashkin.gamehub (0.2.5) xenial; urgency=low + + * Bug fixes + + -- Anatoliy Kashkin Tue, 12 Jun 2018 01:18:28 +0300 + +com.github.tkashkin.gamehub (0.2.4) xenial; urgency=low + + * Settings dialog + + -- Anatoliy Kashkin Fri, 01 Jun 2018 23:20:42 +0300 + +com.github.tkashkin.gamehub (0.2.3) xenial; urgency=low + + * GOG games installation and running + * Various improvements + + -- Anatoliy Kashkin Fri, 01 Jun 2018 14:36:24 +0300 + +com.github.tkashkin.gamehub (0.1.3) xenial; urgency=low + + * Fix Steam config path + + -- Anatoliy Kashkin Mon, 28 May 2018 02:28:30 +0300 + +com.github.tkashkin.gamehub (0.1.2) xenial; urgency=low + + * Initial release + * Steam and GOG linux-compatible games fetching + + -- Anatoliy Kashkin Mon, 28 May 2018 00:15:21 +0300 diff --git a/debian/control b/debian/control deleted file mode 100644 index 081d911f..00000000 --- a/debian/control +++ /dev/null @@ -1,14 +0,0 @@ -Source: com.github.tkashkin.gamehub -Section: x11 -Priority: extra -Maintainer: tkashkin -Build-Depends: cmake (>= 2.8), - debhelper (>= 9), - libgtk-3-dev, - valac (>= 0.16) -Standards-Version: 3.9.3 - -Package: com.github.tkashkin.gamehub -Architecture: any -Depends: ${misc:Depends}, ${shlibs:Depends} -Description: Have all your games in one place diff --git a/debian/control.in b/debian/control.in new file mode 100644 index 00000000..1bd81235 --- /dev/null +++ b/debian/control.in @@ -0,0 +1,26 @@ +Source: com.github.tkashkin.gamehub +Section: x11 +Priority: optional +Maintainer: tkashkin +Build-Depends: meson (>= 0.40), + valac (>= 0.16), + debhelper (>= 9), + libgranite-dev, + libgtk-3-dev, + libglib2.0-dev, + libwebkit2gtk-4.0-dev, + libjson-glib-dev, + libgee-0.8-dev, + libsoup2.4-dev, + libsqlite3-dev, + libxml2-dev, + libpolkit-gobject-1-dev, + libx11-dev, libmanette-0.2-dev, libxtst-dev +Standards-Version: 4.1.4 + +Package: com.github.tkashkin.gamehub +Architecture: any +Depends: ${misc:Depends}, ${shlibs:Depends} +Recommends: file-roller, innoextract, wine, dosbox +Suggests: steam +Description: All your games in one place diff --git a/debian/copyright b/debian/copyright index 081c4178..04dae05f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -3,20 +3,20 @@ Upstream-Name: com.github.tkashkin.gamehub Source: https://github.com/tkashkin/GameHub Files: * -Copyright: 2018 tkashkin -License: WTFPL - -License: WTFPL - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - - Copyright (C) 2004 Sam Hocevar - - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. +Copyright: 2018 Anatoliy Kashkin +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/debian/rules b/debian/rules index f82cad85..e0af2d2b 100755 --- a/debian/rules +++ b/debian/rules @@ -1,13 +1,24 @@ #!/usr/bin/make -f # -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 +DEB_BUILD_OPTIONS=noopt nostrip nocheck +CFLAGS+=-O0 %: - dh $@ --buildsystem=meson + dh $@ + +override_dh_auto_clean: + rm -rf debian/build + +override_dh_auto_configure: + mkdir -p debian/build + cd debian/build && meson --prefix=/usr -Ddistro=debian --buildtype=debug ../.. + +override_dh_auto_build: + cd debian/build && ninja -v + +override_dh_auto_test: + cd debian/build && ninja test + +override_dh_auto_install: + cd debian/build && DESTDIR=${CURDIR}/debian/com.github.tkashkin.gamehub ninja install diff --git a/flatpak/README.md b/flatpak/README.md new file mode 100644 index 00000000..5e9dfd85 --- /dev/null +++ b/flatpak/README.md @@ -0,0 +1,32 @@ +# flatpak +This directory contains flatpak manifest + +#### Runtime dependencies + +* `org.gnome.Platform//3.28` +* `org.freedesktop.Platform//1.6` +* `io.elementary.Loki.BaseApp//stable` + +#### Build dependencies + +* `org.gnome.Sdk//3.28` + +## Building + +#### Add flathub repo + +```bash +flatpak remote-add [--user] --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +``` + +#### Install dependencies and build + +```bash +scripts/build.sh build_flatpak +``` + +#### Run + +```bash +flatpak run [-v] com.github.tkashkin.gamehub [--debug] +``` diff --git a/flatpak/bin/bin.json b/flatpak/bin/bin.json new file mode 100644 index 00000000..e278e679 --- /dev/null +++ b/flatpak/bin/bin.json @@ -0,0 +1,19 @@ +{ + "name": "bin", + "buildsystem": "simple", + "sources": [ + { + "type": "file", + "path": "file-roller" + }, + { + "type": "file", + "path": "org.gnome.FileRoller.gschema.xml" + } + ], + "build-commands": [ + "install -Dm755 file-roller /app/bin/file-roller", + "install -Dm644 org.gnome.FileRoller.gschema.xml /app/share/glib-2.0/schemas/org.gnome.FileRoller.gschema.xml", + "glib-compile-schemas /app/share/glib-2.0/schemas/" + ] +} diff --git a/flatpak/bin/file-roller b/flatpak/bin/file-roller new file mode 100755 index 00000000..69a32847 Binary files /dev/null and b/flatpak/bin/file-roller differ diff --git a/flatpak/bin/org.gnome.FileRoller.gschema.xml b/flatpak/bin/org.gnome.FileRoller.gschema.xml new file mode 100644 index 00000000..385d1bd5 --- /dev/null +++ b/flatpak/bin/org.gnome.FileRoller.gschema.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'name' + How to sort files + What criteria must be used to arrange files. Possible values: name, size, type, time, path. + + + 'ascending' + Sort type + Whether to sort in ascending or descending direction. Possible values: ascending, descending. + + + 'as-folder' + List Mode + Use “all-files” to view all the files in the archive in a single list, use “as-folder” to navigate the archive as a folder. + + + true + Display type + Display the type column in the main window. + + + true + Display size + Display the size column in the main window. + + + true + Display time + Display the time column in the main window. + + + true + Display path + Display the path column in the main window. + + + 250 + Name column width + The default width of the name column in the file list. + + + + + + 600 + + + 480 + + + 200 + + + false + View the sidebar + Whether to display the sidebar. + + + + + + (-1, -1) + + + false + + + -1 + + + + + + [] + Editors + List of applications entered in the “Open File” dialog and not associated with the file type. + + + 'normal' + Compression level + Compression level used when adding files to an archive. Possible values: very-fast, fast, normal, maximum. + + + false + Encrypt the archive header + Whether to encrypt the archive header. If the header is encrypted the password will be required to list the archive content as well. + + + + + + + + + + + + + false + Do not overwrite newer files + + + true + Recreate the folders stored in the archive + + + + + + '.tar.gz' + + + false + + + false + Encrypt the archive header + Whether to encrypt the archive header. If the header is encrypted the password will be required to list the archive content as well. + + + 0 + Default volume size + The default size for volumes. + + + + + + '' + + + [] + + + '' + + + '' + + + '' + + + '' + + + false + + + true + + + true + + + + + + -1 + + + -1 + + + + diff --git a/flatpak/com.github.tkashkin.gamehub.json.in b/flatpak/com.github.tkashkin.gamehub.json.in new file mode 100644 index 00000000..b397bbd2 --- /dev/null +++ b/flatpak/com.github.tkashkin.gamehub.json.in @@ -0,0 +1,89 @@ +{ + "app-id": "com.github.tkashkin.gamehub", + "base": "io.elementary.BaseApp", + "base-version": "juno", + "runtime": "org.gnome.Platform", + "runtime-version": "3.28", + "sdk": "org.gnome.Sdk", + "command": "com.github.tkashkin.gamehub", + "separate-locales": false, + "build-options": { + "env": { + "PKG_CONFIG_GOBJECT_INTROSPECTION_1_0_GIRDIR": "/app/share/gir-1.0", + "PKG_CONFIG_GOBJECT_INTROSPECTION_1_0_TYPELIBDIR": "/app/lib/girepository-1.0" + } + }, + "finish-args": [ + "--allow=devel", + "--share=ipc", "--socket=x11", + "--socket=pulseaudio", + "--device=dri", + "--socket=wayland", + "--share=network", + "--talk-name=org.gnome.SettingsDaemon", + "--talk-name=org.freedesktop.NetworkManager", + "--talk-name=org.kde.StatusNotifierWatcher", + "--talk-name=org.freedesktop.Flatpak", + "--system-talk-name=org.freedesktop.PolicyKit1", + "--socket=session-bus", + + "--persist=.", + "--device=all", + "--allow=multiarch", + + "--extension=org.freedesktop.Platform.Compat32=directory=lib/32bit", + "--extension=org.freedesktop.Platform.Compat32=version=1.6", + "--extension=org.freedesktop.Platform.GL32=directory=lib/32bit/lib/GL", + "--extension=org.freedesktop.Platform.GL32=version=1.4", + "--extension=org.freedesktop.Platform.GL32=versions=1.6;1.4", + "--extension=org.freedesktop.Platform.GL32=subdirectories=true", + "--extension=org.freedesktop.Platform.GL32=no-autodownload=true", + "--extension=org.freedesktop.Platform.GL32=autodelete=false", + "--extension=org.freedesktop.Platform.GL32=add-ld-path=lib", + "--extension=org.freedesktop.Platform.GL32=merge-dirs=vulkan/icd.d;glvnd/egl_vendor.d", + "--extension=org.freedesktop.Platform.GL32=download-if=active-gl-driver", + "--extension=org.freedesktop.Platform.GL32=enable-if=active-gl-driver", + "--filesystem=/sys/module/nvidia:ro", + + "--talk-name=ca.desrt.dconf", + "--filesystem=xdg-run/dconf", + "--filesystem=~/.config/dconf:ro", + "--env=DCONF_USER_CONFIG_DIR=.config/dconf", + + "--filesystem=host", + "--filesystem=home", + "--filesystem=~/.var/app/com.valvesoftware.Steam" + ], + "cleanup": [ + "/include", + "/lib/pkgconfig", + "/lib/*.la", + "/lib/*.a", + "/share/gir-1.0", + "/lib/girepository-1.0", + "/share/vala", + "/share/gtk-doc" + ], + "modules": [ + "libs/libs.json", + + { + "name": "gamehub", + "buildsystem": "meson", + "config-opts": [ + "--buildtype=debug", + "-Dflatpak=true", + "-Druntime=/app/lib/steamrt:/app/lib/32bit/steamrt" + ], + "sources": [ + { + "type": "git", + "path": "../", + "branch": "$BRANCH" + } + ] + }, + + "bin/bin.json" + ] +} diff --git a/flatpak/flathub.json b/flatpak/flathub.json new file mode 100644 index 00000000..d39c5a15 --- /dev/null +++ b/flatpak/flathub.json @@ -0,0 +1,3 @@ +{ + "only-arches": ["i386", "x86_64"] +} diff --git a/flatpak/libs/ld.so.conf b/flatpak/libs/ld.so.conf new file mode 100644 index 00000000..ecc96efd --- /dev/null +++ b/flatpak/libs/ld.so.conf @@ -0,0 +1,7 @@ +# We just make any GL32 extension have higher priority +include /run/flatpak/ld.so.conf.d/app-*-org.freedesktop.Platform.GL32.*.conf + +include /run/flatpak/ld.so.conf.d/*.conf + +/app/lib +/app/lib/32bit diff --git a/flatpak/libs/libopenssl/configure_i386 b/flatpak/libs/libopenssl/configure_i386 new file mode 100755 index 00000000..c0800b2d --- /dev/null +++ b/flatpak/libs/libopenssl/configure_i386 @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "OPENSSL CONFIGURE" +./Configure \ + $@ zlib enable-camellia enable-seed enable-rfc3779 enable-sctp enable-cms enable-md2 enable-rc5 enable-ssl3 enable-ssl3-method no-mdc2 no-ec2m no-gost no-srp shared linux-elf +echo "OPENSSL MAKE DEPEND" +make depend +echo "OPENSSL MAKE BUILD_LIBS" +make build_libs +echo "OPENSSL COPY STUFF" +cp libssl.so /app/lib/ +cp libcrypto.so /app/lib/ +cd /app/lib +ln -s libssl.so libssl.so.1.0.0 +ln -s libcrypto.so libcrypto.so.1.0.0 +cd - +echo "OPENSSL END" +cat < Makefile +install: + true +all: + true +EOF diff --git a/flatpak/libs/libopenssl/configure_x86_64 b/flatpak/libs/libopenssl/configure_x86_64 new file mode 100755 index 00000000..01609eeb --- /dev/null +++ b/flatpak/libs/libopenssl/configure_x86_64 @@ -0,0 +1,24 @@ +#!/bin/bash + +set -eu + +echo "OPENSSL CONFIGURE" +./config $@ shared +echo "OPENSSL MAKE DEPEND" +make depend +echo "OPENSSL MAKE BUILD_LIBS" +make build_libs +echo "OPENSSL COPY STUFF" +cp libssl.so /app/lib/ +cp libcrypto.so /app/lib/ +cd /app/lib +ln -s libssl.so libssl.so.1.0.0 +ln -s libcrypto.so libcrypto.so.1.0.0 +cd - +echo "OPENSSL END" +cat < Makefile +install: + true +all: + true +EOF diff --git a/flatpak/libs/libopenssl/libopenssl.json b/flatpak/libs/libopenssl/libopenssl.json new file mode 100644 index 00000000..f3b38e5f --- /dev/null +++ b/flatpak/libs/libopenssl/libopenssl.json @@ -0,0 +1,25 @@ +{ + "name": "libopenssl", + "no-autogen": true, + "sources": [ + { + "type": "archive", + "url": "https://www.openssl.org/source/old/1.0.0/openssl-1.0.0k.tar.gz", + "sha256": "2982b2e9697a857b336c5c1b1b7b463747e5c1d560f25f6ace95365791b1efd1" + }, + { + "type": "file", + "only-arches": ["i386"], + "path": "configure_i386", + "dest-filename": "configure", + "sha256": "f2dc21cb3749c3e751a0989490287cce151071220fe4a0d2817d9ad360f394f1" + }, + { + "type": "file", + "only-arches": ["x86_64"], + "path": "configure_x86_64", + "dest-filename": "configure", + "sha256": "cd578ac0a2096e95ce323cd4143cca0bd422423f29abba33fbae208583604c6c" + } + ] +} diff --git a/flatpak/libs/libs.json b/flatpak/libs/libs.json new file mode 100644 index 00000000..cafbad13 --- /dev/null +++ b/flatpak/libs/libs.json @@ -0,0 +1,40 @@ +{ + "name": "libs", + "buildsystem": "simple", + "sources": [ + { + "type": "file", + "path": "ld.so.conf" + } + ], + "build-commands": [ + "mkdir -p /app/lib/32bit/lib/GL", + "mkdir -p /app/lib/GL", + "ln -s /app/lib/32bit/lib/ld-linux.so.2 /app/lib/ld-linux.so.2", + "install -Dm644 ld.so.conf /app/etc/ld.so.conf" + ], + + "modules": [ + "polkit/polkit.json", + { + "name": "libevdev", + "sources": [ + { + "type": "git", + "url": "https://gitlab.freedesktop.org/libevdev/libevdev.git" + } + ] + }, + { + "name": "manette", + "buildsystem": "meson", + "config-opts": ["--libdir=lib"], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/aplazas/libmanette.git" + } + ] + } + ] +} diff --git a/flatpak/libs/polkit/polkit-autogen b/flatpak/libs/polkit/polkit-autogen new file mode 100755 index 00000000..3ba457e5 --- /dev/null +++ b/flatpak/libs/polkit/polkit-autogen @@ -0,0 +1,4 @@ +#!/bin/sh + +gtkdocize --flavour no-tmpl +autoreconf -if diff --git a/flatpak/libs/polkit/polkit-build-Add-option-to-build-without-polkitd.patch b/flatpak/libs/polkit/polkit-build-Add-option-to-build-without-polkitd.patch new file mode 100644 index 00000000..f201c204 --- /dev/null +++ b/flatpak/libs/polkit/polkit-build-Add-option-to-build-without-polkitd.patch @@ -0,0 +1,101 @@ +From dab179770380918462d0d76e08b11e4abe55c933 Mon Sep 17 00:00:00 2001 +From: Patrick Griffis +Date: Thu, 8 Sep 2016 16:15:54 -0400 +Subject: [PATCH] build: Add option to build without polkitd + +This is for any consumer that needs libpolkit but does +not need polkitd. For example an application running in +flatpak. +--- + configure.ac | 29 ++++++++++++++++++++--------- + src/Makefile.am | 6 +++++- + test/Makefile.am | 6 +++++- + 3 files changed, 30 insertions(+), 11 deletions(-) + +diff --git a/configure.ac b/configure.ac +index 97d4222..a08785c 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -129,20 +129,30 @@ AC_DEFINE([GLIB_VERSION_MIN_REQUIRED], [GLIB_VERSION_2_30], + AC_DEFINE([GLIB_VERSION_MAX_ALLOWED], [G_ENCODE_VERSION(2,34)], + [Notify us when we'll need to transition away from g_type_init()]) + ++ ++AC_ARG_ENABLE([polkitd], ++ [AS_HELP_STRING([--disable-polkitd], [Do not build polkitd])], ++ [enable_polkitd=$enableval], [enable_polkitd=yes]) ++AM_CONDITIONAL(BUILD_POLKITD, [test x${enable_polkitd} = yes]) ++ ++ + AC_ARG_WITH(mozjs, AS_HELP_STRING([--with-mozjs=@<:@mozjs185/mozjs-17.0|auto@:>@], + [Specify version of Spidermonkey to use]),, + with_mozjs=auto) +-AS_IF([test x${with_mozjs} != xauto], [ +- PKG_CHECK_MODULES(LIBJS, ${with_mozjs}) +-], [ +- PKG_CHECK_MODULES(LIBJS, [mozjs185], have_mozjs185=yes, have_mozjs185=no) +- AS_IF([test x${have_mozjs185} = xno], [ +- PKG_CHECK_MODULES(LIBJS, [mozjs-17.0], have_mozjs17=yes, +- [AC_MSG_ERROR([Could not find mozjs185 or mozjs-17.0; see http://ftp.mozilla.org/pub/mozilla.org/js/])]) ++ ++AS_IF([test x${enable_polkitd} = yes], [ ++ AS_IF([test x${with_mozjs} != xauto], [ ++ PKG_CHECK_MODULES(LIBJS, ${with_mozjs}) ++ ], [ ++ PKG_CHECK_MODULES(LIBJS, [mozjs185], have_mozjs185=yes, have_mozjs185=no) ++ AS_IF([test x${have_mozjs185} = xno], [ ++ PKG_CHECK_MODULES(LIBJS, [mozjs-17.0], have_mozjs17=yes, ++ [AC_MSG_ERROR([Could not find mozjs185 or mozjs-17.0; see http://ftp.mozilla.org/pub/mozilla.org/js/])]) ++ ]) + ]) ++ AC_SUBST(LIBJS_CFLAGS) ++ AC_SUBST(LIBJS_LIBS) + ]) +-AC_SUBST(LIBJS_CFLAGS) +-AC_SUBST(LIBJS_LIBS) + + EXPAT_LIB="" + AC_ARG_WITH(expat, [ --with-expat= Use expat from here], +@@ -605,6 +615,7 @@ echo " + Session tracking: ${SESSION_TRACKING} + PAM support: ${have_pam} + systemdsystemunitdir: ${systemdsystemunitdir} ++ polkitd: ${enable_polkitd} + polkitd user: ${POLKITD_USER}" + + if test "$have_pam" = yes ; then +diff --git a/src/Makefile.am b/src/Makefile.am +index 09fc7b3..c6fe91b 100644 +--- a/src/Makefile.am ++++ b/src/Makefile.am +@@ -1,5 +1,9 @@ + +-SUBDIRS = polkit polkitbackend polkitagent programs ++SUBDIRS = polkit polkitagent programs ++ ++if BUILD_POLKITD ++SUBDIRS += polkitbackend ++endif + + if BUILD_EXAMPLES + SUBDIRS += examples +diff --git a/test/Makefile.am b/test/Makefile.am +index 59d0680..d43b0fe 100644 +--- a/test/Makefile.am ++++ b/test/Makefile.am +@@ -1,7 +1,11 @@ + +-SUBDIRS = mocklibc . polkit polkitbackend ++SUBDIRS = mocklibc . polkit + AM_CFLAGS = $(GLIB_CFLAGS) + ++if BUILD_POLKITD ++SUBDIRS += polkitbackend ++endif ++ + noinst_LTLIBRARIES = libpolkit-test-helper.la + libpolkit_test_helper_la_SOURCES = polkittesthelper.c polkittesthelper.h + libpolkit_test_helper_la_LIBADD = $(GLIB_LIBS) +-- +2.9.3 + diff --git a/flatpak/libs/polkit/polkit.json b/flatpak/libs/polkit/polkit.json new file mode 100644 index 00000000..8dc67b8b --- /dev/null +++ b/flatpak/libs/polkit/polkit.json @@ -0,0 +1,29 @@ +{ + "name": "polkit", + "config-opts": ["--disable-polkitd", "--disable-man-pages", "--disable-introspection"], + "rm-configure": true, + "cleanup": [ + "/bin/*", + "/etc/pam.d", + "/etc/dbus-1", + "/share/dbus-1/system-services/*", + "/share/polkit-1/actions/*", + "/lib/polkit-1" + ], + "sources": [ + { + "type": "archive", + "url": "https://www.freedesktop.org/software/polkit/releases/polkit-0.113.tar.gz", + "sha256": "e1c095093c654951f78f8618d427faf91cf62abdefed98de40ff65eca6413c81" + }, + { + "type": "patch", + "path": "polkit-build-Add-option-to-build-without-polkitd.patch" + }, + { + "type": "file", + "path": "polkit-autogen", + "dest-filename": "autogen.sh" + } + ] +} diff --git a/flatpak/libs/shared b/flatpak/libs/shared new file mode 160000 index 00000000..fe0c3d93 --- /dev/null +++ b/flatpak/libs/shared @@ -0,0 +1 @@ +Subproject commit fe0c3d93636875e90362e2bbc5ba88114d98e482 diff --git a/flatpak/libs/steamrt/etc.tar.gz b/flatpak/libs/steamrt/etc.tar.gz new file mode 100644 index 00000000..e0bfd640 Binary files /dev/null and b/flatpak/libs/steamrt/etc.tar.gz differ diff --git a/flatpak/libs/steamrt/i386.tar.gz b/flatpak/libs/steamrt/i386.tar.gz new file mode 100644 index 00000000..a6666676 Binary files /dev/null and b/flatpak/libs/steamrt/i386.tar.gz differ diff --git a/flatpak/libs/steamrt/steamrt.json b/flatpak/libs/steamrt/steamrt.json new file mode 100644 index 00000000..681c38e5 --- /dev/null +++ b/flatpak/libs/steamrt/steamrt.json @@ -0,0 +1,53 @@ +{ + "name": "steamrt", + "buildsystem": "simple", + "sources": [ + { + "type": "file", + "path": "etc.tar.gz" + } + ], + "build-commands": [ + "mkdir -p $FLATPAK_DEST/etc", + "tar -xzf etc.tar.gz -C $FLATPAK_DEST/etc" + ], + + "modules": [ + { + "name": "steamrt_x86_64", + "buildsystem": "simple", + "only-arches": ["x86_64"], + "sources": [ + { + "type": "file", + "path": "x86_64.tar.gz" + }, + { + "type": "file", + "path": "i386.tar.gz" + } + ], + "build-commands": [ + "mkdir -p $FLATPAK_DEST/lib/steamrt", + "mkdir -p $FLATPAK_DEST/lib/32bit/steamrt", + "tar -xzf x86_64.tar.gz -C $FLATPAK_DEST/lib/steamrt", + "tar -xzf i386.tar.gz -C $FLATPAK_DEST/lib/32bit/steamrt" + ] + }, + { + "name": "steamrt_i386", + "buildsystem": "simple", + "only-arches": ["i386"], + "sources": [ + { + "type": "file", + "path": "i386.tar.gz" + } + ], + "build-commands": [ + "mkdir -p $FLATPAK_DEST/lib/steamrt", + "tar -xzf i386.tar.gz -C $FLATPAK_DEST/lib/steamrt" + ] + } + ] +} diff --git a/flatpak/libs/steamrt/version.txt b/flatpak/libs/steamrt/version.txt new file mode 100644 index 00000000..e69de29b diff --git a/flatpak/libs/steamrt/x86_64.tar.gz b/flatpak/libs/steamrt/x86_64.tar.gz new file mode 100644 index 00000000..ff88ad40 Binary files /dev/null and b/flatpak/libs/steamrt/x86_64.tar.gz differ diff --git a/meson.build b/meson.build index 48e82f23..e601331d 100644 --- a/meson.build +++ b/meson.build @@ -1,56 +1,36 @@ -project('com.github.tkashkin.gamehub', 'vala', 'c') +project('com.github.tkashkin.gamehub', 'vala', 'c', version: '0.12.1') i18n = import('i18n') gnome = import('gnome') -add_global_arguments('-g', language: 'vala') -add_global_arguments('-X', language: 'vala') -add_global_arguments('-rdynamic', language: 'vala') add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), language: 'c') +if get_option('appimage') + add_global_arguments('-D', 'APPIMAGE', language: 'vala') +elif get_option('flatpak') + add_global_arguments('-D', 'FLATPAK', language: 'vala') +elif get_option('snap') + add_global_arguments('-D', 'SNAP', language: 'vala') +endif + +if get_option('distro') == 'debian' + add_global_arguments('-D', 'DISTRO_DEBIAN', '-D', 'PM_APT', language: 'vala') +elif get_option('distro') == 'arch' + add_global_arguments('-D', 'DISTRO_ARCH', '-D', 'PM_PACMAN', language: 'vala') +endif + +conf_data = configuration_data() +conf_data.set('PROJECT_NAME', meson.project_name()) +conf_data.set('GETTEXT_PACKAGE', meson.project_name()) +conf_data.set('GETTEXT_DIR', join_paths(get_option('prefix'), get_option('localedir'))) +conf_data.set('VERSION', meson.project_version()) +conf_data.set('PREFIX', get_option('prefix')) +conf_data.set('DATADIR', join_paths(get_option('prefix'), get_option('datadir'))) +conf_data.set('BINDIR', join_paths(get_option('prefix'), get_option('bindir'))) +conf_data.set('RUNTIME', get_option('runtime')) + subdir('data') +subdir('src') subdir('po') -executable( - meson.project_name(), - - 'src/app.vala', - - 'src/data/Game.vala', - 'src/data/GameSource.vala', - - 'src/data/sources/steam/Steam.vala', - 'src/data/sources/steam/SteamGame.vala', - - 'src/data/sources/gog/GOG.vala', - 'src/data/sources/gog/GOGGame.vala', - - 'src/ui/windows/MainWindow.vala', - 'src/ui/windows/WebAuthWindow.vala', - - 'src/ui/views/BaseView.vala', - 'src/ui/views/WelcomeView.vala', - - 'src/ui/views/GamesGridView/GamesGridView.vala', - 'src/ui/views/GamesGridView/GameCard.vala', - - 'src/utils/Utils.vala', - 'src/utils/FSUtils.vala', - 'src/utils/Parser.vala', - - icons_gresource, - css_gresource, - - dependencies: [ - dependency('granite'), - dependency('gtk+-3.0'), - dependency('gdk-3.0'), - dependency('webkit2gtk-4.0'), - dependency('glib-2.0'), - dependency('json-glib-1.0'), - dependency('gee-0.8'), - dependency('libsoup-2.4'), - dependency('ivy') - ], - install: true -) +meson.add_install_script('meson/post_install.py') diff --git a/meson/post_install.py b/meson/post_install.py new file mode 100644 index 00000000..36c807e7 --- /dev/null +++ b/meson/post_install.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import os +import subprocess + +prefix = os.environ.get('MESON_INSTALL_PREFIX', '/usr/local') +datadir = os.path.join(prefix, 'share') +schemadir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas') + +if not os.environ.get('DESTDIR'): + print('Updating icon cache...') + icon_cache_dir = os.path.join(datadir, 'icons', 'hicolor') + if not os.path.exists(icon_cache_dir): + os.makedirs(icon_cache_dir) + subprocess.call(['gtk-update-icon-cache', '-qtf', icon_cache_dir]) + + print('Updating desktop database...') + desktop_database_dir = os.path.join(datadir, 'applications') + if not os.path.exists(desktop_database_dir): + os.makedirs(desktop_database_dir) + subprocess.call(['update-desktop-database', '-q', desktop_database_dir]) + + print('Compiling gsettings schemas...') + subprocess.call(['glib-compile-schemas', schemadir]) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..49042092 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,7 @@ +option('distro', type: 'combo', choices: ['generic', 'debian', 'arch'], value: 'generic') + +option('appimage', type: 'boolean', value: false) +option('flatpak', type: 'boolean', value: false) +option('snap', type: 'boolean', value: false) + +option('runtime', type: 'string', value: '') diff --git a/po/LINGUAS b/po/LINGUAS index 562ba4cf..e4ac6a54 100644 --- a/po/LINGUAS +++ b/po/LINGUAS @@ -1 +1,4 @@ ru +pt_BR +id +nb_NO diff --git a/po/POTFILES b/po/POTFILES index 329fa12e..afe8a701 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,26 +1,75 @@ -data/com.github.tkashkin.gamehub.appdata.xml.in -data/com.github.tkashkin.gamehub.desktop.in - src/app.vala - +src/data/Runnable.vala src/data/Game.vala src/data/GameSource.vala - +src/data/Emulator.vala src/data/sources/steam/Steam.vala src/data/sources/steam/SteamGame.vala - src/data/sources/gog/GOG.vala src/data/sources/gog/GOGGame.vala - +src/data/sources/humble/Humble.vala +src/data/sources/humble/HumbleGame.vala +src/data/sources/humble/Trove.vala +src/data/sources/user/User.vala +src/data/sources/user/UserGame.vala +src/data/db/Database.vala +src/data/db/Table.vala +src/data/db/tables/Games.vala +src/data/db/tables/Tags.vala +src/data/db/tables/Merges.vala +src/data/db/tables/Emulators.vala +src/data/CompatTool.vala +src/data/compat/CustomScript.vala +src/data/compat/Innoextract.vala +src/data/compat/Proton.vala +src/data/compat/Wine.vala +src/data/compat/DOSBox.vala +src/data/compat/ScummVM.vala +src/data/compat/RetroArch.vala +src/data/compat/CustomEmulator.vala src/ui/windows/MainWindow.vala src/ui/windows/WebAuthWindow.vala - +src/ui/dialogs/SettingsDialog/SettingsDialog.vala +src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala +src/ui/dialogs/SettingsDialog/tabs/UI.vala +src/ui/dialogs/SettingsDialog/tabs/Collection.vala +src/ui/dialogs/SettingsDialog/tabs/Steam.vala +src/ui/dialogs/SettingsDialog/tabs/GOG.vala +src/ui/dialogs/SettingsDialog/tabs/Humble.vala +src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala +src/ui/dialogs/SettingsDialog/tabs/Emulators.vala +src/ui/dialogs/InstallDialog.vala +src/ui/dialogs/GameDetailsDialog.vala +src/ui/dialogs/GamePropertiesDialog.vala +src/ui/dialogs/GameFSOverlaysDialog.vala +src/ui/dialogs/CompatRunDialog.vala src/ui/views/BaseView.vala src/ui/views/WelcomeView.vala - -src/ui/views/GamesGridView/GamesGridView.vala -src/ui/views/GamesGridView/GameCard.vala - +src/ui/views/GamesView/GamesView.vala +src/ui/views/GamesView/GameCard.vala +src/ui/views/GamesView/GameListRow.vala +src/ui/views/GamesView/DownloadProgressView.vala +src/ui/views/GamesView/FiltersPopover.vala +src/ui/views/GamesView/AddGamePopover.vala +src/ui/views/GamesView/GameContextMenu.vala +src/ui/views/GameDetailsView/GameDetailsView.vala +src/ui/views/GameDetailsView/GameDetailsPage.vala +src/ui/views/GameDetailsView/GameDetailsBlock.vala +src/ui/views/GameDetailsView/blocks/Achievements.vala +src/ui/views/GameDetailsView/blocks/Playtime.vala +src/ui/views/GameDetailsView/blocks/Description.vala +src/ui/views/GameDetailsView/blocks/GOGDetails.vala +src/ui/views/GameDetailsView/blocks/SteamDetails.vala +src/ui/widgets/AutoSizeImage.vala +src/ui/widgets/ActionButton.vala +src/ui/widgets/FileChooserEntry.vala +src/ui/widgets/CompatToolOptions.vala +src/ui/widgets/CompatToolPicker.vala +src/ui/widgets/TagRow.vala src/utils/Utils.vala src/utils/FSUtils.vala +src/utils/FSOverlay.vala src/utils/Parser.vala +src/utils/Settings.vala +src/utils/downloader/Downloader.vala +src/utils/downloader/SoupDownloader.vala diff --git a/po/com.github.tkashkin.gamehub.pot b/po/com.github.tkashkin.gamehub.pot index a6723c34..ec40a03a 100644 --- a/po/com.github.tkashkin.gamehub.pot +++ b/po/com.github.tkashkin.gamehub.pot @@ -8,65 +8,905 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-27 03:39+0300\n" +"POT-Creation-Date: 2018-11-20 01:35+0300\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=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:8 -#: data/com.github.tkashkin.gamehub.desktop.in:3 -#: data/com.github.tkashkin.gamehub.desktop.in:4 -msgid "GameHub" +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +msgid "Select executable" msgstr "" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:9 -#: data/com.github.tkashkin.gamehub.desktop.in:5 -#: src/ui/views/WelcomeView.vala:16 -msgid "All your games in one place" +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:140 src/ui/views/GamesView/GamesView.vala:352 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select" +msgstr "" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:139 src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Cancel" msgstr "" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:12 -msgid "Manage your Steam and GOG games in one place." +#: src/data/Runnable.vala:125 src/ui/widgets/FileChooserEntry.vala:47 +msgid "Select directory" msgstr "" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:15 -msgid "tkashkin" +#: src/data/Runnable.vala:354 +#, c-format +msgid "Part %u of %u: " msgstr "" -#: data/com.github.tkashkin.gamehub.desktop.in:7 -msgid "Game;Hub;Steam;GOG;" +#: src/data/Game.vala:424 +msgctxt "status" +msgid "Running" msgstr "" -#: data/com.github.tkashkin.gamehub.desktop.in:10 -msgid "com.github.tkashkin.gamehub" +#: src/data/Game.vala:427 +msgctxt "status" +msgid "Installed" msgstr "" -#: src/data/sources/steam/Steam.vala:13 +#: src/data/Game.vala:428 +msgctxt "status" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:429 +msgctxt "status" +msgid "Download started" +msgstr "" + +#: src/data/Game.vala:431 +msgctxt "status" +msgid "Not installed" +msgstr "" + +#: src/data/Game.vala:441 +msgctxt "status_header" +msgid "Installed" +msgstr "" + +#: src/data/Game.vala:442 +msgctxt "status_header" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:443 +msgctxt "status_header" +msgid "Downloading" +msgstr "" + +#: src/data/Game.vala:445 +msgctxt "status_header" +msgid "Not installed" +msgstr "" + +#: src/data/sources/steam/Steam.vala:43 msgid "Your SteamID will be read from Steam configuration file" msgstr "" -#: src/ui/views/WelcomeView.vala:16 +#: src/data/sources/steam/Steam.vala:46 +msgid "" +"Steam config file not found.\n" +"Login into your account in Steam client and return to GameHub" +msgstr "" + +#: src/data/sources/user/User.vala:30 +msgid "User games" +msgstr "" + +#: src/data/db/tables/Tags.vala:192 +msgctxt "tag" +msgid "Favorites" +msgstr "" + +#: src/data/db/tables/Tags.vala:193 +msgctxt "tag" +msgid "Not installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:194 +msgctxt "tag" +msgid "Installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:195 +msgctxt "tag" +msgid "Hidden" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit script" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit custom script" +msgstr "" + +#: src/data/compat/Proton.vala:43 src/data/compat/Wine.vala:49 +msgid "Environment variables" +msgstr "" + +#: src/data/compat/Proton.vala:47 +msgid "Disable esync" +msgstr "" + +#: src/data/compat/Proton.vala:48 +msgid "Disable DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:49 +msgid "Use WineD3D11 as DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:50 +msgid "Show DXVK info overlay" +msgstr "" + +#: src/data/compat/Proton.vala:68 src/data/compat/Wine.vala:68 +msgid "Open prefix directory" +msgstr "" + +#: src/data/compat/Proton.vala:71 src/data/compat/Wine.vala:71 +msgid "Run winecfg" +msgstr "" + +#: src/data/compat/Proton.vala:74 src/data/compat/Wine.vala:74 +msgid "Run winetricks" +msgstr "" + +#: src/data/compat/Proton.vala:77 src/data/compat/Wine.vala:77 +msgid "Run taskmgr" +msgstr "" + +#: src/data/compat/Proton.vala:80 src/data/compat/Wine.vala:80 +msgid "Kill apps in prefix" +msgstr "" + +#: src/data/compat/Wine.vala:55 +msgid "InnoSetup default options" +msgstr "" + +#: src/data/compat/Wine.vala:59 +msgid "Silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:60 +msgid "Very silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:61 +msgid "Suppress messages" +msgstr "" + +#: src/data/compat/Wine.vala:62 +msgid "No GUI" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Windowed" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Disable fullscreen" +msgstr "" + +#: src/data/compat/RetroArch.vala:51 +msgid "Libretro core file" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:43 +msgid "Custom emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:48 +msgid "Emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:49 +msgid "Launch in game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:37 +#: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:76 +#: src/ui/views/GamesView/GamesView.vala:220 +#: src/ui/views/GamesView/GamesView.vala:674 +msgid "Settings" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:54 +msgid "Some settings will be applied after application restart" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:68 +msgid "Interface" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:69 +msgid "Collection" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:74 +msgid "Emulators" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:181 +msgid "Open" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:188 +msgid "Clear" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:204 +#, c-format +msgid "%llu installer; %s" +msgid_plural "%llu installers; %s" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:36 +msgid "Use dark theme" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:37 +msgid "Compact list" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:41 +msgid "Merge games from different sources" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:45 +msgid "Show non-native games" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:46 +msgid "Use compatibility layers and consider Windows games compatible" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:50 +msgid "Use imported tags" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:50 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:69 +msgid "Collection directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:55 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:63 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:71 +msgid "Game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:56 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:64 +msgid "Installers" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:57 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:116 +msgid "DLC" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:58 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:89 +msgid "Bonus content" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:68 +msgid "Variables" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:70 +msgid "Game name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:41 +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:42 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:42 +msgid "Enabled" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Steam API keys have limited number of uses per day" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Generate key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:49 +msgid "Installation directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:66 +msgid "Default" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:74 +msgid "Restore default API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:85 +msgid "Steam API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:46 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:49 +msgid "Games directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:52 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:55 +msgid "Logout" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:46 +msgid "Load games from Humble Trove" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:36 +msgid "Libretro core directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:37 +msgid "Libretro core info directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:194 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/dialogs/GamePropertiesDialog.vala:216 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +#: src/ui/views/GamesView/AddGamePopover.vala:66 +#: src/ui/views/GamesView/AddGamePopover.vala:76 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Executable" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:195 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/views/GamesView/AddGamePopover.vala:67 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Installer" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:200 +msgid "Save" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:206 +#: src/ui/dialogs/CompatRunDialog.vala:100 +#: src/ui/views/GamesView/GameContextMenu.vala:42 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:209 +msgid "Run" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:212 +#: src/ui/dialogs/GamePropertiesDialog.vala:130 +#: src/ui/dialogs/GamePropertiesDialog.vala:135 +#: src/ui/views/GamesView/AddGamePopover.vala:72 +msgid "Name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:228 +#: src/ui/dialogs/GamePropertiesDialog.vala:238 +#: src/ui/views/GamesView/AddGamePopover.vala:77 +msgid "Arguments" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +msgid "Select emulator directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:277 +#: src/ui/dialogs/GamePropertiesDialog.vala:256 +msgid "Force compatibility mode" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:53 src/ui/dialogs/InstallDialog.vala:199 +#: src/ui/views/GamesView/GameContextMenu.vala:49 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:208 +msgid "Install" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:147 +msgid "Select installer" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:152 +#, c-format +msgid "Installer size: %s" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:197 +msgid "Import" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:203 +msgid "Download only" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:278 +msgid "Unknown" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:51 +#, c-format +msgid "%s: Properties" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:66 +#: src/ui/views/GamesView/FiltersPopover.vala:153 +msgid "Tags" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:111 +msgid "Add tag" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:149 +msgid "Images" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:183 +msgid "Image URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:187 +msgid "Icon URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:195 +msgid "Search images:" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:251 +msgid "Compatibility" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:52 +#, c-format +msgid "%s: Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "Overlays are disabled" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "" +"Enable overlays to manage DLCs and mods\n" +"\n" +"Enabling will move game to the “base“ overlay" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:71 +#: src/ui/views/GamesView/GameContextMenu.vala:64 +msgid "Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:99 +msgid "Overlay ID (directory name)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:104 +msgid "Overlay name (optional)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:106 +msgid "Add" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:121 +msgid "Enable overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:211 +msgid "Open directory" +msgstr "" + +#: src/ui/dialogs/CompatRunDialog.vala:48 +#: src/ui/views/GamesView/GameContextMenu.vala:45 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:210 +msgid "Run with compatibility layer" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "No enabled game sources" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "Enable some game sources in settings" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 +msgid "All your games in one place" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 msgid "Let's get started" msgstr "" -#: src/ui/views/WelcomeView.vala:24 +#: src/ui/views/WelcomeView.vala:70 msgid "Skip" msgstr "" -#: src/ui/views/WelcomeView.vala:60 -#, c-format -msgid "%d games loaded" +#: src/ui/views/WelcomeView.vala:134 +msgid "Ready" msgstr "" -#: src/ui/views/WelcomeView.vala:65 +#: src/ui/views/WelcomeView.vala:140 msgid "Authentication required" msgstr "" -#: src/ui/views/WelcomeView.vala:70 +#: src/ui/views/WelcomeView.vala:145 +msgid "Authenticating..." +msgstr "" + +#: src/ui/views/WelcomeView.vala:156 #, c-format msgid "Install %s" msgstr "" + +#: src/ui/views/WelcomeView.vala:157 +msgid "Return to GameHub after installing" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "No games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "Get some games or enable some game sources in settings" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:107 +msgid "Reload" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:155 +msgid "Grid view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:156 +msgid "List view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:166 +msgid "All games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:170 +#, c-format +msgid "%s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:177 +msgid "Downloads" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:201 +msgid "Filters" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:208 +#: src/ui/views/GamesView/AddGamePopover.vala:83 +msgid "Add game" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:215 +msgid "Search" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:266 +msgctxt "status_header" +msgid "Favorites" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:350 +msgid "Menu" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:351 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:79 +msgid "Back" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:492 +#, c-format +msgid "Loading games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:521 +#, c-format +msgid "%u game" +msgid_plural "%u games" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GamesView/GamesView.vala:536 +msgid "No user-added games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:537 +msgid "Add some games using plus button" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:541 +#, c-format +msgid "No %s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:542 +msgid "Get some Linux-compatible games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:563 +#, c-format +msgid "No games matching “%s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:568 +#, c-format +msgid "No %1$s games matching “%2$s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:672 +msgid "" +"No games were loaded from Steam. Set your games list privacy to public or " +"use your own Steam API key in settings." +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:673 +msgid "Privacy" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:892 +msgid "Updating game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:898 +#, c-format +msgid "Updating %s game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:916 +msgid "Merging games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:932 +#, c-format +msgid "Merging games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:949 +#, c-format +msgid "Merging %s (%s)" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:124 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:148 +msgid "Pause download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:129 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:154 +msgid "Resume download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:134 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:160 +msgid "Cancel download" +msgstr "" + +#: src/ui/views/GamesView/FiltersPopover.vala:68 +msgid "Sort:" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:76 +msgid "Select game executable" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Select game directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:53 +msgid "Details" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:56 +msgctxt "game_context_menu" +msgid "Favorite" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:60 +msgctxt "game_context_menu" +msgid "Hidden" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:67 +msgid "Properties" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:103 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:211 +msgid "Open installation directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:111 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:212 +msgid "Open installers collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:119 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:213 +msgid "Open bonus collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Remove" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Uninstall" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:214 +msgid "Open store page" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:216 +msgid "Game properties" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:55 +msgid "Achievements" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:103 +#, c-format +msgid "Unlocked: %s" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:108 +#, c-format +msgid "Global percentage: %g%%" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:47 +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:61 +msgid "Playtime" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:53 +msgid "Playtime (local)" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:70 +msgid "Last launch" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dh" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dm" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Description.vala:49 +msgid "Description" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:65 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:89 +msgid "Language" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:68 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:92 +msgid "Languages" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:59 +msgid "Category" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:62 +msgid "Categories" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:77 +msgid "Genre" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:80 +msgid "Genres" +msgstr "" + +#: src/ui/widgets/FileChooserEntry.vala:47 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select file" +msgstr "" + +#: src/ui/widgets/CompatToolPicker.vala:51 +msgid "Compatibility layer:" +msgstr "" + +#: src/utils/Settings.vala:43 +msgctxt "sort_mode" +msgid "By name" +msgstr "" + +#: src/utils/Settings.vala:44 +msgctxt "sort_mode" +msgid "By last launch" +msgstr "" + +#: src/utils/Settings.vala:45 +msgctxt "sort_mode" +msgid "By playtime" +msgstr "" + +#: src/utils/downloader/Downloader.vala:155 +msgctxt "dl_status" +msgid "Starting download" +msgstr "" + +#: src/utils/downloader/Downloader.vala:156 +msgctxt "dl_status" +msgid "Download started" +msgstr "" + +#: src/utils/downloader/Downloader.vala:157 +msgctxt "dl_status" +msgid "Download finished" +msgstr "" + +#: src/utils/downloader/Downloader.vala:158 +msgctxt "dl_status" +msgid "Download failed" +msgstr "" + +#: src/utils/downloader/Downloader.vala:160 +#, c-format +msgctxt "dl_status" +msgid "Downloading: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:162 +#, c-format +msgctxt "dl_status" +msgid "Paused: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:164 +msgctxt "dl_status" +msgid "Download cancelled" +msgstr "" diff --git a/po/id.po b/po/id.po new file mode 100644 index 00000000..ba61b962 --- /dev/null +++ b/po/id.po @@ -0,0 +1,911 @@ +# Indonesian translations for com.github.tkashkin.gamehub package. +# Copyright (C) 2018 THE com.github.tkashkin.gamehub'S COPYRIGHT HOLDER +# This file is distributed under the same license as the com.github.tkashkin.gamehub package. +# Automatically generated, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: com.github.tkashkin.gamehub\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-11-20 01:35+0300\n" +"PO-Revision-Date: 2018-11-08 21:53+0300\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +msgid "Select executable" +msgstr "" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:140 src/ui/views/GamesView/GamesView.vala:352 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select" +msgstr "" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:139 src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Cancel" +msgstr "" + +#: src/data/Runnable.vala:125 src/ui/widgets/FileChooserEntry.vala:47 +msgid "Select directory" +msgstr "" + +#: src/data/Runnable.vala:354 +#, c-format +msgid "Part %u of %u: " +msgstr "" + +#: src/data/Game.vala:424 +msgctxt "status" +msgid "Running" +msgstr "" + +#: src/data/Game.vala:427 +msgctxt "status" +msgid "Installed" +msgstr "" + +#: src/data/Game.vala:428 +msgctxt "status" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:429 +msgctxt "status" +msgid "Download started" +msgstr "" + +#: src/data/Game.vala:431 +msgctxt "status" +msgid "Not installed" +msgstr "" + +#: src/data/Game.vala:441 +msgctxt "status_header" +msgid "Installed" +msgstr "" + +#: src/data/Game.vala:442 +msgctxt "status_header" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:443 +msgctxt "status_header" +msgid "Downloading" +msgstr "" + +#: src/data/Game.vala:445 +msgctxt "status_header" +msgid "Not installed" +msgstr "" + +#: src/data/sources/steam/Steam.vala:43 +msgid "Your SteamID will be read from Steam configuration file" +msgstr "" + +#: src/data/sources/steam/Steam.vala:46 +msgid "" +"Steam config file not found.\n" +"Login into your account in Steam client and return to GameHub" +msgstr "" + +#: src/data/sources/user/User.vala:30 +msgid "User games" +msgstr "" + +#: src/data/db/tables/Tags.vala:192 +msgctxt "tag" +msgid "Favorites" +msgstr "" + +#: src/data/db/tables/Tags.vala:193 +msgctxt "tag" +msgid "Not installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:194 +msgctxt "tag" +msgid "Installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:195 +msgctxt "tag" +msgid "Hidden" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit script" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit custom script" +msgstr "" + +#: src/data/compat/Proton.vala:43 src/data/compat/Wine.vala:49 +msgid "Environment variables" +msgstr "" + +#: src/data/compat/Proton.vala:47 +msgid "Disable esync" +msgstr "" + +#: src/data/compat/Proton.vala:48 +msgid "Disable DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:49 +msgid "Use WineD3D11 as DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:50 +msgid "Show DXVK info overlay" +msgstr "" + +#: src/data/compat/Proton.vala:68 src/data/compat/Wine.vala:68 +msgid "Open prefix directory" +msgstr "" + +#: src/data/compat/Proton.vala:71 src/data/compat/Wine.vala:71 +msgid "Run winecfg" +msgstr "" + +#: src/data/compat/Proton.vala:74 src/data/compat/Wine.vala:74 +msgid "Run winetricks" +msgstr "" + +#: src/data/compat/Proton.vala:77 src/data/compat/Wine.vala:77 +msgid "Run taskmgr" +msgstr "" + +#: src/data/compat/Proton.vala:80 src/data/compat/Wine.vala:80 +msgid "Kill apps in prefix" +msgstr "" + +#: src/data/compat/Wine.vala:55 +msgid "InnoSetup default options" +msgstr "" + +#: src/data/compat/Wine.vala:59 +msgid "Silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:60 +msgid "Very silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:61 +msgid "Suppress messages" +msgstr "" + +#: src/data/compat/Wine.vala:62 +msgid "No GUI" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Windowed" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Disable fullscreen" +msgstr "" + +#: src/data/compat/RetroArch.vala:51 +msgid "Libretro core file" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:43 +msgid "Custom emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:48 +msgid "Emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:49 +msgid "Launch in game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:37 +#: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:76 +#: src/ui/views/GamesView/GamesView.vala:220 +#: src/ui/views/GamesView/GamesView.vala:674 +msgid "Settings" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:54 +msgid "Some settings will be applied after application restart" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:68 +msgid "Interface" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:69 +msgid "Collection" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:74 +msgid "Emulators" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:181 +msgid "Open" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:188 +msgid "Clear" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:204 +#, c-format +msgid "%llu installer; %s" +msgid_plural "%llu installers; %s" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:36 +msgid "Use dark theme" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:37 +msgid "Compact list" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:41 +msgid "Merge games from different sources" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:45 +msgid "Show non-native games" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:46 +msgid "Use compatibility layers and consider Windows games compatible" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:50 +msgid "Use imported tags" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:50 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:69 +msgid "Collection directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:55 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:63 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:71 +msgid "Game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:56 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:64 +msgid "Installers" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:57 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:116 +msgid "DLC" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:58 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:89 +msgid "Bonus content" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:68 +msgid "Variables" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:70 +msgid "Game name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:41 +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:42 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:42 +msgid "Enabled" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Steam API keys have limited number of uses per day" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Generate key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:49 +msgid "Installation directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:66 +msgid "Default" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:74 +msgid "Restore default API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:85 +msgid "Steam API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:46 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:49 +msgid "Games directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:52 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:55 +msgid "Logout" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:46 +msgid "Load games from Humble Trove" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:36 +msgid "Libretro core directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:37 +msgid "Libretro core info directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:194 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/dialogs/GamePropertiesDialog.vala:216 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +#: src/ui/views/GamesView/AddGamePopover.vala:66 +#: src/ui/views/GamesView/AddGamePopover.vala:76 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Executable" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:195 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/views/GamesView/AddGamePopover.vala:67 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Installer" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:200 +msgid "Save" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:206 +#: src/ui/dialogs/CompatRunDialog.vala:100 +#: src/ui/views/GamesView/GameContextMenu.vala:42 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:209 +msgid "Run" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:212 +#: src/ui/dialogs/GamePropertiesDialog.vala:130 +#: src/ui/dialogs/GamePropertiesDialog.vala:135 +#: src/ui/views/GamesView/AddGamePopover.vala:72 +msgid "Name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:228 +#: src/ui/dialogs/GamePropertiesDialog.vala:238 +#: src/ui/views/GamesView/AddGamePopover.vala:77 +msgid "Arguments" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +msgid "Select emulator directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:277 +#: src/ui/dialogs/GamePropertiesDialog.vala:256 +msgid "Force compatibility mode" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:53 src/ui/dialogs/InstallDialog.vala:199 +#: src/ui/views/GamesView/GameContextMenu.vala:49 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:208 +msgid "Install" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:147 +msgid "Select installer" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:152 +#, c-format +msgid "Installer size: %s" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:197 +msgid "Import" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:203 +msgid "Download only" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:278 +msgid "Unknown" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:51 +#, c-format +msgid "%s: Properties" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:66 +#: src/ui/views/GamesView/FiltersPopover.vala:153 +msgid "Tags" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:111 +msgid "Add tag" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:149 +msgid "Images" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:183 +msgid "Image URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:187 +msgid "Icon URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:195 +msgid "Search images:" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:251 +msgid "Compatibility" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:52 +#, c-format +msgid "%s: Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "Overlays are disabled" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "" +"Enable overlays to manage DLCs and mods\n" +"\n" +"Enabling will move game to the “base“ overlay" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:71 +#: src/ui/views/GamesView/GameContextMenu.vala:64 +msgid "Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:99 +msgid "Overlay ID (directory name)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:104 +msgid "Overlay name (optional)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:106 +msgid "Add" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:121 +msgid "Enable overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:211 +msgid "Open directory" +msgstr "" + +#: src/ui/dialogs/CompatRunDialog.vala:48 +#: src/ui/views/GamesView/GameContextMenu.vala:45 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:210 +msgid "Run with compatibility layer" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "No enabled game sources" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "Enable some game sources in settings" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 +msgid "All your games in one place" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 +msgid "Let's get started" +msgstr "" + +#: src/ui/views/WelcomeView.vala:70 +msgid "Skip" +msgstr "" + +#: src/ui/views/WelcomeView.vala:134 +msgid "Ready" +msgstr "" + +#: src/ui/views/WelcomeView.vala:140 +msgid "Authentication required" +msgstr "" + +#: src/ui/views/WelcomeView.vala:145 +msgid "Authenticating..." +msgstr "" + +#: src/ui/views/WelcomeView.vala:156 +#, c-format +msgid "Install %s" +msgstr "" + +#: src/ui/views/WelcomeView.vala:157 +msgid "Return to GameHub after installing" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "No games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "Get some games or enable some game sources in settings" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:107 +msgid "Reload" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:155 +msgid "Grid view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:156 +msgid "List view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:166 +msgid "All games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:170 +#, c-format +msgid "%s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:177 +msgid "Downloads" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:201 +msgid "Filters" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:208 +#: src/ui/views/GamesView/AddGamePopover.vala:83 +msgid "Add game" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:215 +msgid "Search" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:266 +msgctxt "status_header" +msgid "Favorites" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:350 +msgid "Menu" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:351 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:79 +msgid "Back" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:492 +#, c-format +msgid "Loading games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:521 +#, c-format +msgid "%u game" +msgid_plural "%u games" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GamesView/GamesView.vala:536 +msgid "No user-added games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:537 +msgid "Add some games using plus button" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:541 +#, c-format +msgid "No %s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:542 +msgid "Get some Linux-compatible games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:563 +#, c-format +msgid "No games matching “%s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:568 +#, c-format +msgid "No %1$s games matching “%2$s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:672 +msgid "" +"No games were loaded from Steam. Set your games list privacy to public or " +"use your own Steam API key in settings." +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:673 +msgid "Privacy" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:892 +msgid "Updating game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:898 +#, c-format +msgid "Updating %s game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:916 +msgid "Merging games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:932 +#, c-format +msgid "Merging games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:949 +#, c-format +msgid "Merging %s (%s)" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:124 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:148 +msgid "Pause download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:129 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:154 +msgid "Resume download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:134 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:160 +msgid "Cancel download" +msgstr "" + +#: src/ui/views/GamesView/FiltersPopover.vala:68 +msgid "Sort:" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:76 +msgid "Select game executable" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Select game directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:53 +msgid "Details" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:56 +msgctxt "game_context_menu" +msgid "Favorite" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:60 +msgctxt "game_context_menu" +msgid "Hidden" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:67 +msgid "Properties" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:103 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:211 +msgid "Open installation directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:111 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:212 +msgid "Open installers collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:119 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:213 +msgid "Open bonus collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Remove" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Uninstall" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:214 +msgid "Open store page" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:216 +msgid "Game properties" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:55 +msgid "Achievements" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:103 +#, c-format +msgid "Unlocked: %s" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:108 +#, c-format +msgid "Global percentage: %g%%" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:47 +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:61 +msgid "Playtime" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:53 +msgid "Playtime (local)" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:70 +msgid "Last launch" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dh" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dm" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Description.vala:49 +msgid "Description" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:65 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:89 +msgid "Language" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:68 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:92 +msgid "Languages" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:59 +msgid "Category" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:62 +msgid "Categories" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:77 +msgid "Genre" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:80 +msgid "Genres" +msgstr "" + +#: src/ui/widgets/FileChooserEntry.vala:47 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select file" +msgstr "" + +#: src/ui/widgets/CompatToolPicker.vala:51 +msgid "Compatibility layer:" +msgstr "" + +#: src/utils/Settings.vala:43 +msgctxt "sort_mode" +msgid "By name" +msgstr "" + +#: src/utils/Settings.vala:44 +msgctxt "sort_mode" +msgid "By last launch" +msgstr "" + +#: src/utils/Settings.vala:45 +msgctxt "sort_mode" +msgid "By playtime" +msgstr "" + +#: src/utils/downloader/Downloader.vala:155 +msgctxt "dl_status" +msgid "Starting download" +msgstr "" + +#: src/utils/downloader/Downloader.vala:156 +msgctxt "dl_status" +msgid "Download started" +msgstr "" + +#: src/utils/downloader/Downloader.vala:157 +msgctxt "dl_status" +msgid "Download finished" +msgstr "" + +#: src/utils/downloader/Downloader.vala:158 +msgctxt "dl_status" +msgid "Download failed" +msgstr "" + +#: src/utils/downloader/Downloader.vala:160 +#, c-format +msgctxt "dl_status" +msgid "Downloading: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:162 +#, c-format +msgctxt "dl_status" +msgid "Paused: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:164 +msgctxt "dl_status" +msgid "Download cancelled" +msgstr "" diff --git a/po/meson.build b/po/meson.build index fc761bef..3fb59396 100644 --- a/po/meson.build +++ b/po/meson.build @@ -1,3 +1,4 @@ -i18n.gettext(meson.project_name(), - args: ['--directory='+meson.source_root(), '--from-code=UTF-8'] +import('i18n').gettext(meson.project_name(), + args: ['--directory=' + meson.source_root(), '--from-code=UTF-8'], + preset: 'glib' ) diff --git a/po/nb_NO.po b/po/nb_NO.po new file mode 100644 index 00000000..4e9819f7 --- /dev/null +++ b/po/nb_NO.po @@ -0,0 +1,911 @@ +# Norwegian Bokmal translations for com.github.tkashkin.gamehub package. +# Copyright (C) 2018 THE com.github.tkashkin.gamehub'S COPYRIGHT HOLDER +# This file is distributed under the same license as the com.github.tkashkin.gamehub package. +# Automatically generated, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: com.github.tkashkin.gamehub\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-11-20 01:35+0300\n" +"PO-Revision-Date: 2018-11-20 01:35+0300\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +msgid "Select executable" +msgstr "" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:140 src/ui/views/GamesView/GamesView.vala:352 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select" +msgstr "" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:139 src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Cancel" +msgstr "" + +#: src/data/Runnable.vala:125 src/ui/widgets/FileChooserEntry.vala:47 +msgid "Select directory" +msgstr "" + +#: src/data/Runnable.vala:354 +#, c-format +msgid "Part %u of %u: " +msgstr "" + +#: src/data/Game.vala:424 +msgctxt "status" +msgid "Running" +msgstr "" + +#: src/data/Game.vala:427 +msgctxt "status" +msgid "Installed" +msgstr "" + +#: src/data/Game.vala:428 +msgctxt "status" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:429 +msgctxt "status" +msgid "Download started" +msgstr "" + +#: src/data/Game.vala:431 +msgctxt "status" +msgid "Not installed" +msgstr "" + +#: src/data/Game.vala:441 +msgctxt "status_header" +msgid "Installed" +msgstr "" + +#: src/data/Game.vala:442 +msgctxt "status_header" +msgid "Installing" +msgstr "" + +#: src/data/Game.vala:443 +msgctxt "status_header" +msgid "Downloading" +msgstr "" + +#: src/data/Game.vala:445 +msgctxt "status_header" +msgid "Not installed" +msgstr "" + +#: src/data/sources/steam/Steam.vala:43 +msgid "Your SteamID will be read from Steam configuration file" +msgstr "" + +#: src/data/sources/steam/Steam.vala:46 +msgid "" +"Steam config file not found.\n" +"Login into your account in Steam client and return to GameHub" +msgstr "" + +#: src/data/sources/user/User.vala:30 +msgid "User games" +msgstr "" + +#: src/data/db/tables/Tags.vala:192 +msgctxt "tag" +msgid "Favorites" +msgstr "" + +#: src/data/db/tables/Tags.vala:193 +msgctxt "tag" +msgid "Not installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:194 +msgctxt "tag" +msgid "Installed" +msgstr "" + +#: src/data/db/tables/Tags.vala:195 +msgctxt "tag" +msgid "Hidden" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit script" +msgstr "" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit custom script" +msgstr "" + +#: src/data/compat/Proton.vala:43 src/data/compat/Wine.vala:49 +msgid "Environment variables" +msgstr "" + +#: src/data/compat/Proton.vala:47 +msgid "Disable esync" +msgstr "" + +#: src/data/compat/Proton.vala:48 +msgid "Disable DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:49 +msgid "Use WineD3D11 as DirectX 11 compatibility layer" +msgstr "" + +#: src/data/compat/Proton.vala:50 +msgid "Show DXVK info overlay" +msgstr "" + +#: src/data/compat/Proton.vala:68 src/data/compat/Wine.vala:68 +msgid "Open prefix directory" +msgstr "" + +#: src/data/compat/Proton.vala:71 src/data/compat/Wine.vala:71 +msgid "Run winecfg" +msgstr "" + +#: src/data/compat/Proton.vala:74 src/data/compat/Wine.vala:74 +msgid "Run winetricks" +msgstr "" + +#: src/data/compat/Proton.vala:77 src/data/compat/Wine.vala:77 +msgid "Run taskmgr" +msgstr "" + +#: src/data/compat/Proton.vala:80 src/data/compat/Wine.vala:80 +msgid "Kill apps in prefix" +msgstr "" + +#: src/data/compat/Wine.vala:55 +msgid "InnoSetup default options" +msgstr "" + +#: src/data/compat/Wine.vala:59 +msgid "Silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:60 +msgid "Very silent installation" +msgstr "" + +#: src/data/compat/Wine.vala:61 +msgid "Suppress messages" +msgstr "" + +#: src/data/compat/Wine.vala:62 +msgid "No GUI" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Windowed" +msgstr "" + +#: src/data/compat/DOSBox.vala:52 +msgid "Disable fullscreen" +msgstr "" + +#: src/data/compat/RetroArch.vala:51 +msgid "Libretro core file" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:43 +msgid "Custom emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:48 +msgid "Emulator" +msgstr "" + +#: src/data/compat/CustomEmulator.vala:49 +msgid "Launch in game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:37 +#: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:76 +#: src/ui/views/GamesView/GamesView.vala:220 +#: src/ui/views/GamesView/GamesView.vala:674 +msgid "Settings" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:54 +msgid "Some settings will be applied after application restart" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:68 +msgid "Interface" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:69 +msgid "Collection" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:74 +msgid "Emulators" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:181 +msgid "Open" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:188 +msgid "Clear" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:204 +#, c-format +msgid "%llu installer; %s" +msgid_plural "%llu installers; %s" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:36 +msgid "Use dark theme" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:37 +msgid "Compact list" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:41 +msgid "Merge games from different sources" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:45 +msgid "Show non-native games" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:46 +msgid "Use compatibility layers and consider Windows games compatible" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:50 +msgid "Use imported tags" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:50 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:69 +msgid "Collection directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:55 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:63 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:71 +msgid "Game directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:56 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:64 +msgid "Installers" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:57 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:116 +msgid "DLC" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:58 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:89 +msgid "Bonus content" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:68 +msgid "Variables" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:70 +msgid "Game name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:41 +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:42 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:42 +msgid "Enabled" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Steam API keys have limited number of uses per day" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Generate key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:49 +msgid "Installation directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:66 +msgid "Default" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:74 +msgid "Restore default API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:85 +msgid "Steam API key" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:46 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:49 +msgid "Games directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:52 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:55 +msgid "Logout" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:46 +msgid "Load games from Humble Trove" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:36 +msgid "Libretro core directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:37 +msgid "Libretro core info directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:194 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/dialogs/GamePropertiesDialog.vala:216 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +#: src/ui/views/GamesView/AddGamePopover.vala:66 +#: src/ui/views/GamesView/AddGamePopover.vala:76 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Executable" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:195 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/views/GamesView/AddGamePopover.vala:67 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Installer" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:200 +msgid "Save" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:206 +#: src/ui/dialogs/CompatRunDialog.vala:100 +#: src/ui/views/GamesView/GameContextMenu.vala:42 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:209 +msgid "Run" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:212 +#: src/ui/dialogs/GamePropertiesDialog.vala:130 +#: src/ui/dialogs/GamePropertiesDialog.vala:135 +#: src/ui/views/GamesView/AddGamePopover.vala:72 +msgid "Name" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:228 +#: src/ui/dialogs/GamePropertiesDialog.vala:238 +#: src/ui/views/GamesView/AddGamePopover.vala:77 +msgid "Arguments" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +msgid "Select emulator directory" +msgstr "" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:277 +#: src/ui/dialogs/GamePropertiesDialog.vala:256 +msgid "Force compatibility mode" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:53 src/ui/dialogs/InstallDialog.vala:199 +#: src/ui/views/GamesView/GameContextMenu.vala:49 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:208 +msgid "Install" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:147 +msgid "Select installer" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:152 +#, c-format +msgid "Installer size: %s" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:197 +msgid "Import" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:203 +msgid "Download only" +msgstr "" + +#: src/ui/dialogs/InstallDialog.vala:278 +msgid "Unknown" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:51 +#, c-format +msgid "%s: Properties" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:66 +#: src/ui/views/GamesView/FiltersPopover.vala:153 +msgid "Tags" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:111 +msgid "Add tag" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:149 +msgid "Images" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:183 +msgid "Image URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:187 +msgid "Icon URL" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:195 +msgid "Search images:" +msgstr "" + +#: src/ui/dialogs/GamePropertiesDialog.vala:251 +msgid "Compatibility" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:52 +#, c-format +msgid "%s: Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "Overlays are disabled" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "" +"Enable overlays to manage DLCs and mods\n" +"\n" +"Enabling will move game to the “base“ overlay" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:71 +#: src/ui/views/GamesView/GameContextMenu.vala:64 +msgid "Overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:99 +msgid "Overlay ID (directory name)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:104 +msgid "Overlay name (optional)" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:106 +msgid "Add" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:121 +msgid "Enable overlays" +msgstr "" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:211 +msgid "Open directory" +msgstr "" + +#: src/ui/dialogs/CompatRunDialog.vala:48 +#: src/ui/views/GamesView/GameContextMenu.vala:45 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:210 +msgid "Run with compatibility layer" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "No enabled game sources" +msgstr "" + +#: src/ui/views/WelcomeView.vala:51 +msgid "Enable some game sources in settings" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 +msgid "All your games in one place" +msgstr "" + +#: src/ui/views/WelcomeView.vala:56 +msgid "Let's get started" +msgstr "" + +#: src/ui/views/WelcomeView.vala:70 +msgid "Skip" +msgstr "" + +#: src/ui/views/WelcomeView.vala:134 +msgid "Ready" +msgstr "" + +#: src/ui/views/WelcomeView.vala:140 +msgid "Authentication required" +msgstr "" + +#: src/ui/views/WelcomeView.vala:145 +msgid "Authenticating..." +msgstr "" + +#: src/ui/views/WelcomeView.vala:156 +#, c-format +msgid "Install %s" +msgstr "" + +#: src/ui/views/WelcomeView.vala:157 +msgid "Return to GameHub after installing" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "No games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "Get some games or enable some game sources in settings" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:107 +msgid "Reload" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:155 +msgid "Grid view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:156 +msgid "List view" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:166 +msgid "All games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:170 +#, c-format +msgid "%s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:177 +msgid "Downloads" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:201 +msgid "Filters" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:208 +#: src/ui/views/GamesView/AddGamePopover.vala:83 +msgid "Add game" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:215 +msgid "Search" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:266 +msgctxt "status_header" +msgid "Favorites" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:350 +msgid "Menu" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:351 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:79 +msgid "Back" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:492 +#, c-format +msgid "Loading games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:521 +#, c-format +msgid "%u game" +msgid_plural "%u games" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GamesView/GamesView.vala:536 +msgid "No user-added games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:537 +msgid "Add some games using plus button" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:541 +#, c-format +msgid "No %s games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:542 +msgid "Get some Linux-compatible games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:563 +#, c-format +msgid "No games matching “%s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:568 +#, c-format +msgid "No %1$s games matching “%2$s”" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:672 +msgid "" +"No games were loaded from Steam. Set your games list privacy to public or " +"use your own Steam API key in settings." +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:673 +msgid "Privacy" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:892 +msgid "Updating game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:898 +#, c-format +msgid "Updating %s game info" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:916 +msgid "Merging games" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:932 +#, c-format +msgid "Merging games from %s" +msgstr "" + +#: src/ui/views/GamesView/GamesView.vala:949 +#, c-format +msgid "Merging %s (%s)" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:124 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:148 +msgid "Pause download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:129 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:154 +msgid "Resume download" +msgstr "" + +#: src/ui/views/GamesView/DownloadProgressView.vala:134 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:160 +msgid "Cancel download" +msgstr "" + +#: src/ui/views/GamesView/FiltersPopover.vala:68 +msgid "Sort:" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:76 +msgid "Select game executable" +msgstr "" + +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Select game directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:53 +msgid "Details" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:56 +msgctxt "game_context_menu" +msgid "Favorite" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:60 +msgctxt "game_context_menu" +msgid "Hidden" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:67 +msgid "Properties" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:103 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:211 +msgid "Open installation directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:111 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:212 +msgid "Open installers collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:119 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:213 +msgid "Open bonus collection directory" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Remove" +msgstr "" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Uninstall" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:214 +msgid "Open store page" +msgstr "" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:216 +msgid "Game properties" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:55 +msgid "Achievements" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:103 +#, c-format +msgid "Unlocked: %s" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:108 +#, c-format +msgid "Global percentage: %g%%" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:47 +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:61 +msgid "Playtime" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:53 +msgid "Playtime (local)" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:70 +msgid "Last launch" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dh" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dm" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/Description.vala:49 +msgid "Description" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:65 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:89 +msgid "Language" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:68 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:92 +msgid "Languages" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:59 +msgid "Category" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:62 +msgid "Categories" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:77 +msgid "Genre" +msgstr "" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:80 +msgid "Genres" +msgstr "" + +#: src/ui/widgets/FileChooserEntry.vala:47 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select file" +msgstr "" + +#: src/ui/widgets/CompatToolPicker.vala:51 +msgid "Compatibility layer:" +msgstr "" + +#: src/utils/Settings.vala:43 +msgctxt "sort_mode" +msgid "By name" +msgstr "" + +#: src/utils/Settings.vala:44 +msgctxt "sort_mode" +msgid "By last launch" +msgstr "" + +#: src/utils/Settings.vala:45 +msgctxt "sort_mode" +msgid "By playtime" +msgstr "" + +#: src/utils/downloader/Downloader.vala:155 +msgctxt "dl_status" +msgid "Starting download" +msgstr "" + +#: src/utils/downloader/Downloader.vala:156 +msgctxt "dl_status" +msgid "Download started" +msgstr "" + +#: src/utils/downloader/Downloader.vala:157 +msgctxt "dl_status" +msgid "Download finished" +msgstr "" + +#: src/utils/downloader/Downloader.vala:158 +msgctxt "dl_status" +msgid "Download failed" +msgstr "" + +#: src/utils/downloader/Downloader.vala:160 +#, c-format +msgctxt "dl_status" +msgid "Downloading: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:162 +#, c-format +msgctxt "dl_status" +msgid "Paused: %d%% (%s / %s)" +msgstr "" + +#: src/utils/downloader/Downloader.vala:164 +msgctxt "dl_status" +msgid "Download cancelled" +msgstr "" diff --git a/po/pt_BR.po b/po/pt_BR.po new file mode 100644 index 00000000..e3d93330 --- /dev/null +++ b/po/pt_BR.po @@ -0,0 +1,923 @@ +# Portuguese translations for com.github.tkashkin.gamehub package. +# Copyright (C) 2018 THE com.github.tkashkin.gamehub'S COPYRIGHT HOLDER +# This file is distributed under the same license as the com.github.tkashkin.gamehub package. +# Automatically generated, 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: com.github.tkashkin.gamehub\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-11-20 01:35+0300\n" +"PO-Revision-Date: 2018-11-10 22:04+0300\n" +"Last-Translator: Leandro Stanger\n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 2.0.6\n" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +msgid "Select executable" +msgstr "Selecione o executável" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:140 src/ui/views/GamesView/GamesView.vala:352 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select" +msgstr "Selecione" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:139 src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Cancel" +msgstr "Cancelar" + +#: src/data/Runnable.vala:125 src/ui/widgets/FileChooserEntry.vala:47 +msgid "Select directory" +msgstr "Selecione o diretório" + +#: src/data/Runnable.vala:354 +#, c-format +msgid "Part %u of %u: " +msgstr "Parte %u de %u: " + +#: src/data/Game.vala:424 +msgctxt "status" +msgid "Running" +msgstr "Correndo" + +#: src/data/Game.vala:427 +msgctxt "status" +msgid "Installed" +msgstr "Instalado" + +#: src/data/Game.vala:428 +msgctxt "status" +msgid "Installing" +msgstr "Instalando" + +#: src/data/Game.vala:429 +msgctxt "status" +msgid "Download started" +msgstr "Download iniciado" + +#: src/data/Game.vala:431 +msgctxt "status" +msgid "Not installed" +msgstr "Não instalado" + +#: src/data/Game.vala:441 +msgctxt "status_header" +msgid "Installed" +msgstr "Instalado" + +#: src/data/Game.vala:442 +msgctxt "status_header" +msgid "Installing" +msgstr "Instalando" + +#: src/data/Game.vala:443 +msgctxt "status_header" +msgid "Downloading" +msgstr "Baixar" + +#: src/data/Game.vala:445 +msgctxt "status_header" +msgid "Not installed" +msgstr "Não instalado" + +#: src/data/sources/steam/Steam.vala:43 +msgid "Your SteamID will be read from Steam configuration file" +msgstr "Seu SteamID será lido a partir do arquivo de configuração do Steam" + +#: src/data/sources/steam/Steam.vala:46 +msgid "" +"Steam config file not found.\n" +"Login into your account in Steam client and return to GameHub" +msgstr "" +"Arquivo de configuração do Steam não encontrado.\n" +"Entre na sua conta no cliente Steam e retorne ao GameHub" + +#: src/data/sources/user/User.vala:30 +msgid "User games" +msgstr "Jogos do usuário" + +#: src/data/db/tables/Tags.vala:192 +msgctxt "tag" +msgid "Favorites" +msgstr "Favoritos" + +#: src/data/db/tables/Tags.vala:193 +msgctxt "tag" +msgid "Not installed" +msgstr "Não instalado" + +#: src/data/db/tables/Tags.vala:194 +msgctxt "tag" +msgid "Installed" +msgstr "Instalado" + +#: src/data/db/tables/Tags.vala:195 +msgctxt "tag" +msgid "Hidden" +msgstr "Escondido" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit script" +msgstr "Editar script" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit custom script" +msgstr "Editar script personalizado" + +#: src/data/compat/Proton.vala:43 src/data/compat/Wine.vala:49 +msgid "Environment variables" +msgstr "Variáveis ambientais" + +#: src/data/compat/Proton.vala:47 +msgid "Disable esync" +msgstr "Desativar esync" + +#: src/data/compat/Proton.vala:48 +msgid "Disable DirectX 11 compatibility layer" +msgstr "Desativar camada de compatibilidade do DirectX 11" + +#: src/data/compat/Proton.vala:49 +msgid "Use WineD3D11 as DirectX 11 compatibility layer" +msgstr "Use WineD3D11 como camada de compatibilidade DirectX 11" + +#: src/data/compat/Proton.vala:50 +msgid "Show DXVK info overlay" +msgstr "Mostrar sobreposição de informações do DXVK" + +#: src/data/compat/Proton.vala:68 src/data/compat/Wine.vala:68 +msgid "Open prefix directory" +msgstr "Abrir diretório de prefixo" + +#: src/data/compat/Proton.vala:71 src/data/compat/Wine.vala:71 +msgid "Run winecfg" +msgstr "Executar winecfg" + +#: src/data/compat/Proton.vala:74 src/data/compat/Wine.vala:74 +msgid "Run winetricks" +msgstr "Executar winetricks" + +#: src/data/compat/Proton.vala:77 src/data/compat/Wine.vala:77 +msgid "Run taskmgr" +msgstr "Executar taskmgr" + +#: src/data/compat/Proton.vala:80 src/data/compat/Wine.vala:80 +msgid "Kill apps in prefix" +msgstr "Mate aplicativos no prefixo" + +#: src/data/compat/Wine.vala:55 +msgid "InnoSetup default options" +msgstr "Opções padrão do InnoSetup" + +#: src/data/compat/Wine.vala:59 +msgid "Silent installation" +msgstr "Instalação silenciosa" + +#: src/data/compat/Wine.vala:60 +msgid "Very silent installation" +msgstr "Instalação muito silenciosa" + +#: src/data/compat/Wine.vala:61 +msgid "Suppress messages" +msgstr "Suprimir mensagens" + +#: src/data/compat/Wine.vala:62 +msgid "No GUI" +msgstr "Sem GUI" + +#: src/data/compat/DOSBox.vala:52 +msgid "Windowed" +msgstr "Windowed" + +#: src/data/compat/DOSBox.vala:52 +msgid "Disable fullscreen" +msgstr "Desativar tela inteira" + +#: src/data/compat/RetroArch.vala:51 +msgid "Libretro core file" +msgstr "Arquivo principal do Libretro" + +#: src/data/compat/CustomEmulator.vala:43 +msgid "Custom emulator" +msgstr "Emulador personalizado" + +#: src/data/compat/CustomEmulator.vala:48 +msgid "Emulator" +msgstr "Emulador" + +#: src/data/compat/CustomEmulator.vala:49 +msgid "Launch in game directory" +msgstr "Lançamento no diretório do jogo" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:37 +#: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:76 +#: src/ui/views/GamesView/GamesView.vala:220 +#: src/ui/views/GamesView/GamesView.vala:674 +msgid "Settings" +msgstr "Configurações" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:54 +msgid "Some settings will be applied after application restart" +msgstr "" +"Algumas configurações serão aplicadas após a reinicialização do aplicativo" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:68 +msgid "Interface" +msgstr "Interface" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:69 +msgid "Collection" +msgstr "Coleção" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:74 +msgid "Emulators" +msgstr "Emuladores" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:181 +msgid "Open" +msgstr "Abrir" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:188 +msgid "Clear" +msgstr "Claro" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:204 +#, c-format +msgid "%llu installer; %s" +msgid_plural "%llu installers; %s" +msgstr[0] "%llu instalador; %s" +msgstr[1] "%llu instaladores; %s" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:36 +msgid "Use dark theme" +msgstr "Use o tema escuro" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:37 +msgid "Compact list" +msgstr "Lista compacta" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:41 +msgid "Merge games from different sources" +msgstr "Mesclar jogos de diferentes fontes" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:45 +msgid "Show non-native games" +msgstr "Mostrar jogos não nativos" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:46 +msgid "Use compatibility layers and consider Windows games compatible" +msgstr "" +"Use camadas de compatibilidade e considere os jogos do Windows compatíveis" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:50 +msgid "Use imported tags" +msgstr "Use tags importadas" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:50 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:69 +msgid "Collection directory" +msgstr "Diretório de coleção" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:55 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:63 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:71 +msgid "Game directory" +msgstr "Diretório de jogos" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:56 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:64 +msgid "Installers" +msgstr "Instaladores" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:57 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:116 +msgid "DLC" +msgstr "DLC" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:58 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:89 +msgid "Bonus content" +msgstr "Conteúdo de bônus" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:68 +msgid "Variables" +msgstr "Variáveis" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:70 +msgid "Game name" +msgstr "Nome do jogo" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:41 +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:42 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:42 +msgid "Enabled" +msgstr "Ativado" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Steam API keys have limited number of uses per day" +msgstr "" +"As chaves da API do Steam têm um número limitado de utilizações por dia" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Generate key" +msgstr "Gerar chave" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:49 +msgid "Installation directory" +msgstr "Diretório de instalação" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:66 +msgid "Default" +msgstr "Padrão" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:74 +msgid "Restore default API key" +msgstr "Restaurar chave de API padrão" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:85 +msgid "Steam API key" +msgstr "Chave da API do Steam" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:46 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:49 +msgid "Games directory" +msgstr "Diretório de jogos" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:52 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:55 +msgid "Logout" +msgstr "Sair" + +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:46 +msgid "Load games from Humble Trove" +msgstr "Carregar jogos de Humble Trove" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:36 +msgid "Libretro core directory" +msgstr "Diretório do núcleo do Libretro" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:37 +msgid "Libretro core info directory" +msgstr "Diretório de informações do núcleo do Libretro" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:194 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/dialogs/GamePropertiesDialog.vala:216 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +#: src/ui/views/GamesView/AddGamePopover.vala:66 +#: src/ui/views/GamesView/AddGamePopover.vala:76 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Executable" +msgstr "Executável" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:195 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/views/GamesView/AddGamePopover.vala:67 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Installer" +msgstr "Instalar" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:200 +msgid "Save" +msgstr "Salve" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:206 +#: src/ui/dialogs/CompatRunDialog.vala:100 +#: src/ui/views/GamesView/GameContextMenu.vala:42 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:209 +msgid "Run" +msgstr "Jogar" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:212 +#: src/ui/dialogs/GamePropertiesDialog.vala:130 +#: src/ui/dialogs/GamePropertiesDialog.vala:135 +#: src/ui/views/GamesView/AddGamePopover.vala:72 +msgid "Name" +msgstr "Nome" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:228 +#: src/ui/dialogs/GamePropertiesDialog.vala:238 +#: src/ui/views/GamesView/AddGamePopover.vala:77 +msgid "Arguments" +msgstr "Argumentos" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Directory" +msgstr "Diretório" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +msgid "Select emulator directory" +msgstr "Selecione o diretório do emulador" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:277 +#: src/ui/dialogs/GamePropertiesDialog.vala:256 +msgid "Force compatibility mode" +msgstr "Forçar o modo de compatibilidade" + +#: src/ui/dialogs/InstallDialog.vala:53 src/ui/dialogs/InstallDialog.vala:199 +#: src/ui/views/GamesView/GameContextMenu.vala:49 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:208 +msgid "Install" +msgstr "Instalar" + +#: src/ui/dialogs/InstallDialog.vala:147 +msgid "Select installer" +msgstr "Selecione o instalador" + +#: src/ui/dialogs/InstallDialog.vala:152 +#, c-format +msgid "Installer size: %s" +msgstr "Tamanho do instalador: %s" + +#: src/ui/dialogs/InstallDialog.vala:197 +msgid "Import" +msgstr "Importar" + +#: src/ui/dialogs/InstallDialog.vala:203 +msgid "Download only" +msgstr "Baixe apenas" + +#: src/ui/dialogs/InstallDialog.vala:278 +msgid "Unknown" +msgstr "Desconhecido" + +#: src/ui/dialogs/GamePropertiesDialog.vala:51 +#, c-format +msgid "%s: Properties" +msgstr "%s: Propriedades" + +#: src/ui/dialogs/GamePropertiesDialog.vala:66 +#: src/ui/views/GamesView/FiltersPopover.vala:153 +msgid "Tags" +msgstr "Tags" + +#: src/ui/dialogs/GamePropertiesDialog.vala:111 +msgid "Add tag" +msgstr "Adicionar etiqueta" + +#: src/ui/dialogs/GamePropertiesDialog.vala:149 +msgid "Images" +msgstr "Imagens" + +#: src/ui/dialogs/GamePropertiesDialog.vala:183 +msgid "Image URL" +msgstr "URL da imagem" + +#: src/ui/dialogs/GamePropertiesDialog.vala:187 +msgid "Icon URL" +msgstr "URL do ícone" + +#: src/ui/dialogs/GamePropertiesDialog.vala:195 +msgid "Search images:" +msgstr "Pesquisar imagens:" + +#: src/ui/dialogs/GamePropertiesDialog.vala:251 +msgid "Compatibility" +msgstr "Compatibilidade" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:52 +#, c-format +msgid "%s: Overlays" +msgstr "%s: Sobreposições" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "Overlays are disabled" +msgstr "As sobreposições estão desativadas" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "" +"Enable overlays to manage DLCs and mods\n" +"\n" +"Enabling will move game to the “base“ overlay" +msgstr "" +"Ativar sobreposições para gerenciar DLCs e mods\n" +"\n" +"Ativar moverá o jogo para a sobreposição de “base”" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:71 +#: src/ui/views/GamesView/GameContextMenu.vala:64 +msgid "Overlays" +msgstr "Sobreposições" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:99 +msgid "Overlay ID (directory name)" +msgstr "ID de sobreposição (nome do diretório)" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:104 +msgid "Overlay name (optional)" +msgstr "Nome da sobreposição (opcional)" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:106 +msgid "Add" +msgstr "Adicionar" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:121 +msgid "Enable overlays" +msgstr "Ativar sobreposições" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:211 +msgid "Open directory" +msgstr "Diretório aberto" + +#: src/ui/dialogs/CompatRunDialog.vala:48 +#: src/ui/views/GamesView/GameContextMenu.vala:45 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:210 +msgid "Run with compatibility layer" +msgstr "Jogar com camada de compatibilidade do Windows" + +#: src/ui/views/WelcomeView.vala:51 +msgid "No enabled game sources" +msgstr "No enabled game sources" + +#: src/ui/views/WelcomeView.vala:51 +msgid "Enable some game sources in settings" +msgstr "Ativar algumas fontes de jogos nas configurações" + +#: src/ui/views/WelcomeView.vala:56 +msgid "All your games in one place" +msgstr "Todos os seus jogos em um só lugar" + +#: src/ui/views/WelcomeView.vala:56 +msgid "Let's get started" +msgstr "Vamos começar" + +#: src/ui/views/WelcomeView.vala:70 +msgid "Skip" +msgstr "Pular" + +#: src/ui/views/WelcomeView.vala:134 +msgid "Ready" +msgstr "Pronto" + +#: src/ui/views/WelcomeView.vala:140 +msgid "Authentication required" +msgstr "Autentificação requerida" + +#: src/ui/views/WelcomeView.vala:145 +msgid "Authenticating..." +msgstr "Autenticando..." + +#: src/ui/views/WelcomeView.vala:156 +#, c-format +msgid "Install %s" +msgstr "Instale %s" + +#: src/ui/views/WelcomeView.vala:157 +msgid "Return to GameHub after installing" +msgstr "Retornar ao GameHub depois de instalar" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "No games" +msgstr "Sem jogos" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "Get some games or enable some game sources in settings" +msgstr "" +"Obtenha alguns jogos ou ative algumas origens de jogos nas configurações" + +#: src/ui/views/GamesView/GamesView.vala:107 +msgid "Reload" +msgstr "Recarregar" + +#: src/ui/views/GamesView/GamesView.vala:155 +msgid "Grid view" +msgstr "Vista de grade" + +#: src/ui/views/GamesView/GamesView.vala:156 +msgid "List view" +msgstr "Exibição de lista" + +#: src/ui/views/GamesView/GamesView.vala:166 +msgid "All games" +msgstr "Todos os jogos" + +#: src/ui/views/GamesView/GamesView.vala:170 +#, c-format +msgid "%s games" +msgstr "%s jogos" + +#: src/ui/views/GamesView/GamesView.vala:177 +msgid "Downloads" +msgstr "Transferências" + +#: src/ui/views/GamesView/GamesView.vala:201 +msgid "Filters" +msgstr "Filtros" + +#: src/ui/views/GamesView/GamesView.vala:208 +#: src/ui/views/GamesView/AddGamePopover.vala:83 +msgid "Add game" +msgstr "Adicionar jogo" + +#: src/ui/views/GamesView/GamesView.vala:215 +msgid "Search" +msgstr "Procurar" + +#: src/ui/views/GamesView/GamesView.vala:266 +msgctxt "status_header" +msgid "Favorites" +msgstr "Favoritos" + +#: src/ui/views/GamesView/GamesView.vala:350 +msgid "Menu" +msgstr "Menu" + +#: src/ui/views/GamesView/GamesView.vala:351 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:79 +msgid "Back" +msgstr "Voltar" + +#: src/ui/views/GamesView/GamesView.vala:492 +#, c-format +msgid "Loading games from %s" +msgstr "Carregando jogos de %s" + +#: src/ui/views/GamesView/GamesView.vala:521 +#, c-format +msgid "%u game" +msgid_plural "%u games" +msgstr[0] "%u jogo" +msgstr[1] "%u jogos" + +#: src/ui/views/GamesView/GamesView.vala:536 +msgid "No user-added games" +msgstr "Nenhum jogo adicionado pelo usuário" + +#: src/ui/views/GamesView/GamesView.vala:537 +msgid "Add some games using plus button" +msgstr "Adicione alguns jogos usando o botão mais" + +#: src/ui/views/GamesView/GamesView.vala:541 +#, c-format +msgid "No %s games" +msgstr "Nenhum %s jogos" + +#: src/ui/views/GamesView/GamesView.vala:542 +msgid "Get some Linux-compatible games" +msgstr "Obtenha alguns jogos compatíveis com o Linux" + +#: src/ui/views/GamesView/GamesView.vala:563 +#, c-format +msgid "No games matching “%s”" +msgstr "Nenhum jogo corresponde a “%s”" + +#: src/ui/views/GamesView/GamesView.vala:568 +#, c-format +msgid "No %1$s games matching “%2$s”" +msgstr "Nenhum jogo %1$s que corresponde a “%2$s”" + +#: src/ui/views/GamesView/GamesView.vala:672 +msgid "" +"No games were loaded from Steam. Set your games list privacy to public or " +"use your own Steam API key in settings." +msgstr "" +"Nenhum jogo foi carregado do Steam. Defina a privacidade da lista de jogos " +"como pública ou use sua própria chave de API do Steam nas configurações." + +#: src/ui/views/GamesView/GamesView.vala:673 +msgid "Privacy" +msgstr "Privacidade" + +#: src/ui/views/GamesView/GamesView.vala:892 +msgid "Updating game info" +msgstr "Atualizando informações do jogo" + +#: src/ui/views/GamesView/GamesView.vala:898 +#, c-format +msgid "Updating %s game info" +msgstr "Atualizando as informações do jogo de %s" + +#: src/ui/views/GamesView/GamesView.vala:916 +msgid "Merging games" +msgstr "Mesclando jogos" + +#: src/ui/views/GamesView/GamesView.vala:932 +#, c-format +msgid "Merging games from %s" +msgstr "Mesclando jogos de %s" + +#: src/ui/views/GamesView/GamesView.vala:949 +#, c-format +msgid "Merging %s (%s)" +msgstr "Mesclando %s (%s)" + +#: src/ui/views/GamesView/DownloadProgressView.vala:124 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:148 +msgid "Pause download" +msgstr "Pausar download" + +#: src/ui/views/GamesView/DownloadProgressView.vala:129 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:154 +msgid "Resume download" +msgstr "Retomar download" + +#: src/ui/views/GamesView/DownloadProgressView.vala:134 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:160 +msgid "Cancel download" +msgstr "Cancelar o download" + +#: src/ui/views/GamesView/FiltersPopover.vala:68 +msgid "Sort:" +msgstr "Ordenar:" + +#: src/ui/views/GamesView/AddGamePopover.vala:76 +msgid "Select game executable" +msgstr "Selecione o executável do jogo" + +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Select game directory" +msgstr "Selecione o diretório do jogo" + +#: src/ui/views/GamesView/GameContextMenu.vala:53 +msgid "Details" +msgstr "Detalhes" + +#: src/ui/views/GamesView/GameContextMenu.vala:56 +msgctxt "game_context_menu" +msgid "Favorite" +msgstr "Favorito" + +#: src/ui/views/GamesView/GameContextMenu.vala:60 +msgctxt "game_context_menu" +msgid "Hidden" +msgstr "Escondido" + +#: src/ui/views/GamesView/GameContextMenu.vala:67 +msgid "Properties" +msgstr "Propriedades" + +#: src/ui/views/GamesView/GameContextMenu.vala:103 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:211 +msgid "Open installation directory" +msgstr "Abra o diretório de instalação" + +#: src/ui/views/GamesView/GameContextMenu.vala:111 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:212 +msgid "Open installers collection directory" +msgstr "Abrir diretório de coleção de instaladores" + +#: src/ui/views/GamesView/GameContextMenu.vala:119 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:213 +msgid "Open bonus collection directory" +msgstr "Abra o diretório de coleção de bônus" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Remove" +msgstr "Remover" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Uninstall" +msgstr "Desinstalar" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:214 +msgid "Open store page" +msgstr "Abra a página da loja" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:216 +msgid "Game properties" +msgstr "Propriedades do jogo" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:55 +msgid "Achievements" +msgstr "Conquistas" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:103 +#, c-format +msgid "Unlocked: %s" +msgstr "Desbloqueado: %s" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:108 +#, c-format +msgid "Global percentage: %g%%" +msgstr "Porcentagem global: %g%%" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:47 +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:61 +msgid "Playtime" +msgstr "Playtime" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:53 +msgid "Playtime (local)" +msgstr "Playtime (local)" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:70 +msgid "Last launch" +msgstr "Último lançamento" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dh" +msgstr "%dh" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dm" +msgstr "%dm" + +#: src/ui/views/GameDetailsView/blocks/Description.vala:49 +msgid "Description" +msgstr "Descrição" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:65 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:89 +msgid "Language" +msgstr "Língua" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:68 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:92 +msgid "Languages" +msgstr "Línguas" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:59 +msgid "Category" +msgstr "Categoria" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:62 +msgid "Categories" +msgstr "Categorias" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:77 +msgid "Genre" +msgstr "Gênero" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:80 +msgid "Genres" +msgstr "Gêneros" + +#: src/ui/widgets/FileChooserEntry.vala:47 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select file" +msgstr "Selecione o arquivo" + +#: src/ui/widgets/CompatToolPicker.vala:51 +msgid "Compatibility layer:" +msgstr "Camada de compatibilidade:" + +#: src/utils/Settings.vala:43 +msgctxt "sort_mode" +msgid "By name" +msgstr "Por nome do jogo" + +#: src/utils/Settings.vala:44 +msgctxt "sort_mode" +msgid "By last launch" +msgstr "Pelo último lançamento" + +#: src/utils/Settings.vala:45 +msgctxt "sort_mode" +msgid "By playtime" +msgstr "Por playtime" + +#: src/utils/downloader/Downloader.vala:155 +msgctxt "dl_status" +msgid "Starting download" +msgstr "Iniciando a baixar" + +#: src/utils/downloader/Downloader.vala:156 +msgctxt "dl_status" +msgid "Download started" +msgstr "Começando a baixar" + +#: src/utils/downloader/Downloader.vala:157 +msgctxt "dl_status" +msgid "Download finished" +msgstr "Baixou com sucesso" + +#: src/utils/downloader/Downloader.vala:158 +msgctxt "dl_status" +msgid "Download failed" +msgstr "Falha ao baixar" + +#: src/utils/downloader/Downloader.vala:160 +#, c-format +msgctxt "dl_status" +msgid "Downloading: %d%% (%s / %s)" +msgstr "Baixando: %d%% (%s / %s)" + +#: src/utils/downloader/Downloader.vala:162 +#, c-format +msgctxt "dl_status" +msgid "Paused: %d%% (%s / %s)" +msgstr "Pausado: %d%% (%s / %s)" + +#: src/utils/downloader/Downloader.vala:164 +msgctxt "dl_status" +msgid "Download cancelled" +msgstr "Download cancelado" diff --git a/po/ru.po b/po/ru.po index eb696721..51d1fbca 100644 --- a/po/ru.po +++ b/po/ru.po @@ -7,9 +7,9 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-27 03:39+0300\n" -"PO-Revision-Date: 2018-05-27 03:39+0300\n" -"Last-Translator: Automatically generated\n" +"POT-Creation-Date: 2018-11-20 01:35+0300\n" +"PO-Revision-Date: 2018-11-08 23:20+0300\n" +"Last-Translator: Anatoliy Kashkin \n" "Language-Team: none\n" "Language: ru\n" "MIME-Version: 1.0\n" @@ -17,39 +17,907 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.0.6\n" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:9 -#: data/com.github.tkashkin.gamehub.desktop.in:5 -#: src/ui/views/WelcomeView.vala:16 -msgid "All your games in one place" -msgstr "Все игры в одном месте" +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +msgid "Select executable" +msgstr "Выберите исполняемый файл" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:140 src/ui/views/GamesView/GamesView.vala:352 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select" +msgstr "Выбрать" + +#: src/data/Runnable.vala:69 src/data/Runnable.vala:71 +#: src/data/Runnable.vala:139 src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Cancel" +msgstr "Отмена" + +#: src/data/Runnable.vala:125 src/ui/widgets/FileChooserEntry.vala:47 +msgid "Select directory" +msgstr "Выберите папку" + +#: src/data/Runnable.vala:354 +#, c-format +msgid "Part %u of %u: " +msgstr "Часть %u из %u: " + +#: src/data/Game.vala:424 +msgctxt "status" +msgid "Running" +msgstr "Запущена" + +#: src/data/Game.vala:427 +msgctxt "status" +msgid "Installed" +msgstr "Установлена" + +#: src/data/Game.vala:428 +msgctxt "status" +msgid "Installing" +msgstr "Установка" + +#: src/data/Game.vala:429 +msgctxt "status" +msgid "Download started" +msgstr "Загрузка начата" + +#: src/data/Game.vala:431 +msgctxt "status" +msgid "Not installed" +msgstr "Не установлена" + +#: src/data/Game.vala:441 +msgctxt "status_header" +msgid "Installed" +msgstr "Установленные" + +#: src/data/Game.vala:442 +msgctxt "status_header" +msgid "Installing" +msgstr "Установка" -#: data/com.github.tkashkin.gamehub.appdata.xml.in:12 -msgid "Manage your Steam and GOG games in one place." -msgstr "Управляйте играми из Steam и GOG в одном месте" +#: src/data/Game.vala:443 +msgctxt "status_header" +msgid "Downloading" +msgstr "Загрузка" -#: src/data/sources/steam/Steam.vala:13 +#: src/data/Game.vala:445 +msgctxt "status_header" +msgid "Not installed" +msgstr "Не установленные" + +#: src/data/sources/steam/Steam.vala:43 msgid "Your SteamID will be read from Steam configuration file" msgstr "Ваш SteamID будет прочитан из файла конфигурации Steam" -#: src/ui/views/WelcomeView.vala:16 +#: src/data/sources/steam/Steam.vala:46 +msgid "" +"Steam config file not found.\n" +"Login into your account in Steam client and return to GameHub" +msgstr "" +"Файл конфигурации Steam не найден.\n" +"Войдите в ваш аккаунт в клиенте Steam и вернитесь в GameHub" + +#: src/data/sources/user/User.vala:30 +msgid "User games" +msgstr "Пользовательские игры" + +#: src/data/db/tables/Tags.vala:192 +msgctxt "tag" +msgid "Favorites" +msgstr "Избранные" + +#: src/data/db/tables/Tags.vala:193 +msgctxt "tag" +msgid "Not installed" +msgstr "Не установленные" + +#: src/data/db/tables/Tags.vala:194 +msgctxt "tag" +msgid "Installed" +msgstr "Установленные" + +#: src/data/db/tables/Tags.vala:195 +msgctxt "tag" +msgid "Hidden" +msgstr "Избранные" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit script" +msgstr "Редактировать скрипт" + +#: src/data/compat/CustomScript.vala:59 +msgid "Edit custom script" +msgstr "Редактировать пользовательский скрипт" + +#: src/data/compat/Proton.vala:43 src/data/compat/Wine.vala:49 +msgid "Environment variables" +msgstr "Переменные окружения" + +#: src/data/compat/Proton.vala:47 +msgid "Disable esync" +msgstr "Выключить esync" + +#: src/data/compat/Proton.vala:48 +msgid "Disable DirectX 11 compatibility layer" +msgstr "Выключить слой совместимости с DirectX 11" + +#: src/data/compat/Proton.vala:49 +msgid "Use WineD3D11 as DirectX 11 compatibility layer" +msgstr "Использовать WineD3D11 как слой совместимости DirectX 11" + +#: src/data/compat/Proton.vala:50 +msgid "Show DXVK info overlay" +msgstr "Показывать оверлей DXVK" + +#: src/data/compat/Proton.vala:68 src/data/compat/Wine.vala:68 +msgid "Open prefix directory" +msgstr "Открыть директорию префикса" + +#: src/data/compat/Proton.vala:71 src/data/compat/Wine.vala:71 +msgid "Run winecfg" +msgstr "Запустить winecfg" + +#: src/data/compat/Proton.vala:74 src/data/compat/Wine.vala:74 +msgid "Run winetricks" +msgstr "Запустить winetricks" + +#: src/data/compat/Proton.vala:77 src/data/compat/Wine.vala:77 +msgid "Run taskmgr" +msgstr "Запустить taskmgr" + +#: src/data/compat/Proton.vala:80 src/data/compat/Wine.vala:80 +msgid "Kill apps in prefix" +msgstr "Завершить приложения в префиксе" + +#: src/data/compat/Wine.vala:55 +msgid "InnoSetup default options" +msgstr "Опции по умолчанию для InnoSetup" + +#: src/data/compat/Wine.vala:59 +msgid "Silent installation" +msgstr "Тихая установка" + +#: src/data/compat/Wine.vala:60 +msgid "Very silent installation" +msgstr "Очень тихая установка" + +#: src/data/compat/Wine.vala:61 +msgid "Suppress messages" +msgstr "Подавить сообщения" + +#: src/data/compat/Wine.vala:62 +msgid "No GUI" +msgstr "Без интерфейса" + +#: src/data/compat/DOSBox.vala:52 +msgid "Windowed" +msgstr "Оконный режим" + +#: src/data/compat/DOSBox.vala:52 +msgid "Disable fullscreen" +msgstr "Выключить полноэкранный режим" + +#: src/data/compat/RetroArch.vala:51 +msgid "Libretro core file" +msgstr "Файл ядра libretro" + +#: src/data/compat/CustomEmulator.vala:43 +msgid "Custom emulator" +msgstr "Пользовательский эмулятор" + +#: src/data/compat/CustomEmulator.vala:48 +msgid "Emulator" +msgstr "Эмулятор" + +#: src/data/compat/CustomEmulator.vala:49 +msgid "Launch in game directory" +msgstr "Запускать в папке игры" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:37 +#: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:76 +#: src/ui/views/GamesView/GamesView.vala:220 +#: src/ui/views/GamesView/GamesView.vala:674 +msgid "Settings" +msgstr "Настройки" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:54 +msgid "Some settings will be applied after application restart" +msgstr "Некоторые настройки будут применены после перезапуска приложения" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:68 +msgid "Interface" +msgstr "Интерфейс" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:69 +msgid "Collection" +msgstr "Коллекция" + +#: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:74 +msgid "Emulators" +msgstr "Эмуляторы" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:181 +msgid "Open" +msgstr "Открыть" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:188 +msgid "Clear" +msgstr "Очистить" + +#: src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala:204 +#, c-format +msgid "%llu installer; %s" +msgid_plural "%llu installers; %s" +msgstr[0] "%llu установщик; %s" +msgstr[1] "%llu установщика; %s" +msgstr[2] "%llu установщиков; %s" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:36 +msgid "Use dark theme" +msgstr "Использовать тёмную тему" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:37 +msgid "Compact list" +msgstr "Компактный список" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:41 +msgid "Merge games from different sources" +msgstr "Объединять игры из разных источников" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:45 +msgid "Show non-native games" +msgstr "Показывать ненативные игры" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:46 +msgid "Use compatibility layers and consider Windows games compatible" +msgstr "" +"Использовать слои совместимости и считать игры для Windows совместимыми" + +#: src/ui/dialogs/SettingsDialog/tabs/UI.vala:50 +msgid "Use imported tags" +msgstr "Использовать импортированные теги" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:50 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:69 +msgid "Collection directory" +msgstr "Папка коллекции" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:55 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:63 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:71 +msgid "Game directory" +msgstr "Папка игры" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:56 +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:64 +msgid "Installers" +msgstr "Установщики" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:57 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:116 +msgid "DLC" +msgstr "DLC" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:58 +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:89 +msgid "Bonus content" +msgstr "Бонусный контент" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:68 +msgid "Variables" +msgstr "Переменные" + +#: src/ui/dialogs/SettingsDialog/tabs/Collection.vala:70 +msgid "Game name" +msgstr "Название игры" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:41 +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:42 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:42 +msgid "Enabled" +msgstr "Включено" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Steam API keys have limited number of uses per day" +msgstr "API-ключи Steam имеют ограничение на количество использований в день" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:46 +msgid "Generate key" +msgstr "Сгенерировать ключ" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:49 +msgid "Installation directory" +msgstr "Папка установки" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:66 +msgid "Default" +msgstr "По умолчанию" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:74 +msgid "Restore default API key" +msgstr "Восстановить API-ключ по умолчанию" + +#: src/ui/dialogs/SettingsDialog/tabs/Steam.vala:85 +msgid "Steam API key" +msgstr "API-ключ Steam" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:46 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:49 +msgid "Games directory" +msgstr "Папка игр" + +#: src/ui/dialogs/SettingsDialog/tabs/GOG.vala:52 +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:55 +msgid "Logout" +msgstr "Выйти" + +#: src/ui/dialogs/SettingsDialog/tabs/Humble.vala:46 +msgid "Load games from Humble Trove" +msgstr "Загружать игры из Humble Trove" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:36 +msgid "Libretro core directory" +msgstr "Папка ядер libretro" + +#: src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala:37 +msgid "Libretro core info directory" +msgstr "Папка информации о ядрах libretro" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:194 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:226 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/dialogs/GamePropertiesDialog.vala:216 +#: src/ui/dialogs/GamePropertiesDialog.vala:220 +#: src/ui/views/GamesView/AddGamePopover.vala:66 +#: src/ui/views/GamesView/AddGamePopover.vala:76 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Executable" +msgstr "Исполняемый файл" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:195 +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:319 +#: src/ui/views/GamesView/AddGamePopover.vala:67 +#: src/ui/views/GamesView/AddGamePopover.vala:121 +msgid "Installer" +msgstr "Установщик" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:200 +msgid "Save" +msgstr "Сохранить" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:206 +#: src/ui/dialogs/CompatRunDialog.vala:100 +#: src/ui/views/GamesView/GameContextMenu.vala:42 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:209 +msgid "Run" +msgstr "Запустить" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:212 +#: src/ui/dialogs/GamePropertiesDialog.vala:130 +#: src/ui/dialogs/GamePropertiesDialog.vala:135 +#: src/ui/views/GamesView/AddGamePopover.vala:72 +msgid "Name" +msgstr "Название" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:228 +#: src/ui/dialogs/GamePropertiesDialog.vala:238 +#: src/ui/views/GamesView/AddGamePopover.vala:77 +msgid "Arguments" +msgstr "Параметры" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Directory" +msgstr "Папка" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:240 +msgid "Select emulator directory" +msgstr "Выберите папку эмулятора" + +#: src/ui/dialogs/SettingsDialog/tabs/Emulators.vala:277 +#: src/ui/dialogs/GamePropertiesDialog.vala:256 +msgid "Force compatibility mode" +msgstr "Использовать режим совместимости" + +#: src/ui/dialogs/InstallDialog.vala:53 src/ui/dialogs/InstallDialog.vala:199 +#: src/ui/views/GamesView/GameContextMenu.vala:49 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:208 +msgid "Install" +msgstr "Установить" + +#: src/ui/dialogs/InstallDialog.vala:147 +msgid "Select installer" +msgstr "Выбрать установщик" + +#: src/ui/dialogs/InstallDialog.vala:152 +#, c-format +msgid "Installer size: %s" +msgstr "Размер установщика: %s" + +#: src/ui/dialogs/InstallDialog.vala:197 +msgid "Import" +msgstr "Импортировать" + +#: src/ui/dialogs/InstallDialog.vala:203 +msgid "Download only" +msgstr "Только загрузить" + +#: src/ui/dialogs/InstallDialog.vala:278 +msgid "Unknown" +msgstr "Неизвестно" + +#: src/ui/dialogs/GamePropertiesDialog.vala:51 +#, c-format +msgid "%s: Properties" +msgstr "%s: Свойства" + +#: src/ui/dialogs/GamePropertiesDialog.vala:66 +#: src/ui/views/GamesView/FiltersPopover.vala:153 +msgid "Tags" +msgstr "Теги" + +#: src/ui/dialogs/GamePropertiesDialog.vala:111 +msgid "Add tag" +msgstr "Добавить тег" + +#: src/ui/dialogs/GamePropertiesDialog.vala:149 +msgid "Images" +msgstr "Изображения" + +#: src/ui/dialogs/GamePropertiesDialog.vala:183 +msgid "Image URL" +msgstr "URL изображения" + +#: src/ui/dialogs/GamePropertiesDialog.vala:187 +msgid "Icon URL" +msgstr "URL иконки" + +#: src/ui/dialogs/GamePropertiesDialog.vala:195 +msgid "Search images:" +msgstr "Поиск изображений:" + +#: src/ui/dialogs/GamePropertiesDialog.vala:251 +msgid "Compatibility" +msgstr "Совместимость" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:52 +#, c-format +msgid "%s: Overlays" +msgstr "%s: Оверлеи" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "Overlays are disabled" +msgstr "Оверлеи выключены" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:68 +msgid "" +"Enable overlays to manage DLCs and mods\n" +"\n" +"Enabling will move game to the “base“ overlay" +msgstr "" +"Включите оверлеи для управления DLC и модификациями\n" +"\n" +"Включение приведёт к перемещению файлов игры в оверлей «base»" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:71 +#: src/ui/views/GamesView/GameContextMenu.vala:64 +msgid "Overlays" +msgstr "Оверлеи" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:99 +msgid "Overlay ID (directory name)" +msgstr "ID оверлея (название папки)" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:104 +msgid "Overlay name (optional)" +msgstr "Название оверлея (опционально)" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:106 +msgid "Add" +msgstr "Добавить" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:121 +msgid "Enable overlays" +msgstr "Включить оверлеи" + +#: src/ui/dialogs/GameFSOverlaysDialog.vala:211 +msgid "Open directory" +msgstr "Открыть папку" + +#: src/ui/dialogs/CompatRunDialog.vala:48 +#: src/ui/views/GamesView/GameContextMenu.vala:45 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:210 +msgid "Run with compatibility layer" +msgstr "Запустить, используя слой совместимости" + +#: src/ui/views/WelcomeView.vala:51 +msgid "No enabled game sources" +msgstr "Нет активных источников игр" + +#: src/ui/views/WelcomeView.vala:51 +msgid "Enable some game sources in settings" +msgstr "Включите источники в настройках" + +#: src/ui/views/WelcomeView.vala:56 +msgid "All your games in one place" +msgstr "Все игры в одном месте" + +#: src/ui/views/WelcomeView.vala:56 msgid "Let's get started" msgstr "Давайте начнём" -#: src/ui/views/WelcomeView.vala:24 +#: src/ui/views/WelcomeView.vala:70 msgid "Skip" msgstr "Пропустить" -#: src/ui/views/WelcomeView.vala:60 -#, c-format -msgid "%d games loaded" -msgstr "Найдено %d игр" +#: src/ui/views/WelcomeView.vala:134 +msgid "Ready" +msgstr "Готово" -#: src/ui/views/WelcomeView.vala:65 +#: src/ui/views/WelcomeView.vala:140 msgid "Authentication required" msgstr "Требуется авторизация" -#: src/ui/views/WelcomeView.vala:70 +#: src/ui/views/WelcomeView.vala:145 +msgid "Authenticating..." +msgstr "Авторизация..." + +#: src/ui/views/WelcomeView.vala:156 #, c-format msgid "Install %s" msgstr "Установить %s" + +#: src/ui/views/WelcomeView.vala:157 +msgid "Return to GameHub after installing" +msgstr "Вернитесь в GameHub после установки" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "No games" +msgstr "Нет игр" + +#: src/ui/views/GamesView/GamesView.vala:106 +msgid "Get some games or enable some game sources in settings" +msgstr "Получите игры или включите источники в настройках" + +#: src/ui/views/GamesView/GamesView.vala:107 +msgid "Reload" +msgstr "Обновить" + +#: src/ui/views/GamesView/GamesView.vala:155 +msgid "Grid view" +msgstr "Сетка" + +#: src/ui/views/GamesView/GamesView.vala:156 +msgid "List view" +msgstr "Список" + +#: src/ui/views/GamesView/GamesView.vala:166 +msgid "All games" +msgstr "Все игры" + +#: src/ui/views/GamesView/GamesView.vala:170 +#, c-format +msgid "%s games" +msgstr "Игры из %s" + +#: src/ui/views/GamesView/GamesView.vala:177 +msgid "Downloads" +msgstr "Загрузки" + +#: src/ui/views/GamesView/GamesView.vala:201 +msgid "Filters" +msgstr "Фильтры" + +#: src/ui/views/GamesView/GamesView.vala:208 +#: src/ui/views/GamesView/AddGamePopover.vala:83 +msgid "Add game" +msgstr "Добавить игру" + +#: src/ui/views/GamesView/GamesView.vala:215 +msgid "Search" +msgstr "Поиск" + +#: src/ui/views/GamesView/GamesView.vala:266 +msgctxt "status_header" +msgid "Favorites" +msgstr "Избранные" + +#: src/ui/views/GamesView/GamesView.vala:350 +msgid "Menu" +msgstr "Меню" + +#: src/ui/views/GamesView/GamesView.vala:351 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:79 +msgid "Back" +msgstr "Назад" + +#: src/ui/views/GamesView/GamesView.vala:492 +#, c-format +msgid "Loading games from %s" +msgstr "Загрузка игр из %s" + +#: src/ui/views/GamesView/GamesView.vala:521 +#, c-format +msgid "%u game" +msgid_plural "%u games" +msgstr[0] "%u игра" +msgstr[1] "%u игры" +msgstr[2] "%u игр" + +#: src/ui/views/GamesView/GamesView.vala:536 +msgid "No user-added games" +msgstr "Нет пользовательских игр" + +#: src/ui/views/GamesView/GamesView.vala:537 +msgid "Add some games using plus button" +msgstr "Добавьте игры с помощью кнопки +" + +#: src/ui/views/GamesView/GamesView.vala:541 +#, c-format +msgid "No %s games" +msgstr "Нет игр из %s" + +#: src/ui/views/GamesView/GamesView.vala:542 +msgid "Get some Linux-compatible games" +msgstr "Получите игры, совместимые с Linux" + +#: src/ui/views/GamesView/GamesView.vala:563 +#, c-format +msgid "No games matching “%s”" +msgstr "Нет игр, соответствующих запросу «%s»" + +#: src/ui/views/GamesView/GamesView.vala:568 +#, c-format +msgid "No %1$s games matching “%2$s”" +msgstr "Нет игр из %1$s, соответствующих запросу «%2$s»" + +#: src/ui/views/GamesView/GamesView.vala:672 +msgid "" +"No games were loaded from Steam. Set your games list privacy to public or " +"use your own Steam API key in settings." +msgstr "" +"Нет игр, загруженных из Steam. Настройте список игр как публичный или " +"используйте свой API-ключ Steam в настройках." + +#: src/ui/views/GamesView/GamesView.vala:673 +msgid "Privacy" +msgstr "Приватность" + +#: src/ui/views/GamesView/GamesView.vala:892 +msgid "Updating game info" +msgstr "Обновление информации об игре" + +#: src/ui/views/GamesView/GamesView.vala:898 +#, c-format +msgid "Updating %s game info" +msgstr "Обновление информации об играх из %s" + +#: src/ui/views/GamesView/GamesView.vala:916 +msgid "Merging games" +msgstr "Объединение игр" + +#: src/ui/views/GamesView/GamesView.vala:932 +#, c-format +msgid "Merging games from %s" +msgstr "Объединение игр из %s" + +#: src/ui/views/GamesView/GamesView.vala:949 +#, c-format +msgid "Merging %s (%s)" +msgstr "Объединение %s (%s)" + +#: src/ui/views/GamesView/DownloadProgressView.vala:124 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:148 +msgid "Pause download" +msgstr "Приостановить загрузку" + +#: src/ui/views/GamesView/DownloadProgressView.vala:129 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:154 +msgid "Resume download" +msgstr "Возобновить загрузку" + +#: src/ui/views/GamesView/DownloadProgressView.vala:134 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:160 +msgid "Cancel download" +msgstr "Отменить загрузку" + +#: src/ui/views/GamesView/FiltersPopover.vala:68 +msgid "Sort:" +msgstr "Сортировка:" + +#: src/ui/views/GamesView/AddGamePopover.vala:76 +msgid "Select game executable" +msgstr "Выберите исполняемый файл игры" + +#: src/ui/views/GamesView/AddGamePopover.vala:81 +msgid "Select game directory" +msgstr "Выберите папку игры" + +#: src/ui/views/GamesView/GameContextMenu.vala:53 +msgid "Details" +msgstr "Подробности" + +#: src/ui/views/GamesView/GameContextMenu.vala:56 +msgctxt "game_context_menu" +msgid "Favorite" +msgstr "Избранная" + +#: src/ui/views/GamesView/GameContextMenu.vala:60 +msgctxt "game_context_menu" +msgid "Hidden" +msgstr "Скрытая" + +#: src/ui/views/GamesView/GameContextMenu.vala:67 +msgid "Properties" +msgstr "Свойства" + +#: src/ui/views/GamesView/GameContextMenu.vala:103 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:211 +msgid "Open installation directory" +msgstr "Открыть папку установки" + +#: src/ui/views/GamesView/GameContextMenu.vala:111 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:212 +msgid "Open installers collection directory" +msgstr "Открыть папку коллекции установщиков" + +#: src/ui/views/GamesView/GameContextMenu.vala:119 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:213 +msgid "Open bonus collection directory" +msgstr "Открыть папку коллекции бонусного контента" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Remove" +msgstr "Удалить" + +#: src/ui/views/GamesView/GameContextMenu.vala:126 +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:215 +msgid "Uninstall" +msgstr "Деинсталлировать" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:214 +msgid "Open store page" +msgstr "Открыть страницу в магазине" + +#: src/ui/views/GameDetailsView/GameDetailsPage.vala:216 +msgid "Game properties" +msgstr "Свойства игры" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:55 +msgid "Achievements" +msgstr "Достижения" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:103 +#, c-format +msgid "Unlocked: %s" +msgstr "Открыто: %s" + +#: src/ui/views/GameDetailsView/blocks/Achievements.vala:108 +#, c-format +msgid "Global percentage: %g%%" +msgstr "Глобальный процент: %g%%" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:47 +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:61 +msgid "Playtime" +msgstr "Время игры" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:53 +msgid "Playtime (local)" +msgstr "Время игры (локальное)" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:70 +msgid "Last launch" +msgstr "Последний запуск" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dh" +msgstr "%dч" + +#: src/ui/views/GameDetailsView/blocks/Playtime.vala:82 +#, c-format +msgctxt "time" +msgid "%dm" +msgstr "%dм" + +#: src/ui/views/GameDetailsView/blocks/Description.vala:49 +msgid "Description" +msgstr "Описание" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:65 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:89 +msgid "Language" +msgstr "Язык" + +#: src/ui/views/GameDetailsView/blocks/GOGDetails.vala:68 +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:92 +msgid "Languages" +msgstr "Языки" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:59 +msgid "Category" +msgstr "Категория" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:62 +msgid "Categories" +msgstr "Категории" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:77 +msgid "Genre" +msgstr "Жанр" + +#: src/ui/views/GameDetailsView/blocks/SteamDetails.vala:80 +msgid "Genres" +msgstr "Жанры" + +#: src/ui/widgets/FileChooserEntry.vala:47 +#: src/ui/widgets/FileChooserEntry.vala:53 +#: src/ui/widgets/FileChooserEntry.vala:55 +msgid "Select file" +msgstr "Выбрать файл" + +#: src/ui/widgets/CompatToolPicker.vala:51 +msgid "Compatibility layer:" +msgstr "Слой совместимости:" + +#: src/utils/Settings.vala:43 +msgctxt "sort_mode" +msgid "By name" +msgstr "По названию" + +#: src/utils/Settings.vala:44 +msgctxt "sort_mode" +msgid "By last launch" +msgstr "По времени последнего запуска" + +#: src/utils/Settings.vala:45 +msgctxt "sort_mode" +msgid "By playtime" +msgstr "По времени игры" + +#: src/utils/downloader/Downloader.vala:155 +msgctxt "dl_status" +msgid "Starting download" +msgstr "Начало загрузки" + +#: src/utils/downloader/Downloader.vala:156 +msgctxt "dl_status" +msgid "Download started" +msgstr "Загрузка начата" + +#: src/utils/downloader/Downloader.vala:157 +msgctxt "dl_status" +msgid "Download finished" +msgstr "Загрузка завершена" + +#: src/utils/downloader/Downloader.vala:158 +msgctxt "dl_status" +msgid "Download failed" +msgstr "Ошибка загрузки" + +#: src/utils/downloader/Downloader.vala:160 +#, c-format +msgctxt "dl_status" +msgid "Downloading: %d%% (%s / %s)" +msgstr "Загрузка: %d%% (%s / %s)" + +#: src/utils/downloader/Downloader.vala:162 +#, c-format +msgctxt "dl_status" +msgid "Paused: %d%% (%s / %s)" +msgstr "Приостановлено: %d%% (%s / %s)" + +#: src/utils/downloader/Downloader.vala:164 +msgctxt "dl_status" +msgid "Download cancelled" +msgstr "Загрузка отменена" diff --git a/scripts/AppRun b/scripts/AppRun new file mode 100755 index 00000000..90d980de --- /dev/null +++ b/scripts/AppRun @@ -0,0 +1,19 @@ +#!/bin/bash + +echo "[AppRun ] GameHub AppImage" +echo "[AppRun ] AppDir: $APPDIR" + +export LD_LIBRARY_PATH="$APPDIR/usr/lib:$LD_LIBRARY_PATH" +export PATH="$APPDIR/usr/bin:$PATH" +export XDG_DATA_DIRS="$APPDIR/usr/share:$XDG_DATA_DIRS" + +export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas/:$GSETTINGS_SCHEMA_DIR" + +export LD_LIBRARY_PATH="$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0:$LD_LIBRARY_PATH" + +[ -e "$APPDIR/checkrt.sh" ] && . "$APPDIR/checkrt.sh" || echo "[AppRun] Skipping CheckRT" + +echo "[AppRun ] Starting GameHub" + +cd "$APPDIR/usr" +"$APPDIR/usr/bin/com.github.tkashkin.gamehub" "$@" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..d53aa8b2 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,278 @@ +#!/bin/bash + +_GH_RDNN="com.github.tkashkin.gamehub" +_GH_VERSION="0.12.1" + +_GH_BRANCH="${APPVEYOR_REPO_BRANCH:-$(git symbolic-ref --short -q HEAD)}" + +_ROOT="`pwd`" +_SCRIPTROOT="$(dirname "$(readlink -f "$0")")" +_LINUXDEPLOYQT="linuxdeployqt-continuous-x86_64.AppImage" + +_SOURCE="${APPVEYOR_BUILD_VERSION:-$_GH_VERSION-$_GH_BRANCH-local}" +_VERSION="$_SOURCE-$(git rev-parse --short HEAD)" +_DEB_VERSION="${APPVEYOR_BUILD_VERSION:-$_VERSION}" +_DEB_TARGET_DISTRO="bionic" +_BUILD_IMAGE="local" +_GPG_BINARY="gpg1" +_GPG_PACKAGE="gnupg1" + +export CFLAGS=-O0 +export CPPFLAGS=-O0 +export CXXFLAGS=-O0 + +if [[ "$APPVEYOR_BUILD_WORKER_IMAGE" = "Ubuntu1604" ]]; then + _VERSION="xenial-$_VERSION" + _DEB_VERSION="$_DEB_VERSION~ubuntu16.04" + _DEB_TARGET_DISTRO="xenial" + _BUILD_IMAGE="xenial" + _GPG_BINARY="gpg" + _GPG_PACKAGE="gnupg" +elif [[ "$APPVEYOR_BUILD_WORKER_IMAGE" = "Ubuntu1804" ]]; then + _VERSION="bionic-$_VERSION" + _DEB_VERSION="$_DEB_VERSION~ubuntu18.04" + _DEB_TARGET_DISTRO="bionic" + _BUILD_IMAGE="bionic" + _GPG_BINARY="gpg1" + _GPG_PACKAGE="gnupg1" +fi + +BUILDROOT="$_ROOT/build/appimage" +BUILDDIR="$BUILDROOT/build" +APPDIR="$BUILDROOT/appdir" + +ACTION=${1:-build_local} +CHECKRT=${2:---checkrt} + +_usr_patch() +{ + set +e + file="$1" + echo "[scripts/build.sh] Patching $file" + sed -i -e 's#/usr#././#g' "$file" +} + +_mv_deps() +{ + set +e + lib="$1" + src="$2" + dest="$3" + recursive=${4:-true} + echo "[scripts/build.sh] Moving $lib" + [ -e "$src/$lib" ] && mv -f "$src/$lib" "$dest" + [ -e "$dest/$lib" ] && ldd "$dest/$lib" | awk '{print $1}' | while read dep; do + [ -e "$src/$dep" ] && echo "[scripts/build.sh] $dep <- $lib" + if [ "$recursive" = "true" ]; then + [ -e "$src/$dep" ] && _mv_deps "$dep" "$src" "$dest" "$recursive" + else + [ -e "$src/$dep" ] && mv -f "$src/$dep" "$dest" + fi + done +} + +import_keys() +{ + set +e + cd "$_ROOT" + if [[ -n "$keys_enc_secret" ]]; then + echo "[scripts/build.sh] Importing keys" + sudo apt install -y "$_GPG_PACKAGE" + curl -sflL "https://raw.githubusercontent.com/appveyor/secure-file/master/install.sh" | bash -e - + ./appveyor-tools/secure-file -decrypt "$_SCRIPTROOT/launchpad/key_pub.gpg.enc" -secret $keys_enc_secret + ./appveyor-tools/secure-file -decrypt "$_SCRIPTROOT/launchpad/key_sec.gpg.enc" -secret $keys_enc_secret + ./appveyor-tools/secure-file -decrypt "$_SCRIPTROOT/launchpad/passphrase.enc" -secret $keys_enc_secret + "$_GPG_BINARY" --no-use-agent --import "$_SCRIPTROOT/launchpad/key_pub.gpg" + "$_GPG_BINARY" --no-use-agent --allow-secret-key-import --import "$_SCRIPTROOT/launchpad/key_sec.gpg" + sudo apt-key add "$_SCRIPTROOT/launchpad/key_pub.gpg" + rm -f "$_SCRIPTROOT/launchpad/key_pub.gpg" "$_SCRIPTROOT/launchpad/key_sec.gpg" + fi +} + +deps() +{ + set +e + echo "[scripts/build.sh] Installing dependencies" + sudo add-apt-repository ppa:elementary-os/stable -y + sudo add-apt-repository ppa:elementary-os/os-patches -y + sudo add-apt-repository ppa:elementary-os/daily -y + sudo add-apt-repository ppa:vala-team/next -y + sudo apt update -qq + sudo apt install -y meson valac checkinstall build-essential dput elementary-sdk libgranite-dev libgtk-3-dev libglib2.0-dev libwebkit2gtk-4.0-dev libjson-glib-dev libgee-0.8-dev libsoup2.4-dev libsqlite3-dev libxml2-dev libpolkit-gobject-1-dev + #sudo apt full-upgrade -y + if [[ "$APPVEYOR_BUILD_WORKER_IMAGE" = "Ubuntu1604" ]]; then + sudo dpkg -i "$_SCRIPTROOT/deps/xenial/"*.deb + else + sudo apt install -y libmanette-0.2-dev libxtst-dev libx11-dev + fi +} + +build_deb() +{ + set -e + cd "$_ROOT" + sed "s/\$VERSION/$_DEB_VERSION/g; s/\$DISTRO/$_DEB_TARGET_DISTRO/g; s/\$DATE/`date -R`/g" "debian/changelog.in" > "debian/changelog" + if [[ "$APPVEYOR_BUILD_WORKER_IMAGE" = "Ubuntu1604" ]]; then + sed "s/libmanette-0.2-dev,//g" "debian/control.in" > "debian/control" + else + cp -f "debian/control.in" "debian/control" + fi + export DEB_BUILD_OPTIONS="noopt nostrip nocheck" + if [[ -e "$_SCRIPTROOT/launchpad/passphrase" && -n "$keys_enc_secret" ]]; then + echo "[scripts/build.sh] Building source package for launchpad" + dpkg-buildpackage -S -sa -us -uc + set +e + echo "[scripts/build.sh] Signing source package" + debsign -p"$_GPG_BINARY --no-use-agent --passphrase-file $_SCRIPTROOT/launchpad/passphrase --batch" -S -k2744E6BAF20BA10AAE92253F20442B9273408FF9 ../*.changes + rm -f "$_SCRIPTROOT/launchpad/passphrase" + echo "[scripts/build.sh] Uploading package to launchpad" + dput -u -c "$_SCRIPTROOT/launchpad/dput.cf" "gamehub_$_DEB_TARGET_DISTRO" ../*.changes + set -e + fi + echo "[scripts/build.sh] Building deb package" + dpkg-buildpackage -us -uc + mkdir -p "build/$_BUILD_IMAGE" + cp ../*.deb "build/$_BUILD_IMAGE/GameHub-$_VERSION-amd64.deb" + cd "$_ROOT" +} + +build() +{ + set -e + echo "[scripts/build.sh] Building" + cd "$_ROOT" + mkdir -p "$BUILDROOT" + meson "$BUILDDIR" --prefix=/usr --buildtype=debug -Ddistro=generic -Dappimage=true + cd "$BUILDDIR" + ninja + DESTDIR="$APPDIR" ninja install + cd "$_ROOT" +} + +appimage() +{ + set -e + echo "[scripts/build.sh] Preparing AppImage" + cd "$BUILDROOT" + wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/$_LINUXDEPLOYQT" + chmod a+x "./$_LINUXDEPLOYQT" + unset QTDIR; unset QT_PLUGIN_PATH; unset LD_LIBRARY_PATH + export VERSION="$_VERSION" + export LD_LIBRARY_PATH=$APPDIR/usr/lib:$LD_LIBRARY_PATH + "./$_LINUXDEPLOYQT" "$APPDIR/usr/share/applications/$_GH_RDNN.desktop" -appimage -no-plugins -no-copy-copyright-files -verbose=2 +} + +appimage_tweak() +{ + set -e + echo "[scripts/build.sh] Tweaking AppImage" + cd "$BUILDROOT" + rm -f "$APPDIR/AppRun" + cp -f "$_SCRIPTROOT/AppRun" "$APPDIR/AppRun" + glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas" +} + +appimage_bundle_libs() +{ + set +e + echo "[scripts/build.sh] Bundling additional libs" + cd "$BUILDROOT" + + mkdir -p "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" + cp -rf "/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/injected-bundle/" "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" + cp -f "/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/WebKitNetworkProcess" "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" + cp -f "/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/WebKitStorageProcess" "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" + cp -f "/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/WebKitWebProcess" "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" + find "$APPDIR/usr/lib/" -maxdepth 1 -type f -name "libwebkit2gtk-4.0.so.*" -print0 | while read -d $'\0' file; do + _usr_patch "$file" + done + find "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/" -maxdepth 1 -type f -print0 | while read -d $'\0' file; do + _usr_patch "$file" + done +} + +appimage_checkrt() +{ + set +e + echo "[scripts/build.sh] Bundling checkrt libs" + cd "$BUILDROOT" + cp -f "$_SCRIPTROOT/checkrt.sh" "$APPDIR/checkrt.sh" + cp -rf "$_SCRIPTROOT/optlib" "$APPDIR/usr/" + + echo "[scripts/build.sh] Moving GTK and its dependencies" + mkdir -p "$APPDIR/usr/optlib/libgtk-3.so.0/" + _mv_deps "libgtk-3.so.0" "$APPDIR/usr/lib" "$APPDIR/usr/optlib/libgtk-3.so.0/" + + echo "[scripts/build.sh] Moving back non-GTK-specific dependencies" + find "$APPDIR/usr/lib/" -maxdepth 1 -type f -not -name "libgranite.so.*" -not -name "libwebkit2gtk-4.0.so.*" -print0 | while read -d $'\0' dep; do + _mv_deps "$(basename $dep)" "$APPDIR/usr/optlib/libgtk-3.so.0" "$APPDIR/usr/lib/" "false" + done + + if [[ "$APPVEYOR_BUILD_WORKER_IMAGE" = "Ubuntu1804" ]]; then + echo "[scripts/build.sh] Removing GTK and its dependencies" + rm -rf "$APPDIR/usr/optlib/libgtk-3.so.0" + fi + + for lib in 'libstdc++.so.6' 'libgcc_s.so.1'; do + echo "[scripts/build.sh] Bundling $lib" + mkdir -p "$APPDIR/usr/optlib/$lib" + for dir in "/lib" "/usr/lib"; do + libfile="$dir/x86_64-linux-gnu/$lib" + [ -e "$libfile" ] && cp "$libfile" "$APPDIR/usr/optlib/$lib/" + done + done +} + +appimage_pack() +{ + set -e + echo "[scripts/build.sh] Packing AppImage" + cd "$BUILDROOT" + unset QTDIR; unset QT_PLUGIN_PATH; unset LD_LIBRARY_PATH + export VERSION="$_VERSION" + export LD_LIBRARY_PATH=$APPDIR/usr/lib:$LD_LIBRARY_PATH + "./$_LINUXDEPLOYQT" --appimage-extract + PATH=./squashfs-root/usr/bin:$PATH ./squashfs-root/usr/bin/appimagetool --no-appstream "$APPDIR" +} + +build_flatpak() +{ + set +e + echo "[scripts/build.sh] Building flatpak package" + mkdir -p "$_ROOT/build/flatpak" + cd "$_ROOT/flatpak" + echo "[scripts/build.sh] Installing flatpak" + sudo apt install -y flatpak flatpak-builder + flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + sed "s/\$BRANCH/$_GH_BRANCH/g" "$_GH_RDNN.json.in" > "$_GH_RDNN.json" + echo "[scripts/build.sh] Autoinstalling dependencies" + flatpak-builder -y --user --install-deps-from=flathub --install-deps-only "$_ROOT/build/flatpak/build" "$_GH_RDNN.json" + echo "[scripts/build.sh] Building" + flatpak-builder -y --user --repo="$_ROOT/build/flatpak/repo" --force-clean "$_ROOT/build/flatpak/build" "$_GH_RDNN.json" + echo "[scripts/build.sh] Building bundle" + flatpak build-bundle "$_ROOT/build/flatpak/repo" "$_ROOT/build/flatpak/GameHub-$_VERSION.flatpak" "$_GH_RDNN" + echo "[scripts/build.sh] Removing flatpak build and repo directories" + rm -rf ".flatpak-builder" "$_ROOT/build/flatpak/build" "$_ROOT/build/flatpak/repo" + return 0 +} + +set +e +cd "$_ROOT" +git submodule update --init +mkdir -p "$BUILDROOT" + +if [[ "$ACTION" = "import_keys" ]]; then import_keys; fi + +if [[ "$ACTION" = "deps" ]]; then deps; fi + +if [[ "$ACTION" = "build_deb" ]]; then build_deb; fi + +if [[ "$ACTION" = "build" || "$ACTION" = "build_local" ]]; then build; fi + +if [[ "$ACTION" = "appimage" || "$ACTION" = "build_local" ]]; then appimage; fi +if [[ "$ACTION" = "appimage_tweak" || "$ACTION" = "build_local" ]]; then appimage_tweak; fi +if [[ "$ACTION" = "appimage_bundle_libs" || "$ACTION" = "build_local" ]]; then appimage_bundle_libs; fi +if [[ "$ACTION" = "appimage_checkrt" || ( "$ACTION" = "build_local" && "$CHECKRT" = "--checkrt" ) ]]; then appimage_checkrt; fi +if [[ "$ACTION" = "appimage_pack" || "$ACTION" = "build_local" ]]; then appimage_pack; fi + +if [[ "$ACTION" = "build_flatpak" && ! "$_BUILD_IMAGE" = "xenial" ]]; then build_flatpak; fi diff --git a/scripts/checkrt.sh b/scripts/checkrt.sh new file mode 100755 index 00000000..ecfdcfe5 --- /dev/null +++ b/scripts/checkrt.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +_CRT_LIB_PATH="" + +echo "[CheckRT] Checking library versions" + +_CRT_LIBS=( + 'libstdc++.so.6':'^GLIBCXX_[0-9]\.[0-9]' + 'libgcc_s.so.1':'^GCC_[0-9]\.[0-9]' +) + +_CRT_LIBS_PREFER_SYSTEM=( + 'libgtk-3.so.0':'^gtk_scrolled_window_set_propagate_natural_width' # GTK 3.22+ +) + +for lib in ${_CRT_LIBS[@]}; do + lib_filename=$(echo "$lib" | cut -d: -f1) + version_prefix=$(echo "$lib" | cut -d: -f2) + lib_dir="$APPDIR/usr/optlib/$lib_filename" + lib_path="$lib_dir/$lib_filename" + if [ -e "$lib_path" ]; then + lib=$(PATH="/sbin:$PATH" ldconfig -p | grep "$lib_filename" | awk 'NR==1 {print $NF}') + sym_sys=$(tr '\0' '\n' < "$lib" | grep -e "$version_prefix" | tail -n1) + sym_app=$(tr '\0' '\n' < "$lib_path" | grep -e "$version_prefix" | tail -n1) + echo "[CheckRT] $lib_filename: sys: $sym_sys; app: $sym_app" + if [ z$(printf "$sym_sys\n$sym_app" | sort -V | tail -1) != z"$sym_sys" ]; then + _CRT_LIB_PATH="$lib_dir:$_CRT_LIB_PATH" + fi + fi +done + +for lib in ${_CRT_LIBS_PREFER_SYSTEM[@]}; do + lib_filename=$(echo "$lib" | cut -d: -f1) + version_prefix=$(echo "$lib" | cut -d: -f2) + lib_dir="$APPDIR/usr/optlib/$lib_filename" + lib_path="$lib_dir/$lib_filename" + if [ -e "$lib_path" ]; then + lib=$(PATH="/sbin:$PATH" ldconfig -p | grep "$lib_filename" | awk 'NR==1 {print $NF}') + sym_sys=$(tr '\0' '\n' < "$lib" | grep -e "$version_prefix" | tail -n1) + if [ -z "$sym_sys" ]; then + _CRT_LIB_PATH="$lib_dir:$_CRT_LIB_PATH" + else + echo "[CheckRT] Using system version of $lib_filename" + fi + fi +done + +export LD_LIBRARY_PATH="$_CRT_LIB_PATH:$LD_LIBRARY_PATH" + +if [ -e "$APPDIR/usr/optlib/exec.so" ]; then + export LD_PRELOAD="$APPDIR/usr/optlib/exec.so:$LD_PRELOAD" +fi + +echo "[CheckRT] LD_LIBRARY_PATH: $LD_LIBRARY_PATH" +echo "[CheckRT] LD_PRELOAD: $LD_PRELOAD" diff --git a/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_amd64.deb b/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_amd64.deb new file mode 100644 index 00000000..ef8d1e90 Binary files /dev/null and b/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_amd64.deb differ diff --git a/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_i386.deb b/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_i386.deb new file mode 100644 index 00000000..c1a1e4c4 Binary files /dev/null and b/scripts/deps/xenial/gir1.2-soup-2.4_2.64.1-3_i386.deb differ diff --git a/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_amd64.deb b/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_amd64.deb new file mode 100644 index 00000000..b5e6322b Binary files /dev/null and b/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_amd64.deb differ diff --git a/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_i386.deb b/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_i386.deb new file mode 100644 index 00000000..f809672e Binary files /dev/null and b/scripts/deps/xenial/libgssapi-krb5-2_1.16.1-1_i386.deb differ diff --git a/scripts/deps/xenial/libk5crypto3_1.16.1-1_amd64.deb b/scripts/deps/xenial/libk5crypto3_1.16.1-1_amd64.deb new file mode 100644 index 00000000..fd6612cf Binary files /dev/null and b/scripts/deps/xenial/libk5crypto3_1.16.1-1_amd64.deb differ diff --git a/scripts/deps/xenial/libk5crypto3_1.16.1-1_i386.deb b/scripts/deps/xenial/libk5crypto3_1.16.1-1_i386.deb new file mode 100644 index 00000000..a8195271 Binary files /dev/null and b/scripts/deps/xenial/libk5crypto3_1.16.1-1_i386.deb differ diff --git a/scripts/deps/xenial/libkrb5-3_1.16.1-1_amd64.deb b/scripts/deps/xenial/libkrb5-3_1.16.1-1_amd64.deb new file mode 100644 index 00000000..3dd58819 Binary files /dev/null and b/scripts/deps/xenial/libkrb5-3_1.16.1-1_amd64.deb differ diff --git a/scripts/deps/xenial/libkrb5-3_1.16.1-1_i386.deb b/scripts/deps/xenial/libkrb5-3_1.16.1-1_i386.deb new file mode 100644 index 00000000..0993e99a Binary files /dev/null and b/scripts/deps/xenial/libkrb5-3_1.16.1-1_i386.deb differ diff --git a/scripts/deps/xenial/libkrb5support0_1.16.1-1_amd64.deb b/scripts/deps/xenial/libkrb5support0_1.16.1-1_amd64.deb new file mode 100644 index 00000000..5367889c Binary files /dev/null and b/scripts/deps/xenial/libkrb5support0_1.16.1-1_amd64.deb differ diff --git a/scripts/deps/xenial/libkrb5support0_1.16.1-1_i386.deb b/scripts/deps/xenial/libkrb5support0_1.16.1-1_i386.deb new file mode 100644 index 00000000..3d9cfb5a Binary files /dev/null and b/scripts/deps/xenial/libkrb5support0_1.16.1-1_i386.deb differ diff --git a/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_amd64.deb b/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_amd64.deb new file mode 100644 index 00000000..28051a61 Binary files /dev/null and b/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_amd64.deb differ diff --git a/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_i386.deb b/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_i386.deb new file mode 100644 index 00000000..c4ea6afa Binary files /dev/null and b/scripts/deps/xenial/libsoup2.4-1_2.64.1-3_i386.deb differ diff --git a/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_amd64.deb b/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_amd64.deb new file mode 100644 index 00000000..a086e71d Binary files /dev/null and b/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_amd64.deb differ diff --git a/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_i386.deb b/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_i386.deb new file mode 100644 index 00000000..e7932329 Binary files /dev/null and b/scripts/deps/xenial/libsoup2.4-dev_2.64.1-3_i386.deb differ diff --git a/scripts/launchpad/dput.cf b/scripts/launchpad/dput.cf new file mode 100644 index 00000000..5ea50da7 --- /dev/null +++ b/scripts/launchpad/dput.cf @@ -0,0 +1,13 @@ +[gamehub_bionic] +fqdn = ppa.launchpad.net +method = ftp +incoming = ~tkashkin/ubuntu/gamehub/bionic +login = anonymous +allow_unsigned_uploads = 1 + +[gamehub_xenial] +fqdn = ppa.launchpad.net +method = ftp +incoming = ~tkashkin/ubuntu/gamehub/xenial +login = anonymous +allow_unsigned_uploads = 1 diff --git a/scripts/launchpad/key_pub.gpg.enc b/scripts/launchpad/key_pub.gpg.enc new file mode 100644 index 00000000..ea8db355 Binary files /dev/null and b/scripts/launchpad/key_pub.gpg.enc differ diff --git a/scripts/launchpad/key_sec.gpg.enc b/scripts/launchpad/key_sec.gpg.enc new file mode 100644 index 00000000..6e6cc695 Binary files /dev/null and b/scripts/launchpad/key_sec.gpg.enc differ diff --git a/scripts/launchpad/passphrase.enc b/scripts/launchpad/passphrase.enc new file mode 100644 index 00000000..69f478e0 --- /dev/null +++ b/scripts/launchpad/passphrase.enc @@ -0,0 +1 @@ +ЀÂãB 9Bi \ No newline at end of file diff --git a/scripts/optlib/exec.so b/scripts/optlib/exec.so new file mode 100644 index 00000000..c8b4b408 Binary files /dev/null and b/scripts/optlib/exec.so differ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100755 index 00000000..b4e768fd --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,64 @@ +name: gamehub +version: "0.6.0" +summary: Games manager written in Vala that supports GOG, Steam and Humble Bundle +description: | + Games manager/downloader/library written in Vala + Currently supported sources: Steam, GOG, Humble Bundle +icon: ../data/com.github.tkashkin.gamehub.svg +grade: stable +confinement: strict + +plugs: + gnome-3-26-1604: + interface: content + target: $SNAP/gnome-platform + default-provider: gnome-3-26-1604:gnome-3-26-1604 + gtk-3-themes: + interface: content + target: $SNAP/usr/share/themes + default-provider: gtk-common-themes:gtk-3-themes + icon-themes: + interface: content + target: $SNAP/usr/share/icons + default-provider: gtk-common-themes:icon-themes + sound-themes: + interface: content + target: $SNAP/usr/share/sounds + default-provider: gtk-common-themes:sounds-themes + +apps: + gamehub: + command: desktop-launch snapcraft-preload $SNAP/usr/bin/com.github.tkashkin.gamehub + desktop: usr/share/applications/com.github.tkashkin.gamehub.desktop + environment: + LD_LIBRARY_PATH: $SNAP/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0/:$LD_LIBRARY_PATH + plugs: [network, network-observe, browser-support, gsettings, home, desktop, desktop-legacy, pulseaudio, opengl, x11, wayland, mir] + +parts: + gamehub: + source: .. + plugin: meson + meson-parameters: [--prefix=/usr, -Ddistro=generic, -Dsnap=true] + after: [desktop-gtk3, snapcraft-preload] + build-packages: + - build-essential + - meson + - libgtk-3-dev + - libglib2.0-dev + - libwebkit2gtk-4.0-dev + - libjson-glib-dev + - libgee-0.8-dev + - libsoup2.4-dev + - libsqlite3-dev + stage-packages: + - libgl1 + - binutils + - libwebkit2gtk-4.0-37 + - libcairo2 + - libgdk-pixbuf2.0-0 + - libglib2.0-0 + - libgstreamer1.0-0 + - libgtk-3-0 + - libjavascriptcoregtk-4.0-18 + - libpango-1.0-0 + - libsoup2.4-1 diff --git a/src/ProjectConfig.vala.in b/src/ProjectConfig.vala.in new file mode 100644 index 00000000..be0f97bb --- /dev/null +++ b/src/ProjectConfig.vala.in @@ -0,0 +1,16 @@ +using Gtk; + +namespace GameHub.ProjectConfig +{ + public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@"; + public const string GETTEXT_DIR = "@GETTEXT_DIR@"; + + public const string PROJECT_NAME = "@PROJECT_NAME@"; + public const string VERSION = "@VERSION@"; + + public const string PREFIX = "@PREFIX@"; + public const string DATADIR = "@DATADIR@"; + public const string BINDIR = "@BINDIR@"; + + public const string RUNTIME = "@RUNTIME@"; +} diff --git a/src/app.vala b/src/app.vala index dca176d4..f0d656f9 100644 --- a/src/app.vala +++ b/src/app.vala @@ -1,49 +1,105 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using Gdk; using Granite; using GameHub.Data; +using GameHub.Data.DB; using GameHub.Data.Sources.Steam; using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; +using GameHub.Data.Sources.User; using GameHub.Utils; -[CCode(cname="GETTEXT_PACKAGE")] extern const string GETTEXT_PACKAGE; - namespace GameHub { public class Application: Granite.Application { construct { - application_id = "com.github.tkashkin.gamehub"; + application_id = ProjectConfig.PROJECT_NAME; flags = ApplicationFlags.FLAGS_NONE; program_name = "GameHub"; + build_version = ProjectConfig.VERSION; } protected override void activate() { + info("Distro: %s", Utils.get_distro()); + + FSUtils.make_dirs(); + + Database.create(); + + Platforms = { Platform.LINUX, Platform.WINDOWS, Platform.MACOS }; + CurrentPlatform = Platform.LINUX; + + GameSources = { new Steam(), new GOG(), new Humble(), new Trove(), new User() }; + + CompatTool[] tools = { new Compat.CustomScript(), new Compat.CustomEmulator(), new Compat.Innoextract(), new Compat.DOSBox(), new Compat.ScummVM(), new Compat.RetroArch() }; + foreach(var appid in Compat.Proton.APPIDS) + { + tools += new Compat.Proton(appid); + } + + string[] wine_binaries = { "wine"/*, "wine64", "wine32"*/ }; + string[] wine_arches = { "win64", "win32" }; + + foreach(var wine_binary in wine_binaries) + { + foreach(var wine_arch in wine_arches) + { + if(wine_binary == "wine32" && wine_arch == "win64") continue; + tools += new Compat.Wine(wine_binary, wine_arch); + } + } + + CompatTools = tools; + weak IconTheme default_theme = IconTheme.get_default(); default_theme.add_resource_path("/com/github/tkashkin/gamehub/icons"); - + var provider = new CssProvider(); provider.load_from_resource("/com/github/tkashkin/gamehub/GameHub.css"); StyleContext.add_provider_for_screen(Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - + + #if MANETTE + GameHub.Utils.Gamepad.init(); + #endif + new GameHub.UI.Windows.MainWindow(this).show_all(); } public static int main(string[] args) { - Ivy.Stacktrace.register_handlers(); - - Intl.setlocale(LocaleCategory.ALL, ""); - Intl.textdomain(GETTEXT_PACKAGE); - - FSUtils.make_dirs(); - - GameSources = { new Steam(), new GOG() }; - + #if MANETTE + X.init_threads(); + #endif + var app = new Application(); + + var lang = Environment.get_variable("LC_ALL") ?? ""; + Intl.setlocale(LocaleCategory.ALL, lang); + Intl.bindtextdomain(ProjectConfig.GETTEXT_PACKAGE, ProjectConfig.GETTEXT_DIR); + Intl.textdomain(ProjectConfig.GETTEXT_PACKAGE); + return app.run(args); } } diff --git a/src/data/CompatTool.vala b/src/data/CompatTool.vala new file mode 100644 index 00000000..982f5150 --- /dev/null +++ b/src/data/CompatTool.vala @@ -0,0 +1,108 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data +{ + public abstract class CompatTool: Object + { + public string id { get; protected set; default = "null"; } + public string name { get; protected set; default = ""; } + public string icon { get; protected set; default = "application-x-executable-symbolic"; } + public File? executable { get; protected set; default = null; } + public bool installed { get; protected set; default = false; } + + public Option[]? options = null; + public Option[]? install_options = null; + public Action[]? actions = null; + + public virtual bool can_install(Runnable runnable) { return false; } + public virtual bool can_run(Runnable runnable) { return false; } + + public virtual File get_install_root(Runnable runnable) { return runnable.install_dir; } + + public virtual async void install(Runnable runnable, File installer){} + public virtual async void run(Runnable game){} + public virtual async void run_emulator(Emulator emu, Game? game, bool launch_in_game_dir=false){} + + public abstract class Option: Object + { + public string name { get; construct; } + public string description { get; construct; } + } + + public class BoolOption: Option + { + public bool enabled { get; construct set; } + public BoolOption(string name, string description, bool enabled) + { + Object(name: name, description: description, enabled: enabled); + } + } + + public class StringOption: Option + { + public string? value { get; construct set; } + public StringOption(string name, string description, string? value) + { + Object(name: name, description: description, value: value); + } + } + + public class FileOption: Option + { + public File? directory { get; construct set; } + public File? file { get; construct set; } + public FileOption(string name, string description, File? directory, File? file) + { + Object(name: name, description: description, directory: directory, file: file); + } + } + + public class ComboOption: StringOption + { + public ArrayList options { get; construct set; } + public ComboOption(string name, string description, ArrayList options, string? value) + { + Object(name: name, description: description, options: options, value: value); + } + } + + public class Action: Object + { + public delegate void Delegate(Runnable runnable); + public string name { get; construct; } + public string description { get; construct; } + private Delegate action; + public Action(string name, string description, owned Delegate action) + { + Object(name: name, description: description); + this.action = (owned) action; + } + public void invoke(Runnable runnable) + { + action(runnable); + } + } + } + + public static CompatTool[] CompatTools; +} diff --git a/src/data/Emulator.vala b/src/data/Emulator.vala new file mode 100644 index 00000000..0f42909b --- /dev/null +++ b/src/data/Emulator.vala @@ -0,0 +1,238 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data +{ + public class Emulator: Runnable + { + private bool is_removed = false; + public signal void removed(); + + public override File? executable { owned get; set; } + public Installer? installer; + + public Emulator.empty(){} + + public Emulator(string name, File dir, File exec, string args, string? compat=null) + { + this.name = name; + + install_dir = dir; + + executable = exec; + arguments = args; + + compat_tool = compat; + force_compat = compat != null; + + update_status(); + } + + public Emulator.from_db(Sqlite.Statement s) + { + id = Tables.Emulators.ID.get(s); + name = Tables.Emulators.NAME.get(s); + install_dir = FSUtils.file(Tables.Emulators.INSTALL_PATH.get(s)); + executable = FSUtils.file(Tables.Emulators.EXECUTABLE.get(s)); + compat_tool = Tables.Emulators.COMPAT_TOOL.get(s); + compat_tool_settings = Tables.Emulators.COMPAT_TOOL_SETTINGS.get(s); + arguments = Tables.Emulators.ARGUMENTS.get(s); + + update_status(); + } + + public void remove() + { + is_removed = true; + Tables.Emulators.remove(this); + removed(); + } + + public override void save() + { + update_status(); + + if(is_removed || name == null || executable == null) return; + + Tables.Emulators.add(this); + } + + public override void update_status() + { + if(is_removed || name == null || executable == null) return; + + id = Utils.md5(name); + + platforms.clear(); + platforms.add(Platform.LINUX); + } + + public override async void install() + { + update_status(); + + if(installer == null || install_dir == null) return; + + var installers = new ArrayList(); + installers.add(installer); + + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + + wnd.cancelled.connect(() => Idle.add(install.callback)); + + wnd.install.connect((installer, dl_only, tool) => { + installer.install.begin(this, dl_only, tool, (obj, res) => { + installer.install.end(res); + Idle.add(install.callback); + }); + }); + + wnd.show_all(); + wnd.present(); + + yield; + } + + public string[] get_args(Game? game=null, File? exec=null) + { + string[] result_args = {}; + + if(exec != null) + { + result_args += exec.get_path(); + } + + if(arguments != null && arguments.length > 0) + { + var variables = new HashMap(); + variables.set("emu", name.replace(": ", " - ").replace(":", "")); + variables.set("emu_dir", install_dir.get_path()); + if(game != null) + { + variables.set("game", game.name.replace(": ", " - ").replace(":", "")); + variables.set("file", game.executable.get_path()); + variables.set("game_dir", game.install_dir.get_path()); + } + else + { + variables.set("game", ""); + variables.set("file", ""); + variables.set("game_dir", ""); + } + var args = arguments.split(" "); + foreach(var arg in args) + { + if(arg == "$game_args") + { + if(game != null) + { + var game_args = game.arguments.split(" "); + foreach(var game_arg in game_args) + { + result_args += game_arg; + } + } + continue; + } + if("$" in arg) + { + arg = FSUtils.expand(arg, null, variables); + } + result_args += arg; + } + } + + return result_args; + } + + public override async void run() + { + if(!RunnableIsLaunched && executable.query_exists()) + { + RunnableIsLaunched = is_running = true; + + yield Utils.run_thread(get_args(null, executable), executable.get_parent().get_path(), null, true); + + RunnableIsLaunched = is_running = false; + } + } + + public async void run_game(Game? game, bool launch_in_game_dir=false) + { + if(use_compat) + { + yield run_game_compat(game, launch_in_game_dir); + return; + } + + if(executable.query_exists()) + { + RunnableIsLaunched = is_running = true; + + if(game != null) + { + game.is_running = true; + game.update_status(); + } + + var dir = game != null && launch_in_game_dir ? game.install_dir : install_dir; + yield Utils.run_thread(get_args(game, executable), dir.get_path(), null, true); + RunnableIsLaunched = is_running = false; + + if(game != null) + { + game.is_running = false; + game.update_status(); + } + } + } + + public async void run_game_compat(Game? game, bool launch_in_game_dir=false) + { + new UI.Dialogs.CompatRunDialog(this, false, game, launch_in_game_dir); + } + + public static bool is_equal(Emulator first, Emulator second) + { + return first == second || first.id == second.id; + } + + public static uint hash(Emulator emu) + { + return str_hash(emu.id); + } + + public class Installer: Runnable.Installer + { + private string emu_name; + public override string name { get { return emu_name; } } + + public Installer(Emulator emu, File installer) + { + emu_name = emu.name; + id = "installer"; + platform = installer.get_path().has_suffix(".exe") ? Platform.WINDOWS : Platform.LINUX; + parts.add(new Runnable.Installer.Part("installer", installer.get_uri(), full_size, installer, installer)); + } + } + } +} diff --git a/src/data/Game.vala b/src/data/Game.vala index 3ef5f2ab..41274f3a 100644 --- a/src/data/Game.vala +++ b/src/data/Game.vala @@ -1,22 +1,469 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; using Gtk; +using GameHub.Utils; +using GameHub.Data.DB; + namespace GameHub.Data { - public abstract class Game + public abstract class Game: Runnable { public GameSource source { get; protected set; } - - public string id { get; protected set; } - public string name { get; protected set; } - - public string icon { get; protected set; } - public string image { get; protected set; } - - public string path { get; protected set; } - public string command { get; protected set; } - - public float playtime { get; protected set; default = 0; } - - public virtual async bool is_for_linux(){ return true; } + + public string description { get; protected set; } + + public string icon { get; set; } + public string image { get; set; } + + public string? info { get; protected set; } + public string? info_detailed { get; protected set; } + + public string full_id { owned get { return source.id + ":" + id; } } + + public string? version { get; protected set; } + + public ArrayList tags { get; protected set; default = new ArrayList(Tables.Tags.Tag.is_equal); } + public bool has_tag(Tables.Tags.Tag tag) + { + return has_tag_id(tag.id); + } + public bool has_tag_id(string tag) + { + foreach(var t in tags) + { + if(t.id == tag) return true; + } + return false; + } + public void add_tag(Tables.Tags.Tag tag) + { + if(!tags.contains(tag)) + { + tags.add(tag); + } + if(!(tag in Tables.Tags.DYNAMIC_TAGS)) + { + save(); + status_change(_status); + tags_update(); + } + } + public void remove_tag(Tables.Tags.Tag tag) + { + if(tags.contains(tag)) + { + tags.remove(tag); + } + if(!(tag in Tables.Tags.DYNAMIC_TAGS)) + { + save(); + status_change(_status); + tags_update(); + } + } + public void toggle_tag(Tables.Tags.Tag tag) + { + if(tags.contains(tag)) + { + remove_tag(tag); + } + else + { + add_tag(tag); + } + } + + public override void save() + { + Tables.Games.add(this); + } + + public File? installers_dir { get; protected set; default = null; } + public bool is_installable { get; protected set; default = true; } + + public string? store_page { get; protected set; default = null; } + + public int64 last_launch { get; set; default = 0; } + + public abstract async void uninstall(); + + public override async void run() + { + if(!RunnableIsLaunched && executable.query_exists()) + { + RunnableIsLaunched = is_running = true; + update_status(); + + string[] cmd = { executable.get_path() }; + + if(arguments != null && arguments.length > 0) + { + var variables = new HashMap(); + variables.set("game", name.replace(": ", " - ").replace(":", "")); + variables.set("game_dir", install_dir.get_path()); + var args = arguments.split(" "); + foreach(var arg in args) + { + if("$" in arg) + { + arg = FSUtils.expand(arg, null, variables); + } + cmd += arg; + } + } + + last_launch = get_real_time() / 1000000; + save(); + yield Utils.run_thread(cmd, executable.get_parent().get_path(), null, true); + playtime_tracked += ((get_real_time() / 1000000) - last_launch) / 60; + save(); + + RunnableIsLaunched = is_running = false; + update_status(); + } + } + + public virtual async void update_game_info(){} + + protected Game.Status _status = new Game.Status(Game.State.UNINSTALLED, null, null); + public signal void status_change(Game.Status status); + public signal void tags_update(); + + public Game.Status status + { + get { return _status; } + set { _status = value; status_change(_status); } + } + + public virtual string escaped_name + { + owned get + { + return Utils.strip_name(name.replace(" ", "_"), "_.,"); + } + } + + public int64 playtime_source { get; set; default = 0; } + public int64 playtime_tracked { get; set; default = 0; } + + public int64 playtime { get { return playtime_source + playtime_tracked; } } + + public ArrayList overlays = new ArrayList(); + private FSOverlay? fs_overlay; + private string? fs_overlay_last_options; + + public File? get_file(string? p, bool from_all_overlays=true) + { + if(p == null || p.length == 0 || install_dir == null) return null; + var path = p; + if(!path.has_prefix("$game_dir/") && !path.has_prefix("/")) + { + path = "$game_dir/" + path; + } + File[] dirs = { install_dir }; + if(overlays_enabled) + { + dirs = { install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child("_overlay").get_child("merged") }; + if(from_all_overlays) + { + dirs += install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child(Overlay.BASE); + } + dirs += install_dir; + mount_overlays(); + } + foreach(var dir in dirs) + { + var variables = new HashMap(); + variables.set("game_dir", dir.get_path()); + var file = FSUtils.file(path, null, variables); + if(file != null && file.query_exists()) + { + return file; + } + } + return null; + } + + public string? executable_path; + public override File? executable + { + owned get + { + if(executable_path == null || executable_path.length == 0 || install_dir == null) return null; + return get_file(executable_path); + } + set + { + if(value != null && value.query_exists()) + { + File[] dirs = { install_dir }; + if(overlays_enabled) + { + dirs = { + install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child("_overlay").get_child("merged"), + install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child(Overlay.BASE), + install_dir + }; + } + foreach(var dir in dirs) + { + if(value.get_path().has_prefix(dir.get_path())) + { + executable_path = value.get_path().replace(dir.get_path(), "$game_dir"); + break; + } + } + } + else + { + executable_path = null; + } + save(); + } + } + + public bool overlays_enabled + { + get + { + if(this is Sources.Steam.SteamGame) return false; + return install_dir != null && install_dir.query_exists() + && install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child(FSUtils.OVERLAYS_DIR).get_child(FSUtils.OVERLAYS_LIST).query_exists(); + } + } + + public void enable_overlays() + { + if(this is Sources.Steam.SteamGame || install_dir == null || !install_dir.query_exists() || overlays_enabled) return; + + var base_overlay = new Overlay(this); + + try + { + FileInfo? finfo = null; + var enumerator = install_dir.enumerate_children("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS); + while((finfo = enumerator.next_file()) != null) + { + var fname = finfo.get_name(); + if(fname == FSUtils.GAMEHUB_DIR) continue; + install_dir.get_child(fname).move(base_overlay.directory.get_child(fname), FileCopyFlags.OVERWRITE | FileCopyFlags.NOFOLLOW_SYMLINKS); + } + } + catch(Error e) + { + warning("[Game.enable_overlays] Error while moving game files to `base` overlay: %s", e.message); + } + + overlays.add(base_overlay); + save_overlays(); + save(); + } + + public void save_overlays() + { + var file = install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child(FSUtils.OVERLAYS_DIR).get_child(FSUtils.OVERLAYS_LIST); + + var root_node = new Json.Node(Json.NodeType.OBJECT); + var root = new Json.Object(); + + var overlays_obj = new Json.Object(); + + foreach(var overlay in overlays) + { + if(overlay.id == Overlay.BASE) continue; + var obj = new Json.Object(); + obj.set_string_member("name", overlay.name); + obj.set_boolean_member("enabled", overlay.enabled); + overlays_obj.set_object_member(overlay.id, obj); + } + + root.set_object_member("overlays", overlays_obj); + root_node.set_object(root); + + var json = Json.to_string(root_node, true); + + try + { + FileUtils.set_contents(file.get_path(), json); + } + catch(Error e) + { + warning("[Game.save_overlays] %s", e.message); + } + + notify_property("overlays-enabled"); + } + + public void load_overlays() + { + if(!overlays_enabled) return; + overlays.clear(); + overlays.add(new Overlay(this)); + + var root_node = Parser.parse_json_file(install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child(FSUtils.OVERLAYS_DIR).get_child(FSUtils.OVERLAYS_LIST).get_path()); + + var overlays_obj = Parser.json_object(root_node, {"overlays"}); + if(overlays_obj == null) return; + + foreach(var id in overlays_obj.get_members()) + { + var obj = overlays_obj.get_object_member(id); + overlays.add(new Overlay(this, id, obj.get_string_member("name"), obj.get_boolean_member("enabled"))); + } + } + + public void mount_overlays(File? persist=null) + { + if(!overlays_enabled) return; + load_overlays(); + + var overlay_dir = install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child("_overlay"); + var merged_dir = overlay_dir.get_child("merged"); + var persist_dir = persist ?? overlay_dir.get_child("persist"); + var work_dir = overlay_dir.get_child("workdir"); + + var dirs = new ArrayList(); + + foreach(var overlay in overlays) + { + if(overlay.enabled) + { + dirs.add(overlay.directory); + } + } + + fs_overlay = new FSOverlay(merged_dir, dirs, persist_dir, work_dir); + if(fs_overlay.options != fs_overlay_last_options) + { + fs_overlay.mount.begin(); + } + fs_overlay_last_options = fs_overlay.options; + } + + public async void umount_overlays() + { + if(!overlays_enabled || fs_overlay == null) return; + yield fs_overlay.umount(); + } + + public ArrayList? achievements { get; protected set; default = null; } + public virtual async ArrayList? load_achievements() { return null; } + + public static bool is_equal(Game first, Game second) + { + return first == second || (first.source == second.source && first.id == second.id); + } + + public static uint hash(Game game) + { + return str_hash(game.full_id); + } + + public class Overlay: Object + { + public const string BASE = "base"; + + public Game game { get; construct; } + + public string id { get; construct; } + public string name { get; construct; } + public bool enabled { get; set; } + + public File? directory; + + public Overlay(Game game, string id=BASE, string? name=null, bool enabled=true) + { + Object(game: game, id: id, name: name ?? (id == BASE ? game.name : id)); + this.enabled = id == BASE || enabled; + } + + construct + { + if(game is Sources.Steam.SteamGame || game.install_dir == null || !game.install_dir.query_exists()) return; + + directory = FSUtils.mkdir(game.install_dir.get_child(FSUtils.GAMEHUB_DIR) + .get_child(FSUtils.OVERLAYS_DIR).get_child(id).get_path()); + } + } + + public class Status + { + public Game.State state; + public Game? game; + public Downloader.Download? download; + + public Status(Game.State state, Game? game=null, Downloader.Download? download=null) + { + this.state = state; + this.game = game; + this.download = download; + } + + public string description + { + owned get + { + if(game != null && game.is_running) return C_("status", "Running"); + switch(state) + { + case Game.State.INSTALLED: return C_("status", "Installed") + (game != null && game.version != null ? @" ($(game.version))" : ""); + case Game.State.INSTALLING: return C_("status", "Installing"); + case Game.State.DOWNLOADING: return download != null ? download.status.description : C_("status", "Download started"); + } + return C_("status", "Not installed"); + } + } + + public string header + { + owned get + { + switch(state) + { + case Game.State.INSTALLED: return C_("status_header", "Installed"); + case Game.State.INSTALLING: return C_("status_header", "Installing"); + case Game.State.DOWNLOADING: return C_("status_header", "Downloading"); + } + return C_("status_header", "Not installed"); + } + } + } + + public enum State + { + UNINSTALLED, INSTALLED, DOWNLOADING, INSTALLING; + } + + public abstract class Achievement + { + public string id { get; protected set; } + public string name { get; protected set; } + public string description { get; protected set; } + public bool unlocked { get; protected set; default = false; } + public DateTime? unlock_date { get; protected set; } + public string? unlock_time { get; protected set; } + public float global_percentage { get; protected set; default = 0; } + public string? image_locked { get; protected set; } + public string? image_unlocked { get; protected set; } + public string? image { get { return unlocked ? image_unlocked : image_locked; } } + } } } diff --git a/src/data/GameSource.vala b/src/data/GameSource.vala index 0ca547db..8f700522 100644 --- a/src/data/GameSource.vala +++ b/src/data/GameSource.vala @@ -1,3 +1,21 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using Gee; using GameHub.Utils; @@ -8,21 +26,45 @@ namespace GameHub.Data { public abstract class GameSource { + public virtual string id { get { return ""; } } public virtual string name { get { return ""; } } public virtual string icon { get { return ""; } } public virtual string auth_description { owned get { return ""; } } - + + public abstract bool enabled { get; set; } + public int games_count { get; protected set; } public abstract bool is_installed(bool refresh=false); - + public abstract async bool install(); - + public abstract async bool authenticate(); public abstract bool is_authenticated(); - - public abstract async ArrayList load_games(FutureResult? game_loaded = null); + public abstract bool can_authenticate_automatically(); + + public abstract ArrayList games { get; } + + public abstract async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null); + + public static GameSource? by_id(string id) + { + foreach(var src in GameSources) + { + if(src.id == id) return src; + } + return null; + } + + public static GameSource? by_name(string name) + { + foreach(var src in GameSources) + { + if(src.name == name) return src; + } + return null; + } } - + public static GameSource[] GameSources; } diff --git a/src/data/Runnable.vala b/src/data/Runnable.vala new file mode 100644 index 00000000..1b3de639 --- /dev/null +++ b/src/data/Runnable.vala @@ -0,0 +1,635 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Gtk; + +using GameHub.Utils; +using GameHub.Data.DB; + +namespace GameHub.Data +{ + public abstract class Runnable: Object + { + public string id { get; protected set; } + public string name { get; set; } + + public string? compat_tool { get; set; } + public string? compat_tool_settings { get; set; } + + public string? arguments { get; set; } + + public bool is_running { get; set; default = false; } + + public ArrayList platforms { get; protected set; default = new ArrayList(); } + public virtual bool is_supported(Platform? platform=null, bool with_compat=true) + { + platform = platform ?? CurrentPlatform; + if(platforms == null || platforms.size == 0 || platform in platforms) return true; + if(!with_compat) return false; + foreach(var tool in CompatTools) + { + if(tool.can_run(this)) return true; + } + return false; + } + + public abstract File? executable { owned get; set; } + public File? install_dir { get; set; } + + public abstract async void install(); + public abstract async void run(); + + public virtual async void run_with_compat(bool is_opened_from_menu=false) + { + if(!RunnableIsLaunched) + { + new UI.Dialogs.CompatRunDialog(this, is_opened_from_menu); + } + } + + public virtual FileChooser setup_executable_chooser() + { + #if GTK_3_22 + var chooser = new FileChooserNative(_("Select executable"), GameHub.UI.Windows.MainWindow.instance, FileChooserAction.OPEN, _("Select"), _("Cancel")); + #else + var chooser = new FileChooserDialog(_("Select executable"), GameHub.UI.Windows.MainWindow.instance, FileChooserAction.OPEN, _("Select"), ResponseType.ACCEPT, _("Cancel"), ResponseType.CANCEL); + #endif + + try + { + chooser.set_file(executable); + } + catch(Error e) + { + warning(e.message); + } + + return chooser; + } + + private int run_executable_chooser(FileChooser chooser) + { + #if GTK_3_22 + return (chooser as FileChooserNative).run(); + #else + return (chooser as FileChooserDialog).run(); + #endif + } + + public virtual void choose_executable(bool update=true) + { + var chooser = setup_executable_chooser(); + + if(run_executable_chooser(chooser) == ResponseType.ACCEPT) + { + set_chosen_executable(chooser.get_file(), update); + } + } + + public virtual void set_chosen_executable(File? file, bool update=true) + { + executable = file; + if(executable != null && executable.query_exists()) + { + Utils.run({"chmod", "+x", executable.get_path()}); + } + + if(update) + { + update_status(); + save(); + } + } + + public virtual void save(){} + public virtual void update_status(){} + + public virtual void import(bool update=true) + { + var chooser = new FileChooserDialog(_("Select directory"), GameHub.UI.Windows.MainWindow.instance, FileChooserAction.SELECT_FOLDER); + + var games_dir = ""; + if(this is Sources.GOG.GOGGame) + { + games_dir = FSUtils.Paths.GOG.Games; + } + else if(this is Sources.Humble.HumbleGame) + { + games_dir = FSUtils.Paths.Humble.Games; + } + + chooser.set_current_folder(games_dir); + + chooser.add_button(_("Cancel"), ResponseType.CANCEL); + var select_btn = chooser.add_button(_("Select"), ResponseType.ACCEPT); + + select_btn.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + select_btn.grab_default(); + + if(chooser.run() == ResponseType.ACCEPT) + { + install_dir = chooser.get_file(); + executable = FSUtils.file(install_dir.get_path(), "start.sh"); + + if(!executable.query_exists()) + { + choose_executable(false); + } + + if(install_dir.query_exists()) + { + Utils.run({"chmod", "-R", "+x", install_dir.get_path()}); + } + + if(update) + { + update_status(); + save(); + } + } + + chooser.destroy(); + } + + public bool use_compat + { + get + { + return needs_compat || force_compat; + } + } + + public bool needs_compat + { + get + { + return (!is_supported(null, false) && is_supported(null, true)) || (executable != null && executable.get_basename().has_suffix(".exe")); + } + } + + public bool force_compat + { + get + { + if(this is Sources.Steam.SteamGame) return false; + if(get_compat_option_bool("force_compat") == true) return true; + return false; + } + set + { + if(this is Sources.Steam.SteamGame) return; + set_compat_option_bool("force_compat", value); + notify_property("use-compat"); + } + } + + public bool compat_options_saved + { + get + { + if(this is Sources.Steam.SteamGame) return false; + return get_compat_option_bool("compat_options_saved") == true; + } + set + { + if(this is Sources.Steam.SteamGame) return; + set_compat_option_bool("compat_options_saved", value); + } + } + + public Json.Object get_compat_settings(CompatTool tool) + { + if(compat_tool_settings != null && compat_tool_settings.length > 0) + { + var root = Parser.parse_json(compat_tool_settings); + var settings = Parser.json_object(root, { tool.id }); + if(settings != null) + { + return settings; + } + } + return new Json.Object(); + } + + public void set_compat_settings(CompatTool tool, Json.Object? settings) + { + var root_object = new Json.Object(); + if(compat_tool_settings != null && compat_tool_settings.length > 0) + { + var root = Parser.parse_json(compat_tool_settings); + if(root != null && root.get_node_type() == Json.NodeType.OBJECT) + { + root_object = root.get_object(); + } + } + + if(settings == null) + { + root_object.remove_member(tool.id); + } + else + { + root_object.set_object_member(tool.id, settings); + } + + var root_node = new Json.Node(Json.NodeType.OBJECT); + root_node.set_object(root_object); + compat_tool_settings = Json.to_string(root_node, false); + compat_options_saved = true; + save(); + } + + public bool? get_compat_option_bool(string key) + { + if(compat_tool_settings != null && compat_tool_settings.length > 0) + { + var root = Parser.parse_json(compat_tool_settings); + if(root != null && root.get_node_type() == Json.NodeType.OBJECT) + { + var obj = root.get_object(); + if(obj.has_member(key)) return obj.get_boolean_member(key); + } + } + return null; + } + + public void set_compat_option_bool(string key, bool? value) + { + var root_object = new Json.Object(); + if(compat_tool_settings != null && compat_tool_settings.length > 0) + { + var root = Parser.parse_json(compat_tool_settings); + if(root != null && root.get_node_type() == Json.NodeType.OBJECT) + { + root_object = root.get_object(); + } + } + if(value != null) + { + root_object.set_boolean_member(key, value); + } + else + { + root_object.remove_member(key); + } + var root_node = new Json.Node(Json.NodeType.OBJECT); + root_node.set_object(root_object); + compat_tool_settings = Json.to_string(root_node, false); + save(); + } + + public abstract class Installer + { + private static string NSIS_INSTALLER_DESCRIPTION = "Nullsoft Installer"; + + public class Part: Object + { + public string id { get; construct set; } + public string url { get; construct set; } + public int64 size { get; construct set; } + public File remote { get; construct set; } + public File local { get; construct set; } + public Part(string id, string url, int64 size, File remote, File local) + { + Object(id: id, url: url, size: size, remote: remote, local: local); + } + } + + public string id { get; protected set; } + public Platform platform { get; protected set; default = CurrentPlatform; } + public ArrayList parts { get; protected set; default = new ArrayList(); } + public int64 full_size { get; protected set; default = 0; } + + public virtual string name { get { return id; } } + + public async void install(Runnable runnable, bool dl_only, CompatTool? tool=null) + { + try + { + Game? game = null; + + if(runnable is Game) + { + game = runnable as Game; + } + + if(game != null) game.status = new Game.Status(Game.State.DOWNLOADING, game, null); + + var files = new ArrayList(); + + uint p = 1; + foreach(var part in parts) + { + var ds_id = Downloader.get_instance().download_started.connect(dl => { + if(dl.remote != part.remote) return; + if(game != null) + { + game.status = new Game.Status(Game.State.DOWNLOADING, game, dl); + dl.status_change.connect(s => { + game.status_change(game.status); + }); + } + }); + + var partDesc = ""; + + if(parts.size > 1) + { + partDesc = _("Part %u of %u: ").printf(p, parts.size); + } + + var info = new Downloader.DownloadInfo(runnable.name, partDesc + part.id, game != null ? game.icon : null, null, null, game != null ? game.source.icon : null); + var file = yield Downloader.download(part.remote, part.local, info); + if(file != null && file.query_exists()) + { + files.add(file); + } + Downloader.get_instance().disconnect(ds_id); + + p++; + } + + if(game != null) game.status = new Game.Status(Game.State.UNINSTALLED, game); + runnable.update_status(); + + if(dl_only || files.size == 0) return; + + uint f = 0; + bool windows_installer = false; + bool nsis_installer = false; + foreach(var file in files) + { + var path = file.get_path(); + Utils.run({"chmod", "+x", path}); + + FSUtils.mkdir(runnable.install_dir.get_path()); + + var type = yield guess_type(file, f > 0); + + if(type == InstallerType.WINDOWS_EXECUTABLE && tool is Compat.Innoextract) + { + var desc = yield Utils.run_thread({"file", "-b", path}); + if(desc != null && desc.length > 0 && NSIS_INSTALLER_DESCRIPTION in desc) + { + type = InstallerType.WINDOWS_NSIS_INSTALLER; + } + } + + string[]? cmd = null; + + switch(type) + { + case InstallerType.EXECUTABLE: + cmd = {path, "--", "--i-agree-to-all-licenses", + "--noreadme", "--nooptions", "--noprompt", + "--destination", runnable.install_dir.get_path().replace("'", "\\'")}; // probably mojosetup + break; + + case InstallerType.ARCHIVE: + case InstallerType.WINDOWS_NSIS_INSTALLER: + cmd = {"file-roller", path, "-e", runnable.install_dir.get_path()}; // extract with file-roller + break; + + case InstallerType.WINDOWS_EXECUTABLE: + case InstallerType.GOG_PART: + cmd = null; // use compattool later + break; + + default: + cmd = {"xdg-open", path}; // unknown type, just open + break; + } + + if(game != null) game.status = new Game.Status(Game.State.INSTALLING, game); + + if(cmd != null) + { + yield Utils.run_async(cmd, null, null, false, true); + } + if(type == InstallerType.WINDOWS_EXECUTABLE) + { + windows_installer = true; + if(tool != null && tool.can_install(runnable)) + { + yield tool.install(runnable, file); + } + } + else if(type == InstallerType.WINDOWS_NSIS_INSTALLER) + { + nsis_installer = true; + } + f++; + } + + try + { + if(nsis_installer) + { + FSUtils.rm(runnable.install_dir.get_path(), "\\$*DIR", "-rf"); // remove anything like $PLUGINSDIR + } + + string? dirname = null; + FileInfo? finfo = null; + var enumerator = yield runnable.install_dir.enumerate_children_async("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS); + while((finfo = enumerator.next_file()) != null) + { + if(dirname == null && finfo.get_file_type() == FileType.DIRECTORY) + { + dirname = finfo.get_name(); + } + else + { + dirname = null; + } + } + + if(dirname != null && dirname != FSUtils.GAMEHUB_DIR && !(runnable is GameHub.Data.Sources.GOG.GOGGame.DLC)) + { + dirname = dirname.replace(" ", "\\ "); + Utils.run({"bash", "-c", "mv " + dirname + "/* " + dirname + "/.* ."}, runnable.install_dir.get_path()); + FSUtils.rm(runnable.install_dir.get_path(), dirname, "-rf"); + } + + if(windows_installer || platform == Platform.WINDOWS) + { + runnable.force_compat = true; + } + } + catch(Error e){} + + //Utils.run({"chmod", "-R", "+x", runnable.install_dir.get_path()}); + + if(!(runnable is GameHub.Data.Sources.GOG.GOGGame.DLC) && !runnable.executable.query_exists()) + { + runnable.choose_executable(); + } + } + catch(IOError.CANCELLED e){} + catch(Error e) + { + warning(e.message); + } + runnable.update_status(); + } + + public static async InstallerType guess_type(File file, bool part=false) + { + var type = InstallerType.UNKNOWN; + if(file == null) return type; + + try + { + var finfo = yield file.query_info_async(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE); + var mime = finfo.get_content_type(); + type = InstallerType.from_mime(mime); + + if(type != InstallerType.UNKNOWN) return type; + + var info = yield Utils.run_thread({"file", "-bi", file.get_path()}); + if(info != null && info.length > 0) + { + mime = info.split(";")[0]; + if(mime != null && mime.length > 0) + { + type = InstallerType.from_mime(mime); + } + } + + if(type != InstallerType.UNKNOWN) return type; + + string[] gog_part_ext = {"bin"}; + string[] exe_ext = {"sh", "elf", "bin", "run"}; + string[] win_exe_ext = {"exe"}; + string[] arc_ext = {"zip", "tar", "cpio", "bz2", "gz", "lz", "lzma", "7z", "rar"}; + + if(part) + { + foreach(var ext in gog_part_ext) + { + if(file.get_basename().has_suffix(@".$(ext)")) return InstallerType.GOG_PART; + } + } + + foreach(var ext in exe_ext) + { + if(file.get_basename().has_suffix(@".$(ext)")) return InstallerType.EXECUTABLE; + } + foreach(var ext in win_exe_ext) + { + if(file.get_basename().has_suffix(@".$(ext)")) return InstallerType.EXECUTABLE; + } + foreach(var ext in arc_ext) + { + if(file.get_basename().has_suffix(@".$(ext)")) return InstallerType.ARCHIVE; + } + } + catch(Error e){} + + return type; + } + + public enum InstallerType + { + UNKNOWN, EXECUTABLE, WINDOWS_EXECUTABLE, GOG_PART, ARCHIVE, WINDOWS_NSIS_INSTALLER; + + public static InstallerType from_mime(string type) + { + switch(type.strip()) + { + case "application/x-executable": + case "application/x-elf": + case "application/x-sh": + case "application/x-shellscript": + return InstallerType.EXECUTABLE; + + case "application/x-dosexec": + case "application/x-ms-dos-executable": + case "application/dos-exe": + case "application/exe": + case "application/msdos-windows": + case "application/x-exe": + case "application/x-msdownload": + case "application/x-winexe": + return InstallerType.WINDOWS_EXECUTABLE; + + case "application/octet-stream": + return InstallerType.GOG_PART; + + case "application/zip": + case "application/x-tar": + case "application/x-gtar": + case "application/x-cpio": + case "application/x-bzip2": + case "application/gzip": + case "application/x-lzip": + case "application/x-lzma": + case "application/x-7z-compressed": + case "application/x-rar-compressed": + case "application/x-compressed-tar": + return InstallerType.ARCHIVE; + } + return InstallerType.UNKNOWN; + } + } + } + } + + public enum Platform + { + LINUX, WINDOWS, MACOS; + + public string id() + { + switch(this) + { + case Platform.LINUX: return "linux"; + case Platform.WINDOWS: return "windows"; + case Platform.MACOS: return "mac"; + } + assert_not_reached(); + } + + public string name() + { + switch(this) + { + case Platform.LINUX: return "Linux"; + case Platform.WINDOWS: return "Windows"; + case Platform.MACOS: return "macOS"; + } + assert_not_reached(); + } + + public string icon() + { + switch(this) + { + case Platform.LINUX: return "platform-linux-symbolic"; + case Platform.WINDOWS: return "platform-windows-symbolic"; + case Platform.MACOS: return "platform-macos-symbolic"; + } + assert_not_reached(); + } + } + + public static Platform[] Platforms; + public static Platform CurrentPlatform; + + public static bool RunnableIsLaunched = false; +} diff --git a/src/data/compat/CustomEmulator.vala b/src/data/compat/CustomEmulator.vala new file mode 100644 index 00000000..01331b0f --- /dev/null +++ b/src/data/compat/CustomEmulator.vala @@ -0,0 +1,86 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Compat +{ + public class CustomEmulator: CompatTool + { + public static CustomEmulator instance; + + private ArrayList emulator_names = new ArrayList(); + + private CompatTool.ComboOption emu_option; + private CompatTool.BoolOption game_dir_option; + + public CustomEmulator() + { + instance = this; + } + + construct + { + id = "emulator"; + name = _("Custom emulator"); + icon = "input-gaming-symbolic"; + + installed = true; + + emu_option = new CompatTool.ComboOption("emulator", _("Emulator"), emulator_names, null); + game_dir_option = new CompatTool.BoolOption("launch_in_game_dir", _("Launch in game directory"), false); + + update_emulators(); + + options = { emu_option, game_dir_option }; + } + + public void update_emulators() + { + emulator_names.clear(); + + var emulators = Tables.Emulators.get_all(); + + foreach(var emu in emulators) + { + emulator_names.add(emu.name); + } + + emu_option.options = emulator_names; + } + + public override bool can_run(Runnable runnable) + { + update_emulators(); + return installed && runnable is Game && emulator_names.size > 0; + } + + public override async void run(Runnable runnable) + { + if(!can_run(runnable)) return; + + var emu = Tables.Emulators.by_name(emu_option.value); + if(emu == null) return; + + yield emu.run_game(runnable as Game, game_dir_option.enabled); + } + } +} diff --git a/src/data/compat/CustomScript.vala b/src/data/compat/CustomScript.vala new file mode 100644 index 00000000..7b34b939 --- /dev/null +++ b/src/data/compat/CustomScript.vala @@ -0,0 +1,139 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; + +namespace GameHub.Data.Compat +{ + public class CustomScript: CompatTool + { + public const string SCRIPT = "customscript.sh"; + private const string SCRIPT_TEMPLATE = """#!/bin/bash +GH_EXECUTABLE="$1" +GH_INSTALL_DIR="$2" +GH_GAME_ID="$3" +GH_GAME_ID_FULL="$4" +GH_GAME_NAME="$5" +GH_GAME_NAME_ESCAPED="$6" + +"""; + private const string EMU_SCRIPT_TEMPLATE = """#!/bin/bash +GH_EXECUTABLE="$1" +GH_INSTALL_DIR="$2" +GH_EMU_ID="$3" +GH_EMU_NAME="$4" +GH_GAME_EXECUTABLE="$5" +GH_GAME_INSTALL_DIR="$6" +GH_GAME_ID="$7" +GH_GAME_ID_FULL="$8" +GH_GAME_NAME="$9" +GH_GAME_NAME_ESCAPED="${10}" + +"""; + + construct + { + id = @"customscript"; + name = @"Custom script"; + icon = "application-x-executable-symbolic"; + installed = true; + + actions = { + new CompatTool.Action(_("Edit script"), _("Edit custom script"), edit_script) + }; + } + + public override bool can_run(Runnable runnable) + { + return true; + } + + public override async void run(Runnable runnable) + { + var gh_dir = FSUtils.mkdir(runnable.install_dir.get_path(), FSUtils.GAMEHUB_DIR); + var script = gh_dir.get_child(SCRIPT); + if(script.query_exists()) + { + Utils.run({"chmod", "+x", script.get_path()}); + var executable_path = runnable.executable != null ? runnable.executable.get_path() : "null"; + string[]? cmd = null; + if(runnable is Game) + { + var game = runnable as Game; + cmd = { script.get_path(), executable_path, game.id, game.full_id, game.name, game.escaped_name }; + } + else if(runnable is Emulator) + { + cmd = { script.get_path(), executable_path, runnable.id, runnable.name }; + } + yield Utils.run_thread(cmd, runnable.install_dir.get_path()); + } + else + { + edit_script(runnable); + } + } + + public override async void run_emulator(Emulator emu, Game? game, bool launch_in_game_dir=false) + { + var gh_dir = FSUtils.mkdir(emu.install_dir.get_path(), FSUtils.GAMEHUB_DIR); + var script = gh_dir.get_child(SCRIPT); + if(script.query_exists()) + { + Utils.run({"chmod", "+x", script.get_path()}); + var executable_path = emu.executable != null ? emu.executable.get_path() : "null"; + var game_executable_path = game != null && game.executable != null ? game.executable.get_path() : "null"; + string[] cmd = { script.get_path(), executable_path, emu.id, emu.name, game_executable_path, game.id, game.full_id, game.name, game.escaped_name }; + var dir = game != null && launch_in_game_dir ? game.install_dir : emu.install_dir; + yield Utils.run_thread(cmd, dir.get_path()); + } + else + { + edit_script(emu); + } + } + + public void edit_script(Runnable runnable) + { + var gh_dir = FSUtils.mkdir(runnable.install_dir.get_path(), FSUtils.GAMEHUB_DIR); + var script = gh_dir.get_child(SCRIPT); + if(!script.query_exists()) + { + try + { + if(runnable is Game) + { + FileUtils.set_contents(script.get_path(), SCRIPT_TEMPLATE, SCRIPT_TEMPLATE.length); + } + else if(runnable is Emulator) + { + FileUtils.set_contents(script.get_path(), EMU_SCRIPT_TEMPLATE, EMU_SCRIPT_TEMPLATE.length); + } + } + catch(Error e) + { + warning("[CustomScript.edit_script] %s", e.message); + } + } + Utils.run({"chmod", "+x", script.get_path()}); + Utils.open_uri(script.get_uri()); + } + } +} diff --git a/src/data/compat/DOSBox.vala b/src/data/compat/DOSBox.vala new file mode 100644 index 00000000..30fe65d3 --- /dev/null +++ b/src/data/compat/DOSBox.vala @@ -0,0 +1,150 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data.Compat +{ + public class DOSBox: CompatTool + { + private static string[] DOSBOX_WIN_EXECUTABLE_NAMES = {"DOSBox", "dosbox", "DOSBOX"}; + private static string[] DOSBOX_WIN_EXECUTABLE_EXTENSIONS = {".exe", ".EXE"}; + + public string binary { get; construct; default = "dosbox"; } + + private File conf_windowed; + private CompatTool.BoolOption? opt_windowed; + + public DOSBox(string binary="dosbox") + { + Object(binary: binary); + } + + construct + { + id = @"dosbox"; + name = @"DOSBox"; + icon = "tool-dosbox-symbolic"; + + executable = Utils.find_executable(binary); + installed = executable != null && executable.query_exists(); + + conf_windowed = FSUtils.file(ProjectConfig.DATADIR + "/" + ProjectConfig.PROJECT_NAME, "compat/dosbox/windowed.conf"); + if(conf_windowed.query_exists()) + { + opt_windowed = new CompatTool.BoolOption(_("Windowed"), _("Disable fullscreen"), true); + options = { opt_windowed }; + } + } + + private static ArrayList find_configs(File? dir) + { + var configs = new ArrayList(); + + if(dir == null || !dir.query_exists()) + { + return configs; + } + + try + { + FileInfo? finfo = null; + var enumerator = dir.enumerate_children("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS); + while((finfo = enumerator.next_file()) != null) + { + var fname = finfo.get_name(); + if(fname.has_suffix(".conf")) + { + configs.add(dir.get_child(fname).get_path()); + } + } + } + catch(Error e) + { + warning("[DOSBox.find_configs] %s", e.message); + } + + return configs; + } + + public override bool can_run(Runnable runnable) + { + return installed && runnable is Game && find_configs(runnable.install_dir).size > 0; + } + + public override async void run(Runnable runnable) + { + if(!can_run(runnable)) return; + + string[] cmd = { executable.get_path() }; + + var wdir = runnable.install_dir; + + var configs = find_configs(runnable.install_dir); + + if(configs.size > 2 && runnable is GameHub.Data.Sources.GOG.GOGGame) + { + foreach(var conf in configs) + { + if(conf.has_suffix("_single.conf")) + { + configs.clear(); + configs.add(conf.replace("_single.conf", ".conf")); + configs.add(conf); + break; + } + } + } + + foreach(var conf in configs) + { + cmd += "-conf"; + cmd += conf; + } + + if(conf_windowed.query_exists() && opt_windowed != null && opt_windowed.enabled) + { + cmd += "-conf"; + cmd += conf_windowed.get_path(); + } + + bool bundled_win_dosbox_found = false; + foreach(var dirname in DOSBOX_WIN_EXECUTABLE_NAMES) + { + foreach(var exename in DOSBOX_WIN_EXECUTABLE_NAMES) + { + foreach(var exeext in DOSBOX_WIN_EXECUTABLE_EXTENSIONS) + { + if(runnable.install_dir.get_child(dirname).get_child(exename + exeext).query_exists()) + { + wdir = runnable.install_dir.get_child(dirname); + bundled_win_dosbox_found = true; + break; + } + } + if(bundled_win_dosbox_found) break; + } + if(bundled_win_dosbox_found) break; + } + + yield Utils.run_thread(cmd, wdir.get_path()); + } + } +} diff --git a/src/data/compat/Innoextract.vala b/src/data/compat/Innoextract.vala new file mode 100644 index 00000000..c4b0cfec --- /dev/null +++ b/src/data/compat/Innoextract.vala @@ -0,0 +1,59 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; + +namespace GameHub.Data.Compat +{ + public class Innoextract: CompatTool + { + public string binary { get; construct; default = "innoextract"; } + + public Innoextract(string binary="innoextract") + { + Object(binary: binary); + } + + construct + { + id = @"innoextract"; + name = @"Innoextract"; + icon = "package-x-generic-symbolic"; + + executable = Utils.find_executable(binary); + installed = executable != null && executable.query_exists(); + } + + public override bool can_install(Runnable runnable) + { + return installed && runnable != null && Platform.WINDOWS in runnable.platforms; + } + + public override async void install(Runnable runnable, File installer) + { + if(!can_install(runnable) || (yield Runnable.Installer.guess_type(installer)) != Runnable.Installer.InstallerType.WINDOWS_EXECUTABLE) return; + + string[] cmd = { executable.get_path(), "-e", "-m", "-d", runnable.install_dir.get_path() }; + if(runnable is Sources.GOG.GOGGame) cmd += "--gog"; + cmd += installer.get_path(); + yield Utils.run_thread(cmd, installer.get_parent().get_path()); + } + } +} diff --git a/src/data/compat/Proton.vala b/src/data/compat/Proton.vala new file mode 100644 index 00000000..17bbec39 --- /dev/null +++ b/src/data/compat/Proton.vala @@ -0,0 +1,180 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; + +namespace GameHub.Data.Compat +{ + public class Proton: Wine + { + public const string[] APPIDS = {"961940", "930400", "858280"}; // 3.16, 3.7 Beta, 3.7 + + public string appid { get; construct; } + + public Proton(string appid) + { + Object(appid: appid, binary: "", arch: ""); + } + + construct + { + id = @"proton_$(appid)"; + name = "Proton"; + icon = "source-steam-symbolic"; + installed = false; + + opt_env = new CompatTool.StringOption("env", _("Environment variables"), null); + + options = { + opt_env, + new CompatTool.BoolOption("PROTON_NO_ESYNC", _("Disable esync"), false), + new CompatTool.BoolOption("PROTON_NO_D3D11", _("Disable DirectX 11 compatibility layer"), false), + new CompatTool.BoolOption("PROTON_USE_WINED3D11", _("Use WineD3D11 as DirectX 11 compatibility layer"), false), + new CompatTool.BoolOption("DXVK_HUD", _("Show DXVK info overlay"), true) + }; + + File? proton_dir = null; + if(Steam.find_app_install_dir(appid, out proton_dir)) + { + if(proton_dir != null) + { + name = proton_dir.get_basename(); + executable = proton_dir.get_child("proton"); + installed = executable.query_exists(); + wine_binary = proton_dir.get_child("dist/bin/wine"); + } + } + + if(installed) + { + actions = { + new CompatTool.Action("prefix", _("Open prefix directory"), r => { + Utils.open_uri(get_wineprefix(r).get_uri()); + }), + new CompatTool.Action("winecfg", _("Run winecfg"), r => { + wineutil.begin(null, r, "winecfg"); + }), + new CompatTool.Action("winetricks", _("Run winetricks"), r => { + winetricks.begin(null, r); + }), + new CompatTool.Action("taskmgr", _("Run taskmgr"), r => { + wineutil.begin(null, r, "taskmgr"); + }), + new CompatTool.Action("kill", _("Kill apps in prefix"), r => { + wineboot.begin(null, r, {"-k"}); + }) + }; + } + } + + protected override async void exec(Runnable runnable, File file, File dir, string[]? args=null, bool parse_opts=true) + { + string[] cmd = { executable.get_path(), "run", file.get_path() }; + if(args != null) + { + foreach(var arg in args) cmd += arg; + } + yield Utils.run_thread(cmd, dir.get_path(), prepare_env(runnable, parse_opts)); + } + + protected override File get_wineprefix(Runnable runnable) + { + var prefix = FSUtils.mkdir(runnable.install_dir.get_path(), @"$(FSUtils.GAMEHUB_DIR)/$(FSUtils.COMPAT_DATA_DIR)/$(id)/pfx"); + var dosdevices = prefix.get_child("dosdevices"); + + if(FSUtils.file(runnable.install_dir.get_path(), @"$(FSUtils.GAMEHUB_DIR)/$(id)").query_exists()) + { + Utils.run({"bash", "-c", @"mv -f $(FSUtils.GAMEHUB_DIR)/$(id) $(FSUtils.GAMEHUB_DIR)/$(FSUtils.COMPAT_DATA_DIR)/$(id)"}, runnable.install_dir.get_path()); + FSUtils.rm(dosdevices.get_child("d:").get_path()); + } + + if(dosdevices.get_child("c:").query_exists() && !dosdevices.get_child("d:").query_exists()) + { + Utils.run({"ln", "-nsf", "../../../../../", "d:"}, dosdevices.get_path()); + } + return prefix; + } + + protected override string[] prepare_env(Runnable runnable, bool parse_opts=true) + { + var env = Environ.get(); + + var compatdata = FSUtils.mkdir(runnable.install_dir.get_path(), @"$(FSUtils.GAMEHUB_DIR)/$(FSUtils.COMPAT_DATA_DIR)/$(id)"); + if(compatdata != null && compatdata.query_exists()) + { + env = Environ.set_variable(env, "STEAM_COMPAT_CLIENT_INSTALL_PATH", FSUtils.Paths.Steam.Home); + env = Environ.set_variable(env, "STEAM_COMPAT_DATA_PATH", compatdata.get_path()); + env = Environ.set_variable(env, "PROTON_LOG", "1"); + env = Environ.set_variable(env, "PROTON_DUMP_DEBUG_COMMANDS", "1"); + } + + if(parse_opts) + { + if(opt_env.value != null && opt_env.value.length > 0) + { + var evars = opt_env.value.split(" "); + foreach(var ev in evars) + { + var v = ev.split("="); + env = Environ.set_variable(env, v[0], v[1]); + } + } + + foreach(var opt in options) + { + if(opt is CompatTool.BoolOption && ((CompatTool.BoolOption) opt).enabled) + { + env = Environ.set_variable(env, opt.name, "1"); + } + } + } + + return env; + } + + protected override async void wineboot(File? wineprefix, Runnable runnable, string[]? args=null) + { + if(args == null) + { + yield proton_init_prefix(wineprefix, runnable); + } + + yield wineutil(wineprefix, runnable, "wineboot", args); + } + + protected async void proton_init_prefix(File? wineprefix, Runnable runnable) + { + var env = prepare_env(runnable, false); + env = Environ.set_variable(env, "WINE", wine_binary.get_path()); + env = Environ.set_variable(env, "WINEDLLOVERRIDES", "mshtml=d"); + var prefix = wineprefix ?? get_wineprefix(runnable); + if(prefix != null && prefix.query_exists()) + { + env = Environ.set_variable(env, "WINEPREFIX", prefix.get_path()); + } + if(arch != null && arch.length > 0) + { + env = Environ.set_variable(env, "WINEARCH", arch); + } + + yield Utils.run_thread({ executable.get_path(), "run" }, runnable.install_dir.get_path(), env); + } + } +} diff --git a/src/data/compat/RetroArch.vala b/src/data/compat/RetroArch.vala new file mode 100644 index 00000000..d3a277bd --- /dev/null +++ b/src/data/compat/RetroArch.vala @@ -0,0 +1,120 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data.Compat +{ + public class RetroArch: CompatTool + { + private const string LIBRETRO_CORE_SUFFIX = "_libretro.so"; + public static RetroArch instance; + private ArrayList cores = new ArrayList(); + + public string binary { get; construct; default = "retroarch"; } + public bool has_cores { get; protected set; default = false; } + + private CompatTool.ComboOption core_option; + + public RetroArch(string binary="retroarch") + { + Object(binary: binary); + instance = this; + } + + construct + { + id = @"retroarch"; + name = @"RetroArch"; + icon = "emu-retroarch-symbolic"; + + executable = Utils.find_executable(binary); + installed = executable != null && executable.query_exists(); + + core_option = new CompatTool.ComboOption("core", _("Libretro core file"), cores, null); + + find_cores(); + + options = { core_option }; + } + + public bool find_cores() + { + cores.clear(); + has_cores = false; + core_option.options = cores; + + var dir = FSUtils.file(FSUtils.Paths.Settings.get_instance().libretro_core_dir); + + if(dir == null || !dir.query_exists()) + { + return has_cores; + } + + try + { + FileInfo? finfo = null; + var enumerator = dir.enumerate_children("standard::*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS); + while((finfo = enumerator.next_file()) != null) + { + var fname = finfo.get_name(); + if(fname.has_suffix(LIBRETRO_CORE_SUFFIX)) + { + cores.add(fname.replace(LIBRETRO_CORE_SUFFIX, "")); + } + } + } + catch(Error e) + { + warning("[RetroArch.find_cores] %s", e.message); + } + + has_cores = cores.size > 0; + core_option.options = cores; + + return has_cores; + } + + public override bool can_run(Runnable runnable) + { + return installed && runnable is Game && has_cores; + } + + public override async void run(Runnable runnable) + { + if(!can_run(runnable)) return; + var core = core_option.value; + if(core == null) return; + + if(!core.has_prefix("/")) + { + core = FSUtils.expand(FSUtils.Paths.Settings.get_instance().libretro_core_dir, core); + } + if(!core.has_suffix(LIBRETRO_CORE_SUFFIX)) + { + core += LIBRETRO_CORE_SUFFIX; + } + + string[] cmd = { executable.get_path(), "-L", core, runnable.executable.get_path() }; + + yield Utils.run_thread(cmd, runnable.install_dir.get_path()); + } + } +} diff --git a/src/data/compat/ScummVM.vala b/src/data/compat/ScummVM.vala new file mode 100644 index 00000000..852334be --- /dev/null +++ b/src/data/compat/ScummVM.vala @@ -0,0 +1,77 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data.Compat +{ + public class ScummVM: CompatTool + { + private static string SCUMMVM_NO_GAMES_WARNING = "WARNING: ScummVM could not find any game"; + + public string binary { get; construct; default = "scummvm"; } + + public ScummVM(string binary="scummvm") + { + Object(binary: binary); + } + + construct + { + id = "scummvm"; + name = "ScummVM"; + icon = "tool-scummvm-symbolic"; + + executable = Utils.find_executable(binary); + installed = executable != null && executable.query_exists(); + } + + private bool scummvm_detect(File? dir) + { + if(dir != null && dir.query_exists()) + { + var output = Utils.run({ executable.get_path(), "--detect" }, dir.get_path(), null, false, false); + return !(SCUMMVM_NO_GAMES_WARNING in output); + } + return false; + } + + public override bool can_run(Runnable runnable) + { + return installed && runnable is Game && runnable.install_dir != null + && (scummvm_detect(runnable.install_dir) || scummvm_detect(runnable.install_dir.get_child("data"))); + } + + public override async void run(Runnable runnable) + { + if(!can_run(runnable)) return; + + var dir = runnable.install_dir; + var data_dir = runnable.install_dir.get_child("data"); + + if(!scummvm_detect(dir) && scummvm_detect(data_dir)) + { + dir = data_dir; + } + + yield Utils.run_thread({ executable.get_path(), "--auto-detect" }, dir.get_path()); + } + } +} diff --git a/src/data/compat/Wine.vala b/src/data/compat/Wine.vala new file mode 100644 index 00000000..ee45f947 --- /dev/null +++ b/src/data/compat/Wine.vala @@ -0,0 +1,275 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; + +namespace GameHub.Data.Compat +{ + public class Wine: CompatTool + { + public string binary { get; construct; default = "wine"; } + public string arch { get; construct; default = "win64"; } + public File? wine_binary { get; protected set; } + + public CompatTool.StringOption opt_env; + + public CompatTool.BoolOption install_opt_innosetup_args; + + public Wine(string binary="wine", string arch="win64") + { + Object(binary: binary, arch: arch); + } + + construct + { + id = @"wine_$(binary)_$(arch)"; + name = @"Wine ($(binary)) [arch: $(arch)]"; + icon = "tool-wine-symbolic"; + + executable = wine_binary = Utils.find_executable(binary); + installed = executable != null && executable.query_exists(); + + opt_env = new CompatTool.StringOption("env", _("Environment variables"), null); + + options = { + opt_env + }; + + install_opt_innosetup_args = new CompatTool.BoolOption("InnoSetup", _("InnoSetup default options"), true); + + install_options = { + install_opt_innosetup_args, + new CompatTool.BoolOption("/SILENT", _("Silent installation"), false), + new CompatTool.BoolOption("/VERYSILENT", _("Very silent installation"), true), + new CompatTool.BoolOption("/SUPPRESSMSGBOXES", _("Suppress messages"), true), + new CompatTool.BoolOption("/NOGUI", _("No GUI"), true) + }; + + if(installed) + { + actions = { + new CompatTool.Action("prefix", _("Open prefix directory"), r => { + Utils.open_uri(get_wineprefix(r).get_uri()); + }), + new CompatTool.Action("winecfg", _("Run winecfg"), r => { + wineutil.begin(null, r, "winecfg"); + }), + new CompatTool.Action("winetricks", _("Run winetricks"), r => { + winetricks.begin(null, r); + }), + new CompatTool.Action("taskmgr", _("Run taskmgr"), r => { + wineutil.begin(null, r, "taskmgr"); + }), + new CompatTool.Action("kill", _("Kill apps in prefix"), r => { + wineboot.begin(null, r, {"-k"}); + }) + }; + } + } + + public override bool can_install(Runnable runnable) + { + return can_run(runnable); + } + + public override bool can_run(Runnable runnable) + { + return installed && runnable != null && ((runnable is Game && (runnable is GameHub.Data.Sources.User.UserGame || Platform.WINDOWS in runnable.platforms)) || runnable is Emulator); + } + + protected virtual async string[] prepare_installer_args(Runnable runnable) + { + var tmp_root = (runnable is Game) ? "_gamehub_game_root" : "_gamehub_app_root"; + var win_path = yield convert_path(runnable, runnable.install_dir.get_child(tmp_root)); + + string[] opts = {}; + + if(install_opt_innosetup_args.enabled) + { + opts = { "/SP-", "/NOCANCEL", "/NOGUI", "/NOICONS", @"/DIR=$(win_path)", "/LOG=D:\\install.log" }; + } + + foreach(var opt in install_options) + { + if(opt.name.has_prefix("/") && opt is CompatTool.BoolOption && ((CompatTool.BoolOption) opt).enabled) + { + opts += opt.name; + } + } + + return opts; + } + + public override async void install(Runnable runnable, File installer) + { + if(!can_install(runnable) || (yield Runnable.Installer.guess_type(installer)) != Runnable.Installer.InstallerType.WINDOWS_EXECUTABLE) return; + yield wineboot(null, runnable); + yield exec(runnable, installer, installer.get_parent(), yield prepare_installer_args(runnable)); + } + + public override async void run(Runnable runnable) + { + if(!can_run(runnable)) return; + yield wineboot(null, runnable); + yield exec(runnable, runnable.executable, runnable.install_dir); + } + + public override async void run_emulator(Emulator emu, Game? game, bool launch_in_game_dir=false) + { + if(!can_run(emu)) return; + var dir = game != null && launch_in_game_dir ? game.install_dir : emu.install_dir; + yield exec(emu, emu.executable, dir, emu.get_args(game)); + } + + protected virtual async void exec(Runnable runnable, File file, File dir, string[]? args=null, bool parse_opts=true) + { + string[] cmd = { executable.get_path(), file.get_path() }; + if(args != null) + { + foreach(var arg in args) cmd += arg; + } + yield Utils.run_thread(cmd, dir.get_path(), prepare_env(runnable)); + } + + protected virtual File get_wineprefix(Runnable runnable) + { + var prefix = FSUtils.mkdir(runnable.install_dir.get_path(), @"$(FSUtils.GAMEHUB_DIR)/$(FSUtils.COMPAT_DATA_DIR)/$(binary)_$(arch)"); + var dosdevices = prefix.get_child("dosdevices"); + + if(FSUtils.file(runnable.install_dir.get_path(), @"$(FSUtils.GAMEHUB_DIR)/$(binary)_$(arch)").query_exists()) + { + Utils.run({"bash", "-c", @"mv -f $(FSUtils.GAMEHUB_DIR)/$(binary)_$(arch) $(FSUtils.GAMEHUB_DIR)/$(FSUtils.COMPAT_DATA_DIR)/$(binary)_$(arch)"}, runnable.install_dir.get_path()); + FSUtils.rm(dosdevices.get_child("d:").get_path()); + } + + if(dosdevices.get_child("c:").query_exists() && !dosdevices.get_child("d:").query_exists()) + { + Utils.run({"ln", "-nsf", "../../../../", "d:"}, dosdevices.get_path()); + } + return prefix; + } + + public override File get_install_root(Runnable runnable) + { + return get_wineprefix(runnable).get_child("drive_c"); + } + + protected virtual string[] prepare_env(Runnable runnable, bool parse_opts=true) + { + var env = Environ.get(); + env = Environ.set_variable(env, "WINEDLLOVERRIDES", "mshtml=d"); + var prefix = get_wineprefix(runnable); + if(prefix != null && prefix.query_exists()) + { + env = Environ.set_variable(env, "WINEPREFIX", prefix.get_path()); + } + if(arch != null && arch.length > 0) + { + env = Environ.set_variable(env, "WINEARCH", arch); + } + + if(parse_opts) + { + if(opt_env.value != null && opt_env.value.length > 0) + { + var evars = opt_env.value.split(" "); + foreach(var ev in evars) + { + var v = ev.split("="); + env = Environ.set_variable(env, v[0], v[1]); + } + } + } + + return env; + } + + protected virtual async void wineboot(File? wineprefix, Runnable runnable, string[]? args=null) + { + yield wineutil(wineprefix, runnable, "wineboot", args); + } + + protected async void wineutil(File? wineprefix, Runnable runnable, string util="winecfg", string[]? args=null) + { + var env = Environ.get(); + env = Environ.set_variable(env, "WINE", wine_binary.get_path()); + env = Environ.set_variable(env, "WINEDLLOVERRIDES", "mshtml=d"); + var prefix = wineprefix ?? get_wineprefix(runnable); + if(prefix != null && prefix.query_exists()) + { + env = Environ.set_variable(env, "WINEPREFIX", prefix.get_path()); + } + if(arch != null && arch.length > 0) + { + env = Environ.set_variable(env, "WINEARCH", arch); + } + + string[] cmd = { wine_binary.get_path(), util }; + + if(args != null) + { + foreach(var arg in args) + { + cmd += arg; + } + } + + yield Utils.run_thread(cmd, runnable.install_dir.get_path(), env); + } + + protected async void winetricks(File? wineprefix, Runnable runnable) + { + var env = Environ.get(); + env = Environ.set_variable(env, "WINE", wine_binary.get_path()); + env = Environ.set_variable(env, "WINEDLLOVERRIDES", "mshtml=d"); + var prefix = wineprefix ?? get_wineprefix(runnable); + if(prefix != null && prefix.query_exists()) + { + env = Environ.set_variable(env, "WINEPREFIX", prefix.get_path()); + } + if(arch != null && arch.length > 0) + { + env = Environ.set_variable(env, "WINEARCH", arch); + } + + yield Utils.run_thread({ "winetricks" }, runnable.install_dir.get_path(), env); + } + + public async string convert_path(Runnable runnable, File path) + { + var env = Environ.get(); + env = Environ.set_variable(env, "WINE", wine_binary.get_path()); + env = Environ.set_variable(env, "WINEDLLOVERRIDES", "mshtml=d"); + var prefix = get_wineprefix(runnable); + if(prefix != null && prefix.query_exists()) + { + env = Environ.set_variable(env, "WINEPREFIX", prefix.get_path()); + } + if(arch != null && arch.length > 0) + { + env = Environ.set_variable(env, "WINEARCH", arch); + } + + var win_path = (yield Utils.run_thread({ wine_binary.get_path(), "winepath", "-w", path.get_path() }, runnable.install_dir.get_path(), env)).strip(); + debug("'%s' -> '%s'", path.get_path(), win_path); + return win_path; + } + } +} diff --git a/src/data/db/Database.vala b/src/data/db/Database.vala new file mode 100644 index 00000000..d9f1479c --- /dev/null +++ b/src/data/db/Database.vala @@ -0,0 +1,123 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Sqlite; + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; + +namespace GameHub.Data.DB +{ + public class Database + { + public const int VERSION = 5; + public static Table[] TABLES; + + public static Database instance; + + public Sqlite.Database? db = null; + + public Database() + { + instance = this; + + var path = FSUtils.expand(FSUtils.Paths.Cache.Database); + + var db_file = File.new_for_path(path); + var db_backup = db_file.get_parent().get_child(db_file.get_basename() + ".old"); + + bool err = false; + while(Sqlite.Database.open_v2(path, out db, Sqlite.OPEN_READWRITE | Sqlite.OPEN_CREATE) != Sqlite.OK) + { + warning("[Database] Can't open database (%d): %s", db.errcode(), db.errmsg()); + + if(err) + { + error("[Database] Can't recreate database. Remove '%s' manually and make sure GameHub can write into cache directory", path); + } + + err = true; + + try + { + db_file.move(db_backup, FileCopyFlags.BACKUP | FileCopyFlags.OVERWRITE); + } + catch(Error e) + { + error("[Database] Can't backup current database: %s", e.message); + } + } + + TABLES = { new Tables.Games(), new Tables.Tags(), new Tables.Merges(), new Tables.Emulators() }; + + migrate(); + init(); + } + + private void migrate() + { + Statement s; + + int version = 0; + int res = db.prepare_v2("PRAGMA user_version", -1, out s); + + if((res = s.step()) == Sqlite.ROW) + { + version = s.column_int(0); + + debug("[Database.migrate] Latest db version: %d, current: %d", VERSION, version); + + if(version < VERSION) + { + debug("[Database.migrate] Migrating database from version %d to %d", version, VERSION); + + foreach(var table in TABLES) + { + table.migrate(db, version); + } + + debug("[Database.migrate] Migration completed, new version: %d", VERSION); + + res = db.exec(@"PRAGMA user_version = $(VERSION)"); + + if(res != Sqlite.OK) + { + warning("[Database.migrate] Can't update version (%d): %s", db.errcode(), db.errmsg()); + } + } + } + } + + private void init() + { + foreach(var table in TABLES) + { + table.init(db); + } + } + + public static void create() + { + instance = new Database(); + } + } +} diff --git a/src/data/db/Table.vala b/src/data/db/Table.vala new file mode 100644 index 00000000..0dcb39f1 --- /dev/null +++ b/src/data/db/Table.vala @@ -0,0 +1,95 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Sqlite; + +namespace GameHub.Data.DB +{ + public abstract class Table: Object + { + public class Field + { + public int column = 0; + public int column_for_bind = 0; + public Field(int col) + { + column = col; + column_for_bind = col + 1; + } + + public int bind(Statement s, string? str) + { + if(str == null) return bind_null(s); + return s.bind_text(column_for_bind, str); + } + public int bind_int(Statement s, int? i) + { + if(i == null) return bind_null(s); + return s.bind_int(column_for_bind, i); + } + public int bind_int64(Statement s, int64? i) + { + if(i == null) return bind_null(s); + return s.bind_int64(column_for_bind, i); + } + public int bind_bool(Statement s, bool? b) + { + if(b == null) return bind_null(s); + return s.bind_int(column_for_bind, b ? 1 : 0); + } + public int bind_value(Statement s, Sqlite.Value? v) + { + if(v == null) return bind_null(s); + return s.bind_value(column_for_bind, v); + } + public int bind_null(Statement s) + { + return s.bind_null(column_for_bind); + } + + public string? get(Statement s) + { + return s.column_text(column); + } + public int get_int(Statement s) + { + return s.column_int(column); + } + public bool get_bool(Statement s) + { + return get_int(s) == 0 ? false : true; + } + public int64 get_int64(Statement s) + { + return s.column_int64(column); + } + public unowned Sqlite.Value? get_value(Statement s) + { + return s.column_value(column); + } + } + + protected static Table.Field f(int col) + { + return new Table.Field(col); + } + + public abstract void migrate(Sqlite.Database db, int version); + public virtual void init(Sqlite.Database db){} + } +} diff --git a/src/data/db/tables/Emulators.vala b/src/data/db/tables/Emulators.vala new file mode 100644 index 00000000..78ba559e --- /dev/null +++ b/src/data/db/tables/Emulators.vala @@ -0,0 +1,218 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Sqlite; + +using GameHub.Utils; + +using GameHub.Data; + +namespace GameHub.Data.DB.Tables +{ + public class Emulators: Table + { + public static Emulators instance; + + public static Table.Field ID; + public static Table.Field NAME; + public static Table.Field INSTALL_PATH; + public static Table.Field EXECUTABLE; + public static Table.Field COMPAT_TOOL; + public static Table.Field COMPAT_TOOL_SETTINGS; + public static Table.Field ARGUMENTS; + + public Emulators() + { + instance = this; + + ID = f(0); + NAME = f(1); + INSTALL_PATH = f(2); + EXECUTABLE = f(3); + COMPAT_TOOL = f(4); + COMPAT_TOOL_SETTINGS = f(5); + ARGUMENTS = f(6); + } + + public override void migrate(Sqlite.Database db, int version) + { + for(int ver = version; ver < Database.VERSION; ver++) + { + switch(ver) + { + case 3: + db.exec("CREATE TABLE `emulators`( + `id` string not null, + `name` string not null, + `install_path` string, + `executable` string, + `compat_tool` string, + `compat_tool_settings` string, + `arguments` string, + PRIMARY KEY(`id`))"); + break; + } + } + } + + public static bool add(Emulator emu) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + int res = db.prepare_v2("INSERT OR REPLACE INTO `emulators`( + `id`, + `name`, + `install_path`, + `executable`, + `compat_tool`, + `compat_tool_settings`, + `arguments`) + VALUES (?, ?, ?, ?, ?, ?, ?)", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Emulators.add] Can't prepare INSERT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + ID.bind(s, emu.id); + NAME.bind(s, emu.name); + EXECUTABLE.bind(s, emu.executable == null || !emu.executable.query_exists() ? null : emu.executable.get_path()); + INSTALL_PATH.bind(s, emu.install_dir == null || !emu.install_dir.query_exists() ? null : emu.install_dir.get_path()); + COMPAT_TOOL.bind(s, emu.compat_tool); + COMPAT_TOOL_SETTINGS.bind(s, emu.compat_tool_settings); + ARGUMENTS.bind(s, emu.arguments); + + res = s.step(); + + if(res != Sqlite.DONE) + { + warning("[Database.Emulators.add] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public static bool remove(Emulator emu) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + int res = db.prepare_v2("DELETE FROM `emulators` WHERE `id` = ?", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Emulators.remove] Can't prepare DELETE query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + res = s.bind_text(1, emu.id); + + res = s.step(); + + if(res != Sqlite.DONE) + { + warning("[Database.Emulators.remove] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public static new Emulator? get(string id) + { + if(id == null) return null; + + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement st; + int res = db.prepare_v2("SELECT * FROM `emulators` WHERE `id` = ?", -1, out st); + + if(res != Sqlite.OK) + { + warning("[Database.Emulators.get] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + res = st.bind_text(1, id); + + if((res = st.step()) == Sqlite.ROW) + { + return new Emulator.from_db(st); + } + + return null; + } + + public static Emulator? by_name(string name) + { + if(name == null) return null; + + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement st; + int res = db.prepare_v2("SELECT * FROM `emulators` WHERE `name` = ?", -1, out st); + + if(res != Sqlite.OK) + { + warning("[Database.Emulators.get] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + res = st.bind_text(1, name); + + if((res = st.step()) == Sqlite.ROW) + { + return new Emulator.from_db(st); + } + + return null; + } + + public static ArrayList? get_all() + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement st; + int res = db.prepare_v2("SELECT * FROM `emulators` ORDER BY `name` ASC", -1, out st); + + if(res != Sqlite.OK) + { + warning("[Database.Emulators.get_all] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + var emus = new ArrayList(Emulator.is_equal); + + while((res = st.step()) == Sqlite.ROW) + { + emus.add(new Emulator.from_db(st)); + } + + return emus; + } + } +} diff --git a/src/data/db/tables/Games.vala b/src/data/db/tables/Games.vala new file mode 100644 index 00000000..a02117de --- /dev/null +++ b/src/data/db/tables/Games.vala @@ -0,0 +1,319 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Sqlite; + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; +using GameHub.Data.Sources.User; + +namespace GameHub.Data.DB.Tables +{ + public class Games: Table + { + public static Games instance; + + public static Table.Field SOURCE; + public static Table.Field ID; + public static Table.Field NAME; + public static Table.Field INFO; + public static Table.Field INFO_DETAILED; + public static Table.Field ICON; + public static Table.Field IMAGE; + public static Table.Field TAGS; + public static Table.Field INSTALL_PATH; + public static Table.Field EXECUTABLE; + public static Table.Field PLATFORMS; + public static Table.Field COMPAT_TOOL; + public static Table.Field COMPAT_TOOL_SETTINGS; + public static Table.Field ARGUMENTS; + public static Table.Field LAST_LAUNCH; + public static Table.Field PLAYTIME_SOURCE; + public static Table.Field PLAYTIME_TRACKED; + + public Games() + { + instance = this; + + SOURCE = f(0); + ID = f(1); + NAME = f(2); + INFO = f(3); + INFO_DETAILED = f(4); + ICON = f(5); + IMAGE = f(6); + TAGS = f(7); + INSTALL_PATH = f(8); + EXECUTABLE = f(9); + PLATFORMS = f(10); + COMPAT_TOOL = f(11); + COMPAT_TOOL_SETTINGS = f(12); + ARGUMENTS = f(13); + LAST_LAUNCH = f(14); + PLAYTIME_SOURCE = f(15); + PLAYTIME_TRACKED = f(16); + } + + public override void migrate(Sqlite.Database db, int version) + { + for(int ver = version; ver < Database.VERSION; ver++) + { + switch(ver) + { + case 0: + db.exec("CREATE TABLE `games`( + `source` string not null, + `id` string not null, + `name` string not null, + `info` string, + `info_detailed` string, + `icon` string, + `image` string, + `tags` string, + `install_path` string, + `executable` string, + `platforms` string, + `compat_tool` string, + `compat_tool_settings` string, + PRIMARY KEY(`source`, `id`))"); + break; + + case 1: + db.exec("ALTER TABLE `games` ADD `arguments` string"); + break; + + case 2: + db.exec("ALTER TABLE `games` ADD `last_launch` integer not null default 0"); + break; + + case 4: + db.exec("ALTER TABLE `games` ADD `playtime_source` integer not null default 0"); + db.exec("ALTER TABLE `games` ADD `playtime_tracked` integer not null default 0"); + break; + } + } + } + + public static bool add(Game game) + { + if(game is Sources.GOG.GOGGame.DLC) return false; + + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + int res = db.prepare_v2("INSERT OR REPLACE INTO `games`( + `source`, + `id`, + `name`, + `info`, + `info_detailed`, + `icon`, + `image`, + `tags`, + `install_path`, + `executable`, + `platforms`, + `compat_tool`, + `compat_tool_settings`, + `arguments`, + `last_launch`, + `playtime_source`, + `playtime_tracked`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Games.add] Can't prepare INSERT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + var platforms = ""; + foreach(var p in game.platforms) + { + if(platforms.length > 0) platforms += ","; + platforms += p.id(); + } + + var tags = ""; + foreach(var t in game.tags) + { + if(tags.length > 0) tags += ","; + tags += t.id; + } + + SOURCE.bind(s, game.source.id); + ID.bind(s, game.id); + NAME.bind(s, game.name); + INFO.bind(s, game.info); + INFO_DETAILED.bind(s, game.info_detailed); + ICON.bind(s, game.icon); + IMAGE.bind(s, game.image); + TAGS.bind(s, tags); + EXECUTABLE.bind(s, game.executable_path == null ? null : game.executable_path); + INSTALL_PATH.bind(s, game.install_dir == null ? null : game.install_dir.get_path()); + PLATFORMS.bind(s, platforms); + COMPAT_TOOL.bind(s, game.compat_tool); + COMPAT_TOOL_SETTINGS.bind(s, game.compat_tool_settings); + ARGUMENTS.bind(s, game.arguments); + LAST_LAUNCH.bind_int64(s, game.last_launch); + PLAYTIME_SOURCE.bind_int64(s, game.playtime_source); + PLAYTIME_TRACKED.bind_int64(s, game.playtime_tracked); + + res = s.step(); + + if(res != Sqlite.DONE) + { + warning("[Database.Games.add] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public static bool remove(Game game) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + int res = db.prepare_v2("DELETE FROM `games` WHERE `source` = ? AND `id` = ?", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Games.remove] Can't prepare DELETE query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + res = s.bind_text(1, game.source.id); + res = s.bind_text(2, game.id); + + res = s.step(); + + if(res != Sqlite.DONE) + { + warning("[Database.Games.remove] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public static new Game? get(string src, string id) + { + if(src == null || id == null) return null; + + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement st; + int res; + + res = db.prepare_v2("SELECT * FROM `games` WHERE `source` = ? AND `id` = ?", -1, out st); + res = st.bind_text(1, src); + res = st.bind_text(2, id); + + if(res != Sqlite.OK) + { + warning("[Database.Games.get] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + if((res = st.step()) == Sqlite.ROW) + { + var s = GameSource.by_id(SOURCE.get(st)); + + if(s is Steam) + { + return new SteamGame.from_db((Steam) s, st); + } + else if(s is GOG) + { + return new GOGGame.from_db((GOG) s, st); + } + else if(s is Humble) + { + return new HumbleGame.from_db((Humble) s, st); + } + else if(s is User) + { + return new UserGame.from_db((User) s, st); + } + } + + return null; + } + + public static ArrayList? get_all(GameSource? src = null) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement st; + int res; + + if(src != null) + { + res = db.prepare_v2("SELECT * FROM `games` WHERE `source` = ? ORDER BY (CASE WHEN `tags` LIKE ? THEN 1 ELSE 2 END), `name` ASC", -1, out st); + res = st.bind_text(1, src.id); + res = st.bind_text(2, "%" + Tags.BUILTIN_INSTALLED.id + "%"); + } + else + { + res = db.prepare_v2("SELECT * FROM `games` ORDER BY (CASE WHEN `tags` LIKE ? THEN 1 ELSE 2 END), `name` ASC", -1, out st); + res = st.bind_text(1, "%" + Tags.BUILTIN_INSTALLED.id + "%"); + } + + if(res != Sqlite.OK) + { + warning("[Database.Games.get_all] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + var games = new ArrayList(Game.is_equal); + + while((res = st.step()) == Sqlite.ROW) + { + var s = GameSource.by_id(SOURCE.get(st)); + + if(s is Steam) + { + games.add(new SteamGame.from_db((Steam) s, st)); + } + else if(s is GOG) + { + games.add(new GOGGame.from_db((GOG) s, st)); + } + else if(s is Humble) + { + games.add(new HumbleGame.from_db((Humble) s, st)); + } + else if(s is User) + { + games.add(new UserGame.from_db((User) s, st)); + } + } + + return games; + } + } +} diff --git a/src/data/db/tables/Merges.vala b/src/data/db/tables/Merges.vala new file mode 100644 index 00000000..73fd2706 --- /dev/null +++ b/src/data/db/tables/Merges.vala @@ -0,0 +1,231 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Sqlite; + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; + +namespace GameHub.Data.DB.Tables +{ + public class Merges: Table + { + public static Merges instance; + + public static Table.Field MERGE; + + public Merges() + { + instance = this; + + MERGE = f(0); + } + + public override void migrate(Sqlite.Database db, int version) + { + for(int ver = version; ver < Database.VERSION; ver++) + { + switch(ver) + { + case 0: + db.exec("CREATE TABLE `merges`( + `merge` string not null, + PRIMARY KEY(`merge`))"); + break; + } + } + } + + public static bool add(Game first, Game second) + { + if(first is Sources.GOG.GOGGame.DLC || second is Sources.GOG.GOGGame.DLC) return false; + + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + + int res = db.prepare_v2("SELECT rowid, * FROM `merges` WHERE `merge` LIKE ? OR `merge` LIKE ?", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Merges.add] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + s.bind_text(1, @"%$(first.full_id)%"); + s.bind_text(2, @"%$(second.full_id)%"); + + int64? row = null; + int merge_var = 1; + + string old_merge = null; + + if((res = s.step()) == Sqlite.ROW) + { + row = s.column_int64(0); + old_merge = s.column_text(1); + merge_var = 2; + res = db.prepare_v2("INSERT OR REPLACE INTO `merges`(rowid, `merge`) VALUES (?, ?)", -1, out s); + } + else + { + res = db.prepare_v2("INSERT OR REPLACE INTO `merges`(`merge`) VALUES (?)", -1, out s); + } + + if(res != Sqlite.OK) + { + warning("[Database.Merges.add] Can't prepare INSERT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + string new_merge = ""; + + var games = new ArrayList(Game.is_equal); + games.add(first); + games.add(second); + if(old_merge != null) + { + foreach(var gameid in old_merge.split("|")) + { + var gparts = gameid.split(":"); + var gsrc = gparts[0]; + var gid = gparts[1]; + + if(gsrc == null || gid == null) continue; + + var game = Games.get(gsrc, gid); + + if(game != null && !games.contains(game)) games.add(game); + } + } + + foreach(var src in GameSources) + { + foreach(var game in games) + { + if(game.source.id == src.id) + { + if(new_merge != "") new_merge += "|"; + new_merge += game.full_id; + } + } + } + + s.bind_text(merge_var, new_merge); + + res = s.step(); + + if(res != Sqlite.DONE) + { + warning("[Database.Merges.add] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public static new ArrayList? get(Game game) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return null; + + Statement s; + + int res = db.prepare_v2("SELECT * FROM `merges` WHERE `merge` LIKE ? LIMIT 1", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Merges.get] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return null; + } + + s.bind_text(1, @"$(game.full_id)|%"); + + if((res = s.step()) == Sqlite.ROW) + { + var games = new ArrayList(Game.is_equal); + var merge = s.column_text(0); + + if(merge != null) + { + foreach(var gameid in merge.split("|")) + { + var gparts = gameid.split(":"); + var gsrc = gparts[0]; + var gid = gparts[1]; + + if(gsrc == null || gid == null) continue; + + var g = Games.get(gsrc, gid); + + if(g != null && !games.contains(g) && !Game.is_equal(game, g)) games.add(g); + } + } + + return games; + } + + return null; + } + + public static bool is_game_merged(Game game) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + + int res = db.prepare_v2("SELECT * FROM `merges` WHERE `merge` LIKE ? LIMIT 1", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Merges.is_game_merged] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + s.bind_text(1, @"%|$(game.full_id)%"); + + return s.step() == Sqlite.ROW; + } + + public static bool is_game_merged_as_primary(Game game) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + + int res = db.prepare_v2("SELECT * FROM `merges` WHERE `merge` LIKE ? LIMIT 1", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Merges.is_game_merged_as_primary] Can't prepare SELECT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + s.bind_text(1, @"$(game.full_id)|%"); + + return s.step() == Sqlite.ROW; + } + } +} diff --git a/src/data/db/tables/Tags.vala b/src/data/db/tables/Tags.vala new file mode 100644 index 00000000..712a2046 --- /dev/null +++ b/src/data/db/tables/Tags.vala @@ -0,0 +1,242 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using Sqlite; + +using GameHub.Utils; + +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; + +namespace GameHub.Data.DB.Tables +{ + public class Tags: Table + { + public static Tags instance; + + public static Table.Field ID; + public static Table.Field NAME; + public static Table.Field ICON; + public static Table.Field SELECTED; + + public static ArrayList TAGS; + public static ArrayList DYNAMIC_TAGS; + + public static Tag BUILTIN_FAVORITES; + public static Tag BUILTIN_UNINSTALLED; + public static Tag BUILTIN_INSTALLED; + public static Tag BUILTIN_HIDDEN; + + public signal void tags_updated(); + + public Tags() + { + instance = this; + + ID = f(0); + NAME = f(1); + ICON = f(2); + SELECTED = f(3); + + TAGS = new ArrayList(Tag.is_equal); + DYNAMIC_TAGS = new ArrayList(Tag.is_equal); + } + + public override void migrate(Sqlite.Database db, int version) + { + for(int ver = version; ver < Database.VERSION; ver++) + { + switch(ver) + { + case 0: + db.exec("CREATE TABLE `tags`( + `id` string, + `name` string, + `icon` string, + `selected` int, + PRIMARY KEY(`id`))"); + break; + } + } + } + + public override void init(Sqlite.Database db) + { + Statement s; + + int res = db.prepare_v2("SELECT * FROM `tags` ORDER BY SUBSTR(`id`, 1, 1) ASC, `name` ASC", -1, out s); + while((res = s.step()) == Sqlite.ROW) + { + var tag = new Tag.from_db(s); + if(!TAGS.contains(tag)) TAGS.add(tag); + + if(BUILTIN_FAVORITES == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.FAVORITES.id()) BUILTIN_FAVORITES = tag; + if(BUILTIN_UNINSTALLED == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.UNINSTALLED.id()) BUILTIN_UNINSTALLED = tag; + if(BUILTIN_INSTALLED == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.INSTALLED.id()) BUILTIN_INSTALLED = tag; + if(BUILTIN_HIDDEN == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.HIDDEN.id()) BUILTIN_HIDDEN = tag; + } + + Tag.Builtin[] builtin = { Tag.Builtin.FAVORITES, Tag.Builtin.UNINSTALLED, Tag.Builtin.INSTALLED, Tag.Builtin.HIDDEN }; + + foreach(var bt in builtin) + { + var tag = new Tag.from_builtin(bt); + Tags.add(tag); + + if(BUILTIN_FAVORITES == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.FAVORITES.id()) BUILTIN_FAVORITES = tag; + if(BUILTIN_UNINSTALLED == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.UNINSTALLED.id()) BUILTIN_UNINSTALLED = tag; + if(BUILTIN_INSTALLED == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.INSTALLED.id()) BUILTIN_INSTALLED = tag; + if(BUILTIN_HIDDEN == null && tag.id == Tag.BUILTIN_PREFIX + Tag.Builtin.HIDDEN.id()) BUILTIN_HIDDEN = tag; + } + + DYNAMIC_TAGS.add(BUILTIN_UNINSTALLED); + DYNAMIC_TAGS.add(BUILTIN_INSTALLED); + + var ui_settings = GameHub.Settings.UI.get_instance(); + ui_settings.notify["use-imported-tags"].connect(() => { + foreach(var tag in TAGS) + { + if(tag.id.has_prefix(Tag.IMPORTED_GOG_PREFIX)) + { + tag.enabled = ui_settings.use_imported_tags; + } + } + tags_updated(); + }); + ui_settings.notify_property("use-imported-tags"); + } + + public static bool add(Tag tag, bool replace=false) + { + unowned Sqlite.Database? db = Database.instance.db; + if(db == null) return false; + + Statement s; + int res = db.prepare_v2("INSERT OR " + (replace ? "REPLACE" : "IGNORE") + " INTO `tags` (`id`, `name`, `icon`, `selected`) VALUES (?, ?, ?, ?)", -1, out s); + + if(res != Sqlite.OK) + { + warning("[Database.Tags.add] Can't prepare INSERT query (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + ID.bind(s, tag.id); + NAME.bind(s, tag.name); + ICON.bind(s, tag.icon); + SELECTED.bind_bool(s, tag.selected); + + res = s.step(); + + if(!TAGS.contains(tag)) + { + TAGS.add(tag); + if(tag.id.has_prefix(Tag.IMPORTED_GOG_PREFIX)) + { + tag.enabled = GameHub.Settings.UI.get_instance().use_imported_tags; + } + instance.tags_updated(); + } + + if(res != Sqlite.DONE) + { + warning("[Database.Tags.add] Error (%d): %s", db.errcode(), db.errmsg()); + return false; + } + + return true; + } + + public class Tag: Object + { + public const string BUILTIN_PREFIX = "builtin:"; + public const string USER_PREFIX = "user:"; + public const string IMPORTED_GOG_PREFIX = "gog:"; + + public enum Builtin + { + FAVORITES, UNINSTALLED, INSTALLED, HIDDEN; + + public string id() + { + switch(this) + { + case Builtin.FAVORITES: return "favorites"; + case Builtin.UNINSTALLED: return "uninstalled"; + case Builtin.INSTALLED: return "installed"; + case Builtin.HIDDEN: return "hidden"; + } + assert_not_reached(); + } + + public string name() + { + switch(this) + { + case Builtin.FAVORITES: return C_("tag", "Favorites"); + case Builtin.UNINSTALLED: return C_("tag", "Not installed"); + case Builtin.INSTALLED: return C_("tag", "Installed"); + case Builtin.HIDDEN: return C_("tag", "Hidden"); + } + assert_not_reached(); + } + + public string icon() + { + switch(this) + { + case Builtin.FAVORITES: return "gh-tag-favorites-symbolic"; + case Builtin.UNINSTALLED: return "gh-tag-symbolic"; + case Builtin.INSTALLED: return "gh-tag-symbolic"; + case Builtin.HIDDEN: return "gh-tag-hidden-symbolic"; + } + assert_not_reached(); + } + } + + public string? id { get; construct set; } + public string? name { get; construct set; } + public string icon { get; construct set; } + public bool selected { get; construct set; default = true; } + public bool enabled { get; construct set; default = true; } + + public Tag(string? id, string? name, string icon="gh-tag-symbolic", bool selected=true) + { + Object(id: id, name: name, icon: icon, selected: selected); + } + public Tag.from_db(Statement s) + { + this(ID.get(s), NAME.get(s), ICON.get(s), SELECTED.get_bool(s)); + } + public Tag.from_builtin(Builtin t) + { + this(BUILTIN_PREFIX + t.id(), t.name(), t.icon(), true); + } + public Tag.from_name(string name) + { + this(USER_PREFIX + Utils.md5(name), name); + } + + public static bool is_equal(Tag first, Tag second) + { + return first == second || first.id == second.id; + } + } + } +} diff --git a/src/data/sources/gog/GOG.vala b/src/data/sources/gog/GOG.vala index 83ea0b8e..c86afeac 100644 --- a/src/data/sources/gog/GOG.vala +++ b/src/data/sources/gog/GOG.vala @@ -1,5 +1,23 @@ -using Gtk; +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gee; +using GameHub.Data.DB; using GameHub.Utils; namespace GameHub.Data.Sources.GOG @@ -9,16 +27,45 @@ namespace GameHub.Data.Sources.GOG private const string CLIENT_ID = "46899977096215655"; private const string CLIENT_SECRET = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"; private const string REDIRECT = "https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient"; - + + private const string[] GAMES_BLACKLIST = {"1424856371" /* Hotline Miami 2: Wrong Number - Digital Comics */}; + + public static GOG instance; + + public override string id { get { return "gog"; } } public override string name { get { return "GOG"; } } - public override string icon { get { return "gog"; } } - + public override string icon { get { return "source-gog-symbolic"; } } + + public override bool enabled + { + get { return Settings.Auth.GOG.get_instance().enabled; } + set { Settings.Auth.GOG.get_instance().enabled = value; } + } + public string? user_id { get; protected set; } public string? user_name { get; protected set; } - + private string? user_auth_code = null; - private string? user_token = null; + public string? user_token = null; private string? user_refresh_token = null; + private bool token_needs_refresh = false; + + private Settings.Auth.GOG settings; + + public GOG() + { + instance = this; + + settings = Settings.Auth.GOG.get_instance(); + var access_token = settings.access_token; + var refresh_token = settings.refresh_token; + if(access_token.length > 0 && refresh_token.length > 0) + { + user_token = access_token; + user_refresh_token = refresh_token; + token_needs_refresh = true; + } + } public override bool is_installed(bool refresh) { @@ -29,106 +76,291 @@ namespace GameHub.Data.Sources.GOG { return true; } - + public override async bool authenticate() { + settings.authenticated = true; + + if(token_needs_refresh && user_refresh_token != null) + { + return (yield refresh_token()); + } + return (yield get_auth_code()) && (yield get_token()); } - + public override bool is_authenticated() { - return user_token != null; + return !token_needs_refresh && user_token != null; } - + + public override bool can_authenticate_automatically() + { + return user_refresh_token != null && settings.authenticated; + } + private async bool get_auth_code() { if(user_auth_code != null) { return true; } - + var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, @"https://auth.gog.com/auth?client_id=$(CLIENT_ID)&redirect_uri=$(REDIRECT)&response_type=code&layout=client2", "https://embed.gog.com/on_login_success?origin=client&code="); - + wnd.finished.connect(code => { user_auth_code = code; + debug("[Auth] GOG auth code: %s", code); Idle.add(get_auth_code.callback); }); - + wnd.canceled.connect(() => Idle.add(get_auth_code.callback)); - - wnd.set_size_request(550, 680); + + wnd.set_size_request(550, 680); wnd.show_all(); wnd.present(); - + yield; - + return user_auth_code != null; } - + private async bool get_token() { if(user_token != null) { return true; } - - print("Fetching access token...\n"); - + var url = @"https://auth.gog.com/token?client_id=$(CLIENT_ID)&client_secret=$(CLIENT_SECRET)&grant_type=authorization_code&redirect_uri=$(REDIRECT)&code=$(user_auth_code)"; - var root = yield Parser.parse_remote_json_file_async(url); + var root = (yield Parser.parse_remote_json_file_async(url)).get_object(); user_token = root.get_string_member("access_token"); user_refresh_token = root.get_string_member("refresh_token"); user_id = root.get_string_member("user_id"); - + + settings.access_token = user_token ?? ""; + settings.refresh_token = user_refresh_token ?? ""; + + debug("[Auth] GOG access token: %s", user_token); + debug("[Auth] GOG refresh token: %s", user_refresh_token); + debug("[Auth] GOG user id: %s", user_id); + return user_token != null; } - + private async bool refresh_token() { - if(user_token == null) + if(user_refresh_token == null) { - return true; + return false; } - + + debug("[Auth] Refreshing GOG access token with refresh token: %s", user_refresh_token); + var url = @"https://auth.gog.com/token?client_id=$(CLIENT_ID)&client_secret=$(CLIENT_SECRET)&grant_type=refresh_token&refresh_token=$(user_refresh_token)"; - var root = yield Parser.parse_remote_json_file_async(url); + var root_node = yield Parser.parse_remote_json_file_async(url); + var root = root_node != null && root_node.get_node_type() == Json.NodeType.OBJECT ? root_node.get_object() : null; + + if(root == null) + { + token_needs_refresh = false; + return false; + } + user_token = root.get_string_member("access_token"); user_refresh_token = root.get_string_member("refresh_token"); user_id = root.get_string_member("user_id"); - + + settings.access_token = user_token ?? ""; + settings.refresh_token = user_refresh_token ?? ""; + + debug("[Auth] GOG access token: %s", user_token); + debug("[Auth] GOG refresh token: %s", user_refresh_token); + debug("[Auth] GOG user id: %s", user_id); + + token_needs_refresh = false; + return user_token != null; } - private ArrayList games = new ArrayList(); - public override async ArrayList load_games(FutureResult? game_loaded = null) + private HashMap load_player_stats() { - if(user_id == null || user_token == null || games.size > 0) + var player_stats = new HashMap(); + + if(user_name == null) { - return games; + var userdata_node = Parser.parse_remote_json_file(@"https://embed.gog.com/userData.json", "GET", user_token); + var userdata = userdata_node != null && userdata_node.get_node_type() == Json.NodeType.OBJECT ? userdata_node.get_object() : null; + user_name = userdata != null && userdata.has_member("username") ? userdata.get_string_member("username") : null; } - - var url = @"https://embed.gog.com/account/getFilteredProducts?mediaType=1"; - var root = yield Parser.parse_remote_json_file_async(url, "GET", user_token); - - var products = root.get_array_member("products"); - - games.clear(); - - foreach(var g in products.get_elements()) + + if(user_name == null) return player_stats; + + var page = 1; + var pages = 1; + + while(page <= pages) { - var game = new GOGGame(this, g.get_object()); - if(yield game.is_for_linux()) + var url = @"https://embed.gog.com/u/$(user_name)/games/stats?sort=total_playtime&order=desc&page=$(page)"; + var root_node = Parser.parse_remote_json_file(url, "GET", user_token); + var root = root_node != null && root_node.get_node_type() == Json.NodeType.OBJECT ? root_node.get_object() : null; + + if(root == null) break; + + page = (int) root.get_int_member("page"); + pages = (int) root.get_int_member("pages"); + + debug("[GOG] Loading player stats: page %d of %d", page, pages); + + var items = root.get_object_member("_embedded").get_array_member("items"); + + foreach(var i in items.get_elements()) + { + var game = Parser.json_object(i, {"game"}); + var stats = Parser.json_object(i, {"stats", user_id}); + if(game == null) continue; + var id = game.get_string_member("id"); + var image = game.get_string_member("image"); + var playtime = stats != null ? stats.get_int_member("playtime") : 0; + var last_launch = stats != null ? new DateTime.from_iso8601(stats.get_string_member("lastSession"), new TimeZone.utc()).to_unix() : 0; + player_stats.set(id, new PlayerStatItem(id, playtime, last_launch, image)); + } + + page++; + } + + return player_stats; + } + + private ArrayList _games = new ArrayList(Game.is_equal); + + public override ArrayList games { get { return _games; } } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null) + { + if(((user_id == null || user_token == null) && token_needs_refresh) || _games.size > 0) + { + return _games; + } + + Utils.thread("GOGLoading", () => { + _games.clear(); + + var stats = load_player_stats(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + if(cached.size > 0) + { + foreach(var g in cached) + { + if(!(g.id in GAMES_BLACKLIST) && (!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(g))) + { + //g.update_game_info.begin(); + _games.add(g); + if(stats.has_key(g.id)) + { + var s = stats.get(g.id); + //g.image = s.image; + g.last_launch = int64.max(g.last_launch, s.last_launch); + g.playtime_source = s.playtime; + } + if(game_loaded != null) + { + Idle.add(() => { game_loaded(g, true); return Source.REMOVE; }); + Thread.usleep(100000); + } + } + games_count++; + } + } + + if(cache_loaded != null) { - games.add(game); - games_count = games.size; - if(game_loaded != null) game_loaded(game); + Idle.add(() => { cache_loaded(); return Source.REMOVE; }); } + + var page = 1; + var pages = 1; + + while(page <= pages) + { + var url = @"https://embed.gog.com/account/getFilteredProducts?mediaType=1&page=$(page)"; + var root_node = Parser.parse_remote_json_file(url, "GET", user_token); + var root = root_node != null && root_node.get_node_type() == Json.NodeType.OBJECT ? root_node.get_object() : null; + + if(root == null) break; + + page = (int) root.get_int_member("page"); + pages = (int) root.get_int_member("totalPages"); + + debug("[GOG] Loading games: page %d of %d", page, pages); + + if(page == 1) + { + var tags = root.has_member("tags") ? root.get_array_member("tags") : null; + if(tags != null) + { + foreach(var t in tags.get_elements()) + { + var id = t.get_object().get_string_member("id"); + var name = t.get_object().get_string_member("name"); + Tables.Tags.add(new Tables.Tags.Tag("gog:" + id, name, icon)); + debug("[GOG] Imported tag: %s (%s)", name, id); + } + } + } + + var products = root.get_array_member("products"); + + foreach(var g in products.get_elements()) + { + var game = new GOGGame(this, g); + bool is_new_game = !(game.id in GAMES_BLACKLIST) && !_games.contains(game); + if(is_new_game && (!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(game))) + { + _games.add(game); + if(stats.has_key(game.id)) + { + var s = stats.get(game.id); + //game.image = s.image; + game.last_launch = int64.max(game.last_launch, s.last_launch); + game.playtime_source = s.playtime; + } + if(game_loaded != null) + { + Idle.add(() => { game_loaded(game, false); return Source.REMOVE; }); + } + } + if(is_new_game) games_count++; + } + + page++; + } + + Idle.add(load_games.callback); + }); + + yield; + + return _games; + } + + private class PlayerStatItem + { + public string id; + public int64 playtime; + public int64 last_launch; + public string image; + + public PlayerStatItem(string id, int64 playtime, int64 last_launch, string image) + { + this.id = id; + this.playtime = playtime; + this.last_launch = last_launch; + this.image = image; } - - games_count = games.size; - - return games; } } } diff --git a/src/data/sources/gog/GOGGame.vala b/src/data/sources/gog/GOGGame.vala index 1bba7707..b4075bc3 100644 --- a/src/data/sources/gog/GOGGame.vala +++ b/src/data/sources/gog/GOGGame.vala @@ -1,28 +1,755 @@ -using Gtk; +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; using GameHub.Utils; namespace GameHub.Data.Sources.GOG { public class GOGGame: Game { - private bool? _is_for_linux = null; - - public GOGGame(GOG src, Json.Object json) - { - this.source = src; - - this.id = json.get_int_member("id").to_string(); - this.name = json.get_string_member("title"); - - this.image = "https:" + json.get_string_member("image") + "_392.jpg"; - this.icon = this.image; - - this._is_for_linux = json.get_object_member("worksOn").get_boolean_member("Linux"); - } - - public async bool is_for_linux() - { - return (!) _is_for_linux; + public ArrayList? installers { get; protected set; default = new ArrayList(); } + public ArrayList? bonus_content { get; protected set; default = new ArrayList(); } + public ArrayList? dlc { get; protected set; default = new ArrayList(); } + + public File? bonus_content_dir { get; protected set; default = null; } + + private bool game_info_updated = false; + + public GOGGame.default(){} + + public GOGGame(GOG src, Json.Node json_node) + { + source = src; + + var json_obj = json_node.get_object(); + + id = json_obj.get_int_member("id").to_string(); + name = json_obj.get_string_member("title"); + icon = ""; + + if(json_obj.has_member("image")) + { + image = "https:" + json_obj.get_string_member("image") + "_392.jpg"; + } + + info = Json.to_string(json_node, false); + + var worksOn = json_obj != null && json_obj.has_member("worksOn") ? json_obj.get_object_member("worksOn") : null; + if(worksOn != null && worksOn.get_boolean_member("Linux")) platforms.add(Platform.LINUX); + if(worksOn != null && worksOn.get_boolean_member("Windows")) platforms.add(Platform.WINDOWS); + if(worksOn != null && worksOn.get_boolean_member("Mac")) platforms.add(Platform.MACOS); + + var tags_json = !json_obj.has_member("tags") ? null : json_obj.get_array_member("tags"); + if(tags_json != null) + { + foreach(var tag_json in tags_json.get_elements()) + { + var tid = source.id + ":" + tag_json.get_string(); + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + } + + install_dir = FSUtils.file(FSUtils.Paths.GOG.Games, escaped_name); + executable_path = "$game_dir/start.sh"; + update_status(); + } + + public GOGGame.from_db(GOG src, Sqlite.Statement s) + { + source = src; + id = Tables.Games.ID.get(s); + name = Tables.Games.NAME.get(s); + info = Tables.Games.INFO.get(s); + info_detailed = Tables.Games.INFO_DETAILED.get(s); + icon = Tables.Games.ICON.get(s); + image = Tables.Games.IMAGE.get(s); + install_dir = Tables.Games.INSTALL_PATH.get(s) != null ? FSUtils.file(Tables.Games.INSTALL_PATH.get(s)) : FSUtils.file(FSUtils.Paths.GOG.Games, escaped_name); + executable_path = Tables.Games.EXECUTABLE.get(s); + compat_tool = Tables.Games.COMPAT_TOOL.get(s); + compat_tool_settings = Tables.Games.COMPAT_TOOL_SETTINGS.get(s); + arguments = Tables.Games.ARGUMENTS.get(s); + last_launch = Tables.Games.LAST_LAUNCH.get_int64(s); + playtime_source = Tables.Games.PLAYTIME_SOURCE.get_int64(s); + playtime_tracked = Tables.Games.PLAYTIME_TRACKED.get_int64(s); + + platforms.clear(); + var pls = Tables.Games.PLATFORMS.get(s).split(","); + foreach(var pl in pls) + { + foreach(var p in Platforms) + { + if(pl == p.id()) + { + platforms.add(p); + break; + } + } + } + + tags.clear(); + var tag_ids = (Tables.Games.TAGS.get(s) ?? "").split(","); + foreach(var tid in tag_ids) + { + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + + update_status(); + } + + public override async void update_game_info() + { + update_status(); + + mount_overlays(); + + if(info_detailed == null || info_detailed.length == 0) + { + var lang = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2); + var url = @"https://api.gog.com/products/$(id)?expand=downloads,description,expanded_dlcs" + (lang != null && lang.length > 0 ? "&locale=" + lang : ""); + info_detailed = (yield Parser.load_remote_file_async(url, "GET", ((GOG) source).user_token)); + } + + var root = Parser.parse_json(info_detailed); + + var images = Parser.json_object(root, {"images"}); + var desc = Parser.json_object(root, {"description"}); + var links = Parser.json_object(root, {"links"}); + + if(image == null || image == "") + { + var i = Parser.parse_json(info).get_object(); + image = "https:" + i.get_string_member("image") + "_392.jpg"; + } + + if((icon == null || icon == "") && (images != null && images.has_member("icon"))) + { + icon = images.get_string_member("icon"); + if(icon != null) icon = "https:" + icon; + else icon = image; + } + + if(game_info_updated) return; + + is_installable = root != null && root.get_node_type() == Json.NodeType.OBJECT + && root.get_object().has_member("is_installable") && root.get_object().get_boolean_member("is_installable"); + + if(desc != null) + { + description = desc.get_string_member("full"); + var cool = desc.get_string_member("whats_cool_about_it"); + if(cool != null && cool.length > 0) + { + description += "
    "; + var cool_parts = cool.split("\n"); + foreach(var part in cool_parts) + { + part = part.strip(); + if(part.length > 0) + { + description += "
  • " + part + "
  • "; + } + } + description += "
"; + } + } + + if(links != null) + { + store_page = links.get_string_member("product_card"); + } + + var downloads = Parser.json_object(root, {"downloads"}); + + var installers_json = downloads == null || !downloads.has_member("installers") ? null : downloads.get_array_member("installers"); + if(installers_json != null && installers.size == 0) + { + foreach(var installer_json in installers_json.get_elements()) + { + var installer = new Installer(this, installer_json.get_object()); + installers.add(installer); + } + } + + if(installers.size == 0) + { + is_installable = false; + } + + var bonuses_json = downloads == null || !downloads.has_member("bonus_content") ? null : downloads.get_array_member("bonus_content"); + if(bonuses_json != null && bonus_content.size == 0) + { + foreach(var bonus_json in bonuses_json.get_elements()) + { + bonus_content.add(new BonusContent(this, bonus_json.get_object())); + } + } + + var dlcs_json = root == null || root.get_node_type() != Json.NodeType.OBJECT || !root.get_object().has_member("expanded_dlcs") ? null : root.get_object().get_array_member("expanded_dlcs"); + if(dlcs_json != null && dlc.size == 0) + { + foreach(var dlc_json in dlcs_json.get_elements()) + { + dlc.add(new GOGGame.DLC(this, dlc_json)); + } + } + + root = Parser.parse_json(info); + + var tags_json = root == null || root.get_node_type() != Json.NodeType.OBJECT || !root.get_object().has_member("tags") ? null : root.get_object().get_array_member("tags"); + + if(tags_json != null) + { + foreach(var tag_json in tags_json.get_elements()) + { + var tid = source.id + ":" + tag_json.get_string(); + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + } + + save(); + + update_status(); + + game_info_updated = true; + } + + public override async void install() + { + yield update_game_info(); + + if(installers == null || installers.size < 1) return; + + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + + wnd.cancelled.connect(() => Idle.add(install.callback)); + + wnd.install.connect((installer, dl_only, tool) => { + FSUtils.mkdir(FSUtils.Paths.GOG.Games); + + if(installer.parts.size > 0) + { + FSUtils.mkdir(installer.parts.get(0).local.get_parent().get_path()); + } + + installer.install.begin(this, dl_only, tool, (obj, res) => { + installer.install.end(res); + Idle.add(install.callback); + }); + }); + + wnd.import.connect(() => { + import(); + Idle.add(install.callback); + }); + + wnd.show_all(); + wnd.present(); + + yield; + } + + public override async void uninstall() + { + if(install_dir.query_exists()) + { + string? uninstaller = null; + try + { + FileInfo? finfo = null; + var enumerator = yield install_dir.enumerate_children_async("standard::*", FileQueryInfoFlags.NONE); + while((finfo = enumerator.next_file()) != null) + { + if(finfo.get_name().has_prefix("uninstall-")) + { + uninstaller = finfo.get_name(); + break; + } + } + } + catch(Error e){} + + yield umount_overlays(); + + if(uninstaller != null) + { + uninstaller = FSUtils.expand(install_dir.get_path(), uninstaller); + debug("[GOGGame] Running uninstaller '%s'...", uninstaller); + yield Utils.run_thread({uninstaller, "--noprompt", "--force"}, null, null, true); + } + else + { + FSUtils.rm(install_dir.get_path(), "", "-rf"); + } + update_status(); + } + if(!install_dir.query_exists() && !executable.query_exists()) + { + install_dir = FSUtils.file(FSUtils.Paths.GOG.Games, escaped_name); + executable = FSUtils.file(install_dir.get_path(), "start.sh"); + save(); + update_status(); + } + } + + public override void update_status() + { + if(status.state == Game.State.DOWNLOADING && status.download.status.state != Downloader.DownloadState.CANCELLED) return; + + var gameinfo = get_file("gameinfo", false); + var goggame = get_file(@"goggame-$(id).info", false); + + var files = new ArrayList(); + files.add(executable); + files.add(gameinfo); + files.add(goggame); + var state = Game.State.UNINSTALLED; + foreach(var file in files) + { + if(file != null && file.query_exists()) + { + state = Game.State.INSTALLED; + break; + } + } + status = new Game.Status(state, this); + if(state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + + if(gameinfo != null && gameinfo.query_exists()) + { + try + { + string info; + FileUtils.get_contents(gameinfo.get_path(), out info); + var lines = info.split("\n"); + if(lines.length >= 2) + { + version = lines[1]; + } + } + catch(Error e) + { + warning("[GOGGame.update_status] Error while reading gameinfo: %s", e.message); + } + } + + string g = name; + string? d = null; + if(this is DLC) + { + g = (this as DLC).game.name; + d = name; + } + installers_dir = FSUtils.file(FSUtils.Paths.Collection.GOG.expand_installers(g, d)); + bonus_content_dir = FSUtils.file(FSUtils.Paths.Collection.GOG.expand_bonus(g, d)); + } + + private bool loading_achievements = false; + public override async ArrayList? load_achievements() + { + if(achievements != null || loading_achievements) + { + return achievements; + } + + loading_achievements = true; + + var url = @"https://gameplay.gog.com/clients/$(id)/users/$(GOG.instance.user_id)/achievements"; + + var root = (yield Parser.parse_remote_json_file_async(url, "GET", GOG.instance.user_token)); + var root_obj = root != null && root.get_node_type() == Json.NodeType.OBJECT + ? root.get_object() : null; + + if(root_obj == null || !root_obj.has_member("items")) + { + loading_achievements = false; + return null; + } + + var achievements_array = root_obj.get_array_member("items"); + + var _achievements = new ArrayList(); + + foreach(var a_node in achievements_array.get_elements()) + { + var a_obj = a_node != null && a_node.get_node_type() == Json.NodeType.OBJECT + ? a_node.get_object() : null; + + if(a_obj == null || !a_obj.has_member("achievement_key")) continue; + + var a_id = a_obj.get_string_member("achievement_key"); + var a_name = a_obj.has_member("name") ? a_obj.get_string_member("name") : a_id; + var a_desc = a_obj.has_member("description") ? a_obj.get_string_member("description") : ""; + var a_image_unlocked = a_obj.has_member("image_url_unlocked") ? a_obj.get_string_member("image_url_unlocked") : null; + var a_image_locked = a_obj.has_member("image_url_locked") ? a_obj.get_string_member("image_url_locked") : null; + string? a_unlock_date = null; + + if(a_obj.has_member("date_unlocked")) + { + var date = a_obj.get_member("date_unlocked"); + if(date.get_node_type() == Json.NodeType.VALUE) + { + a_unlock_date = date.get_string(); + } + } + + bool a_unlocked = a_unlock_date != null; + float a_global_percentage = a_obj.has_member("rarity") ? (float) a_obj.get_double_member("rarity") : 0; + + _achievements.add(new Achievement(a_id, a_name, a_desc, a_image_locked, a_image_unlocked, + a_unlocked, a_unlock_date, a_global_percentage)); + } + + _achievements.sort((first, second) => { + var a1 = first as Achievement; + var a2 = second as Achievement; + + if(a1.unlock_date != null || a2.unlock_date != null) + { + return (a2.unlock_date ?? new DateTime.from_unix_utc(0)).compare(a1.unlock_date ?? new DateTime.from_unix_utc(0)); + } + + if(a1.global_percentage < a2.global_percentage) return 1; + if(a1.global_percentage > a2.global_percentage) return -1; + return 0; + }); + + achievements = _achievements; + loading_achievements = false; + return achievements; + } + + public class Achievement: Game.Achievement + { + public Achievement(string id, string name, string desc, string? image_locked, string? image_unlocked, + bool unlocked, string? unlock_date, float global_percentage) + { + this.id = id; + this.name = name; + this.description = desc; + this.image_locked = image_locked; + this.image_unlocked = image_unlocked; + this.unlocked = unlocked; + this.global_percentage = global_percentage; + if(unlock_date != null) + { + this.unlock_date = new DateTime.from_iso8601(unlock_date, new TimeZone.utc()); + this.unlock_time = Granite.DateTime.get_relative_datetime(this.unlock_date); + } + } + } + + public class Installer: Runnable.Installer + { + public string lang; + public string lang_full; + + public override string name { get { return lang_full; } } + + public Installer(GOGGame game, Json.Object json) + { + id = json.get_string_member("id"); + lang = json.get_string_member("language"); + lang_full = json.get_string_member("language_full"); + + var os = json.get_string_member("os"); + platform = CurrentPlatform; + foreach(var p in Platforms) + { + if(os == p.id()) + { + platform = p; + break; + } + } + + full_size = json.get_int_member("total_size"); + + if(!json.has_member("files") || json.get_member("files").get_node_type() != Json.NodeType.ARRAY) return; + + if(game.installers_dir == null) return; + + foreach(var file_node in json.get_array_member("files").get_elements()) + { + var file = file_node != null && file_node.get_node_type() == Json.NodeType.OBJECT ? file_node.get_object() : null; + if(file != null) + { + var id = file.get_string_member("id"); + var size = file.get_int_member("size"); + var downlink_url = file.get_string_member("downlink"); + + var root_node = Parser.parse_remote_json_file(downlink_url, "GET", ((GOG) game.source).user_token); + if(root_node == null || root_node.get_node_type() != Json.NodeType.OBJECT) continue; + + var root = root_node.get_object(); + if(root == null || !root.has_member("downlink")) continue; + + var url = root.get_string_member("downlink"); + var remote = File.new_for_uri(url); + + var local = game.installers_dir.get_child("gog_" + game.id + "_" + this.id + "_" + id); + + parts.add(new Runnable.Installer.Part(id, url, size, remote, local)); + } + } + } + } + + public class BonusContent + { + public GOGGame game; + + public string id; + public string name; + public string type; + public int64 count; + public string file; + public int64 size; + + protected BonusContent.Status _status = new BonusContent.Status(); + public signal void status_change(BonusContent.Status status); + + public BonusContent.Status status + { + get { return _status; } + set { _status = value; status_change(_status); } + } + + public Downloader.DownloadInfo dl_info; + + public File? downloaded_file; + + public string text { owned get { return count > 1 ? @"$(count) $(name)" : name; } } + + public string icon + { + get + { + switch(type) + { + case "wallpapers": + case "images": + case "avatars": + case "artworks": + return "folder-pictures-symbolic"; + + case "audio": + case "soundtrack": + return "folder-music-symbolic"; + + case "video": + return "folder-videos-symbolic"; + + default: return "folder-documents-symbolic"; + } + } + } + + public BonusContent(GOGGame game, Json.Object json) + { + this.game = game; + id = json.get_int_member("id").to_string(); + name = json.get_string_member("name"); + type = json.get_string_member("type"); + count = json.get_int_member("count"); + file = json.get_array_member("files").get_object_element(0).get_string_member("downlink"); + size = json.get_int_member("total_size"); + + dl_info = new Downloader.DownloadInfo(text, game.name, game.icon, null, null, icon); + } + + public async File? download() + { + var root_node = yield Parser.parse_remote_json_file_async(file, "GET", ((GOG) game.source).user_token); + if(root_node == null || root_node.get_node_type() != Json.NodeType.OBJECT) return null; + var root = root_node.get_object(); + if(root == null || !root.has_member("downlink")) return null; + + var link = root.get_string_member("downlink"); + var remote = File.new_for_uri(link); + + if(game.bonus_content_dir == null) return null; + + var local = game.bonus_content_dir.get_child("gog_" + game.id + "_bonus_" + id); + + FSUtils.mkdir(FSUtils.Paths.GOG.Games); + FSUtils.mkdir(game.bonus_content_dir.get_path()); + + status = new BonusContent.Status(BonusContent.State.DOWNLOADING, null); + var ds_id = Downloader.get_instance().download_started.connect(dl => { + if(dl.remote != remote) return; + status = new BonusContent.Status(BonusContent.State.DOWNLOADING, dl); + dl.status_change.connect(s => { + status_change(status); + }); + }); + + var start_date = new DateTime.now_local(); + + try + { + downloaded_file = yield Downloader.download(remote, local, dl_info); + } + catch(Error e){} + + Downloader.get_instance().disconnect(ds_id); + + status = new BonusContent.Status(downloaded_file != null && downloaded_file.query_exists() ? BonusContent.State.DOWNLOADED : BonusContent.State.NOT_DOWNLOADED); + + var elapsed = new DateTime.now_local().difference(start_date); + + if(elapsed <= 10 * TimeSpan.SECOND) open(); + + return downloaded_file; + } + + public void open() + { + if(downloaded_file != null && downloaded_file.query_exists()) + { + Idle.add(() => { + Utils.open_uri(downloaded_file.get_uri()); + return Source.REMOVE; + }); + } + } + + public class Status + { + public BonusContent.State state; + + public Downloader.Download? download; + + public Status(BonusContent.State state=BonusContent.State.NOT_DOWNLOADED, Downloader.Download? download=null) + { + this.state = state; + this.download = download; + } + } + + public enum State + { + NOT_DOWNLOADED, DOWNLOADING, DOWNLOADED; + } + } + + public class DLC: GOGGame + { + public GOGGame game; + + public DLC(GOGGame game, Json.Node json_node) + { + base(game.source as GOG, json_node); + + image = game.image; + + install_dir = game.install_dir; + executable = game.executable; + + this.game = game; + update_status(); + } + + public override void update_status() + { + if(game == null) return; + + if(status.state == Game.State.DOWNLOADING && status.download.status.state != Downloader.DownloadState.CANCELLED) return; + + var files = new ArrayList(); + files.add(FSUtils.file(install_dir.get_path(), @"goggame-$(id).info")); + var state = Game.State.UNINSTALLED; + foreach(var file in files) + { + if(file.query_exists()) + { + warning(file.get_path()); + state = Game.State.INSTALLED; + break; + } + } + status = new Game.Status(state, this); + if(state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + + installers_dir = FSUtils.file(FSUtils.Paths.Collection.GOG.expand_installers(game.name, name)); + bonus_content_dir = FSUtils.file(FSUtils.Paths.Collection.GOG.expand_bonus(game.name, name)); + } + + public override async void install() + { + yield game.umount_overlays(); + game.enable_overlays(); + var dlc_overlay = new Game.Overlay(game, "dlc_" + id, "DLC: " + name, true); + + game.mount_overlays(dlc_overlay.directory); + + install_dir = game.install_dir.get_child(FSUtils.GAMEHUB_DIR).get_child("_overlay").get_child("merged"); + + yield base.install(); + + debug("[GOGGame.DLC.install] before umount"); + yield game.umount_overlays(); + debug("[GOGGame.DLC.install] after umount"); + + game.overlays.add(dlc_overlay); + game.save_overlays(); + game.mount_overlays(); + } } } } diff --git a/src/data/sources/humble/Humble.vala b/src/data/sources/humble/Humble.vala new file mode 100644 index 00000000..39b032fd --- /dev/null +++ b/src/data/sources/humble/Humble.vala @@ -0,0 +1,198 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Sources.Humble +{ + public class Humble: GameSource + { + public const string AUTH_COOKIE = "_simpleauth_sess"; + + public override string id { get { return "humble"; } } + public override string name { get { return "Humble Bundle"; } } + public override string icon { get { return "source-humble-symbolic"; } } + + public override bool enabled + { + get { return Settings.Auth.Humble.get_instance().enabled; } + set { Settings.Auth.Humble.get_instance().enabled = value; } + } + + public string? user_token = null; + + private Settings.Auth.Humble settings; + + public Humble() + { + settings = Settings.Auth.Humble.get_instance(); + var access_token = settings.access_token; + if(access_token.length > 0) + { + user_token = access_token; + } + } + + public override bool is_installed(bool refresh) + { + return true; + } + + public override async bool install() + { + return true; + } + + public override async bool authenticate() + { + settings.authenticated = true; + + return yield get_token(); + } + + public override bool is_authenticated() + { + return user_token != null; + } + + public override bool can_authenticate_automatically() + { + return user_token != null && settings.authenticated; + } + + private async bool get_token() + { + if(user_token != null) + { + return true; + } + + var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, "https://www.humblebundle.com/login?goto=home", "https://www.humblebundle.com/home/library", AUTH_COOKIE); + + wnd.finished.connect(token => + { + user_token = token.replace("\"", ""); + settings.access_token = user_token ?? ""; + debug("[Auth] Humble access token: %s", user_token); + Idle.add(get_token.callback); + }); + + wnd.canceled.connect(() => Idle.add(get_token.callback)); + + wnd.show_all(); + wnd.present(); + + yield; + + return user_token != null; + } + + private ArrayList _games = new ArrayList(Game.is_equal); + + public override ArrayList games { get { return _games; } } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null) + { + if(user_token == null || _games.size > 0) + { + return _games; + } + + Utils.thread("HumbleLoading", () => { + _games.clear(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + if(cached.size > 0) + { + foreach(var g in cached) + { + if(g.platforms.size == 0) continue; + if(!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(g)) + { + g.update_game_info.begin((obj, res) => { + g.update_game_info.end(res); + _games.add(g); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(g, true); return Source.REMOVE; }); + } + }); + Thread.usleep(100000); + } + games_count++; + } + } + + if(cache_loaded != null) + { + Idle.add(() => { cache_loaded(); return Source.REMOVE; }); + } + + var headers = new HashMap(); + headers["Cookie"] = @"$(AUTH_COOKIE)=\"$(user_token)\";"; + + var orders = Parser.parse_remote_json_file("https://www.humblebundle.com/api/v1/user/order?ajax=true", "GET", null, headers).get_array(); + + foreach(var order in orders.get_elements()) + { + var key = order.get_object().get_string_member("gamekey"); + + var root_node = Parser.parse_remote_json_file(@"https://www.humblebundle.com/api/v1/order/$(key)?ajax=true", "GET", null, headers); + + if(root_node == null || root_node.get_node_type() != Json.NodeType.OBJECT) continue; + + var root = root_node.get_object(); + + if(root == null) continue; + + var products = root.get_array_member("subproducts"); + + if(products == null) continue; + + foreach(var product in products.get_elements()) + { + var game = new HumbleGame(this, key, product); + if(game.platforms.size == 0) continue; + bool is_new_game = !_games.contains(game); + if(is_new_game && (!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(game))) + { + game.update_game_info.begin((obj, res) => { + game.update_game_info.end(res); + _games.add(game); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(game, false); return Source.REMOVE; }); + } + }); + } + if(is_new_game) games_count++; + } + } + + Idle.add(load_games.callback); + }); + + yield; + + return _games; + } + } +} diff --git a/src/data/sources/humble/HumbleGame.vala b/src/data/sources/humble/HumbleGame.vala new file mode 100644 index 00000000..559b44df --- /dev/null +++ b/src/data/sources/humble/HumbleGame.vala @@ -0,0 +1,360 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Sources.Humble +{ + public class HumbleGame: Game + { + public string order_id; + + private bool game_info_updated = false; + private bool game_info_refreshed = false; + + public ArrayList? installers { get; protected set; default = new ArrayList(); } + + public HumbleGame(Humble src, string order, Json.Node json_node) + { + source = src; + + var json_obj = json_node.get_object(); + + id = json_obj.get_string_member("machine_name"); + name = json_obj.get_string_member("human_name"); + image = json_obj.get_string_member("icon"); + icon = image; + order_id = order; + + info = Json.to_string(json_node, false); + + platforms.clear(); + if(json_obj.has_member("downloads") && json_obj.get_member("downloads").get_node_type() == Json.NodeType.ARRAY) + { + foreach(var dl in json_obj.get_array_member("downloads").get_elements()) + { + var pl = dl.get_object().get_string_member("platform"); + foreach(var p in Platforms) + { + if(pl == p.id()) + { + platforms.add(p); + } + } + } + } + + install_dir = FSUtils.file(FSUtils.Paths.Humble.Games, escaped_name); + executable_path = "$game_dir/start.sh"; + info_detailed = @"{\"order\":\"$(order_id)\"}"; + update_status(); + } + + public HumbleGame.from_db(Humble src, Sqlite.Statement s) + { + source = src; + id = Tables.Games.ID.get(s); + name = Tables.Games.NAME.get(s); + info = Tables.Games.INFO.get(s); + info_detailed = Tables.Games.INFO_DETAILED.get(s); + icon = Tables.Games.ICON.get(s); + image = Tables.Games.IMAGE.get(s); + install_dir = Tables.Games.INSTALL_PATH.get(s) != null ? FSUtils.file(Tables.Games.INSTALL_PATH.get(s)) : FSUtils.file(FSUtils.Paths.Humble.Games, escaped_name); + executable_path = Tables.Games.EXECUTABLE.get(s); + compat_tool = Tables.Games.COMPAT_TOOL.get(s); + compat_tool_settings = Tables.Games.COMPAT_TOOL_SETTINGS.get(s); + arguments = Tables.Games.ARGUMENTS.get(s); + last_launch = Tables.Games.LAST_LAUNCH.get_int64(s); + playtime_source = Tables.Games.PLAYTIME_SOURCE.get_int64(s); + playtime_tracked = Tables.Games.PLAYTIME_TRACKED.get_int64(s); + + platforms.clear(); + var pls = Tables.Games.PLATFORMS.get(s).split(","); + foreach(var pl in pls) + { + foreach(var p in Platforms) + { + if(pl == p.id()) + { + platforms.add(p); + break; + } + } + } + + tags.clear(); + var tag_ids = (Tables.Games.TAGS.get(s) ?? "").split(","); + foreach(var tid in tag_ids) + { + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + + var json_node = Parser.parse_json(info_detailed); + if(json_node != null && json_node.get_node_type() == Json.NodeType.OBJECT) + { + var json = json_node.get_object(); + if(json.has_member("order")) + { + order_id = json.get_string_member("order"); + } + } + + update_status(); + } + + public override void update_status() + { + if(status.state == Game.State.DOWNLOADING && status.download.status.state != Downloader.DownloadState.CANCELLED) return; + + var exec = executable; + status = new Game.Status(exec != null && exec.query_exists() ? Game.State.INSTALLED : Game.State.UNINSTALLED, this); + if(status.state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + + installers_dir = FSUtils.file(FSUtils.Paths.Collection.Humble.expand_installers(name)); + } + + public override async void update_game_info() + { + update_status(); + + mount_overlays(); + + if((icon == null || icon == "") && (info != null && info.length > 0)) + { + var i = Parser.parse_json(info).get_object(); + icon = i.get_string_member("icon"); + } + + if(image == null || image == "") + { + image = icon; + } + + if(game_info_updated) return; + + if(info == null || info.length == 0) + { + var token = ((Humble) source).user_token; + + var headers = new HashMap(); + headers["Cookie"] = @"$(Humble.AUTH_COOKIE)=\"$(token)\";"; + + var root_node = yield Parser.parse_remote_json_file_async(@"https://www.humblebundle.com/api/v1/order/$(order_id)?ajax=true", "GET", null, headers); + if(root_node == null || root_node.get_node_type() != Json.NodeType.OBJECT) return; + var root = root_node.get_object(); + if(root == null) return; + var products = root.get_array_member("subproducts"); + if(products == null) return; + foreach(var product_node in products.get_elements()) + { + if(product_node.get_object().get_string_member("machine_name") != id) continue; + info = Json.to_string(product_node, false); + break; + } + } + + if(installers.size > 0) return; + + var product = Parser.parse_json(info).get_object(); + if(product == null) return; + + if(product.has_member("_gamehub_description")) + { + description = product.get_string_member("_gamehub_description"); + } + + if(product.has_member("downloads") && product.get_member("downloads").get_node_type() == Json.NodeType.ARRAY) + { + foreach(var dl_node in product.get_array_member("downloads").get_elements()) + { + var dl = dl_node.get_object(); + var id = dl.get_string_member("machine_name"); + var dl_id = dl.has_member("download_identifier") ? dl.get_string_member("download_identifier") : null; + var os = dl.get_string_member("platform"); + var platform = CurrentPlatform; + foreach(var p in Platforms) + { + if(os == p.id()) + { + platform = p; + break; + } + } + + bool refresh = false; + + if(dl.has_member("download_struct") && dl.get_member("download_struct").get_node_type() == Json.NodeType.ARRAY) + { + foreach(var dls_node in dl.get_array_member("download_struct").get_elements()) + { + var installer = new Installer(this, id, dl_id, platform, dls_node.get_object()); + if(installer.is_url_update_required()) + { + if(source is Trove) + { + var old_url = installer.part.url; + var new_url = installer.update_url(this); + if(new_url != null) + { + info = info.replace(old_url, new_url); + } + refresh = true; + } + else + { + info = null; + refresh = true; + } + } + if(!refresh) installers.add(installer); + } + } + + if(refresh && !game_info_refreshed) + { + //debug("[HumbleGame.update_game_info] Refreshing"); + game_info_refreshed = true; + game_info_updated = false; + installers.clear(); + yield update_game_info(); + return; + } + } + } + + save(); + + update_status(); + + game_info_updated = true; + } + + public override async void install() + { + yield update_game_info(); + + if(installers.size < 1) return; + + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + + wnd.cancelled.connect(() => Idle.add(install.callback)); + + wnd.install.connect((installer, dl_only, tool) => { + FSUtils.mkdir(FSUtils.Paths.Humble.Games); + FSUtils.mkdir(installer.parts.get(0).local.get_parent().get_path()); + + installer.install.begin(this, dl_only, tool, (obj, res) => { + installer.install.end(res); + update_status(); + Idle.add(install.callback); + }); + }); + + wnd.import.connect(() => { + import(); + Idle.add(install.callback); + }); + + wnd.show_all(); + wnd.present(); + + yield; + } + + public override async void uninstall() + { + yield umount_overlays(); + FSUtils.rm(install_dir.get_path(), "", "-rf"); + update_status(); + if(!install_dir.query_exists() && !executable.query_exists()) + { + install_dir = FSUtils.file(FSUtils.Paths.GOG.Games, escaped_name); + executable = FSUtils.file(install_dir.get_path(), "start.sh"); + save(); + update_status(); + } + } + + public class Installer: Runnable.Installer + { + public string dl_name; + public string? dl_id; + public Runnable.Installer.Part part; + + public override string name { get { return dl_name; } } + + public Installer(HumbleGame game, string machine_name, string? download_identifier, Platform platform, Json.Object download) + { + id = machine_name; + this.platform = platform; + this.dl_id = download_identifier; + dl_name = download.has_member("name") ? download.get_string_member("name") : ""; + var url_obj = download.has_member("url") ? download.get_object_member("url") : null; + var url = url_obj != null && url_obj.has_member("web") ? url_obj.get_string_member("web") : ""; + full_size = download.has_member("file_size") ? download.get_int_member("file_size") : 0; + if(game.installers_dir == null) return; + var remote = File.new_for_uri(url); + var local = game.installers_dir.get_child("humble_" + game.id + "_" + id); + part = new Runnable.Installer.Part(id, url, full_size, remote, local); + parts.add(part); + } + + public bool is_url_update_required() + { + if(part.url == null || part.url.length == 0) return true; + if(!part.url.contains("&ttl=")) return false; + var ttl_string = part.url.split("&ttl=")[1].split("&")[0]; + var ttl = new DateTime.from_unix_utc(int64.parse(ttl_string)); + var now = new DateTime.now_utc(); + var res = ttl.compare(now); + return res != 1; + } + + public string? update_url(HumbleGame game) + { + if(!(game.source is Trove) || !is_url_update_required()) return null; + + //debug("[HumbleGame.Installer.update_url] Old URL: '%s'; (%s)", part.url, game.full_id); + var new_url = Trove.sign_url(id, dl_id, ((Humble) game.source).user_token); + //debug("[HumbleGame.Installer.update_url] New URL: '%s'; (%s)", new_url, game.full_id); + + if(new_url != null) part.url = new_url; + + return new_url; + } + } + } +} diff --git a/src/data/sources/humble/Trove.vala b/src/data/sources/humble/Trove.vala new file mode 100644 index 00000000..89ded9ff --- /dev/null +++ b/src/data/sources/humble/Trove.vala @@ -0,0 +1,215 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Sources.Humble +{ + public class Trove: Humble + { + public const string PAGE_URL = "https://www.humblebundle.com/monthly/trove"; + public const string SIGN_URL = "https://www.humblebundle.com/api/v1/user/download/sign"; + public const string FAKE_ORDER = "humble-trove"; + + public override string id { get { return "humble-trove"; } } + public override string name { get { return "Humble Trove"; } } + public override string icon { get { return "source-humble-trove-symbolic"; } } + + public override bool enabled + { + get { return Settings.Auth.Humble.get_instance().enabled && Settings.Auth.Humble.get_instance().load_trove_games; } + set { Settings.Auth.Humble.get_instance().load_trove_games = value; } + } + + private ArrayList _games = new ArrayList(Game.is_equal); + + public override ArrayList games { get { return _games; } } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null) + { + if(user_token == null || _games.size > 0) + { + return _games; + } + + Utils.thread("HumbleTroveLoading", () => { + _games.clear(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + if(cached.size > 0) + { + foreach(var g in cached) + { + if(g.platforms.size == 0) continue; + if(!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(g)) + { + g.update_game_info.begin((obj, res) => { + g.update_game_info.end(res); + _games.add(g); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(g, true); return Source.REMOVE; }); + } + }); + Thread.usleep(100000); + } + games_count++; + } + } + + if(cache_loaded != null) + { + Idle.add(() => { cache_loaded(); return Source.REMOVE; }); + } + + var headers = new HashMap(); + headers["Cookie"] = @"$(AUTH_COOKIE)=\"$(user_token)\";"; + + var html = Parser.parse_remote_html_file(Trove.PAGE_URL, "GET", null, headers); + + if(html != null) + { + var xpath = new Xml.XPath.Context(html); + + var items = xpath.eval("//div[starts-with(@class, 'trove-grid-item')]")->nodesetval; + if(items != null && !items->is_empty()) + { + for(int i = 0; i < items->length(); i++) + { + var item = items->item(i); + var id = item->get_prop("data-machine-name"); + var xr = @"//div[starts-with(@class, 'trove-product-detail')][@data-machine-name='$(id)']"; + + var dl_btn = xpath.eval(@"$(xr)//button[contains(@class, 'js-download-button')]")->nodesetval; + + if(dl_btn == null || dl_btn->is_empty()) + { + continue; // no dl button, can't download + } + + var image = Parser.html_subnode(item, "img")->get_prop("src"); + + var name = xpath.eval(@"$(xr)//h1[@class='product-human-name']/text()")->nodesetval->item(0)->content; + + var desc_nodes = xpath.eval(@"$(xr)//div[@class='trove-product-description']/node()")->nodesetval; + + string desc = ""; + + if(desc_nodes != null && desc_nodes->length() > 0) + { + for(int dn = 0; dn < desc_nodes->length(); dn++) + { + desc += Parser.xml_node_to_string(desc_nodes->item(dn)); + } + desc = desc.strip(); + } + + var json = new Json.Object(); + json.set_string_member("machine_name", id); + json.set_string_member("human_name", name); + json.set_string_member("icon", image); + json.set_string_member("_gamehub_description", desc); + + var dl_nodes = xpath.eval(@"$(xr)//div[starts-with(@class, 'trove-platform-selector')]")->nodesetval; + + var dls = new Json.Array(); + + if(dl_nodes != null && !dl_nodes->is_empty()) + { + for(int d = 0; d < dl_nodes->length(); d++) + { + var dn = dl_nodes->item(d); + var dl = new Json.Object(); + + dl.set_string_member("platform", dn->get_prop("data-platform")); + dl.set_string_member("download_identifier", dn->get_prop("data-url")); + dl.set_string_member("machine_name", dn->get_prop("data-machine-name")); + + var signed_url = sign_url(dn->get_prop("data-machine-name"), dn->get_prop("data-url"), user_token); + + var dl_struct = new Json.Object(); + dl_struct.set_string_member("name", @"$(name) (Trove)"); + + var urls = new Json.Object(); + urls.set_string_member("web", signed_url); + + dl_struct.set_object_member("url", urls); + + var dl_struct_arr = new Json.Array(); + dl_struct_arr.add_object_element(dl_struct); + + dl.set_array_member("download_struct", dl_struct_arr); + + dls.add_object_element(dl); + } + } + + json.set_array_member("downloads", dls); + + var json_node = new Json.Node(Json.NodeType.OBJECT); + json_node.set_object(json); + + var game = new HumbleGame(this, Trove.FAKE_ORDER, json_node); + + if(game.platforms.size == 0) continue; + bool is_new_game = !_games.contains(game); + if(is_new_game && (!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(game))) + { + game.update_game_info.begin((obj, res) => { + game.update_game_info.end(res); + _games.add(game); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(game, false); return Source.REMOVE; }); + } + }); + } + if(is_new_game) games_count++; + } + } + } + + delete html; + + Idle.add(load_games.callback); + }); + + yield; + + return _games; + } + + public static string? sign_url(string machine_name, string filename, string humble_token) + { + var headers = new HashMap(); + headers["Cookie"] = @"$(AUTH_COOKIE)=\"$(humble_token)\";"; + + var data = new HashMap(); + data["machine_name"] = machine_name; + data["filename"] = filename; + + var signed_node = Parser.parse_remote_json_file(Trove.SIGN_URL, "POST", null, headers, data); + var signed = signed_node != null && signed_node.get_node_type() == Json.NodeType.OBJECT ? signed_node.get_object() : null; + + return signed != null && signed.has_member("signed_url") ? signed.get_string_member("signed_url") : null; + } + } +} diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala index 562cfaea..e3206c09 100644 --- a/src/data/sources/steam/Steam.vala +++ b/src/data/sources/steam/Steam.vala @@ -1,99 +1,287 @@ -using Gtk; +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gee; +using GameHub.Data.DB; using GameHub.Utils; namespace GameHub.Data.Sources.Steam { public class Steam: GameSource { - private const string API_KEY = "8B10B604CAC6AC90F57AACE025DD904C"; - + public static Steam instance; + + public Steam() + { + instance = this; + } + + public string api_key; + + public override string id { get { return "steam"; } } public override string name { get { return "Steam"; } } - public override string icon { get { return "steam-symbolic"; } } - public override string auth_description { owned get { return ".\n%s".printf(_("Your SteamID will be read from Steam configuration file")); } } - + public override string icon { get { return "source-steam-symbolic"; } } + public override string auth_description + { + owned get + { + var text = _("Your SteamID will be read from Steam configuration file"); + if(!is_authenticated_in_steam_client) + { + text = _("Steam config file not found.\nLogin into your account in Steam client and return to GameHub"); + } + return ".\n%s".printf(text); + } + } + + public override bool enabled + { + get { return Settings.Auth.Steam.get_instance().enabled; } + set { Settings.Auth.Steam.get_instance().enabled = value; } + } + public string? user_id { get; protected set; } public string? user_name { get; protected set; } private bool? installed = null; + private bool is_authenticated_in_steam_client { get { return FSUtils.file(FSUtils.Paths.Steam.LoginUsersVDF).query_exists(); } } + public override bool is_installed(bool refresh) { if(installed != null && !refresh) { return (!) installed; } - - installed = Utils.is_package_installed("steam"); + + if("elementary" in Utils.get_distro()) + { + installed = Utils.is_package_installed("steam"); + } + else + { + installed = FSUtils.file(FSUtils.Paths.Steam.Home).query_exists(); + } + return (!) installed; } + public static bool find_app_install_dir(string app, out File? install_dir) + { + install_dir = null; + foreach(var dir in Steam.LibraryFolders) + { + var acf = FSUtils.file(dir, @"appmanifest_$(app).acf"); + if(acf.query_exists()) + { + var root = Parser.parse_vdf_file(acf.get_path()).get_object(); + var d = FSUtils.file(dir, "common/" + root.get_object_member("AppState").get_string_member("installdir")); + install_dir = d; + return d.query_exists(); + } + } + return false; + } + + public static bool is_app_installed(string app) + { + return find_app_install_dir(app, null); + } + public override async bool install() { - Utils.open_uri("appstream://steam.desktop"); + if("elementary" in Utils.get_distro()) + { + Utils.open_uri("appstream://steam.desktop"); + } return true; } public override async bool authenticate() { + Settings.Auth.Steam.get_instance().authenticated = true; + if(is_authenticated()) return true; - + var result = false; - - new Thread("steam-loginusers-thread", () => { - Json.Object config = Parser.parse_vdf_file(FSUtils.Paths.Steam.LoginUsersVDF); - var users = config.get_object_member("users"); - + + if(!is_authenticated_in_steam_client) + { + Utils.open_uri("steam://"); + return false; + } + + Utils.thread("Steam-loginusers", () => { + var config = Parser.parse_vdf_file(FSUtils.Paths.Steam.LoginUsersVDF); + var users = Parser.json_object(config, {"users"}); + + if(users == null) + { + result = false; + Idle.add(authenticate.callback); + return; + } + foreach(var uid in users.get_members()) { + var user = users.get_object_member(uid); + user_id = uid; - user_name = users.get_object_member(uid).get_string_member("PersonaName"); - - result = true; - break; + user_name = user.get_string_member("PersonaName"); + + var last = !user.has_member("mostrecent") || user.get_string_member("mostrecent") == "1"; + + debug(@"[Auth] SteamID: $(user_id), PersonaName: $(user_name), last: $(last)"); + + if(last) + { + result = true; + break; + } } - + Idle.add(authenticate.callback); - return null; }); - + yield; return result; } - + public override bool is_authenticated() { return user_id != null; } - private ArrayList games = new ArrayList(); - public override async ArrayList load_games(FutureResult? game_loaded = null) + public override bool can_authenticate_automatically() + { + return Settings.Auth.Steam.get_instance().authenticated && is_authenticated_in_steam_client; + } + + private ArrayList _games = new ArrayList(Game.is_equal); + + public override ArrayList games { get { return _games; } } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null) + { + api_key = Settings.Auth.Steam.get_instance().api_key; + + if(!is_authenticated() || _games.size > 0) + { + return _games; + } + + Utils.thread("SteamLoading", () => { + _games.clear(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + if(cached.size > 0) + { + foreach(var g in cached) + { + if(!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(g)) + { + //g.update_game_info.begin(); + _games.add(g); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(g, true); return Source.REMOVE; }); + Thread.usleep(100000); + } + } + games_count++; + } + } + + if(cache_loaded != null) + { + Idle.add(() => { cache_loaded(); return Source.REMOVE; }); + } + + var url = @"https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=$(api_key)&steamid=$(user_id)&format=json&include_appinfo=1&include_played_free_games=1"; + + var root = Parser.parse_remote_json_file(url); + var response = Parser.json_object(root, {"response"}); + var json_games = response != null && response.has_member("games") ? response.get_array_member("games") : null; + + add_games.begin(json_games, game_loaded, (obj, res) => { + add_games.end(res); + Idle.add(load_games.callback); + }); + }); + + yield; + + return _games; + } + + private async void add_games(Json.Array json_games, FutureResult2? game_loaded = null) { - if(!is_authenticated() || games.size > 0) + if(json_games != null) { - return games; + foreach(var g in json_games.get_elements()) + { + var game = new SteamGame(this, g); + bool is_new_game = !_games.contains(game); + if(is_new_game && (!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(game))) + { + _games.add(game); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(game, false); return Source.REMOVE; }); + } + } + if(is_new_game) + { + games_count++; + } + else if(g != null && g.get_node_type() == Json.NodeType.OBJECT) + { + _games.get(_games.index_of(game)).playtime_source = g.get_object().get_int_member("playtime_forever"); + } + } } - - var url = @"https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=$(Steam.API_KEY)&steamid=$(this.user_id)&format=json&include_appinfo=1"; - - var root = yield Parser.parse_remote_json_file_async(url); - var json_games = root.get_object_member("response").get_array_member("games"); - - games.clear(); - - foreach(var g in json_games.get_elements()) + } + + public static ArrayList? folders = null; + public static ArrayList LibraryFolders + { + get { - var game = new SteamGame(this, g.get_object()); - if(yield game.is_for_linux()) + if(folders != null) return folders; + + folders = new ArrayList(); + folders.add(FSUtils.Paths.Steam.SteamApps); + + var root = Parser.parse_vdf_file(FSUtils.Paths.Steam.LibraryFoldersVDF); + var lf = Parser.json_object(root, {"LibraryFolders"}); + + if(lf != null) { - games.add(game); - games_count = games.size; - if(game_loaded != null) game_loaded(game); + foreach(var key in lf.get_members()) + { + var dir = lf.get_string_member(key) + "/steamapps"; + if(FSUtils.file(dir).query_exists()) folders.add(dir); + } } + + return folders; } - - - return games; } } } diff --git a/src/data/sources/steam/SteamGame.vala b/src/data/sources/steam/SteamGame.vala index 8ea62d17..61f6b90d 100644 --- a/src/data/sources/steam/SteamGame.vala +++ b/src/data/sources/steam/SteamGame.vala @@ -1,60 +1,364 @@ -using Gtk; +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; + +using GameHub.Data.DB; using GameHub.Utils; namespace GameHub.Data.Sources.Steam { public class SteamGame: Game { - private bool? _is_for_linux = null; private int metadata_tries = 0; - - public SteamGame(Steam src, Json.Object json) - { - this.source = src; - - this.id = json.get_int_member("appid").to_string(); - this.name = json.get_string_member("name"); - - var icon_hash = json.get_string_member("img_icon_url"); - var image_hash = json.get_string_member("img_logo_url"); - - this.icon = @"https://media.steampowered.com/steamcommunity/public/images/apps/$(this.id)/$(icon_hash).jpg"; - this.image = @"https://cdn.akamai.steamstatic.com/steam/apps/$(this.id)/header.jpg"; - - this.command = @"xdg-open steam://rungameid/$(this.id)"; - - this.playtime = ((float) json.get_int_member("playtime_forever")) / 60.0f; - } - - public override async bool is_for_linux() - { - if(_is_for_linux != null) return _is_for_linux; - + + private bool game_info_updated = false; + + public SteamGame(Steam src, Json.Node json_node) + { + source = src; + + var json_obj = json_node.get_object(); + + id = json_obj.get_int_member("appid").to_string(); + name = json_obj.get_string_member("name"); + var icon_hash = json_obj.get_string_member("img_icon_url"); + icon = @"http://media.steampowered.com/steamcommunity/public/images/apps/$(id)/$(icon_hash).jpg"; + image = @"http://cdn.akamai.steamstatic.com/steam/apps/$(id)/header.jpg"; + + info = Json.to_string(json_node, false); + + store_page = @"steam://store/$(id)"; + + update_status(); + } + + public SteamGame.from_db(Steam src, Sqlite.Statement s) + { + source = src; + id = Tables.Games.ID.get(s); + name = Tables.Games.NAME.get(s); + info = Tables.Games.INFO.get(s); + info_detailed = Tables.Games.INFO_DETAILED.get(s); + icon = Tables.Games.ICON.get(s); + image = Tables.Games.IMAGE.get(s); + info = Tables.Games.INFO.get(s); + info_detailed = Tables.Games.INFO_DETAILED.get(s); + compat_tool = Tables.Games.COMPAT_TOOL.get(s); + compat_tool_settings = Tables.Games.COMPAT_TOOL_SETTINGS.get(s); + arguments = Tables.Games.ARGUMENTS.get(s); + last_launch = Tables.Games.LAST_LAUNCH.get_int64(s); + playtime_source = Tables.Games.PLAYTIME_SOURCE.get_int64(s); + playtime_tracked = Tables.Games.PLAYTIME_TRACKED.get_int64(s); + + platforms.clear(); + var pls = Tables.Games.PLATFORMS.get(s).split(","); + foreach(var pl in pls) + { + foreach(var p in Platforms) + { + if(pl == p.id()) + { + platforms.add(p); + break; + } + } + } + + tags.clear(); + var tag_ids = (Tables.Games.TAGS.get(s) ?? "").split(","); + foreach(var tid in tag_ids) + { + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + + store_page = @"steam://store/$(id)"; + + update_status(); + } + + public override async void update_game_info() + { + update_status(); + + if(image == null || image == "") + { + image = @"http://cdn.akamai.steamstatic.com/steam/apps/$(id)/header.jpg"; + } + + if((info != null && info.length > 0)) + { + var i = Parser.parse_json(info).get_object(); + if((icon == null || icon == "")) + { + var icon_hash = i.get_string_member("img_icon_url"); + icon = @"http://media.steampowered.com/steamcommunity/public/images/apps/$(id)/$(icon_hash).jpg"; + } + if(playtime_source == 0) + { + playtime_source = i.get_int_member("playtime_forever"); + } + } + + File? dir; + Steam.find_app_install_dir(id, out dir); + install_dir = dir; + + if(game_info_updated) return; + + if(info_detailed == null || info_detailed.length == 0) + { + debug("[Steam:%s] No cached app data for '%s', fetching...", id, name); + var lang = Utils.get_language_name().down(); + var url = @"https://store.steampowered.com/api/appdetails?appids=$(id)" + (lang != null && lang.length > 0 ? "&l=" + lang : ""); + info_detailed = (yield Parser.load_remote_file_async(url)); + } + + var root = Parser.parse_json(info_detailed); + + var app = Parser.json_object(root, {id}); + + if(app == null) + { + debug("[Steam:%s] No app data for '%s', store page does not exist", id, name); + game_info_updated = true; + return; + } + + var data = Parser.json_object(root, {id, "data"}); + + if(data == null) + { + bool success = app.has_member("success") && app.get_boolean_member("success"); + debug("[Steam:%s] No app data for '%s', success: %s, store page does not exist", id, name, success.to_string()); + if(metadata_tries > 0) + { + game_info_updated = true; + return; + } + } + + description = data != null && data.has_member("detailed_description") ? data.get_string_member("detailed_description") : ""; + metadata_tries++; - - print("[Steam app %s] Checking for linux compatibility [%d]...\n", this.id, metadata_tries); - - var url = @"https://store.steampowered.com/api/appdetails?appids=$(this.id)"; - var root = yield Parser.parse_remote_json_file_async(url); - var platforms = Parser.json_object(root, {this.id, "data", "platforms"}); - - if(platforms == null) - { - if(metadata_tries > 2) - { - print("[Steam app %s] No data, %d tries failed, assuming no linux support\n", this.id, metadata_tries); - _is_for_linux = false; - return _is_for_linux; - } - - print("[Steam app %s] No data, sleeping for 2.5s\n", this.id); - yield Utils.sleep_async(2500); - return yield is_for_linux(); - } - - _is_for_linux = platforms.get_boolean_member("linux"); - - return _is_for_linux; + + var platforms_json = Parser.json_object(root, {id, "data", "platforms"}); + + platforms.clear(); + if(platforms_json == null) + { + debug("[Steam:%s] No platform support data, %d tries failed, assuming Windows support", id, metadata_tries); + platforms.add(Platform.WINDOWS); + save(); + game_info_updated = true; + return; + } + + foreach(var p in Platforms) + { + if(platforms_json.get_boolean_member(p.id())) + { + platforms.add(p); + } + } + + save(); + + game_info_updated = true; + update_status(); + } + + public override void update_status() + { + status = new Game.Status(Steam.is_app_installed(id) ? Game.State.INSTALLED : Game.State.UNINSTALLED, this); + if(status.state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + } + + public override async void install() + { + yield run(); + } + + public override async void run() + { + last_launch = get_real_time() / 1000000; + save(); + Utils.open_uri(@"steam://rungameid/$(id)"); + update_status(); + } + + public override async void run_with_compat(bool is_opened_from_menu=false) + { + yield run(); + } + + public override async void uninstall() + { + Utils.open_uri(@"steam://uninstall/$(id)"); + update_status(); + } + + private bool loading_achievements = false; + public override async ArrayList? load_achievements() + { + if(achievements != null || loading_achievements) + { + return achievements; + } + + loading_achievements = true; + + var lang = Utils.get_language_name().down(); + lang = (lang != null && lang.length > 0 ? "&l=" + lang : ""); + + var schema_url = @"https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/?key=$(Steam.instance.api_key)&format=json&appid=$(id)$(lang)"; + var achievements_url = @"https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v1/?key=$(Steam.instance.api_key)&steamid=$(Steam.instance.user_id)&format=json&appid=$(id)$(lang)"; + var global_percentages_url = @"https://api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/?key=$(Steam.instance.api_key)&format=json&gameid=$(id)"; + + var schema_root = (yield Parser.parse_remote_json_file_async(schema_url)); + var achievements_root = (yield Parser.parse_remote_json_file_async(achievements_url)); + var global_percentages_root = (yield Parser.parse_remote_json_file_async(global_percentages_url)); + + var schema_achievements_obj = Parser.json_object(schema_root, {"game", "availableGameStats"}); + if(schema_achievements_obj == null || !schema_achievements_obj.has_member("achievements")) + { + loading_achievements = false; + return null; + } + var schema_achievements = schema_achievements_obj.get_array_member("achievements"); + + var achievements_obj = Parser.json_object(achievements_root, {"playerstats"}); + if(achievements_obj == null || !achievements_obj.has_member("achievements")) + { + loading_achievements = false; + return null; + } + var player_achievements = achievements_obj.get_array_member("achievements"); + + var global_percentages_obj = Parser.json_object(global_percentages_root, {"achievementpercentages"}); + if(global_percentages_obj == null || !global_percentages_obj.has_member("achievements")) + { + loading_achievements = false; + return null; + } + var global_percentages = global_percentages_obj.get_array_member("achievements"); + + var _achievements = new ArrayList(); + + foreach(var s_achievement_node in schema_achievements.get_elements()) + { + var s_achievement = s_achievement_node != null && s_achievement_node.get_node_type() == Json.NodeType.OBJECT + ? s_achievement_node.get_object() : null; + + if(s_achievement == null || !s_achievement.has_member("name")) continue; + + var a_id = s_achievement.get_string_member("name"); + var a_name = s_achievement.has_member("displayName") ? s_achievement.get_string_member("displayName") : a_id; + var a_desc = s_achievement.has_member("description") ? s_achievement.get_string_member("description") : ""; + var a_image_unlocked = s_achievement.has_member("icon") ? s_achievement.get_string_member("icon") : null; + var a_image_locked = s_achievement.has_member("icongray") ? s_achievement.get_string_member("icongray") : null; + bool a_unlocked = false; + int64 a_unlock_time = 0; + float a_global_percentage = 0; + + foreach(var p_achievement_node in player_achievements.get_elements()) + { + var p_achievement = p_achievement_node != null && p_achievement_node.get_node_type() == Json.NodeType.OBJECT + ? p_achievement_node.get_object() : null; + + if(p_achievement == null || !p_achievement.has_member("apiname") + || p_achievement.get_string_member("apiname") != a_id) continue; + + a_unlocked = p_achievement.has_member("achieved") && p_achievement.get_int_member("achieved") > 0; + a_unlock_time = p_achievement.has_member("unlocktime") ? p_achievement.get_int_member("unlocktime") : 0; + } + + foreach(var gp_achievement_node in global_percentages.get_elements()) + { + var gp_achievement = gp_achievement_node != null && gp_achievement_node.get_node_type() == Json.NodeType.OBJECT + ? gp_achievement_node.get_object() : null; + + if(gp_achievement == null || !gp_achievement.has_member("name") + || gp_achievement.get_string_member("name") != a_id) continue; + + a_global_percentage = (float) (gp_achievement.has_member("percent") ? gp_achievement.get_double_member("percent") : 0); + } + + _achievements.add(new Achievement(a_id, a_name, a_desc, a_image_locked, a_image_unlocked, + a_unlocked, a_unlock_time, a_global_percentage)); + } + + _achievements.sort((first, second) => { + var a1 = first as Achievement; + var a2 = second as Achievement; + + if(a1.unlock_timestamp > 0 || a2.unlock_timestamp > 0) + { + return (int) (a2.unlock_timestamp - a1.unlock_timestamp); + } + + if(a1.global_percentage < a2.global_percentage) return 1; + if(a1.global_percentage > a2.global_percentage) return -1; + return 0; + }); + + achievements = _achievements; + loading_achievements = false; + return achievements; + } + + public override void import(bool update=true){} + public override void choose_executable(bool update=true){} + + public class Achievement: Game.Achievement + { + public int64 unlock_timestamp; + + public Achievement(string id, string name, string desc, string? image_locked, string? image_unlocked, + bool unlocked, int64 unlock_time, float global_percentage) + { + this.id = id; + this.name = name; + this.description = desc; + this.image_locked = image_locked; + this.image_unlocked = image_unlocked; + this.unlocked = unlocked; + this.global_percentage = global_percentage; + this.unlock_timestamp = unlock_time; + this.unlock_date = new DateTime.from_unix_utc(unlock_time); + this.unlock_time = Granite.DateTime.get_relative_datetime(this.unlock_date); + } } } } diff --git a/src/data/sources/user/User.vala b/src/data/sources/user/User.vala new file mode 100644 index 00000000..49d4d674 --- /dev/null +++ b/src/data/sources/user/User.vala @@ -0,0 +1,132 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Sources.User +{ + public class User: GameSource + { + public static User instance; + + public override string id { get { return "user"; } } + public override string name { get { return _("User games"); } } + public override string icon { get { return "avatar-default-symbolic"; } } + + public User() + { + instance = this; + } + + public override bool enabled + { + get { return true; } + set {} + } + + public override bool is_installed(bool refresh) + { + return true; + } + + public override async bool install() + { + return true; + } + + public override async bool authenticate() + { + return true; + } + + public override bool is_authenticated() + { + return true; + } + + public override bool can_authenticate_automatically() + { + return true; + } + + private ArrayList _games = new ArrayList(Game.is_equal); + + public override ArrayList games { get { return _games; } } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded=null, Utils.Future? cache_loaded=null) + { + if(_games.size > 0) + { + return _games; + } + + Utils.thread("UserGamesLoading", () => { + _games.clear(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + if(cached.size > 0) + { + foreach(var g in cached) + { + if(!Settings.UI.get_instance().merge_games || !Tables.Merges.is_game_merged(g)) + { + g.update_game_info.begin(); + _games.add(g); + if(game_loaded != null) + { + Idle.add(() => { game_loaded(g, true); return Source.REMOVE; }); + } + ((UserGame) g).removed.connect(() => { + _games.remove(g); + }); + } + games_count++; + } + } + + if(cache_loaded != null) + { + Idle.add(() => { cache_loaded(); return Source.REMOVE; }); + } + + Idle.add(load_games.callback); + }); + + yield; + + return _games; + } + + public void add_game(UserGame game) + { + if(_games.contains(game)) return; + _games.add(game); + games_count++; + } + + public void remove_game(UserGame game) + { + _games.remove(game); + games_count--; + Tables.Games.remove(game); + } + } +} diff --git a/src/data/sources/user/UserGame.vala b/src/data/sources/user/UserGame.vala new file mode 100644 index 00000000..fceb2f55 --- /dev/null +++ b/src/data/sources/user/UserGame.vala @@ -0,0 +1,205 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.Data.Sources.User +{ + public class UserGame: Game + { + private bool is_removed = false; + public signal void removed(); + + private Installer? installer; + + public UserGame(string name, File dir, File exec, string args, bool is_installer) + { + source = User.instance; + + this.id = Utils.md5(name + Random.next_int().to_string()); + this.name = name; + + platforms.clear(); + platforms.add(exec.get_path().has_suffix(".exe") ? Platform.WINDOWS : Platform.LINUX); + + install_dir = dir; + + arguments = args; + + if(!is_installer) + { + executable = exec; + } + else + { + installer = new Installer(this, exec); + var root_object = new Json.Object(); + root_object.set_string_member("installer", exec.get_path()); + var root_node = new Json.Node(Json.NodeType.OBJECT); + root_node.set_object(root_object); + info = Json.to_string(root_node, false); + save(); + } + + ((User) source).add_game(this); + update_status(); + } + + public UserGame.from_db(User src, Sqlite.Statement s) + { + source = src; + id = Tables.Games.ID.get(s); + name = Tables.Games.NAME.get(s); + info = Tables.Games.INFO.get(s); + info_detailed = Tables.Games.INFO_DETAILED.get(s); + icon = Tables.Games.ICON.get(s); + image = Tables.Games.IMAGE.get(s); + install_dir = FSUtils.file(Tables.Games.INSTALL_PATH.get(s)); + executable_path = Tables.Games.EXECUTABLE.get(s); + compat_tool = Tables.Games.COMPAT_TOOL.get(s); + compat_tool_settings = Tables.Games.COMPAT_TOOL_SETTINGS.get(s); + arguments = Tables.Games.ARGUMENTS.get(s); + last_launch = Tables.Games.LAST_LAUNCH.get_int64(s); + playtime_source = Tables.Games.PLAYTIME_SOURCE.get_int64(s); + playtime_tracked = Tables.Games.PLAYTIME_TRACKED.get_int64(s); + + platforms.clear(); + var pls = Tables.Games.PLATFORMS.get(s).split(","); + foreach(var pl in pls) + { + foreach(var p in Platforms) + { + if(pl == p.id()) + { + platforms.add(p); + break; + } + } + } + + tags.clear(); + var tag_ids = (Tables.Games.TAGS.get(s) ?? "").split(","); + foreach(var tid in tag_ids) + { + foreach(var t in Tables.Tags.TAGS) + { + if(tid == t.id) + { + if(!tags.contains(t)) tags.add(t); + break; + } + } + } + + update_status(); + } + + public override async void update_game_info() + { + update_status(); + + mount_overlays(); + + if(installer == null && info != null && info.length > 0) + { + var i = Parser.parse_json(info).get_object(); + installer = new Installer(this, File.new_for_path(i.get_string_member("installer"))); + } + save(); + } + + public override async void install() + { + yield update_game_info(); + + if(installer == null) return; + + var installers = new ArrayList(); + installers.add(installer); + + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + + wnd.cancelled.connect(() => Idle.add(install.callback)); + + wnd.install.connect((installer, dl_only, tool) => { + installer.install.begin(this, dl_only, tool, (obj, res) => { + installer.install.end(res); + Idle.add(install.callback); + }); + }); + + wnd.show_all(); + wnd.present(); + + yield; + } + + public override async void uninstall() + { + yield umount_overlays(); + remove(); + } + + public void remove() + { + is_removed = true; + ((User) source).remove_game(this); + removed(); + } + + public override void save() + { + if(!is_removed) + { + base.save(); + } + } + + public override void update_status() + { + var exec = executable; + status = new Game.Status(exec != null && exec.query_exists() ? Game.State.INSTALLED : Game.State.UNINSTALLED, this); + if(status.state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + } + + public class Installer: Runnable.Installer + { + private string game_name; + public override string name { get { return game_name; } } + + public Installer(UserGame game, File installer) + { + game_name = game.name; + id = "installer"; + platform = installer.get_path().has_suffix(".exe") ? Platform.WINDOWS : Platform.LINUX; + parts.add(new Runnable.Installer.Part("installer", installer.get_uri(), full_size, installer, installer)); + } + } + } +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 00000000..357c6b3a --- /dev/null +++ b/src/meson.build @@ -0,0 +1,155 @@ +project_config = configure_file( + input: 'ProjectConfig.vala.in', + output: 'ProjectConfig.vala', + configuration: conf_data +) + +deps = [ + dependency('granite'), + dependency('gdk-3.0'), + dependency('webkit2gtk-4.0'), + dependency('glib-2.0'), + dependency('json-glib-1.0'), + dependency('gee-0.8'), + dependency('sqlite3'), + dependency('libxml-2.0'), + dependency('polkit-gobject-1'), + meson.get_compiler('vala').find_library('posix'), + meson.get_compiler('vala').find_library('linux') +] + +sources = [ + 'app.vala', + + 'data/Runnable.vala', + 'data/Game.vala', + 'data/GameSource.vala', + 'data/Emulator.vala', + + 'data/sources/steam/Steam.vala', + 'data/sources/steam/SteamGame.vala', + + 'data/sources/gog/GOG.vala', + 'data/sources/gog/GOGGame.vala', + + 'data/sources/humble/Humble.vala', + 'data/sources/humble/HumbleGame.vala', + 'data/sources/humble/Trove.vala', + + 'data/sources/user/User.vala', + 'data/sources/user/UserGame.vala', + + 'data/db/Database.vala', + 'data/db/Table.vala', + 'data/db/tables/Games.vala', + 'data/db/tables/Tags.vala', + 'data/db/tables/Merges.vala', + 'data/db/tables/Emulators.vala', + + 'data/CompatTool.vala', + 'data/compat/CustomScript.vala', + 'data/compat/Innoextract.vala', + 'data/compat/Proton.vala', + 'data/compat/Wine.vala', + 'data/compat/DOSBox.vala', + 'data/compat/ScummVM.vala', + 'data/compat/RetroArch.vala', + 'data/compat/CustomEmulator.vala', + + 'ui/windows/MainWindow.vala', + 'ui/windows/WebAuthWindow.vala', + + 'ui/dialogs/SettingsDialog/SettingsDialog.vala', + 'ui/dialogs/SettingsDialog/SettingsDialogTab.vala', + + 'ui/dialogs/SettingsDialog/tabs/UI.vala', + 'ui/dialogs/SettingsDialog/tabs/Collection.vala', + 'ui/dialogs/SettingsDialog/tabs/Steam.vala', + 'ui/dialogs/SettingsDialog/tabs/GOG.vala', + 'ui/dialogs/SettingsDialog/tabs/Humble.vala', + 'ui/dialogs/SettingsDialog/tabs/RetroArch.vala', + 'ui/dialogs/SettingsDialog/tabs/Emulators.vala', + + 'ui/dialogs/InstallDialog.vala', + 'ui/dialogs/GameDetailsDialog.vala', + 'ui/dialogs/GamePropertiesDialog.vala', + 'ui/dialogs/GameFSOverlaysDialog.vala', + 'ui/dialogs/CompatRunDialog.vala', + + 'ui/views/BaseView.vala', + 'ui/views/WelcomeView.vala', + + 'ui/views/GamesView/GamesView.vala', + 'ui/views/GamesView/GameCard.vala', + 'ui/views/GamesView/GameListRow.vala', + 'ui/views/GamesView/DownloadProgressView.vala', + 'ui/views/GamesView/FiltersPopover.vala', + 'ui/views/GamesView/AddGamePopover.vala', + 'ui/views/GamesView/GameContextMenu.vala', + + 'ui/views/GameDetailsView/GameDetailsView.vala', + 'ui/views/GameDetailsView/GameDetailsPage.vala', + + 'ui/views/GameDetailsView/GameDetailsBlock.vala', + 'ui/views/GameDetailsView/blocks/Playtime.vala', + 'ui/views/GameDetailsView/blocks/Achievements.vala', + 'ui/views/GameDetailsView/blocks/Description.vala', + 'ui/views/GameDetailsView/blocks/GOGDetails.vala', + 'ui/views/GameDetailsView/blocks/SteamDetails.vala', + + 'ui/widgets/AutoSizeImage.vala', + 'ui/widgets/ActionButton.vala', + 'ui/widgets/FileChooserEntry.vala', + 'ui/widgets/CompatToolOptions.vala', + 'ui/widgets/CompatToolPicker.vala', + 'ui/widgets/TagRow.vala', + + 'utils/Utils.vala', + 'utils/FSUtils.vala', + 'utils/FSOverlay.vala', + 'utils/Parser.vala', + 'utils/Settings.vala', + + 'utils/downloader/Downloader.vala', + 'utils/downloader/SoupDownloader.vala' +] + +gtk322 = dependency('gtk+-3.0', version: '>=3.22', required: false) +if gtk322.found() + add_global_arguments('-D', 'GTK_3_22', language: 'vala') + deps += gtk322 +else + deps += dependency('gtk+-3.0') +endif + +# not sure which version fixed Message.response_headers.get_content_range(), 2.60+ should work +soup260 = dependency('libsoup-2.4', version: '>=2.60', required: false) +if soup260.found() + add_global_arguments('-D', 'SOUP_2_60', language: 'vala') + deps += soup260 +else + deps += dependency('libsoup-2.4') +endif + +manette = dependency('manette-0.2', required: false) +if manette.found() + add_global_arguments('-D', 'MANETTE', language: 'vala') + deps += manette + deps += dependency('x11') + deps += dependency('gdk-x11-3.0') + deps += dependency('xtst') + sources += 'utils/Gamepad.vala' +endif + +executable( + meson.project_name(), + project_config, + + sources, + + icons_gresource, + css_gresource, + + dependencies: deps, + install: true +) diff --git a/src/ui/dialogs/CompatRunDialog.vala b/src/ui/dialogs/CompatRunDialog.vala new file mode 100644 index 00000000..5a67fdf7 --- /dev/null +++ b/src/ui/dialogs/CompatRunDialog.vala @@ -0,0 +1,155 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; +using GLib; +using Gee; +using GameHub.Utils; +using GameHub.UI.Widgets; + +using GameHub.Data; +using GameHub.Data.Sources.Steam; + +namespace GameHub.UI.Dialogs +{ + public class CompatRunDialog: Dialog + { + public bool is_opened_from_menu { get; construct; default = false; } + + public Runnable game { get; construct; } + public Game? emulated_game { get; construct; } + public bool launch_in_game_dir { get; construct; } + + private Box content; + private Label title_label; + private CompatToolOptions opts_list; + + private CompatToolPicker compat_tool_picker; + + public CompatRunDialog(Runnable game, bool is_opened_from_menu=false, Game? emulated_game=null, bool launch_in_game_dir=false) + { + Object(game: game, emulated_game: emulated_game, launch_in_game_dir: launch_in_game_dir, transient_for: Windows.MainWindow.instance, resizable: false, title: _("Run with compatibility layer"), is_opened_from_menu: is_opened_from_menu); + } + + construct + { + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + modal = true; + + var hbox = new Box(Orientation.HORIZONTAL, 8); + hbox.margin_start = hbox.margin_end = 5; + + content = new Box(Orientation.VERTICAL, 0); + + title_label = new Label(game.name); + title_label.margin_start = 8; + title_label.halign = Align.START; + title_label.valign = Align.START; + title_label.hexpand = true; + title_label.get_style_context().add_class(Granite.STYLE_CLASS_H2_LABEL); + content.add(title_label); + + compat_tool_picker = new CompatToolPicker(game, false); + compat_tool_picker.margin_start = 4; + content.add(compat_tool_picker); + + opts_list = new CompatToolOptions(game, compat_tool_picker, false); + + content.add(opts_list); + + if(game is Game && (game as Game).icon != null) + { + var icon = new AutoSizeImage(); + icon.set_constraint(48, 48, 1); + icon.set_size_request(48, 48); + Utils.load_image.begin(icon, (game as Game).icon, "icon"); + hbox.add(icon); + } + + hbox.add(content); + + response.connect((source, response_id) => { + switch(response_id) + { + case ResponseType.ACCEPT: + run_with_compat(); + destroy(); + break; + } + }); + + var run_btn = add_button(_("Run"), ResponseType.ACCEPT); + run_btn.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + run_btn.grab_default(); + + get_content_area().add(hbox); + get_content_area().set_size_request(340, 96); + + var tool = game.compat_tool; + + if(!is_opened_from_menu && tool != null && compat_tool_picker.selected != null && compat_tool_picker.selected.id == tool && game.compat_options_saved) + { + Idle.add(() => { + run_with_compat(); + destroy(); + return Source.REMOVE; + }); + return; + } + + show_all(); + } + + private void run_with_compat() + { + if(compat_tool_picker == null || compat_tool_picker.selected == null) return; + + RunnableIsLaunched = game.is_running = true; + + if(game is Emulator) + { + emulated_game.is_running = true; + emulated_game.update_status(); + compat_tool_picker.selected.run_emulator.begin(game as Emulator, emulated_game, launch_in_game_dir, (obj, res) => { + compat_tool_picker.selected.run_emulator.end(res); + RunnableIsLaunched = game.is_running = emulated_game.is_running = false; + emulated_game.update_status(); + }); + } + else + { + game.update_status(); + (game as Game).last_launch = get_real_time() / 1000000; + game.save(); + compat_tool_picker.selected.run.begin(game, (obj, res) => { + compat_tool_picker.selected.run.end(res); + RunnableIsLaunched = game.is_running = false; + game.update_status(); + (game as Game).playtime_tracked += ((get_real_time() / 1000000) - (game as Game).last_launch) / 60; + game.save(); + }); + } + + opts_list.save_options(); + } + } +} diff --git a/src/ui/dialogs/GameDetailsDialog.vala b/src/ui/dialogs/GameDetailsDialog.vala new file mode 100644 index 00000000..5aaf2e37 --- /dev/null +++ b/src/ui/dialogs/GameDetailsDialog.vala @@ -0,0 +1,68 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs +{ + public class GameDetailsDialog: Dialog + { + public Game? game { get; construct; } + + public GameDetailsDialog(Game? game) + { + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: game.name, game: game); + } + + construct + { + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + gravity = Gdk.Gravity.CENTER; + + var content = get_content_area(); + content.set_size_request(560, -1); + + content.add(new GameHub.UI.Views.GameDetailsView.GameDetailsView(game)); + + response.connect((source, response_id) => { + switch(response_id) + { + case ResponseType.CLOSE: + destroy(); + break; + } + }); + + get_style_context().add_class("gameinfo-background"); + var ui_settings = GameHub.Settings.UI.get_instance(); + ui_settings.notify["dark-theme"].connect(() => { + get_style_context().remove_class("dark"); + if(ui_settings.dark_theme) get_style_context().add_class("dark"); + }); + ui_settings.notify_property("dark-theme"); + + show_all(); + } + } +} diff --git a/src/ui/dialogs/GameFSOverlaysDialog.vala b/src/ui/dialogs/GameFSOverlaysDialog.vala new file mode 100644 index 00000000..a61b1f0f --- /dev/null +++ b/src/ui/dialogs/GameFSOverlaysDialog.vala @@ -0,0 +1,238 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Dialogs +{ + public class GameFSOverlaysDialog: Dialog + { + private const int RESPONSE_ENABLE_OVERLAYS = 1; + + public Game? game { get; construct; } + + private Stack stack; + + private Granite.Widgets.AlertView disabled_alert; + private Button enable_btn; + + private Box content; + private ListBox overlays_list; + private ScrolledWindow overlays_scrolled; + + private Entry id_entry; + private Entry name_entry; + private Button add_btn; + + public GameFSOverlaysDialog(Game? game) + { + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("%s: Overlays").printf(game.name), game: game); + } + + construct + { + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + gravity = Gdk.Gravity.NORTH; + + stack = new Stack(); + stack.transition_type = StackTransitionType.CROSSFADE; + + content = new Box(Orientation.VERTICAL, 0); + content.margin_start = content.margin_end = 6; + + disabled_alert = new Granite.Widgets.AlertView(_("Overlays are disabled"), _("Enable overlays to manage DLCs and mods\n\nEnabling will move game to the “base“ overlay"), "dialog-information"); + disabled_alert.get_style_context().remove_class(Gtk.STYLE_CLASS_VIEW); + + var overlays_header = new HeaderLabel(_("Overlays")); + overlays_header.xpad = 8; + content.add(overlays_header); + + overlays_list = new ListBox(); + overlays_list.get_style_context().add_class("overlays-list"); + overlays_list.selection_mode = SelectionMode.NONE; + + overlays_scrolled = new ScrolledWindow(null, null); + overlays_scrolled.vexpand = true; + #if GTK_3_22 + overlays_scrolled.propagate_natural_width = true; + overlays_scrolled.propagate_natural_height = true; + overlays_scrolled.max_content_height = 320; + #endif + overlays_scrolled.add(overlays_list); + + content.add(overlays_scrolled); + + var separator = new Separator(Orientation.HORIZONTAL); + separator.margin_bottom = 8; + content.add(separator); + + var add_box = new Box(Orientation.HORIZONTAL, 0); + add_box.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED); + + id_entry = new Entry(); + id_entry.hexpand = true; + id_entry.placeholder_text = _("Overlay ID (directory name)"); + id_entry.primary_icon_name = "list-add-symbolic"; + + name_entry = new Entry(); + name_entry.hexpand = true; + name_entry.placeholder_text = _("Overlay name (optional)"); + + add_btn = new Button.with_label(_("Add")); + add_btn.sensitive = false; + + add_box.add(id_entry); + add_box.add(name_entry); + add_box.add(add_btn); + + content.add(add_box); + + stack.add(disabled_alert); + stack.add(content); + + get_content_area().add(stack); + get_content_area().set_size_request(480, 240); + + enable_btn = add_button(_("Enable overlays"), RESPONSE_ENABLE_OVERLAYS) as Button; + enable_btn.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + enable_btn.grab_default(); + + response.connect((source, response_id) => { + switch(response_id) + { + case RESPONSE_ENABLE_OVERLAYS: + game.enable_overlays(); + break; + } + }); + + destroy.connect(() => { + game.save_overlays(); + game.mount_overlays(); + }); + + show_all(); + + game.notify["overlays-enabled"].connect(update); + + add_btn.clicked.connect(add_overlay); + + id_entry.activate.connect(() => name_entry.grab_focus()); + name_entry.activate.connect(add_overlay); + + id_entry.changed.connect(() => add_btn.sensitive = id_entry.text.strip().length > 0); + + Idle.add(() => { + update(); + return Source.REMOVE; + }); + } + + private void update() + { + game.load_overlays(); + + stack.set_visible_child(game.overlays_enabled ? (Widget) content : disabled_alert); + enable_btn.visible = !game.overlays_enabled; + + overlays_list.foreach(w => overlays_list.remove(w)); + + foreach(var overlay in game.overlays) + { + overlays_list.add(new OverlayRow(overlay)); + } + + overlays_list.show_all(); + } + + private void add_overlay() + { + var id = id_entry.text.strip(); + var name = name_entry.text.strip(); + if(name.length == 0) name = id; + if(id.length == 0) return; + game.overlays.add(new Game.Overlay(game, id, name, true)); + game.save_overlays(); + id_entry.text = name_entry.text = ""; + id_entry.grab_focus(); + } + + private class OverlayRow: ListBoxRow + { + public Game.Overlay overlay { get; construct; } + + public OverlayRow(Game.Overlay overlay) + { + Object(overlay: overlay); + } + + construct + { + var grid = new Grid(); + grid.margin_start = grid.margin_end = 8; + grid.margin_top = grid.margin_bottom = 6; + grid.column_spacing = 8; + + var name = new Label(overlay.name); + name.get_style_context().add_class("category-label"); + name.hexpand = true; + name.xalign = 0; + + var id = new Label(overlay.id); + id.hexpand = true; + id.xalign = 0; + + var open = new Button.from_icon_name("folder-symbolic", IconSize.SMALL_TOOLBAR); + open.tooltip_text = _("Open directory"); + open.valign = Align.CENTER; + open.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + var enabled = new Switch(); + enabled.active = overlay.enabled; + enabled.sensitive = overlay.id != Game.Overlay.BASE; + enabled.valign = Align.CENTER; + + grid.attach(name, 0, 0); + grid.attach(id, 0, 1); + grid.attach(open, 1, 0, 1, 2); + grid.attach(enabled, 2, 0, 1, 2); + + child = grid; + + enabled.notify["active"].connect(() => { + overlay.enabled = enabled.active; + overlay.game.save_overlays(); + }); + + open.clicked.connect(() => { + Utils.open_uri(overlay.directory.get_uri()); + }); + } + } + } +} diff --git a/src/ui/dialogs/GamePropertiesDialog.vala b/src/ui/dialogs/GamePropertiesDialog.vala new file mode 100644 index 00000000..43a83ee6 --- /dev/null +++ b/src/ui/dialogs/GamePropertiesDialog.vala @@ -0,0 +1,399 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Dialogs +{ + public class GamePropertiesDialog: Dialog + { + public Game? game { get; construct; } + + private Box content; + private ListBox tags_list; + private ScrolledWindow tags_scrolled; + private Entry new_entry; + + private Entry name_entry; + private AutoSizeImage image_view; + private AutoSizeImage icon_view; + private FileChooserEntry image_entry; + private FileChooserEntry icon_entry; + + private Box properties_box; + private Box image_search_links; + + public GamePropertiesDialog(Game? game) + { + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("%s: Properties").printf(game.name), game: game); + } + + construct + { + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + gravity = Gdk.Gravity.NORTH; + + content = new Box(Orientation.HORIZONTAL, 8); + content.margin_start = content.margin_end = 6; + + var tags_box = new Box(Orientation.VERTICAL, 0); + + var tags_header = new HeaderLabel(_("Tags")); + tags_header.xpad = 8; + tags_box.add(tags_header); + + tags_list = new ListBox(); + tags_list.get_style_context().add_class("tags-list"); + tags_list.selection_mode = SelectionMode.NONE; + + tags_list.set_sort_func((row1, row2) => { + var item1 = row1 as TagRow; + var item2 = row2 as TagRow; + + if(row1 != null && row2 != null) + { + var t1 = item1.tag.id; + var t2 = item2.tag.id; + + var b1 = t1.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + var b2 = t2.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + if(b1 && !b2) return -1; + if(!b1 && b2) return 1; + + var u1 = t1.has_prefix(Tables.Tags.Tag.USER_PREFIX); + var u2 = t2.has_prefix(Tables.Tags.Tag.USER_PREFIX); + if(u1 && !u2) return -1; + if(!u1 && u2) return 1; + + return item1.tag.name.collate(item1.tag.name); + } + + return 0; + }); + + tags_scrolled = new ScrolledWindow(null, null); + tags_scrolled.vexpand = true; + #if GTK_3_22 + tags_scrolled.propagate_natural_width = true; + tags_scrolled.propagate_natural_height = true; + tags_scrolled.max_content_height = 320; + #endif + tags_scrolled.add(tags_list); + + tags_box.add(tags_scrolled); + + new_entry = new Entry(); + new_entry.placeholder_text = _("Add tag"); + new_entry.primary_icon_name = "gh-tag-add-symbolic"; + new_entry.primary_icon_activatable = false; + new_entry.secondary_icon_name = "list-add-symbolic"; + new_entry.secondary_icon_activatable = true; + new_entry.margin = 4; + + new_entry.icon_press.connect((icon, event) => { + if(icon == EntryIconPosition.SECONDARY && ((EventButton) event).button == 1) + { + add_tag(); + } + }); + new_entry.activate.connect(add_tag); + + tags_box.add(new_entry); + + properties_box = new Box(Orientation.VERTICAL, 0); + + var name_header = new HeaderLabel(_("Name")); + name_header.xpad = 8; + properties_box.add(name_header); + + name_entry = new Entry(); + name_entry.placeholder_text = name_entry.primary_icon_tooltip_text = _("Name"); + name_entry.primary_icon_name = "insert-text-symbolic"; + name_entry.primary_icon_activatable = false; + name_entry.margin = 4; + name_entry.margin_top = 0; + properties_box.add(name_entry); + + name_entry.text = game.name; + name_entry.changed.connect(() => { + game.name = name_entry.text.strip(); + game.update_status(); + game.save(); + }); + + var images_header = new HeaderLabel(_("Images")); + images_header.xpad = 8; + properties_box.add(images_header); + + var images_card = new Frame(null); + images_card.get_style_context().add_class(Granite.STYLE_CLASS_CARD); + images_card.get_style_context().add_class("gamecard"); + images_card.get_style_context().add_class("static"); + images_card.shadow_type = ShadowType.NONE; + images_card.margin = 4; + + icon_view = new AutoSizeImage(); + icon_view.margin = 4; + icon_view.set_constraint(48, 48, 1); + icon_view.halign = Align.START; + icon_view.valign = Align.END; + + image_view = new AutoSizeImage(); + image_view.hexpand = false; + image_view.set_constraint(360, 400, 0.467f); + + var actions = new Box(Orientation.VERTICAL, 0); + actions.get_style_context().add_class("actions"); + actions.hexpand = true; + actions.vexpand = false; + + var images_overlay = new Overlay(); + images_overlay.add(image_view); + images_overlay.add_overlay(actions); + images_overlay.add_overlay(icon_view); + + images_card.add(images_overlay); + properties_box.add(images_card); + + image_entry = add_image_entry(_("Image URL"), "image-x-generic"); + + properties_box.add(image_entry); + + icon_entry = add_image_entry(_("Icon URL"), "image-x-generic-symbolic"); + icon_entry.margin_top = 0; + + properties_box.add(icon_entry); + + image_search_links = new Box(Orientation.HORIZONTAL, 8); + image_search_links.margin = 8; + + var image_search_links_label = new Label(_("Search images:")); + image_search_links_label.halign = Align.START; + image_search_links_label.xalign = 0; + image_search_links_label.hexpand = true; + image_search_links.add(image_search_links_label); + + add_image_search_link("SteamGridDB", @"http://www.steamgriddb.com/game/$(game.name)"); + add_image_search_link("Jinx's SGVI", @"http://steam.cryotank.net/?s=$(game.name)"); + add_image_search_link("Google", @"https://www.google.com/search?tbm=isch&tbs=isz:ex,iszw:460,iszh:215&q=$(game.name)"); + + properties_box.add(image_search_links); + + Utils.load_image.begin(image_view, game.image, "image"); + Utils.load_image.begin(icon_view, game.icon, "icon"); + + var space = new Box(Orientation.VERTICAL, 0); + space.vexpand = true; + properties_box.add(space); + + if(!(game is Data.Sources.Steam.SteamGame) && game.install_dir != null && game.install_dir.query_exists()) + { + var executable_header = new HeaderLabel(_("Executable")); + executable_header.xpad = 8; + properties_box.add(executable_header); + + var executable_picker = new FileChooserEntry(_("Select executable"), FileChooserAction.OPEN, "application-x-executable", _("Executable"), false, true); + try + { + executable_picker.select_file(game.executable); + } + catch(Error e) + { + warning(e.message); + } + executable_picker.margin_start = executable_picker.margin_end = 4; + properties_box.add(executable_picker); + + executable_picker.file_set.connect(() => { + game.set_chosen_executable(executable_picker.file); + }); + + var args_entry = new Entry(); + args_entry.text = game.arguments ?? ""; + args_entry.placeholder_text = args_entry.primary_icon_tooltip_text = _("Arguments"); + args_entry.primary_icon_name = "utilities-terminal-symbolic"; + args_entry.primary_icon_activatable = false; + args_entry.margin = 4; + + args_entry.changed.connect(() => { + game.arguments = args_entry.text.strip(); + game.update_status(); + game.save(); + }); + + properties_box.add(args_entry); + + var compat_header = new HeaderLabel(_("Compatibility")); + compat_header.no_show_all = true; + compat_header.xpad = 8; + properties_box.add(compat_header); + + var compat_force_switch = add_switch(_("Force compatibility mode"), game.force_compat, f => { game.force_compat = f; }); + compat_force_switch.no_show_all = true; + + var compat_tool = new CompatToolPicker(game, false); + compat_tool.no_show_all = true; + compat_tool.margin_start = compat_tool.margin_end = 4; + properties_box.add(compat_tool); + + game.notify["use-compat"].connect(() => { + compat_force_switch.visible = !game.needs_compat; + compat_tool.visible = game.use_compat; + compat_header.visible = compat_force_switch.visible || compat_tool.visible; + game.update_status(); + }); + game.notify_property("use-compat"); + } + + content.add(tags_box); + content.add(new Separator(Orientation.VERTICAL)); + content.add(properties_box); + + get_content_area().add(content); + get_content_area().set_size_request(640, -1); + + delete_event.connect(() => { + image_entry.activate(); + icon_entry.activate(); + set_image_url(true); + set_icon_url(true); + game.save(); + destroy(); + }); + + Tables.Tags.instance.tags_updated.connect(update); + + update(); + + show_all(); + } + + private void update() + { + tags_list.foreach(w => w.destroy()); + + foreach(var tag in Tables.Tags.TAGS) + { + if(tag in Tables.Tags.DYNAMIC_TAGS || !tag.enabled) continue; + var row = new TagRow(tag, game); + tags_list.add(row); + } + + tags_list.show_all(); + } + + private void add_tag() + { + var name = new_entry.text.strip(); + if(name.length == 0) return; + + new_entry.text = ""; + + var tag = new Tables.Tags.Tag.from_name(name); + Tables.Tags.add(tag); + game.add_tag(tag); + update(); + } + + private void set_image_url(bool replace=false) + { + var url = image_entry.uri; + if(url == null || url.length == 0) url = game.image; + if(replace) + { + game.image = url; + } + else + { + Utils.load_image.begin(image_view, url, "image"); + } + } + + private void set_icon_url(bool replace=false) + { + var url = icon_entry.uri; + if(url == null || url.length == 0) url = game.icon; + if(replace) + { + game.icon = url; + } + else + { + Utils.load_image.begin(icon_view, url, "icon"); + } + } + + private FileChooserEntry add_image_entry(string text, string icon) + { + var entry = new FileChooserEntry(text, FileChooserAction.OPEN, icon, text, true); + entry.margin = 4; + + var filter = new FileFilter(); + filter.add_mime_type("image/*"); + entry.chooser.set_filter(filter); + + entry.uri_set.connect(() => { set_image_url(false); set_icon_url(false); }); + + return entry; + } + + private void add_image_search_link(string text, string url) + { + var link = new LinkButton.with_label(url, text); + link.halign = Align.START; + link.margin = 0; + image_search_links.add(link); + } + + private Box add_switch(string text, bool enabled, owned SwitchAction action) + { + var sw = new Switch(); + sw.active = enabled; + sw.halign = Align.END; + sw.notify["active"].connect(() => { action(sw.active); }); + + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.margin = 4; + hbox.margin_start = 8; + + hbox.add(label); + hbox.add(sw); + + hbox.show_all(); + + properties_box.add(hbox); + return hbox; + } + + protected delegate void SwitchAction(bool active); + } +} diff --git a/src/ui/dialogs/InstallDialog.vala b/src/ui/dialogs/InstallDialog.vala new file mode 100644 index 00000000..232ad1fd --- /dev/null +++ b/src/ui/dialogs/InstallDialog.vala @@ -0,0 +1,311 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using GLib; +using Gee; +using GameHub.Utils; +using GameHub.UI.Widgets; + +using GameHub.Data; +using GameHub.Data.Sources.GOG; +using GameHub.Data.Sources.Humble; + +namespace GameHub.UI.Dialogs +{ + public class InstallDialog: Dialog + { + private const int RESPONSE_IMPORT = 123; + + public signal void import(); + public signal void install(Runnable.Installer installer, bool dl_only, CompatTool? tool); + public signal void cancelled(); + + private Box content; + private Label title_label; + private Label subtitle_label; + + private ListBox installers_list; + + private bool is_finished = false; + + private CompatToolPicker compat_tool_picker; + private CompatToolOptions opts_list; + + public InstallDialog(Runnable runnable, ArrayList installers) + { + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("Install")); + + Game? game = null; + + if(runnable is Game) + { + game = runnable as Game; + } + + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + modal = true; + + var hbox = new Box(Orientation.HORIZONTAL, 8); + hbox.margin_start = hbox.margin_end = 5; + + content = new Box(Orientation.VERTICAL, 0); + + title_label = new Label(null); + title_label.margin_start = title_label.margin_end = 4; + title_label.halign = Align.START; + title_label.xalign = 0; + title_label.wrap = true; + title_label.max_width_chars = 36; + title_label.get_style_context().add_class(Granite.STYLE_CLASS_H2_LABEL); + + subtitle_label = new Label(null); + subtitle_label.margin_start = subtitle_label.margin_end = 4; + subtitle_label.halign = Align.START; + subtitle_label.hexpand = true; + + if(game != null && game.icon != null) + { + var icon = new AutoSizeImage(); + icon.set_constraint(48, 48, 1); + icon.set_size_request(48, 48); + Utils.load_image.begin(icon, game.icon, "icon"); + hbox.add(icon); + title_label.margin_start = title_label.margin_end = 8; + subtitle_label.margin_start = subtitle_label.margin_end = 8; + } + + hbox.add(content); + + content.add(title_label); + content.add(subtitle_label); + + title_label.label = runnable.name; + + installers_list = new ListBox(); + installers_list.margin_top = 4; + installers_list.get_style_context().add_class("installers-list"); + + installers_list.set_sort_func((row1, row2) => { + var item1 = row1 as InstallerRow; + var item2 = row2 as InstallerRow; + if(item1 != null && item2 != null) + { + var i1 = item1.installer; + var i2 = item2.installer; + + if(i1.platform.id() == CurrentPlatform.id() && i2.platform.id() != CurrentPlatform.id()) return -1; + if(i1.platform.id() != CurrentPlatform.id() && i2.platform.id() == CurrentPlatform.id()) return 1; + + return i1.name.collate(i2.name); + } + return 0; + }); + + var sys_langs = Intl.get_language_names(); + + var compatible_installers = new ArrayList(); + + foreach(var installer in installers) + { + if(installer.platform.id() != CurrentPlatform.id() && !Settings.UI.get_instance().show_unsupported_games && !Settings.UI.get_instance().use_compat) continue; + + compatible_installers.add(installer); + var row = new InstallerRow(runnable, installer); + installers_list.add(row); + + if(installer is GOGGame.Installer && (installer as GOGGame.Installer).lang in sys_langs) + { + installers_list.select_row(row); + } + else if(installers_list.get_selected_row() == null && installer.platform.id() == CurrentPlatform.id()) + { + installers_list.select_row(row); + } + } + + if(compatible_installers.size > 1) + { + subtitle_label.label = _("Select installer"); + content.add(installers_list); + } + else + { + subtitle_label.label = _("Installer size: %s").printf(fsize(compatible_installers[0].full_size)); + } + + Revealer? compat_tool_revealer = null; + + if(Settings.UI.get_instance().show_unsupported_games || Settings.UI.get_instance().use_compat) + { + compat_tool_revealer = new Revealer(); + + var compat_tool_box = new Box(Orientation.VERTICAL, 4); + + compat_tool_picker = new CompatToolPicker(runnable, true); + compat_tool_picker.margin_start = game != null && game.icon != null ? 4 : 0; + compat_tool_picker.margin_top = 8; + + compat_tool_box.add(compat_tool_picker); + compat_tool_revealer.add(compat_tool_box); + + if(compatible_installers.size > 1) + { + compat_tool_revealer.reveal_child = false; + + installers_list.row_selected.connect(r => { + var row = r as InstallerRow; + if(row == null) + { + compat_tool_revealer.reveal_child = false; + } + else + { + compat_tool_revealer.reveal_child = row.installer.platform == Platform.WINDOWS; + } + }); + } + else + { + compat_tool_revealer.reveal_child = !runnable.is_supported(null, false) && runnable.is_supported(null, true); + } + + content.add(compat_tool_revealer); + + opts_list = new CompatToolOptions(runnable, compat_tool_picker, true); + compat_tool_box.add(opts_list); + } + + var import_btn = add_button(_("Import"), InstallDialog.RESPONSE_IMPORT); + + var install_btn = add_button(_("Install"), ResponseType.ACCEPT); + install_btn.get_style_context().add_class(STYLE_CLASS_SUGGESTED_ACTION); + install_btn.grab_default(); + + var dl_only_check = new CheckButton.with_label(_("Download only")); + dl_only_check.get_style_context().add_class("default-padding"); + dl_only_check.margin_start = 5; + dl_only_check.valign = Align.CENTER; + + var bbox = install_btn.get_parent() as ButtonBox; + if(bbox != null) + { + bbox.add(dl_only_check); + bbox.set_child_secondary(dl_only_check, true); + bbox.set_child_non_homogeneous(dl_only_check, true); + } + + if(game is GameHub.Data.Sources.User.UserGame || runnable is GameHub.Data.Emulator) + { + subtitle_label.no_show_all = true; + subtitle_label.visible = false; + dl_only_check.no_show_all = true; + dl_only_check.visible = false; + import_btn.no_show_all = true; + import_btn.visible = false; + compat_tool_revealer.reveal_child = true; + } + + if(compat_tool_revealer != null) + { + dl_only_check.toggled.connect(() => { + compat_tool_revealer.sensitive = !dl_only_check.active; + }); + } + + response.connect((source, response_id) => { + switch(response_id) + { + case ResponseType.CLOSE: + destroy(); + break; + + case InstallDialog.RESPONSE_IMPORT: + is_finished = true; + import(); + destroy(); + break; + + case ResponseType.ACCEPT: + var installer = compatible_installers[0]; + if(compatible_installers.size > 1) + { + var row = installers_list.get_selected_row() as InstallerRow; + installer = row.installer; + } + is_finished = true; + if(opts_list != null) + { + opts_list.save_options(); + } + install(installer, dl_only_check.active, compat_tool_picker != null ? compat_tool_picker.selected : null); + destroy(); + break; + } + }); + + destroy.connect(() => { if(!is_finished) cancelled(); }); + + get_content_area().add(hbox); + get_content_area().set_size_request(380, 96); + show_all(); + } + + public static string fsize(int64 size) + { + if(size > 0) + { + return format_size(size); + } + return _("Unknown"); + } + + private class InstallerRow: ListBoxRow + { + public Runnable runnable; + public Runnable.Installer installer; + + public InstallerRow(Runnable runnable, Runnable.Installer installer) + { + this.runnable = runnable; + this.installer = installer; + + var box = new Box(Orientation.HORIZONTAL, 8); + box.margin_start = box.margin_end = 8; + box.margin_top = box.margin_bottom = 4; + + var icon = new Image.from_icon_name(installer.platform.icon(), IconSize.BUTTON); + + var name = new Label(installer.name); + name.hexpand = true; + name.halign = Align.START; + + var size = new Label(fsize(installer.full_size)); + size.halign = Align.END; + + box.add(icon); + box.add(name); + box.add(size); + child = box; + } + } + } +} diff --git a/src/ui/dialogs/SettingsDialog/SettingsDialog.vala b/src/ui/dialogs/SettingsDialog/SettingsDialog.vala new file mode 100644 index 00000000..d0d2ae50 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/SettingsDialog.vala @@ -0,0 +1,116 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog +{ + public class SettingsDialog: Dialog + { + private static bool restart_msg_shown = false; + + private InfoBar restart_msg; + + private Stack tabs; + + private string default_tab; + + public SettingsDialog(string tab="ui") + { + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("Settings")); + default_tab = tab; + } + + construct + { + get_style_context().add_class("rounded"); + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + gravity = Gdk.Gravity.NORTH; + modal = true; + + var content = get_content_area(); + content.set_size_request(480, -1); + + restart_msg = new InfoBar(); + restart_msg.get_style_context().add_class(Gtk.STYLE_CLASS_FRAME); + restart_msg.get_content_area().add(new Label(_("Some settings will be applied after application restart"))); + restart_msg.message_type = MessageType.WARNING; + restart_msg.margin_bottom = 8; + update_restart_message(); + + tabs = new Stack(); + tabs.homogeneous = false; + tabs.interpolate_size = true; + + var tabs_switcher = new StackSwitcher(); + tabs_switcher.stack = tabs; + tabs_switcher.halign = Align.CENTER; + tabs_switcher.margin_bottom = 16; + + add_tab("ui", new Tabs.UI(this), _("Interface")); + add_tab("collection", new Tabs.Collection(this), _("Collection")); + add_tab("gs/steam", new Tabs.Steam(this), "Steam", "source-steam-symbolic"); + add_tab("gs/gog", new Tabs.GOG(this), "GOG", "source-gog-symbolic"); + add_tab("gs/humble", new Tabs.Humble(this), "Humble Bundle", "source-humble-symbolic"); + add_tab("emu/retroarch", new Tabs.RetroArch(this), "RetroArch", "emu-retroarch-symbolic"); + add_tab("emu/custom", new Tabs.Emulators(this), _("Emulators")); + + content.pack_start(restart_msg, false, false, 0); + content.pack_start(tabs_switcher, false, false, 0); + content.pack_start(tabs, false, false, 0); + + response.connect((source, response_id) => { + switch(response_id) + { + case ResponseType.CLOSE: + destroy(); + break; + } + }); + + show_all(); + + tabs.visible_child_name = default_tab; + } + + private void add_tab(string id, SettingsDialogTab tab, string title, string? icon=null) + { + tabs.add_titled(tab, id, title); + tabs.child_set_property(tab, "icon-name", icon); + } + + public void show_restart_message() + { + restart_msg_shown = true; + update_restart_message(); + } + + private void update_restart_message() + { + #if GTK_3_22 + restart_msg.revealed = restart_msg_shown; + #else + restart_msg.visible = restart_msg_shown; + restart_msg.no_show_all = !restart_msg_shown; + #endif + } + } +} diff --git a/src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala b/src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala new file mode 100644 index 00000000..98916777 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/SettingsDialogTab.vala @@ -0,0 +1,240 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; + +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Dialogs.SettingsDialog +{ + public abstract class SettingsDialogTab: Box + { + public SettingsDialog dialog { construct; protected get; } + + public SettingsDialogTab(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + margin_start = margin_end = 8; + } + + protected Box add_switch(string text, bool enabled, owned SwitchAction action) + { + var sw = new Switch(); + sw.active = enabled; + sw.halign = Align.END; + sw.notify["active"].connect(() => { action(sw.active); }); + + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(sw); + return add_widget(hbox); + } + + protected Box add_entry(string text, string val, owned EntryAction action, string? icon=null) + { + var entry = new Entry(); + entry.text = val; + entry.notify["text"].connect(() => { action(entry.text); }); + entry.set_size_request(280, -1); + + entry.primary_icon_name = icon; + + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(entry); + return add_widget(hbox); + } + + protected Box add_file_chooser(string text, FileChooserAction mode, string val, owned EntryAction action, bool create=true, string? icon=null, bool allow_url=false, bool allow_executable=false) + { + var chooser = new FileChooserEntry(text, mode, icon, null, allow_url, allow_executable); + chooser.chooser.create_folders = create; + chooser.chooser.show_hidden = true; + chooser.select_file(FSUtils.file(val)); + chooser.tooltip_text = chooser.file.get_path(); + chooser.file_set.connect(() => { chooser.tooltip_text = chooser.file != null ? chooser.file.get_path() : null; action(chooser.tooltip_text); }); + chooser.set_size_request(280, -1); + + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(chooser); + return add_widget(hbox); + } + + protected Label add_label(string text) + { + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + return add_widget(label); + } + + protected Box add_labels(string text, string text2) + { + var label = new Label(text); + label.max_width_chars = 44; + label.xalign = 0; + label.wrap = true; + label.halign = Align.START; + label.hexpand = true; + + var label2 = new Label(text2); + label2.xalign = 0; + label2.wrap = true; + label2.set_size_request(280, -1); + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(label2); + return add_widget(hbox); + } + + protected HeaderLabel add_header(string text) + { + var label = new HeaderLabel(text); + label.xpad = 4; + label.halign = Align.START; + label.hexpand = true; + return add_widget(label); + } + + protected CheckButton add_header_with_checkbox(string text, bool enabled, owned SwitchAction action) + { + var cb = new CheckButton.with_label(text); + cb.active = enabled; + cb.halign = Align.START; + cb.hexpand = true; + cb.notify["active"].connect(() => { action(cb.active); }); + cb.get_style_context().add_class(Granite.STYLE_CLASS_H4_LABEL); + return add_widget(cb); + } + + protected LinkButton add_link(string text, string uri) + { + var link = new LinkButton.with_label(uri, text); + link.halign = Align.START; + link.hexpand = true; + return add_widget(link); + } + + protected Box add_labeled_link(string label_text, string text, string uri) + { + var label = new Label(label_text); + label.max_width_chars = 44; + label.xalign = 0; + label.wrap = true; + label.halign = Align.START; + label.hexpand = true; + + var link = new LinkButton.with_label(uri, text); + link.halign = Align.END; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(link); + return add_widget(hbox); + } + + protected Box add_cache_directory(string name, string path) + { + var bbox = new Box(Orientation.HORIZONTAL, 2); + bbox.set_size_request(280, -1); + + var size_label = new Label(null); + size_label.margin_end = 8; + size_label.halign = Align.START; + + var open_btn = new Button(); + open_btn.label = _("Open"); + open_btn.clicked.connect(() => { + Utils.open_uri(FSUtils.file(path).get_uri()); + }); + + var clear_btn = new Button(); + clear_btn.get_style_context().add_class(STYLE_CLASS_DESTRUCTIVE_ACTION); + clear_btn.label = _("Clear"); + + var label = new Label(name); + label.halign = Align.START; + label.hexpand = true; + + bbox.pack_start(size_label); + bbox.pack_start(open_btn, false); + bbox.pack_start(clear_btn, false); + + SourceFunc calc_size = () => { + try + { + uint64 dir_size; + uint64 files; + FSUtils.file(path).measure_disk_usage(FileMeasureFlags.NONE, null, null, out dir_size, null, out files); + size_label.label = ngettext("%llu installer; %s", "%llu installers; %s", (ulong) files).printf(files, format_size(dir_size)); + clear_btn.sensitive = dir_size > 32; + } + catch(Error e){} + return false; + }; + + calc_size(); + + clear_btn.clicked.connect(() => { + FSUtils.rm(path, "*"); + calc_size(); + }); + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(bbox); + return add_widget(hbox); + } + + protected Separator add_separator() + { + return add_widget(new Separator(Orientation.HORIZONTAL)); + } + + protected T add_widget(T widget) + { + if(!(widget is HeaderLabel)) (widget as Widget).margin = 4; + add(widget as Widget); + return widget; + } + + public delegate void SwitchAction(bool active); + public delegate void EntryAction(string val); + public delegate void ButtonAction(); + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/Collection.vala b/src/ui/dialogs/SettingsDialog/tabs/Collection.vala new file mode 100644 index 00000000..d1480bea --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/Collection.vala @@ -0,0 +1,90 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; + +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class Collection: SettingsDialogTab + { + private FileChooserEntry collection_root; + + private Entry gog_game_dir; + private Entry gog_installers; + private Entry gog_dlc; + private Entry gog_bonus; + + private Entry humble_game_dir; + private Entry humble_installers; + + public Collection(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var collection = FSUtils.Paths.Collection.get_instance(); + var gog = FSUtils.Paths.Collection.GOG.get_instance(); + var humble = FSUtils.Paths.Collection.Humble.get_instance(); + + collection_root = add_file_chooser(_("Collection directory"), FileChooserAction.SELECT_FOLDER, collection.root, v => { collection.root = v; update_hints(); }).get_children().last().data as FileChooserEntry; + + add_separator(); + + add_header("GOG"); + gog_game_dir = add_entry(_("Game directory") + " ($game_dir)", gog.game_dir, v => { gog.game_dir = v; update_hints(); }, "source-gog-symbolic").get_children().last().data as Entry; + gog_installers = add_entry(_("Installers"), gog.installers, v => { gog.installers = v; update_hints(); }, "source-gog-symbolic").get_children().last().data as Entry; + gog_dlc = add_entry(_("DLC"), gog.dlc, v => { gog.dlc = v; update_hints(); }, "folder-download-symbolic").get_children().last().data as Entry; + gog_bonus = add_entry(_("Bonus content"), gog.bonus, v => { gog.bonus = v; update_hints(); }, "folder-music-symbolic").get_children().last().data as Entry; + + add_separator(); + + add_header("Humble Bundle"); + humble_game_dir = add_entry(_("Game directory") + " ($game_dir)", humble.game_dir, v => { humble.game_dir = v; update_hints(); }, "source-humble-symbolic").get_children().last().data as Entry; + humble_installers = add_entry(_("Installers"), humble.installers, v => { humble.installers = v; update_hints(); }, "source-humble-symbolic").get_children().last().data as Entry; + + add_separator(); + + add_header(_("Variables")).sensitive = false; + add_labels("• $root", _("Collection directory")).sensitive = false; + add_labels("• $game", _("Game name")).sensitive = false; + add_labels("• $game_dir", _("Game directory")).sensitive = false; + + update_hints(); + } + + private void update_hints() + { + var game = "VVVVVV"; + + gog_game_dir.tooltip_text = FSUtils.Paths.Collection.GOG.expand_game_dir(game); + gog_installers.tooltip_text = FSUtils.Paths.Collection.GOG.expand_installers(game); + gog_dlc.tooltip_text = FSUtils.Paths.Collection.GOG.expand_dlc(game); + gog_bonus.tooltip_text = FSUtils.Paths.Collection.GOG.expand_bonus(game); + + humble_game_dir.tooltip_text = FSUtils.Paths.Collection.Humble.expand_game_dir(game); + humble_installers.tooltip_text = FSUtils.Paths.Collection.Humble.expand_installers(game); + } + + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/Emulators.vala b/src/ui/dialogs/SettingsDialog/tabs/Emulators.vala new file mode 100644 index 00000000..3cb2e9cd --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/Emulators.vala @@ -0,0 +1,437 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; + +using GameHub.Data; +using GameHub.Utils; + +using GameHub.UI.Widgets; + +using GameHub.Data.DB; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class Emulators: SettingsDialogTab + { + private Stack stack; + private Button add_btn; + private Button remove_btn; + + private EmulatorPage? previous_page; + + public Emulators(SettingsDialog dlg) + { + Object(orientation: Orientation.HORIZONTAL, dialog: dlg); + } + + construct + { + margin_start = margin_end = 0; + + var paths = FSUtils.Paths.Settings.get_instance(); + + stack = new Stack(); + stack.margin_start = stack.margin_end = 8; + stack.expand = true; + stack.set_size_request(360, 240); + + var sidebar_box = new Box(Orientation.VERTICAL, 0); + sidebar_box.vexpand = true; + + var sidebar = new StackSidebar(); + sidebar.stack = stack; + sidebar.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + sidebar.vexpand = true; + sidebar.set_size_request(128, -1); + + var actionbar = new ActionBar(); + actionbar.vexpand = false; + actionbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR); + + add_btn = new Button.from_icon_name("list-add-symbolic", IconSize.MENU); + add_btn.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + remove_btn = new Button.from_icon_name("list-remove-symbolic", IconSize.MENU); + remove_btn.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + var actions = new Box(Orientation.HORIZONTAL, 0); + actions.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED); + + actions.add(add_btn); + actions.add(remove_btn); + + actionbar.pack_start(actions); + + sidebar_box.add(sidebar); + sidebar_box.add(actionbar); + + add(sidebar_box); + add(new Separator(Orientation.VERTICAL)); + add(stack); + + stack.notify["visible-child"].connect(() => { + var page = stack.visible_child as EmulatorPage; + if(previous_page != null && previous_page != page) + { + previous_page.save(); + } + previous_page = page; + }); + + dialog.destroy.connect(() => { + if(previous_page != null) + { + previous_page.save(); + } + }); + + add_btn.clicked.connect(() => { + add_emu_page(); + }); + + remove_btn.clicked.connect(() => { + remove_emu_page(); + }); + + var emulators = Tables.Emulators.get_all(); + foreach(var emu in emulators) + { + add_emu_page(emu); + } + } + + private void add_emu_page(Emulator? emulator=null) + { + var page = new EmulatorPage(stack, emulator); + var id = emulator != null ? "emu/" + emulator.id : stack.get_children().length().to_string(); + stack.add_titled(page, id, emulator != null ? emulator.name : ""); + page.show_all(); + if(emulator == null) + { + stack.set_visible_child(page); + } + page.emulator.removed.connect(() => { + stack.remove(page); + remove_btn.sensitive = stack.get_children().length() > 0; + }); + remove_btn.sensitive = stack.get_children().length() > 0; + } + + private void remove_emu_page() + { + var page = stack.visible_child as EmulatorPage; + if(page != null) + { + page.remove(); + } + } + + private class EmulatorPage: Grid + { + private string _title; + public string title + { + get + { + return _title; + } + set + { + _title = value.strip(); + if(parent == stack) + { + stack.child_set(this, title: _title); + } + } + } + public Stack stack { get; construct; } + public Emulator emulator { get; construct set; } + + private int rows = 0; + + private Granite.Widgets.ModeButton mode; + + private new Entry name; + private FileChooserEntry emudir; + private FileChooserEntry executable; + private Label executable_label; + private Entry arguments; + private Label arguments_label; + + private Button run_btn; + private Button save_btn; + + public EmulatorPage(Stack stack, Emulator? emulator=null) + { + Object(orientation: Orientation.VERTICAL, stack: stack, emulator: emulator ?? new Emulator.empty()); + } + + construct + { + row_spacing = 4; + column_spacing = 8; + + mode = new Granite.Widgets.ModeButton(); + mode.margin_bottom = 8; + mode.halign = Align.CENTER; + mode.append_text(_("Executable")); + mode.append_text(_("Installer")); + mode.selected = 0; + attach(mode, 0, rows, 2, 1); + rows++; + + save_btn = new Button.with_label(_("Save")); + save_btn.halign = Align.END; + save_btn.valign = Align.END; + save_btn.margin_top = 4; + save_btn.sensitive = false; + + run_btn = new Button.with_label(_("Run")); + run_btn.halign = Align.START; + run_btn.valign = Align.END; + run_btn.margin_top = 4; + run_btn.sensitive = false; + + name = add_entry(_("Name"), "insert-text-symbolic", true); + + name.text = emulator.name ?? ""; + + name.changed.connect(() => { + title = name.text.strip(); + Tables.Emulators.remove(emulator); + emulator.name = title; + }); + + name.changed(); + + add_separator(); + + executable = add_filechooser(_("Executable"), _("Select executable"), FileChooserAction.OPEN, true, out executable_label); + + arguments = add_entry(_("Arguments"), "utilities-terminal-symbolic", false, out arguments_label); + + arguments.text = emulator.arguments ?? "$file $game_args"; + + arguments.changed.connect(() => { + emulator.arguments = arguments.text.strip(); + }); + + arguments.changed(); + + add_separator(); + + emudir = add_filechooser(_("Directory"), _("Select emulator directory"), FileChooserAction.SELECT_FOLDER, true); + + executable.file_set.connect(() => { + emulator.executable = executable.file; + if(name.text.strip().length == 0 && executable.file != null) + { + name.text = executable.file.get_basename(); + } + update(); + }); + + if(emulator.install_dir != null && emulator.install_dir.query_exists()) + { + try + { + emudir.select_file(emulator.install_dir); + } + catch(Error e) + { + warning(e.message); + } + } + + if(emulator.executable != null && emulator.executable.query_exists()) + { + try + { + executable.select_file(emulator.executable); + } + catch(Error e) + { + warning(e.message); + } + } + + add_separator(); + + var compat_force_switch = add_switch(_("Force compatibility mode"), emulator.force_compat, f => { emulator.force_compat = f; }); + compat_force_switch.no_show_all = true; + + var compat_tool = new CompatToolPicker(emulator, false); + compat_tool.no_show_all = true; + attach(compat_tool, 0, rows, 2, 1); + rows++; + + emulator.notify["use-compat"].connect(() => { + compat_force_switch.visible = !emulator.needs_compat; + compat_tool.visible = emulator.use_compat; + }); + + var btn_box = new Box(Orientation.HORIZONTAL, 0); + btn_box.expand = true; + + btn_box.pack_start(run_btn); + btn_box.pack_end(save_btn); + + attach(btn_box, 0, rows, 2, 1); + rows++; + + run_btn.clicked.connect(run); + save_btn.clicked.connect(save); + + mode.mode_changed.connect(update); + + update(); + } + + private void update() + { + if(mode.selected == 0 && executable.file != null && emudir.file == null) + { + emudir.select_file(executable.file.get_parent()); + } + + emulator.name = title; + emulator.arguments = arguments.text.strip(); + + emulator.install_dir = emudir.file; + + executable_label.label = mode.selected == 0 ? _("Executable") : _("Installer"); + arguments.sensitive = arguments_label.sensitive = mode.selected == 0; + + run_btn.sensitive = emulator.name.length > 0 && executable.file != null && mode.selected == 0 && emudir.file != null; + save_btn.sensitive = emulator.name.length > 0 && executable.file != null && ((mode.selected == 0 && emudir.file != null) || mode.selected == 1); + + emulator.notify_property("use-compat"); + } + + public void save() + { + update(); + + if(mode.selected == 1 && executable.file != null && emudir.file != null) + { + sensitive = false; + + emulator.installer = new Emulator.Installer(emulator, emulator.executable); + + emulator.executable = null; + emulator.install.begin((obj, res) => { + emulator.install.end(res); + sensitive = true; + mode.selected = 0; + executable.select_file(emulator.executable); + emulator.save(); + }); + + return; + } + + emulator.save(); + } + + public void run() + { + save(); + emulator.run_game.begin(null); + } + + public new void remove() + { + emulator.remove(); + } + + private Entry add_entry(string text, string icon, bool required=true, out Label label=null) + { + label = new Label(text); + label.halign = Align.START; + label.xalign = 1; + label.margin = 4; + label.hexpand = true; + if(required) + { + label.get_style_context().add_class("category-label"); + } + var entry = new Entry(); + entry.primary_icon_name = icon; + entry.primary_icon_activatable = false; + entry.set_size_request(180, -1); + attach(label, 0, rows); + attach(entry, 1, rows); + rows++; + return entry; + } + + private FileChooserEntry add_filechooser(string text, string title, FileChooserAction action=FileChooserAction.OPEN, bool required=true, out Label label=null) + { + label = new Label(text); + label.halign = Align.START; + label.xalign = 1; + label.margin = 4; + label.hexpand = true; + if(required) + { + label.get_style_context().add_class("category-label"); + } + var entry = new FileChooserEntry(title, action, null, null, false, action == FileChooserAction.OPEN); + entry.set_size_request(180, -1); + attach(label, 0, rows); + attach(entry, 1, rows); + rows++; + return entry; + } + + private void add_separator() + { + var separator = new Separator(Orientation.HORIZONTAL); + separator.margin_top = separator.margin_bottom = 4; + attach(separator, 0, rows, 2, 1); + rows++; + } + + private Box add_switch(string text, bool enabled, owned SettingsDialogTab.SwitchAction action) + { + var sw = new Switch(); + sw.active = enabled; + sw.halign = Align.END; + sw.notify["active"].connect(() => { action(sw.active); }); + + var label = new Label(text); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.margin_start = 4; + + hbox.add(label); + hbox.add(sw); + + hbox.show_all(); + + attach(hbox, 0, rows, 2, 1); + rows++; + return hbox; + } + } + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/GOG.vala b/src/ui/dialogs/SettingsDialog/tabs/GOG.vala new file mode 100644 index 00000000..686c0830 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/GOG.vala @@ -0,0 +1,76 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class GOG: SettingsDialogTab + { + private Settings.Auth.GOG gog_auth; + private Box enabled_box; + private Button logout_btn; + + public GOG(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var paths = FSUtils.Paths.Settings.get_instance(); + + gog_auth = Settings.Auth.GOG.get_instance(); + + enabled_box = add_switch(_("Enabled"), gog_auth.enabled, v => { gog_auth.enabled = v; update(); dialog.show_restart_message(); }); + + add_separator(); + + add_file_chooser(_("Games directory"), FileChooserAction.SELECT_FOLDER, paths.gog_games, v => { paths.gog_games = v; dialog.show_restart_message(); }); + + //add_cache_directory(_("Installers cache"), FSUtils.Paths.GOG.Installers); + + add_separator(); + + logout_btn = new Button.with_label(_("Logout")); + logout_btn.halign = Align.END; + add_widget(logout_btn); + + logout_btn.clicked.connect(() => { + gog_auth.authenticated = false; + gog_auth.access_token = ""; + gog_auth.refresh_token = ""; + update(); + dialog.show_restart_message(); + }); + + update(); + } + + private void update() + { + this.foreach(w => { + if(w != enabled_box) w.sensitive = gog_auth.enabled; + }); + logout_btn.sensitive = gog_auth.enabled && gog_auth.authenticated && gog_auth.access_token.length > 0; + } + + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/Humble.vala b/src/ui/dialogs/SettingsDialog/tabs/Humble.vala new file mode 100644 index 00000000..ecbcaaff --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/Humble.vala @@ -0,0 +1,78 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class Humble: SettingsDialogTab + { + private Settings.Auth.Humble humble_auth; + private Box enabled_box; + private Button logout_btn; + + public Humble(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var paths = FSUtils.Paths.Settings.get_instance(); + + humble_auth = Settings.Auth.Humble.get_instance(); + + enabled_box = add_switch(_("Enabled"), humble_auth.enabled, v => { humble_auth.enabled = v; update(); dialog.show_restart_message(); }); + + add_separator(); + + add_switch(_("Load games from Humble Trove"), humble_auth.load_trove_games, v => { humble_auth.load_trove_games = v; update(); dialog.show_restart_message(); }); + + add_separator(); + add_file_chooser(_("Games directory"), FileChooserAction.SELECT_FOLDER, paths.humble_games, v => { paths.humble_games = v; dialog.show_restart_message(); }); + + //add_cache_directory(_("Installers cache"), FSUtils.Paths.Humble.Installers); + + add_separator(); + + logout_btn = new Button.with_label(_("Logout")); + logout_btn.halign = Align.END; + add_widget(logout_btn); + + logout_btn.clicked.connect(() => { + humble_auth.authenticated = false; + humble_auth.access_token = ""; + update(); + dialog.show_restart_message(); + }); + + update(); + } + + private void update() + { + this.foreach(w => { + if(w != enabled_box) w.sensitive = humble_auth.enabled; + }); + logout_btn.sensitive = humble_auth.enabled && humble_auth.authenticated && humble_auth.access_token.length > 0; + } + + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala b/src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala new file mode 100644 index 00000000..a8858bc7 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/RetroArch.vala @@ -0,0 +1,41 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class RetroArch: SettingsDialogTab + { + public RetroArch(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var paths = FSUtils.Paths.Settings.get_instance(); + + add_file_chooser(_("Libretro core directory"), FileChooserAction.SELECT_FOLDER, paths.libretro_core_dir, v => { paths.libretro_core_dir = v; dialog.show_restart_message(); }); + add_file_chooser(_("Libretro core info directory"), FileChooserAction.SELECT_FOLDER, paths.libretro_core_info_dir, v => { paths.libretro_core_info_dir = v; dialog.show_restart_message(); }); + } + + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/Steam.vala b/src/ui/dialogs/SettingsDialog/tabs/Steam.vala new file mode 100644 index 00000000..e1a6f77b --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/Steam.vala @@ -0,0 +1,95 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class Steam: SettingsDialogTab + { + private Settings.Auth.Steam steam_auth; + private Box enabled_box; + + public Steam(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var paths = FSUtils.Paths.Settings.get_instance(); + + steam_auth = Settings.Auth.Steam.get_instance(); + + enabled_box = add_switch(_("Enabled"), steam_auth.enabled, v => { steam_auth.enabled = v; update(); dialog.show_restart_message(); }); + + add_separator(); + + add_steam_apikey_entry(); + add_labeled_link(_("Steam API keys have limited number of uses per day"), _("Generate key"), "steam://openurl/https://steamcommunity.com/dev/apikey"); + + add_separator(); + add_file_chooser(_("Installation directory"), FileChooserAction.SELECT_FOLDER, paths.steam_home, v => { paths.steam_home = v; dialog.show_restart_message(); }, false); + + update(); + } + + private void update() + { + this.foreach(w => { + if(w != enabled_box) w.sensitive = steam_auth.enabled; + }); + } + + protected void add_steam_apikey_entry() + { + var steam_auth = Settings.Auth.Steam.get_instance(); + + var entry = new Entry(); + entry.placeholder_text = _("Default"); + entry.max_length = 32; + if(steam_auth.api_key != steam_auth.schema.get_default_value("api-key").get_string()) + { + entry.text = steam_auth.api_key; + } + entry.primary_icon_name = "source-steam-symbolic"; + entry.secondary_icon_name = "edit-delete-symbolic"; + entry.secondary_icon_tooltip_text = _("Restore default API key"); + entry.set_size_request(280, -1); + + entry.notify["text"].connect(() => { steam_auth.api_key = entry.text; dialog.show_restart_message(); }); + entry.icon_press.connect((pos, e) => { + if(pos == EntryIconPosition.SECONDARY) + { + entry.text = ""; + } + }); + + var label = new Label(_("Steam API key")); + label.halign = Align.START; + label.hexpand = true; + + var hbox = new Box(Orientation.HORIZONTAL, 12); + hbox.add(label); + hbox.add(entry); + add_widget(hbox); + } + } +} diff --git a/src/ui/dialogs/SettingsDialog/tabs/UI.vala b/src/ui/dialogs/SettingsDialog/tabs/UI.vala new file mode 100644 index 00000000..93c32d75 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/tabs/UI.vala @@ -0,0 +1,53 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Granite; +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Tabs +{ + public class UI: SettingsDialogTab + { + public UI(SettingsDialog dlg) + { + Object(orientation: Orientation.VERTICAL, dialog: dlg); + } + + construct + { + var ui = Settings.UI.get_instance(); + + add_switch(_("Use dark theme"), ui.dark_theme, v => { ui.dark_theme = v; }); + add_switch(_("Compact list"), ui.compact_list, v => { ui.compact_list = v; }); + + add_separator(); + + add_switch(_("Merge games from different sources"), ui.merge_games, v => { ui.merge_games = v; dialog.show_restart_message(); }); + + add_separator(); + + add_switch(_("Show non-native games"), ui.show_unsupported_games, v => { ui.show_unsupported_games = v; }); + add_switch(_("Use compatibility layers and consider Windows games compatible"), ui.use_compat, v => { ui.use_compat = v; }); + + add_separator(); + + add_switch(_("Use imported tags"), ui.use_imported_tags, v => { ui.use_imported_tags = v; }); + } + } +} diff --git a/src/ui/views/BaseView.vala b/src/ui/views/BaseView.vala index 2ba5b6cb..bcb4a1c2 100644 --- a/src/ui/views/BaseView.vala +++ b/src/ui/views/BaseView.vala @@ -1,36 +1,54 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using Granite; using GameHub.UI.Windows; namespace GameHub.UI.Views { - public abstract class BaseView: Gtk.Grid + public abstract class BaseView: Grid { protected MainWindow window; protected HeaderBar titlebar; - + construct { titlebar = new HeaderBar(); titlebar.title = "GameHub"; titlebar.show_close_button = true; } - + public virtual void attach_to_window(MainWindow wnd) { window = wnd; show(); } - + public virtual void on_show() { titlebar.show_all(); window.set_titlebar(titlebar); } - + public virtual void on_window_focus() { - + } } } diff --git a/src/ui/views/GameDetailsView/GameDetailsBlock.vala b/src/ui/views/GameDetailsView/GameDetailsBlock.vala new file mode 100644 index 00000000..8f062514 --- /dev/null +++ b/src/ui/views/GameDetailsView/GameDetailsBlock.vala @@ -0,0 +1,75 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; + +namespace GameHub.UI.Views.GameDetailsView +{ + public abstract class GameDetailsBlock: Box + { + public Game game { get; construct; } + + public bool is_dialog { get; construct; } + + public GameDetailsBlock(Game game, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, is_dialog: is_dialog); + } + + public abstract bool supports_game { get; } + + protected void add_info_label(string title, string? text, bool multiline=true, bool markup=false, Container? parent=null) + { + if(text == null || text == "") return; + + var title_label = new Granite.HeaderLabel(title); + title_label.set_size_request(multiline ? -1 : 128, -1); + title_label.valign = Align.START; + + var text_label = new Label(text); + text_label.halign = Align.START; + text_label.hexpand = false; + text_label.wrap = true; + text_label.xalign = 0; + text_label.max_width_chars = is_dialog ? 60 : -1; + text_label.use_markup = markup; + + if(!multiline) + { + text_label.get_style_context().add_class("gameinfo-singleline-value"); + } + + if(parent != null) + { + text_label.hexpand = true; + //text_label.halign = Align.END; + //text_label.xalign = 1; + } + + var box = new Box(multiline ? Orientation.VERTICAL : Orientation.HORIZONTAL, multiline ? 0 : 16); + box.margin_start = box.margin_end = 8; + box.add(title_label); + box.add(text_label); + (parent ?? this).add(box); + } + } +} diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala new file mode 100644 index 00000000..70b27862 --- /dev/null +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -0,0 +1,446 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Utils; +using GameHub.UI.Widgets; +using GameHub.UI.Views.GamesView; +using WebKit; + +namespace GameHub.UI.Views.GameDetailsView +{ + public class GameDetailsPage: Grid + { + public Game game { get; construct; } + + public GameDetailsView details_view { get; construct; } + + public GameDetailsPage(Game game, GameDetailsView parent) + { + Object(game: game, details_view: parent); + } + + private bool is_dialog = false; + private bool is_updated = false; + + private Stack stack; + private Spinner spinner; + + private ScrolledWindow content_scrolled; + public Box content; + private Box actions; + + private Label title; + private Label status; + private ProgressBar download_progress; + private AutoSizeImage icon; + private Image src_icon; + + private Box platform_icons; + + private Downloader.Download? download; + + private Button action_pause; + private Button action_resume; + private Button action_cancel; + + private ActionButton action_install; + private ActionButton action_run; + private ActionButton action_run_with_compat; + private ActionButton action_properties; + private ActionButton action_open_directory; + private ActionButton action_open_installer_collection_directory; + private ActionButton action_open_bonus_collection_directory; + private ActionButton action_open_store_page; + private ActionButton action_uninstall; + + private Box blocks; + + construct + { + stack = new Stack(); + stack.transition_type = StackTransitionType.NONE; + stack.vexpand = true; + + spinner = new Spinner(); + spinner.active = true; + spinner.set_size_request(36, 36); + spinner.halign = Align.CENTER; + spinner.valign = Align.CENTER; + + content_scrolled = new ScrolledWindow(null, null); + #if GTK_3_22 + content_scrolled.propagate_natural_width = true; + content_scrolled.propagate_natural_height = true; + #endif + + content = new Box(Orientation.VERTICAL, 0); + content.margin_start = content.margin_end = 8; + + var title_hbox_eventbox = new EventBox(); + + var title_overlay = new Overlay(); + title_overlay.margin_start = title_overlay.margin_end = 7; + + var title_icons = new Box(Orientation.HORIZONTAL, 15); + title_icons.valign = Align.END; + title_icons.halign = Align.END; + + var title_hbox = new Box(Orientation.HORIZONTAL, 15); + + icon = new AutoSizeImage(); + icon.set_constraint(48, 48, 1); + icon.set_size_request(48, 48); + + title = new Label(null); + title.halign = Align.START; + title.wrap = true; + title.xalign = 0; + title.hexpand = true; + title.get_style_context().add_class(Granite.STYLE_CLASS_H2_LABEL); + + status = new Label(null); + status.halign = Align.START; + status.hexpand = true; + + download_progress = new ProgressBar(); + download_progress.hexpand = true; + download_progress.fraction = 0d; + download_progress.get_style_context().add_class(Gtk.STYLE_CLASS_OSD); + download_progress.hide(); + + src_icon = new Image(); + src_icon.icon_size = IconSize.DIALOG; + src_icon.opacity = 0.1; + + platform_icons = new Box(Orientation.HORIZONTAL, 15); + + var title_vbox = new Box(Orientation.VERTICAL, 0); + var vbox_labels = new Box(Orientation.VERTICAL, 0); + vbox_labels.hexpand = true; + + var hbox_inner = new Box(Orientation.HORIZONTAL, 8); + var hbox_actions = new Box(Orientation.HORIZONTAL, 0); + hbox_actions.vexpand = false; + hbox_actions.valign = Align.CENTER; + + action_pause = new Button.from_icon_name("media-playback-pause-symbolic"); + action_pause.set_size_request(36, 36); + action_pause.tooltip_text = _("Pause download"); + action_pause.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_pause.visible = false; + + action_resume = new Button.from_icon_name("media-playback-start-symbolic"); + action_resume.set_size_request(36, 36); + action_resume.tooltip_text = _("Resume download"); + action_resume.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_resume.visible = false; + + action_cancel = new Button.from_icon_name("process-stop-symbolic"); + action_cancel.set_size_request(36, 36); + action_cancel.tooltip_text = _("Cancel download"); + action_cancel.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_cancel.visible = false; + + vbox_labels.add(title); + vbox_labels.add(status); + + hbox_inner.add(vbox_labels); + hbox_inner.add(hbox_actions); + + hbox_actions.add(action_pause); + hbox_actions.add(action_resume); + hbox_actions.add(action_cancel); + + title_vbox.add(hbox_inner); + title_vbox.add(download_progress); + + title_hbox.add(icon); + title_hbox.add(title_vbox); + + title_icons.add(platform_icons); + title_icons.add(src_icon); + + title_overlay.add(title_hbox); + title_overlay.add_overlay(title_icons); + + title_hbox_eventbox.add(title_overlay); + + content.add(title_hbox_eventbox); + + blocks = new Box(Orientation.VERTICAL, 0); + blocks.hexpand = false; + + actions = new Box(Orientation.HORIZONTAL, 0); + actions.margin_top = actions.margin_bottom = 16; + + content.add(actions); + content.add(blocks); + + content_scrolled.add(content); + + stack.add(spinner); + stack.add(content_scrolled); + + stack.set_visible_child(spinner); + + add(stack); + + action_install = add_action("go-down", null, _("Install"), install_game, true); + action_run = add_action("media-playback-start", null, _("Run"), run_game, true); + action_run_with_compat = add_action("media-playback-start", "platform-windows-symbolic", _("Run with compatibility layer"), run_game_with_compat, true); + action_open_directory = add_action("folder", null, _("Open installation directory"), open_game_directory); + action_open_installer_collection_directory = add_action("folder-download", null, _("Open installers collection directory"), open_installer_collection_directory); + action_open_bonus_collection_directory = add_action("folder-documents", null, _("Open bonus collection directory"), open_bonus_collection_directory); + action_open_store_page = add_action("web-browser", null, _("Open store page"), open_game_store_page); + action_uninstall = add_action("edit-delete", null, (game is Sources.User.UserGame) ? _("Remove") : _("Uninstall"), uninstall_game); + action_properties = add_action("system-run", null, _("Game properties"), game_properties); + + action_cancel.clicked.connect(() => { + if(download != null) download.cancel(); + }); + + action_pause.clicked.connect(() => { + if(download != null && download is Downloader.PausableDownload) + { + ((Downloader.PausableDownload) download).pause(); + } + }); + + action_resume.clicked.connect(() => { + if(download != null && download is Downloader.PausableDownload) + { + ((Downloader.PausableDownload) download).resume(); + } + }); + + title_hbox_eventbox.add_events(EventMask.BUTTON_RELEASE_MASK); + title_hbox_eventbox.button_release_event.connect(e => { + switch(e.button) + { + case 3: + open_context_menu(e, true); + break; + } + return true; + }); + } + + public void update() + { + update_game.begin(); + } + + private async void update_game() + { + is_dialog = !(get_toplevel() is GameHub.UI.Windows.MainWindow); + + title.max_width_chars = is_dialog ? 36 : -1; + + #if GTK_3_22 + content_scrolled.max_content_height = is_dialog ? 640 : -1; + #endif + + if(is_updated) return; + + stack.set_visible_child(spinner); + + if(game == null) return; + + yield game.update_game_info(); + + is_updated = true; + + title.label = game.name; + src_icon.icon_name = game.source.icon; + + platform_icons.foreach(w => platform_icons.remove(w)); + foreach(var p in game.platforms) + { + var icon = new Image(); + icon.icon_name = p.icon(); + icon.icon_size = IconSize.DIALOG; + icon.opacity = 0.1; + platform_icons.add(icon); + } + platform_icons.show_all(); + + blocks.foreach(b => blocks.remove(b)); + + GameDetailsBlock[] blk = { new Blocks.Playtime(game, is_dialog), new Blocks.Achievements(game, is_dialog), new Blocks.GOGDetails(game, this, is_dialog), new Blocks.SteamDetails(game, is_dialog), new Blocks.Description(game, is_dialog) }; + foreach(var b in blk) + { + if(b.supports_game) + { + blocks.add(b); + } + } + blocks.show_all(); + + game.status_change.connect(s => { + status.label = s.description; + download_progress.hide(); + if(s.state == Game.State.DOWNLOADING && s.download != null) + { + download = s.download; + var ds = download.status.state; + + download_progress.show(); + download_progress.fraction = s.download.status.progress; + + action_cancel.visible = true; + action_cancel.sensitive = ds == Downloader.DownloadState.DOWNLOADING || ds == Downloader.DownloadState.PAUSED; + action_pause.visible = download is Downloader.PausableDownload && ds != Downloader.DownloadState.PAUSED; + action_resume.visible = download is Downloader.PausableDownload && ds == Downloader.DownloadState.PAUSED; + } + else + { + action_cancel.visible = false; + action_pause.visible = false; + action_resume.visible = false; + } + action_install.visible = s.state != Game.State.INSTALLED; + action_install.sensitive = s.state == Game.State.UNINSTALLED && game.is_installable; + action_run_with_compat.visible = s.state == Game.State.INSTALLED && game.use_compat; + action_run_with_compat.sensitive = Settings.UI.get_instance().use_compat; + action_run.visible = s.state == Game.State.INSTALLED && !action_run_with_compat.visible; + action_open_directory.visible = s.state == Game.State.INSTALLED && game.install_dir != null && game.install_dir.query_exists(); + action_open_installer_collection_directory.visible = game.installers_dir != null && game.installers_dir.query_exists(); + action_open_bonus_collection_directory.visible = game is GameHub.Data.Sources.GOG.GOGGame && (game as GameHub.Data.Sources.GOG.GOGGame).bonus_content_dir != null && (game as GameHub.Data.Sources.GOG.GOGGame).bonus_content_dir.query_exists(); + action_open_store_page.visible = game.store_page != null; + action_uninstall.visible = s.state == Game.State.INSTALLED && !(game is GameHub.Data.Sources.GOG.GOGGame.DLC); + action_properties.visible = !(game is GameHub.Data.Sources.GOG.GOGGame.DLC); + + if(action_run_with_compat.visible && game.compat_tool != null) + { + foreach(var tool in CompatTools) + { + if(tool.id == game.compat_tool) + { + action_run_with_compat.icon_overlay = tool.icon; + break; + } + } + } + }); + game.status_change(game.status); + + yield Utils.load_image(icon, game.icon, "icon"); + + stack.set_visible_child(content_scrolled); + } + + private void install_game() + { + if(_game != null && game.status.state == Game.State.UNINSTALLED) + { + game.install.begin(); + } + } + + private void game_properties() + { + if(_game != null) + { + new Dialogs.GamePropertiesDialog(game).show_all(); + } + } + + private void open_game_directory() + { + if(_game != null && game.status.state == Game.State.INSTALLED) + { + Utils.open_uri(game.install_dir.get_uri()); + } + } + + private void open_installer_collection_directory() + { + if(_game != null && game.installers_dir != null && game.installers_dir.query_exists()) + { + Utils.open_uri(game.installers_dir.get_uri()); + } + } + + private void open_bonus_collection_directory() + { + if(_game != null && game is GameHub.Data.Sources.GOG.GOGGame) + { + var gog_game = game as GameHub.Data.Sources.GOG.GOGGame; + if(gog_game != null && gog_game.bonus_content_dir != null && gog_game.bonus_content_dir.query_exists()) + { + Utils.open_uri(gog_game.bonus_content_dir.get_uri()); + } + } + } + + private void open_game_store_page() + { + if(_game != null && game.store_page != null) + { + Utils.open_uri(game.store_page); + } + } + + private void run_game() + { + if(_game != null && game.status.state == Game.State.INSTALLED) + { + game.run.begin(); + } + } + + private void run_game_with_compat() + { + if(_game != null && game.status.state == Game.State.INSTALLED) + { + game.run_with_compat.begin(false); + } + } + + private void uninstall_game() + { + if(_game != null && game.status.state == Game.State.INSTALLED) + { + game.uninstall.begin(); + } + } + + private void open_context_menu(Event e, bool at_pointer=true) + { + if(_game != null) + { + new GameContextMenu(game, this).open(e, at_pointer); + } + } + + private delegate void Action(); + private ActionButton add_action(string icon, string? icon_overlay, string title, Action action, bool primary=false) + { + var button = new ActionButton(icon, icon_overlay, title, primary); + button.hexpand = primary; + actions.add(button); + button.clicked.connect(() => action()); + return button; + } + } +} diff --git a/src/ui/views/GameDetailsView/GameDetailsView.vala b/src/ui/views/GameDetailsView/GameDetailsView.vala new file mode 100644 index 00000000..5a1a4c9f --- /dev/null +++ b/src/ui/views/GameDetailsView/GameDetailsView.vala @@ -0,0 +1,201 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; +using WebKit; + +namespace GameHub.UI.Views.GameDetailsView +{ + public class GameDetailsView: BaseView + { + private Game? _game; + + public GameSource? preferred_source { get; set; } + + public int content_margin = 8; + + public Game? game + { + get { return _game; } + set + { + _game = value; + navigation.clear(); + navigation.add(game); + Idle.add(update); + } + } + + public GameDetailsView(Game? game=null) + { + Object(game: game); + } + + private Stack stack; + + private Button back_button; + private StackSwitcher stack_switcher; + + private Revealer actions; + + private ArrayList navigation = new ArrayList(Game.is_equal); + + construct + { + stack = new Stack(); + stack.transition_type = StackTransitionType.SLIDE_LEFT_RIGHT; + stack.expand = true; + + stack_switcher = new StackSwitcher(); + stack_switcher.valign = Align.CENTER; + stack_switcher.halign = Align.CENTER; + stack_switcher.expand = false; + stack_switcher.visible = false; + stack_switcher.stack = stack; + + back_button = new Button.with_label(""); + back_button.tooltip_text = _("Back"); + back_button.valign = Align.CENTER; + back_button.expand = false; + back_button.visible = false; + back_button.get_style_context().add_class(Granite.STYLE_CLASS_BACK_BUTTON); + + back_button.clicked.connect(() => { + if(navigation.size > 1) + { + navigation.remove_at(navigation.size - 1); + } + update(); + }); + + actions = new Revealer(); + actions.transition_type = RevealerTransitionType.SLIDE_DOWN; + actions.reveal_child = false; + + var actionbar = new ActionBar(); + actionbar.get_style_context().add_class("gameinfo-toolbar"); + actionbar.pack_start(back_button); + actionbar.set_center_widget(stack_switcher); + + actions.add(actionbar); + + attach(actions, 0, 0); + attach(stack, 0, 1); + + stack.notify["visible-child"].connect(() => { + var page = stack.visible_child as GameDetailsPage; + if(page != null) page.update(); + }); + + get_style_context().add_class("gameinfo-background"); + var ui_settings = GameHub.Settings.UI.get_instance(); + ui_settings.notify["dark-theme"].connect(() => { + get_style_context().remove_class("dark"); + if(ui_settings.dark_theme) get_style_context().add_class("dark"); + }); + ui_settings.notify_property("dark-theme"); + + notify["preferred-source"].connect(() => { + if(preferred_source != null) + { + var name = preferred_source.id; + if(stack.get_child_by_name(name) != null) + { + stack.set_visible_child_full(name, StackTransitionType.NONE); + } + } + }); + + Idle.add(update); + } + + public void navigate(Game g) + { + navigation.add(g); + + Idle.add(update); + } + + private bool update() + { + stack.foreach(p => stack.remove(p)); + + back_button.visible = false; + if(navigation.size > 1) + { + back_button.visible = true; + back_button.label = navigation.get(navigation.size - 2).name; + } + + var g = navigation.get(navigation.size - 1); + + if(g == null) return Source.REMOVE; + + var merges = Settings.UI.get_instance().merge_games ? Tables.Merges.get(game) : null; + bool merged = merges != null && merges.size > 0; + + stack_switcher.visible = merged; + + add_page(g); + + if(merged) + { + foreach(var m in merges) + { + if(Game.is_equal(g, m) + || (!Settings.UI.get_instance().show_unsupported_games && !m.is_supported(null, Settings.UI.get_instance().use_compat)) + || (g is Sources.GOG.GOGGame.DLC && Game.is_equal((g as Sources.GOG.GOGGame.DLC).game, m))) + { + continue; + } + + add_page(m); + } + } + + stack_switcher.visible = stack.get_children().length() > 1; + + actions.reveal_child = back_button.visible || stack_switcher.visible; + + stack.show_all(); + + Idle.add(() => { + notify_property("preferred-source"); + return Source.REMOVE; + }); + + return Source.REMOVE; + } + + private void add_page(Game g) + { + if(stack.get_child_by_name(g.source.id) != null) return; + + var page = new GameDetailsPage(g, this); + page.content.margin = content_margin; + stack.add_titled(page, g.source.id, g.source.name); + } + } +} diff --git a/src/ui/views/GameDetailsView/blocks/Achievements.vala b/src/ui/views/GameDetailsView/blocks/Achievements.vala new file mode 100644 index 00000000..f1a910cf --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/Achievements.vala @@ -0,0 +1,120 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; + +using GameHub.Utils; + +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class Achievements: GameDetailsBlock + { + private const int IMAGE_SIZE = 32; + + public Achievements(Game game, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, is_dialog: is_dialog); + } + + construct + { + if(!supports_game) return; + + var revealer = new Revealer(); + revealer.transition_type = RevealerTransitionType.SLIDE_DOWN; + revealer.reveal_child = false; + + var vbox = new Box(Orientation.VERTICAL, 0); + + vbox.add(new Separator(Orientation.HORIZONTAL)); + + var header = new Granite.HeaderLabel(_("Achievements")); + header.margin_start = header.margin_end = 7; + header.get_style_context().add_class("description-header"); + + var achievements_scrolled = new ScrolledWindow(null, null); + achievements_scrolled.hscrollbar_policy = PolicyType.AUTOMATIC; + achievements_scrolled.vscrollbar_policy = PolicyType.NEVER; + + var achievements_box = new Box(Orientation.HORIZONTAL, 4); + achievements_box.margin_top = 8; + achievements_box.margin_start = achievements_box.margin_end = 7; + achievements_box.margin_bottom = 12; + + achievements_scrolled.add(achievements_box); + + vbox.add(header); + vbox.add(achievements_scrolled); + + revealer.add(vbox); + + add(revealer); + + game.load_achievements.begin((obj, res) => { + game.load_achievements.end(res); + + revealer.set_reveal_child(game.achievements != null && game.achievements.size > 0); + + if(!revealer.reveal_child) return; + + achievements_box.foreach(a => achievements_box.remove(a)); + + foreach(var achievement in game.achievements) + { + var image = new AutoSizeImage(); + image.corner_radius = IMAGE_SIZE / 2; + image.set_constraint(IMAGE_SIZE, IMAGE_SIZE, 1); + image.set_size_request(IMAGE_SIZE, IMAGE_SIZE); + image.opacity = achievement.unlocked ? 1 : 0.2; + + image.tooltip_markup = """%s""".printf(achievement.name) + "\n"; + + if(achievement.description.length > 0) + { + image.tooltip_markup += """%s""".printf(achievement.description) + "\n"; + } + + if(achievement.unlocked) + { + image.tooltip_markup += "\n" + """%s""".printf(_("Unlocked: %s").printf(achievement.unlock_time)); + } + + if(achievement.global_percentage > 0) + { + image.tooltip_markup += "\n" + """%s""".printf(_("Global percentage: %g%%").printf(achievement.global_percentage)); + } + + Utils.load_image.begin(image, achievement.image, @"achievement_$(game.source.id)_$(game.id)"); + achievements_box.add(image); + } + achievements_box.show_all(); + }); + } + + public override bool supports_game { get { return game is SteamGame || game is GOGGame; } } + } +} diff --git a/src/ui/views/GameDetailsView/blocks/Description.vala b/src/ui/views/GameDetailsView/blocks/Description.vala new file mode 100644 index 00000000..19b805fd --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/Description.vala @@ -0,0 +1,82 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using WebKit; + +using GameHub.Data; +using GameHub.Data.Sources.Humble; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class Description: GameDetailsBlock + { + private Granite.HeaderLabel description_header; + private WebView description; + + private const string CSS_LIGHT = "background: rgb(245, 245, 245); color: rgb(66, 66, 66)"; + private const string CSS_DARK = "background: rgb(59, 63, 69); color: white"; + + public Description(Game game, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, is_dialog: is_dialog); + } + + construct + { + if(!supports_game) return; + + add(new Separator(Orientation.HORIZONTAL)); + + description_header = new Granite.HeaderLabel(_("Description")); + description_header.margin_start = description_header.margin_end = 7; + description_header.get_style_context().add_class("description-header"); + + description = new WebView(); + description.hexpand = true; + description.vexpand = false; + description.sensitive = false; + description.get_settings().hardware_acceleration_policy = HardwareAccelerationPolicy.NEVER; + + var ui_settings = GameHub.Settings.UI.get_instance(); + ui_settings.notify["dark-theme"].connect(() => { + description.user_content_manager.remove_all_style_sheets(); + var style = ui_settings.dark_theme ? CSS_DARK : CSS_LIGHT; + description.user_content_manager.add_style_sheet(new UserStyleSheet(@"body{overflow: hidden; font-size: 0.8em; margin: 7px; line-height: 1.4; $(style)} h1,h2,h3{line-height: 1.2;} ul{padding: 4px 0 4px 16px;} img{max-width: 100%; display: block;}", UserContentInjectedFrames.TOP_FRAME, UserStyleLevel.USER, null, null)); + }); + ui_settings.notify_property("dark-theme"); + + description.set_size_request(-1, -1); + var desc = game.description + ""; + description.load_html(desc, null); + description.notify["title"].connect(e => { + description.set_size_request(-1, -1); + var height = int.parse(description.title); + description.set_size_request(-1, height); + }); + + add(description_header); + add(description); + } + + public override bool supports_game { get { return game.description != null; } } + } +} diff --git a/src/ui/views/GameDetailsView/blocks/GOGDetails.vala b/src/ui/views/GameDetailsView/blocks/GOGDetails.vala new file mode 100644 index 00000000..39ad376f --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/GOGDetails.vala @@ -0,0 +1,256 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.Sources.GOG; + +using GameHub.UI.Views.GamesView; + +using GameHub.Utils; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class GOGDetails: GameDetailsBlock + { + public GameDetailsPage details_page { get; construct; } + + public GOGDetails(Game game, GameDetailsPage page, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, details_page: page, is_dialog: is_dialog); + } + + construct + { + if(!supports_game) return; + + add(new Separator(Orientation.HORIZONTAL)); + + var gog_game = game as GOGGame; + + var root = Parser.parse_json(game.info_detailed); + + if(root == null || gog_game == null) return; + + var sys_langs = Intl.get_language_names(); + var langs = root.get_object().get_object_member("languages"); + if(langs != null) + { + var langs_string = ""; + foreach(var l in langs.get_members()) + { + var lang = langs.get_string_member(l); + if(l in sys_langs) lang = @"$(lang)"; + langs_string += (langs_string.length > 0 ? ", " : "") + lang; + } + var langs_label = _("Language"); + if(langs_string.contains(",")) + { + langs_label = _("Languages"); + } + add_info_label(langs_label, langs_string, false, true); + } + + var dlbox = new Box(Orientation.HORIZONTAL, 0); + + var downloads_visible = false; + + if(gog_game.bonus_content != null && gog_game.bonus_content.size > 0) + { + var bonusbox = new Box(Orientation.VERTICAL, 0); + var bonuslist = new ListBox(); + bonuslist.selection_mode = SelectionMode.NONE; + bonuslist.get_style_context().add_class("gameinfo-content-list"); + + foreach(var bonus in gog_game.bonus_content) + { + bonuslist.add(new BonusContentRow(bonus)); + } + + var header = new Granite.HeaderLabel(_("Bonus content")); + header.margin_start = header.margin_end = 8; + + downloads_visible = true; + bonusbox.add(header); + bonusbox.add(bonuslist); + + dlbox.add(bonusbox); + } + + if(gog_game.dlc != null && gog_game.dlc.size > 0) + { + if(downloads_visible) + { + dlbox.add(new Separator(Orientation.VERTICAL)); + } + + var dlcbox = new Box(Orientation.VERTICAL, 0); + var dlclist = new ListBox(); + dlclist.selection_mode = SelectionMode.NONE; + dlclist.get_style_context().add_class("gameinfo-content-list"); + + foreach(var dlc in gog_game.dlc) + { + dlclist.add(new DLCRow(dlc, details_page)); + } + + var header = new Granite.HeaderLabel(_("DLC")); + header.margin_start = header.margin_end = 8; + + downloads_visible = true; + dlcbox.add(header); + dlcbox.add(dlclist); + + dlbox.add(dlcbox); + } + + if(downloads_visible) + { + add(new Separator(Orientation.HORIZONTAL)); + } + + add(dlbox); + } + + public override bool supports_game { get { return (game is GOGGame) && game.info_detailed != null && game.info_detailed.length > 0; } } + + public class BonusContentRow: ListBoxRow + { + public GOGGame.BonusContent bonus; + + public BonusContentRow(GOGGame.BonusContent bonus) + { + this.bonus = bonus; + + var content = new Overlay(); + + var progress_bar = new Frame(null); + progress_bar.halign = Align.START; + progress_bar.vexpand = true; + progress_bar.get_style_context().add_class("progress"); + + var box = new Box(Orientation.HORIZONTAL, 8); + box.margin_start = box.margin_end = 8; + box.margin_top = box.margin_bottom = 4; + + var icon = new Image.from_icon_name(bonus.icon, IconSize.BUTTON); + + var name = new Label(bonus.text); + name.ellipsize = Pango.EllipsizeMode.END; + name.hexpand = true; + name.halign = Align.START; + name.xalign = 0; + + var size = new Label(format_size(bonus.size)); + size.halign = Align.END; + + box.add(icon); + box.add(name); + box.add(size); + + var event_box = new Box(Orientation.VERTICAL, 0); + event_box.expand = true; + + content.add(box); + content.add_overlay(progress_bar); + content.add_overlay(event_box); + + bonus.status_change.connect(s => { + switch(s.state) + { + case GOGGame.BonusContent.State.DOWNLOADING: + Allocation alloc; + content.get_allocation(out alloc); + if(s.download != null) + { + progress_bar.get_style_context().add_class("downloading"); + progress_bar.set_size_request((int) (s.download.status.progress * alloc.width), alloc.height); + } + break; + + default: + progress_bar.get_style_context().remove_class("downloading"); + progress_bar.set_size_request(0, 0); + break; + } + }); + bonus.status_change(bonus.status); + + content.add_events(EventMask.ALL_EVENTS_MASK); + content.button_release_event.connect(e => { + if(e.button == 1) + { + if(bonus.status.state == GOGGame.BonusContent.State.NOT_DOWNLOADED) + { + bonus.download.begin(); + } + else if(bonus.status.state == GOGGame.BonusContent.State.DOWNLOADED) + { + bonus.open(); + } + } + return true; + }); + + child = content; + } + } + + public class DLCRow: ListBoxRow + { + public GOGGame.DLC dlc; + + public DLCRow(GOGGame.DLC dlc, GameDetailsPage details_page) + { + this.dlc = dlc; + + var box = new EventBox(); + box.margin_start = box.margin_end = 8; + box.margin_top = box.margin_bottom = 4; + + var name = new Label(dlc.name); + name.ellipsize = Pango.EllipsizeMode.END; + name.hexpand = true; + name.halign = Align.START; + name.xalign = 0; + + box.add_events(EventMask.BUTTON_RELEASE_MASK); + box.button_release_event.connect(e => { + switch(e.button) + { + case 1: + details_page.details_view.navigate(dlc); + break; + + case 3: + new GameContextMenu(dlc, this).open(e, true); + break; + } + return true; + }); + + box.add(name); + child = box; + } + } + } +} diff --git a/src/ui/views/GameDetailsView/blocks/Playtime.vala b/src/ui/views/GameDetailsView/blocks/Playtime.vala new file mode 100644 index 00000000..11a516a5 --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/Playtime.vala @@ -0,0 +1,87 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.GOG; + +using GameHub.Utils; + +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class Playtime: GameDetailsBlock + { + public Playtime(Game game, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, is_dialog: is_dialog); + } + + construct + { + if(!supports_game) return; + + var hbox = new Box(Orientation.HORIZONTAL, 0); + + var header = new Granite.HeaderLabel(_("Playtime")); + + var add_separator = false; + + if(game.playtime_tracked > 0) + { + add_info_label(_("Playtime (local)"), minutes_to_string(game.playtime_tracked), false, false, hbox); + add_separator = true; + } + + if(game.playtime_source > 0) + { + if(add_separator) hbox.add(new Separator(Orientation.VERTICAL)); + add_separator = true; + add_info_label(_("Playtime"), minutes_to_string(game.playtime_source), false, false, hbox); + } + + if(game.last_launch > 0) + { + var date = new GLib.DateTime.from_unix_local(game.last_launch); + if(date != null) + { + if(add_separator) hbox.add(new Separator(Orientation.VERTICAL)); + add_info_label(_("Last launch"), Granite.DateTime.get_relative_datetime(date), false, false, hbox); + } + } + + add(new Separator(Orientation.HORIZONTAL)); + add(hbox); + } + + private string minutes_to_string(int64 min) + { + int h = (int) min / 60; + int m = (int) min - (h * 60); + return (h > 0 ? C_("time", "%dh").printf(h) + " " : "") + C_("time", "%dm").printf(m); + } + + public override bool supports_game { get { return game.playtime_source > 0 || game.playtime_tracked > 0 || game.last_launch > 0; } } + } +} diff --git a/src/ui/views/GameDetailsView/blocks/SteamDetails.vala b/src/ui/views/GameDetailsView/blocks/SteamDetails.vala new file mode 100644 index 00000000..ea550299 --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/SteamDetails.vala @@ -0,0 +1,101 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.Sources.Steam; + +using GameHub.Utils; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class SteamDetails: GameDetailsBlock + { + public SteamDetails(Game game, bool is_dialog) + { + Object(game: game, orientation: Orientation.VERTICAL, is_dialog: is_dialog); + } + + construct + { + if(!supports_game) return; + + add(new Separator(Orientation.HORIZONTAL)); + + var root = Parser.parse_json(game.info_detailed).get_object(); + var app = root.has_member(game.id) ? root.get_object_member(game.id) : null; + var data = app != null && app.has_member("data") ? app.get_object_member("data") : null; + if(data != null) + { + var categories = data.has_member("categories") ? data.get_array_member("categories") : null; + if(categories != null) + { + var categories_string = ""; + foreach(var c in categories.get_elements()) + { + var cat = c.get_object().get_string_member("description"); + categories_string += (categories_string.length > 0 ? ", " : "") + cat; + } + + var categories_label = _("Category"); + if(categories_string.contains(",")) + { + categories_label = _("Categories"); + } + add_info_label(categories_label, categories_string, false, true); + } + + var genres = data.has_member("genres") ? data.get_array_member("genres") : null; + if(genres != null) + { + var genres_string = ""; + foreach(var g in genres.get_elements()) + { + var genre = g.get_object().get_string_member("description"); + genres_string += (genres_string.length > 0 ? ", " : "") + genre; + } + + var genres_label = _("Genre"); + if(genres_string.contains(",")) + { + genres_label = _("Genres"); + } + add_info_label(genres_label, genres_string, false, true); + } + + var langs = data.has_member("supported_languages") ? data.get_string_member("supported_languages") : null; + if(langs != null) + { + langs = langs.split("
*")[0].replace("strong>", "b>"); + var langs_label = _("Language"); + if(langs.contains(",")) + { + langs_label = _("Languages"); + } + add_info_label(langs_label, langs, false, true); + } + } + } + + public override bool supports_game { get { return (game is SteamGame) && game.info_detailed != null && game.info_detailed.length > 0; } } + } +} diff --git a/src/ui/views/GamesGridView/GameCard.vala b/src/ui/views/GamesGridView/GameCard.vala deleted file mode 100644 index 2a130cab..00000000 --- a/src/ui/views/GamesGridView/GameCard.vala +++ /dev/null @@ -1,98 +0,0 @@ -using Gtk; -using Granite; -using GameHub.Data; -using GameHub.Utils; - -namespace GameHub.UI.Views -{ - public class GameCard: FlowBoxChild - { - public Game game; - - private Overlay content; - private AsyncImage image; - private Image src_icon; - private Label label; - - construct - { - var wrapper = new Box(Orientation.HORIZONTAL, 0); - wrapper.halign = Align.CENTER; - wrapper.valign = Align.CENTER; - - var card = new Box(Orientation.VERTICAL, 0); - card.get_style_context().add_class(Granite.STYLE_CLASS_CARD); - card.get_style_context().add_class("gamecard"); - - card.margin = 4; - card.halign = Align.CENTER; - card.valign = Align.CENTER; - - wrapper.add(card); - - child = wrapper; - - content = new Overlay(); - - image = new AsyncImage(); - - src_icon = new Image(); - src_icon.valign = Align.START; - src_icon.halign = Align.START; - src_icon.margin = 8; - src_icon.opacity = 0.5; - - label = new Label(""); - label.xpad = 8; - label.ypad = 8; - label.hexpand = true; - label.valign = Align.END; - label.justify = Justification.CENTER; - label.lines = 3; - label.set_line_wrap(true); - - content.add(image); - content.add_overlay(label); - content.add_overlay(src_icon); - - card.add(content); - - show_all(); - } - - public GameCard(Game game) - { - this.game = game; - - label.label = game.name; - - src_icon.pixbuf = FSUtils.get_icon(game.source.icon + "-white", 24); - - load_image.begin(); - } - - private async void load_image() - { - var hash = Checksum.compute_for_string(ChecksumType.MD5, game.image, game.image.length); - var remote = File.new_for_uri(game.image); - var cached = FSUtils.file(FSUtils.Paths.Cache.Images, hash + ".jpg"); - try - { - if(!cached.query_exists()) - { - image.set_from_file_async.begin(remote, 306, 143, false); - remote.copy_async.begin(cached, FileCopyFlags.NONE); - } - else - { - image.set_from_file_async.begin(cached, 306, 143, false); - } - } - catch(Error e) - { - error(e.message); - image.set_from_file_async.begin(remote, -1, -1, true); - } - } - } -} diff --git a/src/ui/views/GamesGridView/GamesGridView.vala b/src/ui/views/GamesGridView/GamesGridView.vala deleted file mode 100644 index 9e286e4f..00000000 --- a/src/ui/views/GamesGridView/GamesGridView.vala +++ /dev/null @@ -1,111 +0,0 @@ -using Gtk; -using Gee; -using Granite; -using GameHub.Data; -using GameHub.Utils; - -namespace GameHub.UI.Views -{ - public class GamesGridFlowBox: Gtk.FlowBox - { - - } - - public class GamesGridView: BaseView - { - private ArrayList sources = new ArrayList(); - - private GamesGridFlowBox games_list; - - private Granite.Widgets.ModeButton filter; - private SearchEntry search; - - private Spinner spinner; - private int loading_sources = 0; - - construct - { - foreach(var src in GameSources) - { - if(src.is_authenticated()) sources.add(src); - } - - games_list = new GamesGridFlowBox(); - games_list.margin = 4; - - games_list.activate_on_single_click = false; - games_list.homogeneous = true; - games_list.min_children_per_line = 3; - games_list.selection_mode = SelectionMode.NONE; - games_list.valign = Align.START; - - var scrolled = new ScrolledWindow(null, null); - scrolled.expand = true; - scrolled.hscrollbar_policy = PolicyType.NEVER; - scrolled.add(games_list); - add(scrolled); - - filter = new Granite.Widgets.ModeButton(); - filter.append_icon("view-filter-symbolic", IconSize.SMALL_TOOLBAR); - foreach(var src in sources) filter.append_pixbuf(FSUtils.get_icon(src.icon, 16)); - filter.set_active(sources.size > 1 ? 0 : 1); - - search = new SearchEntry(); - - if(sources.size > 1) titlebar.pack_start(filter); - titlebar.pack_end(search); - - games_list.set_sort_func((child1, child2) => { - var item1 = child1 as GameCard; - var item2 = child2 as GameCard; - if(item1 != null && item2 != null) - { - return item1.game.name.collate(item2.game.name); - } - return 0; - }); - - games_list.set_filter_func(child => { - var item = child as GameCard; - var f = filter.selected; - - GameSource? src = null; - if(f > 0) src = sources[f - 1]; - - var games = src == null ? games_list.get_children().length() : src.games_count; - titlebar.title = "GameHub" + (src == null ? "" : ": " + src.name); - titlebar.subtitle = @"$(games) games"; - - return (src == null || item == null || src == item.game.source) && (item == null || search.text.casefold() in item.game.name.casefold()); - }); - - filter.mode_changed.connect(games_list.invalidate_filter); - search.search_changed.connect(games_list.invalidate_filter); - - spinner = new Spinner(); - titlebar.pack_end(spinner); - - show_all(); - scrolled.show_all(); - games_list.show_all(); - - load_games.begin(); - } - - private async void load_games() - { - foreach(var src in sources) - { - loading_sources++; - spinner.active = loading_sources > 0; - src.load_games.begin(g => { - games_list.add(new GameCard(g)); - games_list.show_all(); - }, (obj, res) => { - loading_sources--; - spinner.active = loading_sources > 0; - }); - } - } - } -} diff --git a/src/ui/views/GamesView/AddGamePopover.vala b/src/ui/views/GamesView/AddGamePopover.vala new file mode 100644 index 00000000..ab6fdfa1 --- /dev/null +++ b/src/ui/views/GamesView/AddGamePopover.vala @@ -0,0 +1,196 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +using GameHub.Data.Sources.User; + +namespace GameHub.UI.Views.GamesView +{ + public class AddGamePopover: Popover + { + public signal void game_added(UserGame game); + + private Grid grid; + private int rows = 0; + + private bool suppress_updates = false; + + private Granite.Widgets.ModeButton mode; + + private new Entry name; + private FileChooserEntry gamedir; + private FileChooserEntry executable; + private Label executable_label; + private Entry arguments; + private Label arguments_label; + private new Button add; + + public AddGamePopover(Widget? relative_to) + { + Object(relative_to: relative_to); + } + + construct + { + grid = new Grid(); + grid.margin = 8; + grid.row_spacing = 4; + grid.column_spacing = 4; + + mode = new Granite.Widgets.ModeButton(); + mode.margin_bottom = 8; + mode.halign = Align.CENTER; + mode.append_text(_("Executable")); + mode.append_text(_("Installer")); + mode.selected = 0; + grid.attach(mode, 0, rows, 2, 1); + rows++; + + name = add_entry(_("Name"), "insert-text-symbolic", true); + + add_separator(); + + executable = add_filechooser(_("Executable"), _("Select game executable"), FileChooserAction.OPEN, true, out executable_label); + arguments = add_entry(_("Arguments"), "utilities-terminal-symbolic", false, out arguments_label); + + add_separator(); + + gamedir = add_filechooser(_("Directory"), _("Select game directory"), FileChooserAction.SELECT_FOLDER, true); + + add = new Button.with_label(_("Add game")); + add.margin_top = 8; + add.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + add.sensitive = false; + + grid.attach(add, 0, rows, 2, 1); + + mode.mode_changed.connect(update); + name.changed.connect(update); + executable.file_set.connect(update); + gamedir.file_set.connect(update); + arguments.changed.connect(update); + + update(); + + add.clicked.connect(add_game); + + child = grid; + grid.show_all(); + + name.grab_focus(); + } + + private void update() + { + if(suppress_updates) return; + + if(mode.selected == 0 && executable.file != null && gamedir.file == null) + { + suppress_updates = true; + gamedir.select_file(executable.file.get_parent()); + suppress_updates = false; + } + + add.sensitive = name.text.strip().length > 0 + && executable.file != null && executable.file.query_exists() + && gamedir.file != null && gamedir.file.query_exists(); + + executable_label.label = mode.selected == 0 ? _("Executable") : _("Installer"); + arguments.sensitive = arguments_label.sensitive = mode.selected == 0; + } + + private void add_game() + { + var game = new UserGame(name.text.strip(), gamedir.file, executable.file, arguments.text.strip(), mode.selected != 0); + name.text = ""; + executable.reset(); + gamedir.reset(); + arguments.text = ""; + update(); + game.save(); + game_added(game); + #if GTK_3_22 + popdown(); + #else + hide(); + #endif + executable.reset(); + gamedir.reset(); + if(mode.selected != 0) + { + game.install.begin(); + } + } + + private Entry add_entry(string text, string icon, bool required=true, out Label label=null) + { + label = new Label(text); + label.set_size_request(72, -1); + label.halign = Align.END; + label.xalign = 1; + label.margin = 4; + if(required) + { + label.get_style_context().add_class("category-label"); + } + var entry = new Entry(); + entry.primary_icon_name = icon; + entry.primary_icon_activatable = false; + entry.set_size_request(180, -1); + grid.attach(label, 0, rows); + grid.attach(entry, 1, rows); + rows++; + return entry; + } + + private FileChooserEntry add_filechooser(string text, string title, FileChooserAction action=FileChooserAction.OPEN, bool required=true, out Label label=null) + { + label = new Label(text); + label.set_size_request(72, -1); + label.halign = Align.END; + label.xalign = 1; + label.margin = 4; + if(required) + { + label.get_style_context().add_class("category-label"); + } + var entry = new FileChooserEntry(title, action, null, null, false, action == FileChooserAction.OPEN); + entry.set_size_request(180, -1); + grid.attach(label, 0, rows); + grid.attach(entry, 1, rows); + rows++; + return entry; + } + + private void add_separator() + { + var separator = new Separator(Orientation.HORIZONTAL); + separator.margin_top = separator.margin_bottom = 4; + grid.attach(separator, 0, rows, 2, 1); + rows++; + } + } +} diff --git a/src/ui/views/GamesView/DownloadProgressView.vala b/src/ui/views/GamesView/DownloadProgressView.vala new file mode 100644 index 00000000..8384b34c --- /dev/null +++ b/src/ui/views/GamesView/DownloadProgressView.vala @@ -0,0 +1,193 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; +using GameHub.Data; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GamesView +{ + public class DownloadProgressView: ListBoxRow + { + public Downloader.DownloadInfo dl_info; + + private Image? icon; + private AutoSizeImage? image; + + private Image? type_icon; + private AutoSizeImage? type_image; + + private Overlay image_overlay; + + private ProgressBar progress_bar; + + private Button action_pause; + private Button action_resume; + private Button action_cancel; + + public DownloadProgressView(Downloader.DownloadInfo info) + { + dl_info = info; + + selectable = false; + + var hbox = new Box(Orientation.HORIZONTAL, 8); + hbox.margin = 8; + var hbox_inner = new Box(Orientation.HORIZONTAL, 8); + var hbox_actions = new Box(Orientation.HORIZONTAL, 0); + hbox_actions.vexpand = false; + hbox_actions.valign = Align.CENTER; + + var vbox = new Box(Orientation.VERTICAL, 0); + var vbox_labels = new Box(Orientation.VERTICAL, 0); + vbox_labels.hexpand = true; + + image_overlay = new Overlay(); + image_overlay.valign = Align.START; + image_overlay.set_size_request(48, 48); + + if(dl_info.icon != null) + { + image = new AutoSizeImage(); + image.set_constraint(48, 48, 1); + image.set_size_request(48, 48); + Utils.load_image.begin(image, dl_info.icon, "icon"); + image_overlay.add(image); + } + else if(dl_info.icon_name != null) + { + icon = new Image.from_icon_name(dl_info.icon_name, IconSize.DIALOG); + icon.set_size_request(48, 48); + image_overlay.add(icon); + } + + if(dl_info.type_icon != null) + { + type_image = new AutoSizeImage(); + type_image.set_constraint(16, 16, 1); + type_image.set_size_request(16, 16); + type_image.halign = Align.END; + type_image.valign = Align.END; + type_image.get_style_context().add_class("dl-progress-type-icon"); + Utils.load_image.begin(type_image, dl_info.type_icon, "icon"); + image_overlay.add_overlay(type_image); + } + else if(dl_info.type_icon_name != null) + { + type_icon = new Image.from_icon_name(dl_info.type_icon_name, IconSize.SMALL_TOOLBAR); + type_icon.set_size_request(16, 16); + type_icon.halign = Align.END; + type_icon.valign = Align.END; + type_icon.get_style_context().add_class("dl-progress-type-icon"); + image_overlay.add_overlay(type_icon); + } + + hbox.add(image_overlay); + + var label = new Label(dl_info.name); + label.halign = Align.START; + label.get_style_context().add_class("category-label"); + label.ypad = 2; + + var desc_label = new Label(dl_info.description); + desc_label.halign = Align.START; + desc_label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + desc_label.ypad = 2; + + var state_label = new Label(null); + state_label.halign = Align.START; + + progress_bar = new ProgressBar(); + progress_bar.hexpand = true; + progress_bar.fraction = 0d; + progress_bar.get_style_context().add_class(Gtk.STYLE_CLASS_OSD); + + action_pause = new Button.from_icon_name("media-playback-pause-symbolic"); + action_pause.tooltip_text = _("Pause download"); + action_pause.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_pause.visible = false; + + action_resume = new Button.from_icon_name("media-playback-start-symbolic"); + action_resume.tooltip_text = _("Resume download"); + action_resume.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_resume.visible = false; + + action_cancel = new Button.from_icon_name("process-stop-symbolic"); + action_cancel.tooltip_text = _("Cancel download"); + action_cancel.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + action_cancel.visible = false; + + vbox_labels.add(label); + vbox_labels.add(desc_label); + vbox_labels.add(state_label); + + hbox_inner.add(vbox_labels); + hbox_inner.add(hbox_actions); + + hbox_actions.add(action_pause); + hbox_actions.add(action_resume); + hbox_actions.add(action_cancel); + + vbox.add(hbox_inner); + vbox.add(progress_bar); + + hbox.add(vbox); + + child = hbox; + + dl_info.download.status_change.connect(s => { + state_label.label = s.description; + var ds = s.state; + + progress_bar.fraction = s.progress; + + action_cancel.visible = true; + action_cancel.sensitive = ds == Downloader.DownloadState.DOWNLOADING || ds == Downloader.DownloadState.PAUSED; + action_pause.visible = dl_info.download is Downloader.PausableDownload && ds != Downloader.DownloadState.PAUSED; + action_resume.visible = dl_info.download is Downloader.PausableDownload && ds == Downloader.DownloadState.PAUSED; + }); + + action_cancel.clicked.connect(() => { + dl_info.download.cancel(); + }); + + action_pause.clicked.connect(() => { + if(dl_info.download is Downloader.PausableDownload) + { + ((Downloader.PausableDownload) dl_info.download).pause(); + } + }); + + action_resume.clicked.connect(() => { + if(dl_info.download is Downloader.PausableDownload) + { + ((Downloader.PausableDownload) dl_info.download).resume(); + } + }); + + Downloader.get_instance().dl_ended.connect(dl => { + if(dl == dl_info) destroy(); + }); + + show_all(); + } + } +} diff --git a/src/ui/views/GamesView/FiltersPopover.vala b/src/ui/views/GamesView/FiltersPopover.vala new file mode 100644 index 00000000..a0828567 --- /dev/null +++ b/src/ui/views/GamesView/FiltersPopover.vala @@ -0,0 +1,256 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; +using GameHub.Settings; + +namespace GameHub.UI.Views.GamesView +{ + public class FiltersPopover: Popover + { + public ArrayList selected_tags { get; private set; } + public signal void filters_changed(ArrayList selected_tags); + + public SortMode sort_mode { get; private set; default = SortMode.NAME; } + public signal void sort_mode_changed(SortMode sort_mode); + + private Granite.Widgets.ModeButton sort_mode_button; + + private CheckButton tags_header_check; + private ListBox tags_list; + + private bool is_toggling_all = false; + private bool is_updating = false; + + public FiltersPopover(Widget? relative_to) + { + Object(relative_to: relative_to); + } + + construct + { + selected_tags = new ArrayList(Tables.Tags.Tag.is_equal); + + set_size_request(220, -1); + + var vbox = new Box(Orientation.VERTICAL, 0); + + var sort_hbox = new Box(Orientation.HORIZONTAL, 6); + sort_hbox.margin_start = sort_hbox.margin_end = 8; + sort_hbox.margin_top = 4; + sort_hbox.margin_bottom = 2; + + var sort_image = new Image.from_icon_name("view-sort-descending-symbolic", IconSize.BUTTON); + sort_hbox.add(sort_image); + + var sort_label = new HeaderLabel(_("Sort:")); + sort_label.margin_end = 8; + sort_label.xpad = 0; + sort_label.halign = Align.START; + sort_label.xalign = 0; + sort_label.hexpand = true; + sort_hbox.add(sort_label); + + sort_mode_button = new Granite.Widgets.ModeButton(); + sort_mode_button.get_style_context().add_class("filters-sort-mode"); + sort_mode_button.halign = Align.END; + sort_mode_button.valign = Align.CENTER; + sort_mode_button.can_focus = true; + add_sort_mode(SortMode.NAME); + add_sort_mode(SortMode.LAST_LAUNCH); + add_sort_mode(SortMode.PLAYTIME); + sort_hbox.add(sort_mode_button); + + var saved_state = Settings.SavedState.get_instance(); + + sort_mode_button.set_active((int) saved_state.sort_mode); + sort_mode = saved_state.sort_mode; + sort_mode_button.mode_changed.connect(() => { + saved_state.sort_mode = (SortMode) sort_mode_button.selected; + sort_mode = saved_state.sort_mode; + sort_mode_changed(sort_mode); + }); + + vbox.add(sort_hbox); + + vbox.add(new Separator(Orientation.HORIZONTAL)); + + tags_list = new ListBox(); + tags_list.get_style_context().add_class("tags-list"); + tags_list.get_style_context().add_class("not-rounded"); + tags_list.selection_mode = SelectionMode.NONE; + + tags_list.set_sort_func((row1, row2) => { + var item1 = row1 as TagRow; + var item2 = row2 as TagRow; + + if(row1 != null && row2 != null) + { + var t1 = item1.tag.id; + var t2 = item2.tag.id; + + var b1 = t1.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + var b2 = t2.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + if(b1 && !b2) return -1; + if(!b1 && b2) return 1; + + var u1 = t1.has_prefix(Tables.Tags.Tag.USER_PREFIX); + var u2 = t2.has_prefix(Tables.Tags.Tag.USER_PREFIX); + if(u1 && !u2) return -1; + if(!u1 && u2) return 1; + + return item1.tag.name.collate(item1.tag.name); + } + + return 0; + }); + + var tags_scrolled = new ScrolledWindow(null, null); + #if GTK_3_22 + tags_scrolled.propagate_natural_width = true; + tags_scrolled.propagate_natural_height = true; + tags_scrolled.max_content_height = 440; + #else + tags_scrolled.min_content_height = 320; + #endif + tags_scrolled.add(tags_list); + tags_scrolled.show_all(); + + var tebox = new EventBox(); + tebox.get_style_context().add_class("tags-list-header"); + tebox.above_child = true; + tebox.can_focus = true; + + var tbox = new Box(Orientation.HORIZONTAL, 8); + tbox.margin_start = tbox.margin_end = 8; + tbox.margin_top = tbox.margin_bottom = 6; + + tags_header_check = new CheckButton(); + tags_header_check.can_focus = false; + + var header = new HeaderLabel(_("Tags")); + header.halign = Align.START; + header.xalign = 0; + header.hexpand = true; + + tbox.add(tags_header_check); + tbox.add(header); + + tebox.add_events(EventMask.ALL_EVENTS_MASK); + tebox.enter_notify_event.connect(e => { tebox.get_style_context().add_class("hover"); }); + tebox.leave_notify_event.connect(e => { tebox.get_style_context().remove_class("hover"); }); + tebox.button_release_event.connect(e => { + if(e.button == 1) + { + toggle_all_tags(); + } + return true; + }); + tebox.key_release_event.connect(e => { + switch(((EventKey) e).keyval) + { + case Key.Return: + case Key.space: + case Key.KP_Space: + toggle_all_tags(); + return true; + } + return false; + }); + + tebox.add(tbox); + + vbox.add(tebox); + vbox.add(new Separator(Orientation.HORIZONTAL)); + vbox.add(tags_scrolled); + + child = vbox; + + load_tags(); + + Tables.Tags.instance.tags_updated.connect(load_tags); + + vbox.show_all(); + } + + private void toggle_all_tags() + { + tags_header_check.inconsistent = false; + tags_header_check.active = !tags_header_check.active; + + is_toggling_all = true; + foreach(var tag in Tables.Tags.TAGS) + { + tag.selected = tags_header_check.active; + } + is_toggling_all = false; + update(); + } + + private void load_tags() + { + tags_list.foreach(w => w.destroy()); + + foreach(var tag in Tables.Tags.TAGS) + { + if(!tag.enabled) continue; + tags_list.add(new TagRow(tag)); + tag.notify["selected"].connect(update); + } + + tags_list.show_all(); + + update(); + } + + private void update() + { + if(is_toggling_all || is_updating) return; + is_updating = true; + + selected_tags.clear(); + + foreach(var tag in Tables.Tags.TAGS) + { + if(tag.selected) selected_tags.add(tag); + Tables.Tags.add(tag, true); + } + + tags_header_check.inconsistent = selected_tags.size != 0 && selected_tags.size != Tables.Tags.TAGS.size; + tags_header_check.active = selected_tags.size > 0; + + filters_changed(selected_tags); + + is_updating = false; + } + + private void add_sort_mode(SortMode mode) + { + var image = new Image.from_icon_name(mode.icon(), IconSize.MENU); + image.tooltip_text = mode.name(); + sort_mode_button.append(image); + } + } +} diff --git a/src/ui/views/GamesView/GameCard.vala b/src/ui/views/GamesView/GameCard.vala new file mode 100644 index 00000000..f9d82f72 --- /dev/null +++ b/src/ui/views/GamesView/GameCard.vala @@ -0,0 +1,301 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GamesView +{ + public class GameCard: FlowBoxChild + { + public Game game { get; construct; } + + public signal void update_tags(); + + private Frame card; + private Overlay content; + private AutoSizeImage image; + private Label label; + private Label status_label; + + private Box src_icons; + private Image src_icon; + + private Box platform_icons; + + private Box actions; + + private const int CARD_WIDTH_MIN = 320; + private const int CARD_WIDTH_MAX = 680; + private const float CARD_RATIO = 0.467f; // 460x215 + + private Frame progress_bar; + + private Image running_indicator; + + construct + { + margin = 0; + + card = new Frame(null); + card.get_style_context().add_class(Granite.STYLE_CLASS_CARD); + card.get_style_context().add_class("gamecard"); + card.shadow_type = ShadowType.NONE; + card.margin = 4; + + child = card; + + content = new Overlay(); + + image = new AutoSizeImage(); + image.set_constraint(CARD_WIDTH_MIN, CARD_WIDTH_MAX, CARD_RATIO); + + src_icons = new Box(Orientation.HORIZONTAL, 4); + src_icons.valign = Align.START; + src_icons.halign = Align.START; + src_icons.margin = 8; + src_icons.set_events(0); + + platform_icons = new Box(Orientation.HORIZONTAL, 4); + platform_icons.valign = Align.START; + platform_icons.halign = Align.END; + platform_icons.margin = 8; + platform_icons.set_events(0); + + src_icon = new Image(); + src_icon.icon_size = IconSize.LARGE_TOOLBAR; + + label = new Label(""); + label.xpad = 8; + label.ypad = 4; + label.hexpand = true; + label.justify = Justification.CENTER; + label.lines = 3; + label.set_line_wrap(true); + + status_label = new Label(""); + status_label.get_style_context().add_class("status"); + status_label.xpad = 8; + status_label.ypad = 2; + status_label.hexpand = true; + status_label.justify = Justification.CENTER; + status_label.lines = 1; + + var info = new Box(Orientation.VERTICAL, 0); + info.get_style_context().add_class("info"); + info.add(label); + info.add(status_label); + info.valign = Align.END; + + actions = new Box(Orientation.VERTICAL, 0); + actions.get_style_context().add_class("actions"); + actions.hexpand = true; + actions.vexpand = true; + + progress_bar = new Frame(null); + progress_bar.halign = Align.START; + progress_bar.valign = Align.END; + progress_bar.get_style_context().add_class("progress"); + + running_indicator = new Image.from_icon_name("system-run-symbolic", IconSize.DIALOG); + running_indicator.get_style_context().add_class("running-indicator"); + running_indicator.halign = Align.CENTER; + running_indicator.valign = Align.CENTER; + + content.add(image); + content.add_overlay(actions); + content.add_overlay(info); + content.add_overlay(platform_icons); + content.add_overlay(src_icons); + content.add_overlay(progress_bar); + content.add_overlay(running_indicator); + + card.add(content); + + content.add_events(EventMask.ALL_EVENTS_MASK); + content.enter_notify_event.connect(e => { card.get_style_context().add_class("hover"); }); + content.leave_notify_event.connect(e => { card.get_style_context().remove_class("hover"); }); + content.button_release_event.connect(e => { + switch(e.button) + { + case 1: + run_game(); + break; + + case 3: + open_context_menu(e, true); + break; + } + return true; + }); + key_release_event.connect(e => { + switch(((EventKey) e).keyval) + { + case Key.Return: + case Key.space: + case Key.KP_Space: + run_game(); + return true; + + case Key.Control_L: + case Key.Control_R: + case Key.Menu: + open_context_menu(e, false); + return true; + } + return false; + }); + + show_all(); + } + + public GameCard(Game game) + { + Object(game: game); + + Idle.add(() => { + label.label = game.name; + src_icon.icon_name = game.source.icon; + return Source.REMOVE; + }); + + update(); + + card.get_style_context().add_class("installed"); + + game.status_change.connect(s => { + Idle.add(() => { + label.label = (game.has_tag(Tables.Tags.BUILTIN_FAVORITES) ? "★ " : "") + game.name; + status_label.label = s.description; + switch(s.state) + { + case Game.State.UNINSTALLED: + card.get_style_context().remove_class("installed"); + card.get_style_context().remove_class("downloading"); + card.get_style_context().remove_class("installing"); + break; + + case Game.State.INSTALLED: + card.get_style_context().add_class("installed"); + card.get_style_context().remove_class("downloading"); + card.get_style_context().remove_class("installing"); + break; + + case Game.State.DOWNLOADING: + card.get_style_context().remove_class("installed"); + card.get_style_context().add_class("downloading"); + card.get_style_context().remove_class("installing"); + Allocation alloc; + card.get_allocation(out alloc); + if(s.download != null) + { + progress_bar.set_size_request((int) (s.download.status.progress * alloc.width), 8); + } + break; + + case Game.State.INSTALLING: + card.get_style_context().remove_class("installed"); + card.get_style_context().remove_class("downloading"); + card.get_style_context().add_class("installing"); + break; + } + if(game.is_running) + { + card.get_style_context().add_class("running"); + running_indicator.opacity = 1; + } + else + { + card.get_style_context().remove_class("running"); + running_indicator.opacity = 0; + } + return Source.REMOVE; + }); + }); + game.status_change(game.status); + + game.notify["image"].connect(() => { + Utils.load_image.begin(image, game.image, "image"); + }); + game.notify_property("image"); + } + + private void run_game() + { + if(game.status.state == Game.State.INSTALLED) + { + if(game.use_compat) + { + game.run_with_compat.begin(false); + } + else + { + game.run.begin(); + } + } + else if(game.status.state == Game.State.UNINSTALLED) + { + game.install.begin(); + } + } + + private void open_context_menu(Event e, bool at_pointer=true) + { + new GameContextMenu(game, this).open(e, at_pointer); + } + + public void update() + { + src_icons.foreach(w => src_icons.remove(w)); + src_icons.add(src_icon); + + var merges = Tables.Merges.get(game); + if(merges != null && merges.size > 0) + { + foreach(var g in merges) + { + var icon_name = g.source.icon; + + src_icons.foreach(w => { if((w as Image).icon_name == icon_name) src_icons.remove(w); }); + + var icon = new Image(); + icon.icon_name = icon_name; + icon.icon_size = IconSize.LARGE_TOOLBAR; + src_icons.add(icon); + } + } + src_icons.show_all(); + + platform_icons.foreach(w => platform_icons.remove(w)); + foreach(var p in game.platforms) + { + var icon = new Image(); + icon.icon_name = p.icon(); + icon.icon_size = IconSize.LARGE_TOOLBAR; + platform_icons.add(icon); + } + platform_icons.show_all(); + } + } +} diff --git a/src/ui/views/GamesView/GameContextMenu.vala b/src/ui/views/GamesView/GameContextMenu.vala new file mode 100644 index 00000000..dcd0d472 --- /dev/null +++ b/src/ui/views/GamesView/GameContextMenu.vala @@ -0,0 +1,188 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GamesView +{ + public class GameContextMenu: Gtk.Menu + { + public Game game { get; construct; } + + public Widget target { get; construct; } + + public GameContextMenu(Game game, Widget target) + { + Object(game: game, target: target); + } + + construct + { + var run = new Gtk.MenuItem.with_label(_("Run")); + run.activate.connect(() => game.run.begin()); + + var run_with_compat = new Gtk.MenuItem.with_label(_("Run with compatibility layer")); + run_with_compat.sensitive = Settings.UI.get_instance().use_compat; + run_with_compat.activate.connect(() => game.run_with_compat.begin(true)); + + var install = new Gtk.MenuItem.with_label(_("Install")); + install.sensitive = game.is_installable; + install.activate.connect(() => game.install.begin()); + + var details = new Gtk.MenuItem.with_label(_("Details")); + details.activate.connect(() => new Dialogs.GameDetailsDialog(game).show_all()); + + var favorite = new Gtk.CheckMenuItem.with_label(C_("game_context_menu", "Favorite")); + favorite.active = game.has_tag(Tables.Tags.BUILTIN_FAVORITES); + favorite.toggled.connect(() => game.toggle_tag(Tables.Tags.BUILTIN_FAVORITES)); + + var hidden = new Gtk.CheckMenuItem.with_label(C_("game_context_menu", "Hidden")); + hidden.active = game.has_tag(Tables.Tags.BUILTIN_HIDDEN); + hidden.toggled.connect(() => game.toggle_tag(Tables.Tags.BUILTIN_HIDDEN)); + + var fs_overlays = new Gtk.MenuItem.with_label(_("Overlays")); + fs_overlays.activate.connect(() => new Dialogs.GameFSOverlaysDialog(game).show_all()); + + var properties = new Gtk.MenuItem.with_label(_("Properties")); + properties.activate.connect(() => new Dialogs.GamePropertiesDialog(game).show_all()); + + if(game.status.state == Game.State.INSTALLED && !(game is Sources.GOG.GOGGame.DLC)) + { + if(game.use_compat) + { + add(run_with_compat); + } + else + { + add(run); + } + add(new Gtk.SeparatorMenuItem()); + } + else if(game.status.state == Game.State.UNINSTALLED) + { + add(install); + add(new Gtk.SeparatorMenuItem()); + } + + add(details); + + if(!(game is Sources.GOG.GOGGame.DLC)) + { + add(new Gtk.SeparatorMenuItem()); + add(favorite); + add(hidden); + } + + bool add_dirs_separator = true; + + if(game.status.state == Game.State.INSTALLED && game.install_dir != null && game.install_dir.query_exists()) + { + if(add_dirs_separator) add(new Gtk.SeparatorMenuItem()); + add_dirs_separator = false; + var open_directory = new Gtk.MenuItem.with_label(_("Open installation directory")); + open_directory.activate.connect(open_game_directory); + add(open_directory); + } + if(game.installers_dir != null && game.installers_dir.query_exists()) + { + if(add_dirs_separator) add(new Gtk.SeparatorMenuItem()); + add_dirs_separator = false; + var open_installers_directory = new Gtk.MenuItem.with_label(_("Open installers collection directory")); + open_installers_directory.activate.connect(open_installer_collection_directory); + add(open_installers_directory); + } + if(game is GameHub.Data.Sources.GOG.GOGGame && (game as GameHub.Data.Sources.GOG.GOGGame).bonus_content_dir != null && (game as GameHub.Data.Sources.GOG.GOGGame).bonus_content_dir.query_exists()) + { + if(add_dirs_separator) add(new Gtk.SeparatorMenuItem()); + add_dirs_separator = false; + var open_bonuses_directory = new Gtk.MenuItem.with_label(_("Open bonus collection directory")); + open_bonuses_directory.activate.connect(open_bonus_collection_directory); + add(open_bonuses_directory); + } + + if((game.status.state == Game.State.INSTALLED || game is Sources.User.UserGame) && !(game is Sources.GOG.GOGGame.DLC)) + { + var uninstall = new Gtk.MenuItem.with_label((game is Sources.User.UserGame) ? _("Remove") : _("Uninstall")); + uninstall.activate.connect(() => game.uninstall.begin()); + add(new Gtk.SeparatorMenuItem()); + add(uninstall); + + add(new Gtk.SeparatorMenuItem()); + add(fs_overlays); + } + + if(!(game is Sources.GOG.GOGGame.DLC)) + { + add(new Gtk.SeparatorMenuItem()); + add(properties); + } + + show_all(); + } + + public void open(Event e, bool at_pointer=true) + { + #if GTK_3_22 + if(at_pointer) + { + popup_at_pointer(e); + } + else + { + popup_at_widget(target, Gravity.SOUTH, Gravity.NORTH, e); + } + #else + popup(null, null, null, 0, ((EventButton) e).time); + #endif + } + + private void open_game_directory() + { + if(game != null && game.status.state == Game.State.INSTALLED) + { + Utils.open_uri(game.install_dir.get_uri()); + } + } + + private void open_installer_collection_directory() + { + if(game != null && game.installers_dir != null && game.installers_dir.query_exists()) + { + Utils.open_uri(game.installers_dir.get_uri()); + } + } + + private void open_bonus_collection_directory() + { + if(game != null && game is GameHub.Data.Sources.GOG.GOGGame) + { + var gog_game = game as GameHub.Data.Sources.GOG.GOGGame; + if(gog_game != null && gog_game.bonus_content_dir != null && gog_game.bonus_content_dir.query_exists()) + { + Utils.open_uri(gog_game.bonus_content_dir.get_uri()); + } + } + } + } +} diff --git a/src/ui/views/GamesView/GameListRow.vala b/src/ui/views/GamesView/GameListRow.vala new file mode 100644 index 00000000..05d9aa90 --- /dev/null +++ b/src/ui/views/GamesView/GameListRow.vala @@ -0,0 +1,129 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; + +namespace GameHub.UI.Views.GamesView +{ + class GameListRow: ListBoxRow + { + public Game game; + + public signal void update_tags(); + + private AutoSizeImage image; + private Label state_label; + + private string old_icon; + + private GameHub.Settings.UI ui_settings; + + public GameListRow(Game game) + { + this.game = game; + + var hbox = new Box(Orientation.HORIZONTAL, 8); + hbox.margin = 4; + var vbox = new Box(Orientation.VERTICAL, 0); + vbox.valign = Align.CENTER; + + image = new AutoSizeImage(); + + hbox.add(image); + + var label = new Label(game.name); + label.halign = Align.START; + label.get_style_context().add_class("category-label"); + + state_label = new Label(null); + state_label.halign = Align.START; + + vbox.add(label); + vbox.add(state_label); + + hbox.add(vbox); + + game.status_change.connect(s => { + Idle.add(() => { + label.label = (game.has_tag(Tables.Tags.BUILTIN_FAVORITES) ? "★ " : "") + game.name; + state_label.label = s.description; + update_icon(); + changed(); + return Source.REMOVE; + }); + }); + game.status_change(game.status); + + notify["is-selected"].connect(update_icon); + + ui_settings = GameHub.Settings.UI.get_instance(); + ui_settings.notify["compact-list"].connect(update); + + var ebox = new EventBox(); + ebox.add(hbox); + + child = ebox; + + ebox.add_events(EventMask.ALL_EVENTS_MASK); + ebox.button_release_event.connect(e => { + switch(e.button) + { + case 1: + activate(); + break; + + case 3: + new GameContextMenu(game, image).open(e, true); + break; + } + return true; + }); + + show_all(); + } + + public override void show_all() + { + base.show_all(); + update(); + } + + public void update() + { + var compact = ui_settings.compact_list; + var image_size = compact ? 16 : 36; + image.set_constraint(image_size, image_size, 1); + image.set_size_request(image_size, image_size); + state_label.visible = !compact; + } + + private void update_icon() + { + image.queue_draw(); + if(game.icon == old_icon) return; + old_icon = game.icon; + Utils.load_image.begin(image, game.icon, "icon"); + } + } +} diff --git a/src/ui/views/GamesView/GamesView.vala b/src/ui/views/GamesView/GamesView.vala new file mode 100644 index 00000000..84d8bc17 --- /dev/null +++ b/src/ui/views/GamesView/GamesView.vala @@ -0,0 +1,1119 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using GLib; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; + +namespace GameHub.UI.Views.GamesView +{ + public class GamesView: BaseView + { + public static GamesView instance; + + private ArrayList sources = new ArrayList(); + private ArrayList loading_sources = new ArrayList(); + + private Box messages; + + private Stack stack; + + private Granite.Widgets.AlertView empty_alert; + + private ScrolledWindow games_grid_scrolled; + private FlowBox games_grid; + + private Paned games_list_paned; + private ListBox games_list; + private GameDetailsView.GameDetailsView games_list_details; + + private Granite.Widgets.ModeButton view; + + private Granite.Widgets.ModeButton filter; + private SearchEntry search; + + private Granite.Widgets.OverlayBar status_overlay; + private string? status_text; + private bool status_changed = false; + + private bool new_games_added = false; + + private Button settings; + + private MenuButton downloads; + private Popover downloads_popover; + private ListBox downloads_list; + private int downloads_count = 0; + + private MenuButton filters; + private FiltersPopover filters_popover; + + private MenuButton add_game_button; + private AddGamePopover add_game_popover; + + private Settings.UI ui_settings; + private Settings.SavedState saved_state; + + private bool view_update_interval_started = false; + private bool view_update_pending = false; + private int view_update_no_updates_cycles = 0; + + #if MANETTE + private Manette.Monitor manette_monitor = new Manette.Monitor(); + private ArrayList connected_gamepads = new ArrayList(); + private bool gamepad_axes_to_keys_thread_running = false; + private ArrayList gamepad_mode_visible_widgets = new ArrayList(); + private ArrayList gamepad_mode_hidden_widgets = new ArrayList(); + #endif + + construct + { + instance = this; + + ui_settings = Settings.UI.get_instance(); + saved_state = Settings.SavedState.get_instance(); + + foreach(var src in GameSources) + { + if(src.enabled && src.is_authenticated()) sources.add(src); + } + + var overlay = new Overlay(); + + stack = new Stack(); + stack.transition_type = StackTransitionType.CROSSFADE; + + empty_alert = new Granite.Widgets.AlertView(_("No games"), _("Get some games or enable some game sources in settings"), "dialog-warning"); + empty_alert.show_action(_("Reload")); + + games_grid = new FlowBox(); + games_grid.get_style_context().add_class("games-grid"); + games_grid.margin = 4; + + games_grid.activate_on_single_click = false; + games_grid.homogeneous = false; + games_grid.min_children_per_line = 2; + games_grid.selection_mode = SelectionMode.BROWSE; + games_grid.valign = Align.START; + + games_grid_scrolled = new ScrolledWindow(null, null); + games_grid_scrolled.expand = true; + games_grid_scrolled.hscrollbar_policy = PolicyType.NEVER; + games_grid_scrolled.add(games_grid); + + games_list_paned = new Paned(Orientation.HORIZONTAL); + + games_list = new ListBox(); + games_list.selection_mode = SelectionMode.BROWSE; + + games_list_details = new GameDetailsView.GameDetailsView(null); + games_list_details.content_margin = 16; + + var games_list_scrolled = new ScrolledWindow(null, null); + games_list_scrolled.hscrollbar_policy = PolicyType.EXTERNAL; + games_list_scrolled.add(games_list); + games_list_scrolled.set_size_request(220, -1); + + games_list_paned.pack1(games_list_scrolled, false, false); + games_list_paned.pack2(games_list_details, true, true); + + stack.add(empty_alert); + stack.add(games_grid_scrolled); + stack.add(games_list_paned); + + overlay.add(stack); + + messages = new Box(Orientation.VERTICAL, 0); + + attach(messages, 0, 0); + attach(overlay, 0, 1); + + view = new Granite.Widgets.ModeButton(); + view.halign = Align.CENTER; + view.valign = Align.CENTER; + + add_view_button("view-grid-symbolic", _("Grid view")); + add_view_button("view-list-symbolic", _("List view")); + + view.mode_changed.connect(() => { + postpone_view_update(); + }); + + filter = new Granite.Widgets.ModeButton(); + filter.halign = Align.CENTER; + filter.valign = Align.CENTER; + + add_filter_button("sources-all-symbolic", _("All games")); + + foreach(var src in sources) + { + add_filter_button(src.icon, _("%s games").printf(src.name)); + filter.set_item_visible((int) filter.n_items - 1, src.games_count > 0); + } + + filter.set_active(sources.size > 1 ? 0 : 1); + + downloads = new MenuButton(); + downloads.tooltip_text = _("Downloads"); + downloads.image = new Image.from_icon_name("emblem-downloads", IconSize.LARGE_TOOLBAR); + downloads_popover = new Popover(downloads); + downloads_list = new ListBox(); + downloads_list.get_style_context().add_class("downloads-list"); + + var downloads_scrolled = new ScrolledWindow(null, null); + #if GTK_3_22 + downloads_scrolled.propagate_natural_width = true; + downloads_scrolled.propagate_natural_height = true; + downloads_scrolled.max_content_height = 440; + #else + downloads_scrolled.min_content_height = 440; + #endif + downloads_scrolled.add(downloads_list); + downloads_scrolled.show_all(); + + downloads_popover.add(downloads_scrolled); + downloads_popover.position = PositionType.BOTTOM; + downloads_popover.set_size_request(384, -1); + downloads.popover = downloads_popover; + downloads.sensitive = false; + + filters = new MenuButton(); + filters.tooltip_text = _("Filters"); + filters.image = new Image.from_icon_name("tag", IconSize.LARGE_TOOLBAR); + filters_popover = new FiltersPopover(filters); + filters_popover.position = PositionType.BOTTOM; + filters.popover = filters_popover; + + add_game_button = new MenuButton(); + add_game_button.tooltip_text = _("Add game"); + add_game_button.image = new Image.from_icon_name("list-add", IconSize.LARGE_TOOLBAR); + add_game_popover = new AddGamePopover(add_game_button); + add_game_popover.position = PositionType.BOTTOM; + add_game_button.popover = add_game_popover; + + search = new SearchEntry(); + search.placeholder_text = _("Search"); + search.halign = Align.CENTER; + search.valign = Align.CENTER; + + settings = new Button(); + settings.tooltip_text = _("Settings"); + settings.image = new Image.from_icon_name("open-menu", IconSize.LARGE_TOOLBAR); + + settings.clicked.connect(() => new Dialogs.SettingsDialog.SettingsDialog()); + + games_grid.set_sort_func((child1, child2) => { + var item1 = child1 as GameCard; + var item2 = child2 as GameCard; + if(item1 != null && item2 != null) + { + return games_sort(item1.game, item2.game); + } + return 0; + }); + + games_list.set_sort_func((row1, row2) => { + var item1 = row1 as GameListRow; + var item2 = row2 as GameListRow; + if(item1 != null && item2 != null) + { + return games_sort(item1.game, item2.game); + } + return 0; + }); + + games_grid.set_filter_func(child => { + var item = child as GameCard; + return games_filter(item.game); + }); + + games_list.set_filter_func(row => { + var item = row as GameListRow; + return games_filter(item.game); + }); + + games_list.set_header_func((row, prev) => { + var item = row as GameListRow; + var prev_item = prev as GameListRow; + var s = item.game.status.state; + var ps = prev_item != null ? prev_item.game.status.state : s; + var f = item.game.has_tag(Tables.Tags.BUILTIN_FAVORITES); + var pf = prev_item != null ? prev_item.game.has_tag(Tables.Tags.BUILTIN_FAVORITES) : f; + + if(prev_item != null && f == pf && (f || s == ps)) row.set_header(null); + else + { + var label = new HeaderLabel(f ? C_("status_header", "Favorites") : item.game.status.header); + label.get_style_context().add_class("games-list-header"); + games_list.size_allocate.connect(alloc => { + label.set_size_request(alloc.width, -1); + }); + Allocation alloc; + games_list.get_allocation(out alloc); + label.set_size_request(alloc.width, -1); + row.set_header(label); + } + }); + + games_list.row_selected.connect(row => { + var item = row as GameListRow; + games_list_details.game = item != null ? item.game : null; + }); + + filter.mode_changed.connect(postpone_view_update); + search.search_changed.connect(postpone_view_update); + + ui_settings.notify["show-unsupported-games"].connect(postpone_view_update); + ui_settings.notify["use-proton"].connect(postpone_view_update); + + filters_popover.filters_changed.connect(postpone_view_update); + filters_popover.sort_mode_changed.connect(() => { + Idle.add(() => { + games_list.invalidate_sort(); + games_grid.invalidate_sort(); + return Source.REMOVE; + }); + }); + + add_game_popover.game_added.connect(g => add_game(g)); + + titlebar.pack_start(view); + + if(sources.size > 1) + { + #if MANETTE + titlebar.pack_start(gamepad_image("bumper-left")); + #endif + + titlebar.pack_start(filter); + + #if MANETTE + titlebar.pack_start(gamepad_image("bumper-right")); + #endif + } + + #if MANETTE + var gamepad_filters_separator = new Separator(Orientation.VERTICAL); + gamepad_filters_separator.no_show_all = true; + gamepad_mode_visible_widgets.add(gamepad_filters_separator); + titlebar.pack_start(gamepad_filters_separator); + #endif + + titlebar.pack_start(filters); + + #if MANETTE + titlebar.pack_start(gamepad_image("y")); + #endif + + var settings_overlay = new Overlay(); + settings_overlay.add(settings); + + #if MANETTE + var settings_gamepad_shortcut = gamepad_image("select"); + settings_gamepad_shortcut.halign = Align.CENTER; + settings_gamepad_shortcut.valign = Align.END; + settings_overlay.add_overlay(settings_gamepad_shortcut); + settings_overlay.set_overlay_pass_through(settings_gamepad_shortcut, true); + #endif + + titlebar.pack_end(settings_overlay); + + titlebar.pack_end(downloads); + titlebar.pack_end(search); + titlebar.pack_end(add_game_button); + + #if MANETTE + var gamepad_shortcuts_separator = new Separator(Orientation.VERTICAL); + gamepad_shortcuts_separator.no_show_all = true; + gamepad_mode_visible_widgets.add(gamepad_shortcuts_separator); + titlebar.pack_end(gamepad_shortcuts_separator); + titlebar.pack_end(gamepad_image("x", _("Menu"))); + titlebar.pack_end(gamepad_image("b", _("Back"))); + titlebar.pack_end(gamepad_image("a", _("Select"))); + #endif + + status_overlay = new Granite.Widgets.OverlayBar(overlay); + + show_all(); + games_grid_scrolled.show_all(); + games_grid.show_all(); + + empty_alert.action_activated.connect(() => load_games()); + + stack.set_visible_child(empty_alert); + + view.opacity = 0; + view.sensitive = false; + filter.opacity = 0; + filter.sensitive = false; + search.opacity = 0; + search.sensitive = false; + downloads.opacity = 0; + filters.opacity = 0; + filters.sensitive = false; + add_game_button.opacity = 0; + add_game_button.sensitive = false; + + Downloader.get_instance().dl_started.connect(dl => { + downloads_list.add(new DownloadProgressView(dl)); + downloads.sensitive = true; + downloads_count++; + }); + Downloader.get_instance().dl_ended.connect(dl => { + downloads_count--; + if(downloads_count < 0) downloads_count = 0; + downloads.sensitive = downloads_count > 0; + if(downloads_count == 0) + { + #if GTK_3_22 + downloads_popover.popdown(); + #else + downloads_popover.hide(); + #endif + } + }); + + #if MANETTE + gamepad_mode_hidden_widgets.add(view); + gamepad_mode_hidden_widgets.add(downloads); + gamepad_mode_hidden_widgets.add(search); + gamepad_mode_hidden_widgets.add(add_game_button); + + var manette_iterator = manette_monitor.iterate(); + Manette.Device manette_device = null; + while(manette_iterator.next(out manette_device)) + { + on_gamepad_connected(manette_device); + } + manette_monitor.device_connected.connect(on_gamepad_connected); + manette_monitor.device_disconnected.connect(on_gamepad_disconnected); + #endif + + add_events(EventMask.KEY_RELEASE_MASK); + key_release_event.connect(e => { + switch(((EventKey) e).keyval) + { + case Key.F1: // LB + case Key.F2: // RB + var tab = filter.selected + (((EventKey) e).keyval == Key.F1 ? -1 : 1); + if(tab < 0) tab = (int) filter.n_items - 1; + else if(tab >= filter.n_items) tab = 0; + filter.selected = tab; + break; + + case Key.F5: // Select + settings.clicked(); + break; + + case Key.R: + case Key.r: + int index = Random.int_range(0, (int32) games_grid.get_children().length()); + var card = games_grid.get_child_at_index(index); + if(card != null) + { + games_grid.select_child(card); + card.grab_focus(); + } + var row = games_list.get_row_at_index(index); + if(row != null) + { + games_list.select_row(row); + row.grab_focus(); + } + break; + + case Key.Alt_L: // Y + case Key.Alt_R: + filters.clicked(); + break; + } + }); + + load_games(); + } + + private void postpone_view_update() + { + view_update_pending = true; + if(!view_update_interval_started) + { + Utils.thread("GamesViewUpdate", () => { + view_update_interval_started = true; + view_update_no_updates_cycles = 0; + while(view_update_no_updates_cycles < 10) + { + if(view_update_pending) + { + Idle.add(() => { update_view(); return Source.REMOVE; }); + view_update_no_updates_cycles = 0; + } + else + { + view_update_no_updates_cycles++; + } + view_update_pending = false; + Thread.usleep(500000); + } + view_update_interval_started = false; + }); + } + } + + private void update_status() + { + Idle.add(() => { + if(status_changed && loading_sources.size > 0) + { + string[] src_names = {}; + foreach(var s in loading_sources) + { + src_names += s.name; + } + status_text = _("Loading games from %s").printf(string.joinv(", ", src_names)); + } + if(status_text != null && status_text.length > 0) + { + status_overlay.label = status_text; + status_overlay.active = true; + status_overlay.show(); + } + else + { + status_overlay.active = false; + status_overlay.hide(); + } + status_changed = false; + return Source.REMOVE; + }); + } + + private void update_view() + { + show_games(); + + games_grid.invalidate_filter(); + games_list.invalidate_filter(); + + var f = filter.selected; + GameSource? src = null; + if(f > 0) src = sources[f - 1]; + var games = src == null ? games_grid.get_children().length() : src.games_count; + titlebar.subtitle = (src == null ? "" : src.name + ": ") + ngettext("%u game", "%u games", games).printf(games); + + update_status(); + + foreach(var s in sources) + { + filter.set_item_visible(sources.index_of(s) + 1, s.games_count > 0); + } + + games_list_details.preferred_source = src; + + if(src != null && src.games_count == 0) + { + if(src is GameHub.Data.Sources.User.User) + { + empty_alert.title = _("No user-added games"); + empty_alert.description = _("Add some games using plus button"); + } + else + { + empty_alert.title = _("No %s games").printf(src.name); + empty_alert.description = _("Get some Linux-compatible games"); + } + empty_alert.icon_name = src.icon; + stack.set_visible_child(empty_alert); + return; + } + else if(search.text.strip().length > 0) + { + var something_shown = false; + + foreach(var card in games_grid.get_children()) + { + if(games_filter(((GameCard) card).game)) + { + something_shown = true; + break; + } + } + + if(!something_shown) + { + empty_alert.title = _("No games matching “%s”").printf(search.text.strip()); + empty_alert.description = null; + empty_alert.icon_name = null; + if(src != null) + { + empty_alert.title = _("No %1$s games matching “%2$s”").printf(src.name, search.text.strip()); + } + empty_alert.hide_action(); + stack.set_visible_child(empty_alert); + return; + } + } + + var tab = view.selected == 0 ? (Widget) games_grid_scrolled : (Widget) games_list_paned; + stack.set_visible_child(tab); + saved_state.games_view = view.selected == 0 ? Settings.GamesView.GRID : Settings.GamesView.LIST; + + Timeout.add(100, () => { select_first_visible_game(); return Source.REMOVE; }); + } + + private void show_games() + { + if(view.opacity != 0 || stack.visible_child != empty_alert) return; + + view.set_active(saved_state.games_view == Settings.GamesView.LIST ? 1 : 0); + stack.set_visible_child(saved_state.games_view == Settings.GamesView.LIST ? (Widget) games_list_paned : (Widget) games_grid_scrolled); + + view.opacity = 1; + view.sensitive = true; + filter.opacity = 1; + filter.sensitive = true; + search.opacity = 1; + search.sensitive = true; + downloads.opacity = 1; + filters.opacity = 1; + filters.sensitive = true; + add_game_button.opacity = 1; + add_game_button.sensitive = true; + } + + private void add_game(Game g, bool cached=false) + { + Idle.add(() => { + var card = new GameCard(g); + var row = new GameListRow(g); + + games_grid.add(card); + games_list.add(row); + + card.show(); + row.show(); + + if(games_grid.get_children().length() == 0) + { + card.grab_focus(); + } + + if(games_list.get_selected_row() == null) + { + games_list.select_row(games_list.get_row_at_index(0)); + } + + return Source.REMOVE; + }); + + g.tags_update.connect(postpone_view_update); + + if(!cached) + { + //merge_game(g); + new_games_added = true; + } + + postpone_view_update(); + + if(g is Sources.User.UserGame) + { + ((Sources.User.UserGame) g).removed.connect(() => { + remove_game(g); + }); + } + } + + private void load_games() + { + messages.get_children().foreach(c => messages.remove(c)); + + foreach(var src in sources) + { + loading_sources.add(src); + status_changed = true; + src.load_games.begin(add_game, postpone_view_update, (obj, res) => { + src.load_games.end(res); + + loading_sources.remove(src); + + status_changed = true; + + if(loading_sources.size == 0) + { + if(new_games_added) merge_games(); + update_games(); + } + postpone_view_update(); + + if(src.games_count == 0) + { + if(src is GameHub.Data.Sources.Steam.Steam) + { + var msg = message(_("No games were loaded from Steam. Set your games list privacy to public or use your own Steam API key in settings."), MessageType.WARNING); + msg.add_button(_("Privacy"), 1); + msg.add_button(_("Settings"), 2); + + msg.close.connect(() => { + #if GTK_3_22 + msg.revealed = false; + #endif + Timeout.add(250, () => { messages.remove(msg); return false; }); + }); + + msg.response.connect(r => { + switch(r) + { + case 1: + Utils.open_uri("steam://openurl/https://steamcommunity.com/my/edit/settings"); + break; + + case 2: + settings.clicked(); + break; + + case ResponseType.CLOSE: + msg.close(); + break; + } + }); + } + } + }); + } + } + + private void add_view_button(string icon, string tooltip) + { + var image = new Image.from_icon_name(icon, IconSize.MENU); + image.tooltip_text = tooltip; + view.append(image); + } + + private void add_filter_button(string icon, string tooltip) + { + var image = new Image.from_icon_name(icon, IconSize.MENU); + image.tooltip_text = tooltip; + filter.append(image); + } + + private bool games_filter(Game game) + { + if(!ui_settings.show_unsupported_games && !game.is_supported(null, ui_settings.use_compat)) return false; + + var f = filter.selected; + GameSource? src = null; + if(f > 0) src = sources[f - 1]; + + bool same_src = (src == null || game == null || src == game.source); + bool merged_src = false; + + ArrayList? merges = null; + + if(ui_settings.merge_games) + { + merges = Tables.Merges.get(game); + if(!same_src && merges != null && merges.size > 0) + { + foreach(var g in merges) + { + if(g.source == src) + { + merged_src = true; + break; + } + } + } + } + + var tags = filters_popover.selected_tags; + bool tags_all_enabled = tags == null || tags.size == 0 || tags.size == Tables.Tags.TAGS.size; + bool tags_all_except_hidden_enabled = tags != null && tags.size == Tables.Tags.TAGS.size - 1 && !(Tables.Tags.BUILTIN_HIDDEN in tags); + bool tags_match = false; + bool tags_match_merged = false; + + if(!tags_all_enabled) + { + foreach(var tag in tags) + { + tags_match = game.has_tag(tag); + if(tags_match) break; + } + if(!tags_match && merges != null && merges.size > 0) + { + foreach(var g in merges) + { + foreach(var tag in tags) + { + tags_match_merged = g.has_tag(tag); + if(tags_match_merged) break; + } + } + } + } + + bool hidden = game.has_tag(Tables.Tags.BUILTIN_HIDDEN) && (tags == null || tags.size == 0 || !(Tables.Tags.BUILTIN_HIDDEN in tags)); + + return (same_src || merged_src) && (tags_all_enabled || tags_all_except_hidden_enabled || tags_match || tags_match_merged) && !hidden && Utils.strip_name(search.text).casefold() in Utils.strip_name(game.name).casefold(); + } + + private int games_sort(Game game1, Game game2) + { + if(game1 != null && game2 != null) + { + var s1 = game1.status.state; + var s2 = game2.status.state; + + var f1 = game1.has_tag(Tables.Tags.BUILTIN_FAVORITES); + var f2 = game2.has_tag(Tables.Tags.BUILTIN_FAVORITES); + + if(f1 && !f2) return -1; + if(f2 && !f1) return 1; + + if(s1 == Game.State.DOWNLOADING && s2 != Game.State.DOWNLOADING) return -1; + if(s1 != Game.State.DOWNLOADING && s2 == Game.State.DOWNLOADING) return 1; + if(s1 == Game.State.INSTALLING && s2 != Game.State.INSTALLING) return -1; + if(s1 != Game.State.INSTALLING && s2 == Game.State.INSTALLING) return 1; + if(s1 == Game.State.INSTALLED && s2 != Game.State.INSTALLED) return -1; + if(s1 != Game.State.INSTALLED && s2 == Game.State.INSTALLED) return 1; + + switch(filters_popover.sort_mode) + { + case Settings.SortMode.LAST_LAUNCH: + if(game1.last_launch > game2.last_launch) return -1; + if(game1.last_launch < game2.last_launch) return 1; + break; + + case Settings.SortMode.PLAYTIME: + if(game1.playtime > game2.playtime) return -1; + if(game1.playtime < game2.playtime) return 1; + break; + } + + return game1.name.collate(game2.name); + } + return 0; + } + + private void select_first_visible_game() + { + var row = games_list.get_selected_row() as GameListRow?; + if(row != null && games_filter(row.game)) return; + row = games_list.get_row_at_y(32) as GameListRow?; + if(row != null) games_list.select_row(row); + + var cards = games_grid.get_selected_children(); + var card = cards != null && cards.length() > 0 ? cards.first().data as GameCard? : null; + if(card != null && games_filter(card.game)) return; + #if GTK_3_22 + card = games_grid.get_child_at_pos(0, 0) as GameCard?; + #else + card = null; + #endif + if(card != null) + { + games_grid.select_child(card); + if(!search.has_focus) + { + card.grab_focus(); + } + } + } + + private InfoBar message(string text, MessageType type=MessageType.OTHER) + { + var bar = new InfoBar(); + bar.message_type = type; + + #if GTK_3_22 + bar.revealed = false; + #endif + + bar.show_close_button = true; + bar.get_content_area().add(new Label(text)); + + messages.add(bar); + + bar.show_all(); + + #if GTK_3_22 + bar.revealed = true; + #endif + + return bar; + } + + private void remove_game(Game game) + { + Idle.add(() => { + games_list.foreach(r => { + var gr = r as GameListRow; + if(gr.game == game) + { + games_list.remove(gr); + return; + } + }); + games_grid.foreach(c => { + var gc = c as GameCard; + if(gc.game == game) + { + games_grid.remove(gc); + return; + } + }); + postpone_view_update(); + return Source.REMOVE; + }); + } + + private void update_games() + { + if(in_destruction()) return; + status_text = _("Updating game info"); + status_changed = true; + update_status(); + Utils.thread("Updating", () => { + foreach(var src in sources) + { + status_text = _("Updating %s game info").printf(src.name); + status_changed = true; + update_status(); + foreach(var game in src.games) + { + game.update_game_info.begin(); + Thread.usleep(50000); + } + } + status_text = null; + status_changed = true; + update_status(); + }); + } + + private void merge_games() + { + if(!ui_settings.merge_games || in_destruction()) return; + status_text = _("Merging games"); + status_changed = true; + update_status(); + Utils.thread("Merging", () => { + foreach(var src in sources) + { + merge_games_from(src); + } + status_text = null; + status_changed = true; + update_status(); + }); + } + + private void merge_games_from(GameSource src) + { + status_text = _("Merging games from %s").printf(src.name); + status_changed = true; + update_status(); + Utils.thread("Merging-" + src.id, () => { + foreach(var game in src.games) + { + merge_game(game); + } + status_text = null; + status_changed = true; + update_status(); + }); + } + + private void merge_game(Game game) + { + if(!ui_settings.merge_games || in_destruction() || game is Sources.GOG.GOGGame.DLC) return; + status_text = _("Merging %s (%s)").printf(game.name, game.full_id); + status_changed = true; + update_status(); + Utils.thread("Merging-" + game.full_id, () => { + foreach(var src in sources) + { + foreach(var game2 in src.games) + { + merge_game_with_game(src, game, game2); + } + } + status_text = null; + status_changed = true; + update_status(); + }); + } + + private void merge_game_with_game(GameSource src, Game game, Game game2) + { + Utils.thread("Merging-" + game.full_id + "-" + game2.full_id, () => { + if(Game.is_equal(game, game2) || game2 is Sources.GOG.GOGGame.DLC) + { + return; + } + + bool name_match_exact = Utils.strip_name(game.name).casefold() == Utils.strip_name(game2.name).casefold(); + bool name_match_fuzzy_prefix = game.source != src + && (Utils.strip_name(game.name, ":").casefold().has_prefix(Utils.strip_name(game2.name).casefold() + ":") + || Utils.strip_name(game2.name, ":").casefold().has_prefix(Utils.strip_name(game.name).casefold() + ":")); + if(name_match_exact || name_match_fuzzy_prefix) + { + Tables.Merges.add(game, game2); + debug("[Merge] Merging '%s' (%s) with '%s' (%s)", game.name, game.full_id, game2.name, game2.full_id); + + Idle.add(() => { + remove_game(game2); + games_list.foreach(r => { (r as GameListRow).update(); }); + games_grid.foreach(c => { (c as GameCard).update(); }); + return Source.REMOVE; + }); + } + }); + } + + #if MANETTE + private void ui_update_gamepad_mode() + { + Idle.add(() => { + var is_gamepad_connected = connected_gamepads.size > 0 && Gamepad.ButtonPressed; + var widgets_to_show = is_gamepad_connected ? gamepad_mode_visible_widgets : gamepad_mode_hidden_widgets; + var widgets_to_hide = is_gamepad_connected ? gamepad_mode_hidden_widgets : gamepad_mode_visible_widgets; + foreach(var w in widgets_to_show) w.show(); + foreach(var w in widgets_to_hide) w.hide(); + if(is_gamepad_connected) + { + view.selected = 0; + games_grid.grab_focus(); + } + return Source.REMOVE; + }); + } + + private void on_gamepad_connected(Manette.Device device) + { + debug("[Gamepad] '%s' connected", device.get_name()); + device.button_press_event.connect(on_gamepad_button_press_event); + device.button_release_event.connect(on_gamepad_button_release_event); + device.absolute_axis_event.connect(on_gamepad_absolute_axis_event); + connected_gamepads.add(device); + gamepad_axes_to_keys_thread(); + ui_update_gamepad_mode(); + } + + private void on_gamepad_disconnected(Manette.Device device) + { + debug("[Gamepad] '%s' disconnected", device.get_name()); + connected_gamepads.remove(device); + ui_update_gamepad_mode(); + } + + private void on_gamepad_button_press_event(Manette.Device device, Manette.Event e) + { + uint16 btn; + if(!e.get_button(out btn)) return; + on_gamepad_button(btn, true); + } + + private void on_gamepad_button_release_event(Manette.Event e) + { + uint16 btn; + if(!e.get_button(out btn)) return; + on_gamepad_button(btn, false); + } + + private void on_gamepad_button(uint16 btn, bool press) + { + if(Gamepad.Buttons.has_key(btn)) + { + var b = Gamepad.Buttons.get(btn); + b.emit_key_event(press); + debug("[Gamepad] Button %s: %s (%s) [%d]", (press ? "pressed" : "released"), b.name, b.long_name, btn); + ui_update_gamepad_mode(); + + if(!press && b == Gamepad.BTN_HOME && !window.has_focus && !RunnableIsLaunched) + { + window.get_window().focus(Gdk.CURRENT_TIME); + } + } + } + + private void on_gamepad_absolute_axis_event(Manette.Event e) + { + uint16 axis; + double value; + if(!e.get_absolute(out axis, out value)) return; + + if(Gamepad.Axes.has_key(axis)) + { + Gamepad.Axes.get(axis).value = value; + } + } + + private void gamepad_axes_to_keys_thread() + { + if(gamepad_axes_to_keys_thread_running) return; + Utils.thread("GamepadAxesToKeysThread", () => { + gamepad_axes_to_keys_thread_running = true; + while(connected_gamepads.size > 0) + { + foreach(var axis in Gamepad.Axes.values) + { + axis.emit_key_event(); + } + Thread.usleep(Gamepad.KEY_EVENT_EMIT_INTERVAL); + ui_update_gamepad_mode(); + } + Gamepad.reset(); + gamepad_axes_to_keys_thread_running = false; + }); + } + + private Widget gamepad_image(string icon, string? text=null) + { + Widget widget; + + var image = new Image.from_icon_name("controller-button-" + icon, IconSize.LARGE_TOOLBAR); + + if(text != null) + { + var label = new HeaderLabel(text); + var box = new Box(Orientation.HORIZONTAL, 8); + box.margin_start = box.margin_end = 4; + box.add(image); + box.add(label); + box.show_all(); + widget = box; + } + else + { + widget = image; + } + + widget.visible = false; + widget.no_show_all = true; + + gamepad_mode_visible_widgets.add(widget); + return widget; + } + #endif + } +} diff --git a/src/ui/views/WelcomeView.vala b/src/ui/views/WelcomeView.vala index f73880f5..dc23f85b 100644 --- a/src/ui/views/WelcomeView.vala +++ b/src/ui/views/WelcomeView.vala @@ -1,3 +1,21 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using Granite; using GameHub.Data; @@ -6,95 +24,174 @@ using GameHub.Utils; namespace GameHub.UI.Views { public class WelcomeView: BaseView - { + { + private Stack stack; + private Granite.Widgets.AlertView empty_alert; private Granite.Widgets.Welcome welcome; - + private Button skip_btn; - + private Button settings; + + private bool is_updating = false; + construct { + var ui_settings = GameHub.Settings.UI.get_instance(); + + stack = new Stack(); + stack.transition_type = StackTransitionType.CROSSFADE; + + var spinner = new Spinner(); + spinner.active = true; + spinner.set_size_request(36, 36); + spinner.halign = Align.CENTER; + spinner.valign = Align.CENTER; + stack.add(spinner); + + empty_alert = new Granite.Widgets.AlertView(_("No enabled game sources"), _("Enable some game sources in settings"), "dialog-warning"); + empty_alert.show_action(_("Settings")); + + stack.add(empty_alert); + welcome = new Granite.Widgets.Welcome(_("All your games in one place"), _("Let's get started")); - + welcome.activated.connect(index => { on_entry_clicked.begin(index); }); - - add(welcome); - + + stack.add(welcome); + + add(stack); + + titlebar.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + welcome.get_style_context().remove_class(Gtk.STYLE_CLASS_VIEW); + empty_alert.get_style_context().remove_class(Gtk.STYLE_CLASS_VIEW); + skip_btn = new Button.with_label(_("Skip")); - skip_btn.clicked.connect(open_games_grid); - skip_btn.set_sensitive(false); - + skip_btn.clicked.connect(open_games_view); + skip_btn.halign = Align.CENTER; + skip_btn.valign = Align.CENTER; + + settings = new Button(); + settings.tooltip_text = _("Settings"); + settings.image = new Image.from_icon_name("open-menu", IconSize.LARGE_TOOLBAR); + + settings.clicked.connect(() => new Dialogs.SettingsDialog.SettingsDialog()); + empty_alert.action_activated.connect(() => settings.clicked()); + + titlebar.pack_end(settings); titlebar.pack_end(skip_btn); - + + settings.opacity = 0; + settings.sensitive = false; + skip_btn.opacity = 0; + skip_btn.sensitive = false; + foreach(var src in GameSources) { - var image = FSUtils.get_icon(src.icon); - welcome.append_with_pixbuf(image, src.name, ""); + welcome.append(src.icon, src.name, ""); } + update_entries.begin(); } - + public override void on_window_focus() { update_entries.begin(); } - - private void open_games_grid() + + private void open_games_view() { - window.add_view(new GamesGridView()); + window.add_view(new GamesView.GamesView()); } - + private async void update_entries() { - skip_btn.set_sensitive(false); + if(is_updating) return; + is_updating = true; + + skip_btn.sensitive = false; var all_authenticated = true; - + int enabled_sources = 0; + for(int index = 0; index < GameSources.length; index++) { var src = GameSources[index]; - + var btn = welcome.get_button_from_index(index); - + + welcome.set_item_visible(index, !(src is Sources.Humble.Trove) && !(src is Sources.User.User) && src.enabled); + + if(src is Sources.Humble.Trove || src is Sources.User.User || !src.enabled) continue; + enabled_sources++; + if(src.is_installed(true)) { btn.title = src.name; - + if(src.is_authenticated()) { btn.description = _("Ready"); welcome.set_item_sensitivity(index, false); - skip_btn.set_sensitive(true); + skip_btn.sensitive = true; } else { btn.description = _("Authentication required") + src.auth_description; all_authenticated = false; + + if(src.can_authenticate_automatically()) + { + btn.description = _("Authenticating..."); + welcome.set_item_sensitivity(index, false); + yield src.authenticate(); + is_updating = false; + update_entries.begin(); + return; + } } } else { btn.title = _("Install %s").printf(src.name); - btn.description = "Return to GameHub after installing"; + btn.description = _("Return to GameHub after installing"); all_authenticated = false; } } - - if(all_authenticated) + + if(enabled_sources > 0 && all_authenticated) + { + Idle.add(() => { open_games_view(); return false; }); + return; + } + + if(enabled_sources == 0) { - open_games_grid(); + settings.opacity = 0; + settings.sensitive = false; + skip_btn.opacity = 0; + stack.set_visible_child(empty_alert); + empty_alert.show_all(); } - - welcome.show_all(); + else + { + settings.opacity = 1; + settings.sensitive = true; + skip_btn.opacity = 1; + stack.set_visible_child(welcome); + welcome.show_all(); + } + + is_updating = false; } - + private async void on_entry_clicked(int index) { welcome.set_item_sensitivity(index, false); - + GameSource src = GameSources[index]; var installed = src.is_installed(); - + if(installed) { if(!src.is_authenticated()) diff --git a/src/ui/widgets/ActionButton.vala b/src/ui/widgets/ActionButton.vala new file mode 100644 index 00000000..59d87d59 --- /dev/null +++ b/src/ui/widgets/ActionButton.vala @@ -0,0 +1,90 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; + +namespace GameHub.UI.Widgets +{ + class ActionButton: Gtk.Button + { + public string icon { get; construct set; } + public string? icon_overlay { get; construct set; } + public string text { get; construct set; } + public bool show_text { get; construct; default = true; } + + public ActionButton(string icon, string? icon_overlay, string text, bool show_text=true) + { + Object(icon: icon, icon_overlay: icon_overlay, text: text, show_text: show_text); + } + + construct + { + get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + + var box = new Box(Orientation.HORIZONTAL, 8); + + var overlay = new Overlay(); + overlay.valign = Align.START; + overlay.set_size_request(48, 48); + + var image = new Image.from_icon_name(icon, IconSize.DIALOG); + image.set_size_request(48, 48); + overlay.add(image); + + notify["icon"].connect(() => { + image.icon_name = icon; + }); + + if(icon_overlay != null) + { + var overlay_image = new Image.from_icon_name(icon_overlay, IconSize.LARGE_TOOLBAR); + overlay_image.set_size_request(24, 24); + overlay_image.halign = Align.END; + overlay_image.valign = Align.END; + overlay.add_overlay(overlay_image); + overlay.set_overlay_pass_through(overlay_image, true); + notify["icon-overlay"].connect(() => { + overlay_image.icon_name = icon_overlay; + }); + } + + box.add(overlay); + + if(show_text) + { + var label = new Label(text); + label.get_style_context().add_class(Granite.STYLE_CLASS_H3_LABEL); + label.halign = Align.START; + label.valign = Align.CENTER; + label.xalign = 0; + label.ellipsize = Pango.EllipsizeMode.END; + box.add(label); + notify["text"].connect(() => { + label.label = text; + }); + } + + tooltip_text = text; + notify["text"].connect(() => { + tooltip_text = text; + }); + + child = box; + } + } +} diff --git a/src/ui/widgets/AutoSizeImage.vala b/src/ui/widgets/AutoSizeImage.vala new file mode 100644 index 00000000..9c366e87 --- /dev/null +++ b/src/ui/widgets/AutoSizeImage.vala @@ -0,0 +1,113 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; + +namespace GameHub.UI.Widgets +{ + public class AutoSizeImage: DrawingArea + { + private Pixbuf? src; + + private int cmin = 0; + private int cmax = 0; + private float ratio = 1; + private Orientation constraint = Orientation.HORIZONTAL; + + public int corner_radius = 4; + + public void set_constraint(int min, int max, float ratio = 1, Orientation orientation = Orientation.HORIZONTAL) + { + this.constraint = orientation; + this.ratio = ratio; + this.cmin = min; + this.cmax = max; + + switch(constraint) + { + case Orientation.HORIZONTAL: + set_size_request(cmin, (int) (cmin * ratio)); + break; + + case Orientation.VERTICAL: + set_size_request((int) (cmin / ratio), cmin); + break; + } + } + + public void set_source(Pixbuf? buf) + { + src = buf; + } + + public override bool draw(Cairo.Context ctx) + { + ctx.scale(1.0 / scale_factor, 1.0 / scale_factor); + + Allocation rect; + get_allocation(out rect); + + int new_width = 0; + int new_height = 0; + + switch(constraint) + { + case Orientation.HORIZONTAL: + new_width = int.max(cmin, int.min(cmax, rect.width)); + new_height = (int) (new_width * ratio); + break; + + case Orientation.VERTICAL: + new_height = int.max(cmin, int.min(cmax, rect.height)); + new_width = (int) (new_height / ratio); + break; + } + + new_width *= scale_factor; + new_height *= scale_factor; + + if(src != null) + { + Pixbuf pixbuf = src; + + if(src.width > cmin || src.height > cmin || src.width != src.height) + { + pixbuf = src.scale_simple(new_width, new_height, InterpType.BILINEAR); + } + Granite.Drawing.Utilities.cairo_rounded_rectangle(ctx, 0, 0, new_width, new_height, corner_radius * scale_factor); + cairo_set_source_pixbuf(ctx, pixbuf, (new_width - pixbuf.width) / 2, (new_height - pixbuf.height) / 2); + ctx.clip(); + ctx.paint(); + } + + switch(constraint) + { + case Orientation.HORIZONTAL: + set_size_request(cmin, new_height / scale_factor); + break; + + case Orientation.VERTICAL: + set_size_request(new_width / scale_factor, cmin); + break; + } + + return false; + } + } +} diff --git a/src/ui/widgets/CompatToolOptions.vala b/src/ui/widgets/CompatToolOptions.vala new file mode 100644 index 00000000..d6d7b457 --- /dev/null +++ b/src/ui/widgets/CompatToolOptions.vala @@ -0,0 +1,293 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; + +using GameHub.Data; +using GameHub.Utils; + +namespace GameHub.UI.Widgets +{ + public class CompatToolOptions: ListBox + { + private CompatToolPicker compat_tool_picker; + private Runnable game; + private bool install; + private string settings_key; + + public CompatToolOptions(Runnable game, CompatToolPicker picker, bool install = false) + { + this.game = game; + this.compat_tool_picker = picker; + this.install = install; + this.settings_key = install ? "install_options" : "options"; + visible = false; + get_style_context().add_class("tags-list"); + selection_mode = SelectionMode.NONE; + update_options(); + compat_tool_picker.notify["selected"].connect(update_options); + } + + public void update_options() + { + this.foreach(r => r.destroy()); + visible = false; + + if(compat_tool_picker == null || compat_tool_picker.selected == null) return; + var options = install ? compat_tool_picker.selected.install_options : compat_tool_picker.selected.options; + if(options == null) return; + + var tool_settings = game.get_compat_settings(compat_tool_picker.selected); + var ts_options = tool_settings.has_member(settings_key) ? tool_settings.get_object_member(settings_key) : new Json.Object(); + + foreach(var opt in options) + { + if(ts_options != null && ts_options.has_member(opt.name)) + { + if(opt is CompatTool.BoolOption) + { + ((CompatTool.BoolOption) opt).enabled = ts_options.get_boolean_member(opt.name); + } + else if(opt is CompatTool.FileOption) + { + ((CompatTool.FileOption) opt).file = FSUtils.file(ts_options.get_string_member(opt.name)); + } + else if(opt is CompatTool.ComboOption) + { + var val = ts_options.get_string_member(opt.name); + if(val == null || val in ((CompatTool.ComboOption) opt).options) + { + ((CompatTool.ComboOption) opt).value = val; + } + } + else if(opt is CompatTool.StringOption) + { + ((CompatTool.StringOption) opt).value = ts_options.get_string_member(opt.name); + } + } + add(new OptionRow(opt)); + } + + show_all(); + } + + public void save_options() + { + game.compat_options_saved = true; + + if(compat_tool_picker == null || compat_tool_picker.selected == null) return; + var options = install ? compat_tool_picker.selected.install_options : compat_tool_picker.selected.options; + if(options == null) return; + + var tool_settings = game.get_compat_settings(compat_tool_picker.selected); + var ts_options = tool_settings.has_member(settings_key) ? tool_settings.get_object_member(settings_key) : new Json.Object(); + + foreach(var opt in options) + { + if(opt is CompatTool.BoolOption) + { + ts_options.set_boolean_member(opt.name, ((CompatTool.BoolOption) opt).enabled); + } + else if(opt is CompatTool.FileOption) + { + var file = ((CompatTool.FileOption) opt).file; + if(file != null && file.query_exists()) + { + ts_options.set_string_member(opt.name, file.get_path()); + } + else + { + ts_options.remove_member(opt.name); + } + } + else if(opt is CompatTool.ComboOption) + { + var val = ((CompatTool.ComboOption) opt).value; + if(val != null && val in ((CompatTool.ComboOption) opt).options) + { + ts_options.set_string_member(opt.name, val); + } + else + { + ts_options.remove_member(opt.name); + } + } + else if(opt is CompatTool.StringOption) + { + var val = ((CompatTool.StringOption) opt).value; + if(val != null) + { + ts_options.set_string_member(opt.name, val); + } + else + { + ts_options.remove_member(opt.name); + } + } + } + tool_settings.set_object_member(settings_key, ts_options); + game.set_compat_settings(compat_tool_picker.selected, tool_settings); + } + + public class OptionRow: ListBoxRow + { + public CompatTool.Option option { get; construct; } + + public OptionRow(CompatTool.Option option) + { + Object(option: option); + } + + construct + { + var ebox = new EventBox(); + ebox.above_child = true; + + var box = new Box(Orientation.HORIZONTAL, 6); + box.margin_start = box.margin_end = 8; + box.margin_top = box.margin_bottom = 6; + + var name = new Label(option.description); + name.halign = Align.START; + name.xalign = 0; + name.hexpand = true; + + ebox.tooltip_text = option.name; + + Widget? option_widget = null; + + if(option is CompatTool.BoolOption) + { + var bool_option = (CompatTool.BoolOption) option; + + var check = new CheckButton(); + check.margin_end = 2; + check.active = bool_option.enabled; + box.add(check); + + ebox.add_events(EventMask.ALL_EVENTS_MASK); + ebox.button_release_event.connect(e => { + if(e.button == 1) + { + check.active = !check.active; + bool_option.enabled = check.active; + } + return true; + }); + } + else if(option is CompatTool.FileOption) + { + var file_option = (CompatTool.FileOption) option; + + var icon = new Image.from_icon_name("document-open-symbolic", IconSize.MENU); + box.add(icon); + + ebox.above_child = false; + box.margin_top = box.margin_bottom = 2; + box.margin_end = 0; + + var chooser = new FileChooserButton(file_option.description, FileChooserAction.OPEN); + chooser.show_hidden = true; + chooser.set_size_request(170, -1); + if(file_option.file != null || file_option.directory != null) + { + try + { + chooser.select_file(file_option.file ?? file_option.directory); + chooser.tooltip_text = chooser.get_filename(); + } + catch(Error e) + { + warning(e.message); + } + } + chooser.file_set.connect(() => { + file_option.file = chooser.get_file(); + chooser.tooltip_text = chooser.get_filename(); + }); + option_widget = chooser; + } + else if(option is CompatTool.ComboOption) + { + var combo_option = (CompatTool.ComboOption) option; + + var icon = new Image.from_icon_name("view-sort-descending-symbolic", IconSize.MENU); + box.add(icon); + + ebox.above_child = false; + box.margin_top = box.margin_bottom = 2; + box.margin_end = 0; + + var model = new Gtk.ListStore(1, typeof(string)); + Gtk.TreeIter iter; + + foreach(var opt in combo_option.options) + { + model.append(out iter); + model.set(iter, 0, opt); + } + + var combo = new ComboBox.with_model(model); + combo.set_size_request(170, -1); + + var renderer = new CellRendererText(); + combo.pack_start(renderer, true); + combo.add_attribute(renderer, "text", 0); + combo.changed.connect(() => { + Value v; + combo.get_active_iter(out iter); + model.get_value(iter, 0, out v); + combo_option.value = v as string; + }); + combo.active = combo_option.value in combo_option.options ? combo_option.options.index_of(combo_option.value) : 0; + option_widget = combo; + } + else if(option is CompatTool.StringOption) + { + var string_option = (CompatTool.StringOption) option; + + var icon = new Image.from_icon_name("insert-text-symbolic", IconSize.MENU); + box.add(icon); + + ebox.above_child = false; + box.margin_top = box.margin_bottom = 2; + box.margin_end = 0; + + var entry = new Entry(); + entry.set_size_request(170, -1); + if(string_option.value != null) + { + entry.text = string_option.value; + } + entry.notify["text"].connect(() => { string_option.value = entry.text; }); + option_widget = entry; + } + + box.add(name); + + if(option_widget != null) box.add(option_widget); + + ebox.add(box); + + child = ebox; + } + } + } +} diff --git a/src/ui/widgets/CompatToolPicker.vala b/src/ui/widgets/CompatToolPicker.vala new file mode 100644 index 00000000..2d5a94f6 --- /dev/null +++ b/src/ui/widgets/CompatToolPicker.vala @@ -0,0 +1,158 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; + +using GameHub.Data; +using GameHub.Data.DB; + +namespace GameHub.UI.Widgets +{ + public class CompatToolPicker: Box + { + public CompatTool? selected { get; private set; default = null; } + + public Runnable runnable { get; construct; } + public bool install_mode { get; construct; } + + private Gtk.ListStore model; + private int model_size = 0; + private Gtk.TreeIter iter; + private ComboBox combo; + + private Box actions; + + public CompatToolPicker(Runnable runnable, bool install_mode) + { + Object(orientation: Orientation.VERTICAL, spacing: 4, runnable: runnable, install_mode: install_mode); + } + + construct + { + margin_bottom = 4; + + var label = new Label(_("Compatibility layer:")); + label.hexpand = true; + label.xalign = 0; + label.margin_start = label.margin_end = 4; + + model = new Gtk.ListStore(3, typeof(string), typeof(string), typeof(CompatTool)); + + foreach(var tool in CompatTools) + { + if(tool.installed && ((install_mode && tool.can_install(runnable)) || (!install_mode && tool.can_run(runnable)))) + { + model.append(out iter); + model.set(iter, 0, tool.icon); + model.set(iter, 1, tool.name); + model.set(iter, 2, tool); + model_size++; + } + } + + combo = new ComboBox.with_model(model); + combo.halign = Align.END; + + CellRendererPixbuf r_icon = new CellRendererPixbuf(); + combo.pack_start(r_icon, false); + combo.add_attribute(r_icon, "icon-name", 0); + + CellRendererText r_name = new CellRendererText(); + r_name.xpad = 8; + combo.pack_start(r_name, true); + combo.add_attribute(r_name, "text", 1); + + var tool_box = new Box(Orientation.HORIZONTAL, 8); + + tool_box.add(label); + tool_box.add(combo); + + actions = new Box(Orientation.HORIZONTAL, 4); + + combo.changed.connect(() => { + if(model_size == 0) return; + + Value v; + combo.get_active_iter(out iter); + model.get_value(iter, 2, out v); + selected = v as CompatTool; + + if(selected == null) return; + + combo.tooltip_text = selected.executable != null ? selected.executable.get_path() : null; + + if(selected.can_run(runnable)) + { + runnable.compat_tool = selected.id; + runnable.save(); + runnable.update_status(); + } + + actions.foreach(w => w.destroy()); + + actions.hide(); + if(selected.actions != null) + { + foreach(var action in selected.actions) + { + add_action(action); + } + actions.show_all(); + } + }); + + int index = 0; + if(runnable.compat_tool != null && runnable.compat_tool.length > 0) + { + model.foreach((m, p, i) => { + if(model_size == 0) return false; + + Value v; + m.get_value(i, 2, out v); + var tool = v as CompatTool; + if(runnable.compat_tool == tool.id) + { + return true; + } + index++; + return false; + }); + } + if(model_size > 0) + { + combo.active = index < model_size ? index : 0; + } + + add(tool_box); + add(actions); + + show_all(); + } + + private void add_action(CompatTool.Action action) + { + var btn = new Button.with_label(action.name); + btn.tooltip_text = action.description; + btn.hexpand = true; + btn.clicked.connect(() => action.invoke(runnable)); + actions.add(btn); + } + } +} diff --git a/src/ui/widgets/FileChooserEntry.vala b/src/ui/widgets/FileChooserEntry.vala new file mode 100644 index 00000000..6409e2ba --- /dev/null +++ b/src/ui/widgets/FileChooserEntry.vala @@ -0,0 +1,187 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; + +namespace GameHub.UI.Widgets +{ + public class FileChooserEntry: Entry + { + public signal void file_set(); + public signal void uri_set(); + + public File? file { get; protected set; } + public string? uri { get; protected set; } + + public string? title { get; construct; } + public FileChooserAction action { get; construct; } + public bool allow_url { get; construct; } + public bool allow_executable { get; construct; } + + public FileChooser chooser { get; protected set; } + + public FileChooserEntry(string? title, FileChooserAction action, string? icon=null, string? hint=null, bool allow_url=false, bool allow_executable=false) + { + Object(title: title, action: action, allow_url: allow_url, allow_executable: allow_executable); + placeholder_text = primary_icon_tooltip_text = hint; + primary_icon_name = icon ?? (directory_mode ? "folder" : "application-x-executable"); + primary_icon_activatable = false; + secondary_icon_name = "folder-symbolic"; + secondary_icon_activatable = true; + secondary_icon_tooltip_text = directory_mode ? _("Select directory") : _("Select file"); + } + + construct + { + #if GTK_3_22 + chooser = new FileChooserNative(title ?? _("Select file"), GameHub.UI.Windows.MainWindow.instance, action, _("Select"), _("Cancel")); + #else + chooser = new FileChooserDialog(title ?? _("Select file"), GameHub.UI.Windows.MainWindow.instance, action, _("Select"), ResponseType.ACCEPT, _("Cancel"), ResponseType.CANCEL); + #endif + + activate.connect(() => { + select_file_path(text); + }); + focus_out_event.connect(() => { + select_file_path(text); + return false; + }); + + icon_press.connect((icon, event) => { + if(icon == EntryIconPosition.SECONDARY && ((EventButton) event).button == 1) + { + if(run_chooser() == ResponseType.ACCEPT) + { + select_file(chooser.get_file()); + } + } + }); + } + + public void select_file_path(string? path_or_uri) + { + if(path_or_uri == null || path_or_uri.strip().length == 0) + { + text = ""; + chooser.unselect_all(); + file = null; + uri = null; + file_set(); + uri_set(); + return; + } + + var path = path_or_uri.strip(); + + if(allow_url && (path.has_prefix("file://") || path.has_prefix("https://") || path.has_prefix("http://") || path.has_prefix("ftp://"))) + { + uri = path; + if(text.has_prefix("file://")) + { + file = File.new_for_uri(uri); + } + } + else if(path.has_prefix("/")) + { + file = File.new_for_path(path); + uri = file.get_uri(); + } + else if(allow_executable && path.length > 0) + { + var executable = Utils.find_executable(path); + if(executable != null && executable.query_exists()) + { + file = executable; + } + else + { + file = File.new_for_path("/usr/bin/" + text); + } + uri = file.get_uri(); + } + + text = ""; + + if(file != null) + { + if(file.query_exists()) + { + try + { + chooser.select_file(file); + } + catch(Error e) + { + warning("[FileChooserEntry.select_file_path] %s", e.message); + } + } + else + { + chooser.unselect_all(); + } + text = file.get_path(); + } + + if(allow_url) + { + text = uri ?? ""; + } + + scroll_to_end(); + + file_set(); + uri_set(); + } + + public void select_file(File? f) + { + select_file_path(f != null ? f.get_path() : null); + } + + public void reset() + { + select_file_path(null); + } + + private int run_chooser() + { + #if GTK_3_22 + return (chooser as FileChooserNative).run(); + #else + return (chooser as FileChooserDialog).run(); + #endif + } + + private void scroll_to_end() + { + if(cursor_position < text.length) + { + move_cursor(MovementStep.BUFFER_ENDS, 1, false); + } + } + + private bool directory_mode + { + get + { + return action == FileChooserAction.SELECT_FOLDER || action == FileChooserAction.CREATE_FOLDER; + } + } + } +} diff --git a/src/ui/widgets/TagRow.vala b/src/ui/widgets/TagRow.vala new file mode 100644 index 00000000..f365fa10 --- /dev/null +++ b/src/ui/widgets/TagRow.vala @@ -0,0 +1,117 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Granite; + +using GameHub.Data; +using GameHub.Data.DB; + +namespace GameHub.UI.Widgets +{ + public class TagRow: ListBoxRow + { + public Game? game; + public Tables.Tags.Tag tag; + public bool toggles_tag_for_game; + private CheckButton check; + + public TagRow(Tables.Tags.Tag tag, Game? game=null) + { + this.game = game; + this.tag = tag; + this.toggles_tag_for_game = game != null; + + can_focus = true; + + var ebox = new EventBox(); + ebox.above_child = true; + + var box = new Box(Orientation.HORIZONTAL, 8); + box.margin_start = box.margin_end = 8; + box.margin_top = box.margin_bottom = 6; + + check = new CheckButton(); + check.can_focus = false; + + if(toggles_tag_for_game) + { + check.active = game.has_tag(tag); + } + else + { + check.active = tag.selected; + tag.notify["selected"].connect(() => { + check.active = tag.selected; + }); + } + + var name = new Label(tag.name); + name.halign = Align.START; + name.xalign = 0; + name.hexpand = true; + + var icon = new Image.from_icon_name(tag.icon, IconSize.BUTTON); + + box.add(check); + box.add(name); + box.add(icon); + + ebox.add_events(EventMask.BUTTON_RELEASE_MASK); + ebox.button_release_event.connect(e => { + if(e.button == 1) + { + toggle(); + } + return true; + }); + + add_events(EventMask.KEY_RELEASE_MASK); + key_release_event.connect(e => { + switch(((EventKey) e).keyval) + { + case Key.Return: + case Key.space: + case Key.KP_Space: + toggle(); + return true; + } + return false; + }); + + ebox.add(box); + + child = ebox; + } + + private void toggle() + { + if(toggles_tag_for_game) + { + game.toggle_tag(tag); + check.active = game.has_tag(tag); + } + else + { + check.active = !check.active; + tag.selected = check.active; + } + } + } +} diff --git a/src/ui/windows/MainWindow.vala b/src/ui/windows/MainWindow.vala index c605244c..a8f93199 100644 --- a/src/ui/windows/MainWindow.vala +++ b/src/ui/windows/MainWindow.vala @@ -1,62 +1,156 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using GameHub.UI.Views; +using GameHub.Settings; namespace GameHub.UI.Windows { public class MainWindow: Gtk.ApplicationWindow { + public static MainWindow instance; + + private SavedState saved_state; + public HeaderBar titlebar; - private Stack stack; - + public MainWindow(GameHub.Application app) { Object(application: app); + instance = this; } - + construct { + var ui_settings = Settings.UI.get_instance(); + ui_settings.notify["dark-theme"].connect(() => { + Gtk.Settings.get_default().gtk_application_prefer_dark_theme = ui_settings.dark_theme; + }); + ui_settings.notify_property("dark-theme"); + title = "GameHub"; titlebar = new HeaderBar(); titlebar.title = title; titlebar.show_close_button = true; + titlebar.has_subtitle = false; set_titlebar(titlebar); set_default_size(1108, 720); set_size_request(640, 520); - + var vbox = new Box(Orientation.VERTICAL, 0); - + stack = new Stack(); stack.transition_type = StackTransitionType.CROSSFADE; stack.notify["visible-child"].connect(stack_updated); - + add_view(new WelcomeView()); - + vbox.add(stack); - + add(vbox); - + notify["has-toplevel-focus"].connect(() => { current_view.on_window_focus(); }); + + saved_state = SavedState.get_instance(); + + delete_event.connect(() => { quit(); return false; }); + + restore_saved_state(); } - + public void add_view(BaseView view, bool show=true) { view.attach_to_window(this); stack.add(view); - if(show) stack.set_visible_child(view); + if(show) + { + stack.set_visible_child(view); + view.show(); + } stack_updated(); } - + private void stack_updated() { current_view.on_show(); } - + + private void restore_saved_state() + { + if(saved_state.window_width > -1) + default_width = saved_state.window_width; + if(saved_state.window_height > -1) + default_height = saved_state.window_height; + + switch(saved_state.window_state) + { + case Settings.WindowState.MAXIMIZED: + maximize(); + break; + case Settings.WindowState.FULLSCREEN: + fullscreen(); + break; + default: + if(saved_state.window_x > -1 && saved_state.window_y > -1) + move(saved_state.window_x, saved_state.window_y); + break; + } + } + + private void update_saved_state() + { + var state = get_window().get_state(); + if(Gdk.WindowState.MAXIMIZED in state) + { + saved_state.window_state = Settings.WindowState.MAXIMIZED; + } + else if(Gdk.WindowState.FULLSCREEN in state) + { + saved_state.window_state = Settings.WindowState.FULLSCREEN; + } + else + { + saved_state.window_state = Settings.WindowState.NORMAL; + + int width, height; + get_size(out width, out height); + saved_state.window_width = width; + saved_state.window_height = height; + } + + int x, y; + get_position(out x, out y); + saved_state.window_x = x; + saved_state.window_y = y; + } + + private void quit() + { + update_saved_state(); + } + public BaseView current_view { get diff --git a/src/ui/windows/WebAuthWindow.vala b/src/ui/windows/WebAuthWindow.vala index 2dce8ab8..0ad23b47 100644 --- a/src/ui/windows/WebAuthWindow.vala +++ b/src/ui/windows/WebAuthWindow.vala @@ -1,6 +1,25 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using GLib; using WebKit; +using Soup; using GameHub.Utils; namespace GameHub.UI.Windows @@ -9,49 +28,91 @@ namespace GameHub.UI.Windows { private WebView webview; - private bool is_finished = false; + private bool is_finished = false; + + public signal void finished(string url); + public signal void canceled(); - public signal void finished(string url); - public signal void canceled(); + public WebAuthWindow(string source, string url, string? success_url_prefix, string? success_cookie_name=null) + { + Object(transient_for: Windows.MainWindow.instance); - public WebAuthWindow(string source, string url, string success_url_prefix) - { - title = source; - var titlebar = new HeaderBar(); + title = source; + var titlebar = new HeaderBar(); titlebar.title = title; titlebar.show_close_button = true; set_titlebar(titlebar); - - set_size_request(640, 800); - + + var spinner = new Spinner(); + titlebar.pack_end(spinner); + + set_size_request(640, 800); + set_modal(true); - - webview = new WebView(); - - var cookies = FSUtils.expand(FSUtils.Paths.Cache.Cookies); - webview.web_context.get_cookie_manager().set_persistent_storage(cookies, CookiePersistentStorage.TEXT); - - webview.get_settings().enable_mediasource = true; - webview.get_settings().enable_smooth_scrolling = true; - - webview.load_changed.connect(e => { + + debug("[WebAuth/%s] Authenticating at `%s`; success_url_prefix: `%s`; success_cookie_name: `%s`", source, url, success_url_prefix, success_cookie_name); + + webview = new WebView(); + + var cookies_file = FSUtils.expand(FSUtils.Paths.Cache.Cookies); + webview.web_context.get_cookie_manager().set_persistent_storage(cookies_file, CookiePersistentStorage.TEXT); + + webview.get_settings().enable_mediasource = true; + webview.get_settings().enable_smooth_scrolling = true; + webview.get_settings().hardware_acceleration_policy = HardwareAccelerationPolicy.NEVER; + + var style = ".banner,.navigation-container-v2,.tabbar,.base-main-wrapper,.site-footer,.evidon-banner{display:none !important}body{overflow:hidden !important}"; + string[] whitelist = {"https://*.humblebundle.com/*"}; + webview.user_content_manager.add_style_sheet(new UserStyleSheet(style, UserContentInjectedFrames.TOP_FRAME, UserStyleLevel.USER, whitelist, null)); + + webview.load_changed.connect(e => { var uri = webview.get_uri(); titlebar.title = webview.title; titlebar.subtitle = uri.split("?")[0]; - - if(uri.has_prefix(success_url_prefix)) + titlebar.tooltip_text = uri; + + spinner.active = e != LoadEvent.FINISHED; + + debug("[WebAuth/%s] URI: `%s`", source, uri); + + if(!is_finished && success_cookie_name != null) + { + webview.web_context.get_cookie_manager().get_cookies.begin(uri, null, (obj, res) => { + try + { + var cookies = webview.web_context.get_cookie_manager().get_cookies.end(res); + foreach(var cookie in cookies) + { + debug("[WebAuth/%s] [Cookie] `%s`=`%s`", source, cookie.name, cookie.value); + if(!is_finished && cookie.name == success_cookie_name && (success_url_prefix == null || uri.has_prefix(success_url_prefix))) + { + is_finished = true; + var token = cookie.value; + debug("[WebAuth/%s] Finished with result `%s`", source, token); + finished(token); + destroy(); + break; + } + } + } + catch(Error e){} + }); + } + else if(!is_finished && success_url_prefix != null && uri.has_prefix(success_url_prefix)) { is_finished = true; - finished(uri.substring(success_url_prefix.length)); + var token = uri.substring(success_url_prefix.length); + debug("[WebAuth/%s] Finished with result `%s`", source, token); + finished(token); destroy(); } - }); + }); + + webview.load_uri(url); - webview.load_uri(url); - - add(webview); + add(webview); - destroy.connect(() => { if(!is_finished) canceled(); }); - } + destroy.connect(() => { if(!is_finished) canceled(); }); + } } } diff --git a/src/utils/FSOverlay.vala b/src/utils/FSOverlay.vala new file mode 100644 index 00000000..ea11c904 --- /dev/null +++ b/src/utils/FSOverlay.vala @@ -0,0 +1,151 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GLib; +using Gee; + +namespace GameHub.Utils +{ + public class FSOverlay: Object + { + private const string POLKIT_ACTION = ProjectConfig.PROJECT_NAME + ".polkit.overlayfs-helper"; + private const string POLKIT_HELPER = ProjectConfig.BINDIR + "/" + ProjectConfig.PROJECT_NAME + "-overlayfs-helper"; + private static Permission? permission; + + public string id { get; construct set; } + public File target { get; construct set; } + public ArrayList overlays { get; construct set; } + public File? persist { get; construct set; } + public File? workdir { get; construct set; } + + public FSOverlay(File target, ArrayList overlays, File? persist=null, File? workdir=null) + { + Object( + id: ProjectConfig.PROJECT_NAME + "_overlay_" + Utils.md5(target.get_path()), + target: target, overlays: overlays, persist: persist, workdir: workdir + ); + } + + construct + { + if(persist != null && workdir == null) + { + workdir = FSUtils.file(persist.get_parent().get_path(), ".gh." + persist.get_basename() + ".overlay_workdir"); + } + } + + private string? _options; + public string options + { + get + { + if(_options != null) return _options; + + string[] options_arr = {}; + + string[] overlay_dirs = {}; + + for(var i = overlays.size - 1; i >= 0; i--) + { + overlay_dirs += overlays.get(i).get_path(); + } + + options_arr += "lowerdir=" + string.joinv(":", overlay_dirs); + + if(persist != null && workdir != null) + { + options_arr += "upperdir=" + persist.get_path(); + options_arr += "workdir=" + workdir.get_path(); + try + { + if(!persist.query_exists()) persist.make_directory_with_parents(); + if(!workdir.query_exists()) workdir.make_directory_with_parents(); + } + catch(Error e) + { + warning("[FSOverlay.mount] Error while creating directories: %s", e.message); + } + } + + _options = string.joinv(",", options_arr); + return _options; + } + } + + public async void mount() + { + yield umount(); + + try + { + if(!target.query_exists()) target.make_directory_with_parents(); + } + catch(Error e) + { + warning("[FSOverlay.mount] Error while creating target directory: %s", e.message); + } + + yield polkit_authenticate(); + + yield Utils.run_thread({"pkexec", POLKIT_HELPER, "mount", id, options, target.get_path()}); + } + + public async void umount() + { + yield polkit_authenticate(); + + while(id in (yield Utils.run_thread({"mount"}, null, null, false, false))) + { + yield Utils.run_thread({"pkexec", POLKIT_HELPER, "umount", id}); + yield Utils.sleep_async(500); + } + + if(workdir != null && !workdir.query_exists()) + { + FSUtils.rm(workdir.get_path(), null, "-rf"); + } + } + + private async void polkit_authenticate() + { + if(permission == null) + { + try + { + permission = yield new Polkit.Permission(POLKIT_ACTION, null); + } + catch(Error e) + { + warning("[FSOverlay.polkit_authenticate] %s", e.message); + } + } + + if(permission != null && !permission.allowed && permission.can_acquire) + { + try + { + yield permission.acquire_async(); + } + catch(Error e) + { + warning("[FSOverlay.polkit_authenticate] %s", e.message); + } + } + } + } +} diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index b053b8ef..cb1d8553 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -1,69 +1,326 @@ -using Gtk; +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gee; using Gdk; using GLib; +using GameHub.Data; + namespace GameHub.Utils { public class FSUtils { + public const string GAMEHUB_DIR = "_gamehub"; + public const string COMPAT_DATA_DIR = "compat"; + public const string OVERLAYS_DIR = "overlays"; + public const string OVERLAYS_LIST = "overlays.json"; + public class Paths { + public class Settings: Granite.Services.Settings + { + public string steam_home { get; set; } + public string gog_games { get; set; } + public string humble_games { get; set; } + + public string libretro_core_dir { get; set; } + public string libretro_core_info_dir { get; set; } + + public Settings() + { + base(ProjectConfig.PROJECT_NAME + ".paths"); + } + + private static Settings? instance; + public static unowned Settings get_instance() + { + if(instance == null) + { + instance = new Settings(); + } + return instance; + } + } + public class Cache { public const string Home = "~/.cache/com.github.tkashkin.gamehub"; - - public const string Cookies = FSUtils.Paths.Cache.Home + @"/cookies"; - public const string Images = FSUtils.Paths.Cache.Home + @"/images"; + + public const string Cookies = FSUtils.Paths.Cache.Home + "/cookies"; + public const string Images = FSUtils.Paths.Cache.Home + "/images"; + + public const string Database = FSUtils.Paths.Cache.Home + "/gamehub.db"; } - + public class Steam { - public const string Home = "~/.steam"; - public const string Config = FSUtils.Paths.Steam.Home + "/config"; - public const string LoginUsersVDF = FSUtils.Paths.Steam.Config + @"/loginusers.vdf"; + public static string Home + { + owned get + { + /*#if FLATPAK + return "/home/" + Environment.get_user_name() + "/.var/app/com.valvesoftware.Steam/.steam"; + #else*/ + return FSUtils.Paths.Settings.get_instance().steam_home; + //#endif + } + } + public static string Config { owned get { return FSUtils.Paths.Steam.Home + "/steam/config"; } } + public static string LoginUsersVDF { owned get { return FSUtils.Paths.Steam.Config + "/loginusers.vdf"; } } + + public static string SteamApps { owned get { return FSUtils.Paths.Steam.Home + "/steam/steamapps"; } } + public static string LibraryFoldersVDF { owned get { return FSUtils.Paths.Steam.SteamApps + "/libraryfolders.vdf"; } } + } + + public class GOG + { + public static string Games + { + owned get + { + /*#if FLATPAK + return Environment.get_user_data_dir() + "/games/GOG"; + #else*/ + return FSUtils.Paths.Settings.get_instance().gog_games; + //#endif + } + } + } + + public class Humble + { + public static string Games + { + owned get + { + /*#if FLATPAK + return Environment.get_user_data_dir() + "/games/HumbleBundle"; + #else*/ + return FSUtils.Paths.Settings.get_instance().humble_games; + //#endif + } + } + } + + public class Collection: Granite.Services.Settings + { + public string root { get; set; } + + public static string expand_root() + { + return FSUtils.expand(get_instance().root); + } + + public Collection() + { + base(ProjectConfig.PROJECT_NAME + ".paths.collection"); + } + + private static Collection? instance; + public static unowned Collection get_instance() + { + if(instance == null) + { + instance = new Collection(); + } + return instance; + } + + public class GOG: Granite.Services.Settings + { + public string game_dir { get; set; } + public string installers { get; set; } + public string dlc { get; set; } + public string bonus { get; set; } + + public static string expand_game_dir(string game) + { + var g = game.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + return FSUtils.expand(get_instance().game_dir, null, variables); + } + public static string expand_dlc(string game) + { + var g = game.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + variables.set("game_dir", expand_game_dir(g)); + return FSUtils.expand(get_instance().dlc, null, variables); + } + public static string expand_installers(string game, string? dlc=null) + { + var g = game.replace(": ", " - ").replace(":", ""); + var d = dlc == null ? null : dlc.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + if(d == null) + { + variables.set("game_dir", expand_game_dir(g)); + } + else + { + variables.set("game_dir", expand_dlc(g) + "/" + d); + } + return FSUtils.expand(get_instance().installers, null, variables); + } + public static string expand_bonus(string game, string? dlc=null) + { + var g = game.replace(": ", " - ").replace(":", ""); + var d = dlc == null ? null : dlc.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + if(d == null) + { + variables.set("game_dir", expand_game_dir(g)); + } + else + { + variables.set("game_dir", expand_dlc(g) + "/" + d); + } + return FSUtils.expand(get_instance().bonus, null, variables); + } + + public GOG() + { + base(ProjectConfig.PROJECT_NAME + ".paths.collection.gog"); + } + + private static GOG? instance; + public static unowned GOG get_instance() + { + if(instance == null) + { + instance = new GOG(); + } + return instance; + } + } + + public class Humble: Granite.Services.Settings + { + public string game_dir { get; set; } + public string installers { get; set; } + + public static string expand_game_dir(string game) + { + var g = game.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + return FSUtils.expand(get_instance().game_dir, null, variables); + } + public static string expand_installers(string game) + { + var g = game.replace(": ", " - ").replace(":", ""); + var variables = new HashMap(); + variables.set("root", Collection.get_instance().root); + variables.set("game", g); + variables.set("game_dir", expand_game_dir(g)); + return FSUtils.expand(get_instance().installers, null, variables); + } + + public Humble() + { + base(ProjectConfig.PROJECT_NAME + ".paths.collection.humble"); + } + + private static Humble? instance; + public static unowned Humble get_instance() + { + if(instance == null) + { + instance = new Humble(); + } + return instance; + } + } } } - - public static string expand(string path, string file="") + + public static string? expand(string? path, string? file=null, HashMap? variables=null) { - return path.replace("~", Environment.get_home_dir()) + (file != "" ? "/" + file : ""); + if(path == null) return null; + var expanded_path = path; + if(variables != null) + { + foreach(var v in variables.entries) + { + expanded_path = expanded_path.replace("${" + v.key + "}", v.value).replace("$" + v.key, v.value); + } + } + return expanded_path.replace("~/.cache", Environment.get_user_cache_dir()).replace("~", Environment.get_home_dir()) + (file != null && file != "" ? "/" + file : ""); } - - public static File file(string path, string file="") + + public static File? file(string? path, string? file=null, HashMap? variables=null) { - return File.new_for_path(FSUtils.expand(path, file)); + var f = FSUtils.expand(path, file, variables); + return f != null ? File.new_for_path(f) : null; } - - private static void mkdir(string path, string file="") + + public static File? mkdir(string? path, string? file=null, HashMap? variables=null) { try { - var dir = FSUtils.file(path, file); - if(!dir.query_exists()) dir.make_directory_with_parents(); + var dir = FSUtils.file(path, file, variables); + if(dir == null || !dir.query_exists()) dir.make_directory_with_parents(); + return dir; } catch(Error e) { - error(e.message); + warning(e.message); } + return null; } - + + public static void rm(string path, string? file=null, string flags="-f", HashMap? variables=null) + { + Utils.run({"bash", "-c", "rm " + flags + " " + FSUtils.expand(path, file, variables)}); + } + public static void make_dirs() { mkdir(FSUtils.Paths.Cache.Home); mkdir(FSUtils.Paths.Cache.Images); - } - - public static Pixbuf? get_icon(string name, int size=48) - { - try + + #if FLATPAK + var paths = Paths.Settings.get_instance(); + if(paths.steam_home == paths.schema.get_default_value("steam-home").get_string()) { - return new Pixbuf.from_resource_at_scale(@"/com/github/tkashkin/gamehub/icons/$(name).svg", size, size, true); + paths.steam_home = "/home/" + Environment.get_user_name() + "/.var/app/com.valvesoftware.Steam/.steam"; } - catch(Error e) + if(paths.gog_games == paths.schema.get_default_value("gog-games").get_string()) { - error(e.message); + paths.gog_games = Environment.get_user_data_dir() + "/games/GOG"; } - return null; + if(paths.humble_games == paths.schema.get_default_value("humble-games").get_string()) + { + paths.humble_games = Environment.get_user_data_dir() + "/games/HumbleBundle"; + } + #endif + + FSUtils.rm(FSUtils.Paths.Collection.GOG.expand_installers("*"), ".goutputstream-*"); + FSUtils.rm(FSUtils.Paths.Collection.Humble.expand_installers("*"), ".goutputstream-*"); } } } diff --git a/src/utils/Gamepad.vala b/src/utils/Gamepad.vala new file mode 100644 index 00000000..3ebc42e8 --- /dev/null +++ b/src/utils/Gamepad.vala @@ -0,0 +1,269 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gdk; +using Gee; + +namespace GameHub.Utils.Gamepad +{ + public const int KEY_EVENT_EMIT_INTERVAL = 50000; + public const int KEY_UP_EMIT_TIMEOUT = 50000; + + public static HashMap Buttons; + public static HashMap Axes; + public static bool ButtonPressed = false; + + public static Button BTN_A; + public static Button BTN_B; + public static Button BTN_C; + public static Button BTN_X; + public static Button BTN_Y; + public static Button BTN_Z; + + public static Button BUMPER_LEFT; + public static Button BUMPER_RIGHT; + + public static Button TRIGGER_LEFT; + public static Button TRIGGER_RIGHT; + + public static Button STICK_LEFT; + public static Button STICK_RIGHT; + + public static Button BTN_SELECT; + public static Button BTN_START; + public static Button BTN_HOME; + + public static Button DPAD_UP; + public static Button DPAD_DOWN; + public static Button DPAD_LEFT; + public static Button DPAD_RIGHT; + + public static Button SC_PAD_TAP_LEFT; + public static Button SC_PAD_TAP_RIGHT; + + public static Button SC_GRIP_LEFT; + public static Button SC_GRIP_RIGHT; + + public static Axis AXIS_LS_X; + public static Axis AXIS_LS_Y; + public static Axis AXIS_RS_X; + public static Axis AXIS_RS_Y; + + public static void init() + { + Buttons = new HashMap(); + Axes = new HashMap(); + + BTN_A = b(0x130, "A", null, { Key.Return }); + BTN_B = b(0x131, "B", null, { Key.Escape }); + BTN_C = b(0x132, "C"); + BTN_X = b(0x133, "X", null, { Key.Menu }); + BTN_Y = b(0x134, "Y", null, { Key.Alt_L }); + BTN_Z = b(0x135, "Z"); + + BUMPER_LEFT = b(0x136, "LB", "Left Bumper", { Key.F1 }); + BUMPER_RIGHT = b(0x137, "RB", "Right Bumper", { Key.F2 }); + + TRIGGER_LEFT = b(0x138, "LT", "Left Trigger", { Key.Shift_L, Key.Tab }); + TRIGGER_RIGHT = b(0x139, "RT", "Right Trigger", { Key.Tab }); + + BTN_SELECT = b(0x13a, "Select", null, { Key.F5 }); + BTN_START = b(0x13b, "Start", null, { Key.F6 }); + BTN_HOME = b(0x13c, "Home", null, { Key.F7 }); + + STICK_LEFT = b(0x13d, "LS", "Left Stick"); + STICK_RIGHT = b(0x13e, "RS", "Right Stick"); + + DPAD_UP = b(0x220, "Up", "D-Pad Up", { Key.Up }); + DPAD_DOWN = b(0x221, "Down", "D-Pad Down", { Key.Down }); + DPAD_LEFT = b(0x222, "Left", "D-Pad Left", { Key.Left }); + DPAD_RIGHT = b(0x223, "Right", "D-Pad Right", { Key.Right }); + + SC_PAD_TAP_LEFT = b(0x121, "L", "Left Trackpad Touch"); + SC_PAD_TAP_RIGHT = b(0x122, "R", "Right Trackpad Touch"); + + SC_GRIP_LEFT = b(0x150, "LG", "Left Grip"); + SC_GRIP_RIGHT = b(0x151, "RG", "Right Grip"); + + AXIS_LS_X = a(0x0, "LS X", "Left Stick X", Key.Left, Key.Right); + AXIS_LS_Y = a(0x1, "LS Y", "Left Stick Y", Key.Up, Key.Down); + AXIS_RS_X = a(0x2, "RS X", "Right Stick X"); + AXIS_RS_Y = a(0x3, "RS Y", "Right Stick Y"); + } + + private static Button b(uint16 code, string name, string? long_name=null, uint[] keys={}) + { + var btn = new Button(code, name, long_name, keys); + Buttons.set(code, btn); + return btn; + } + + private static Axis a(uint16 code, string name, string? long_name=null, uint negative_key=0, uint positive_key=0, double key_threshold=0.5) + { + var axis = new Axis(code, name, long_name, negative_key, positive_key, key_threshold); + Axes.set(code, axis); + return axis; + } + + public class Button: Object + { + public uint16 code { get; construct; } + public string name { get; construct; } + public string long_name { get; construct; } + public uint[] keys; + + private bool pressed = false; + + public Button(uint16 code, string name, string? long_name=null, uint[] keys={}) + { + Object(code: code, name: name, long_name: long_name ?? name); + this.keys = keys; + } + + public void emit_key_event(bool press) + { + foreach(var key in keys) + { + Gamepad.emit_key_event(key, press); + } + pressed = press; + } + + public void reset() + { + if(pressed) + { + emit_key_event(false); + } + } + } + + public class Axis: Object + { + public uint16 code { get; construct; } + public string name { get; construct; } + public string long_name { get; construct; } + public uint negative_key { get; construct; } + public uint positive_key { get; construct; } + public double key_threshold { get; construct; } + + private double _value = 0; + private int _value_sign = 0; + private int _pressed_sign = 0; + private bool _sign_changed = false; + + private Timer timer = new Timer(); + + public double value + { + get + { + return _value; + } + set + { + int sign = value < -key_threshold ? -1 : (value > key_threshold ? 1 : 0); + _sign_changed = _value_sign == sign; + _value_sign = sign; + _value = value; + } + } + + public Axis(uint16 code, string name, string? long_name=null, uint negative_key=0, uint positive_key=0, double key_threshold=0.5) + { + Object(code: code, name: name, long_name: long_name ?? name, negative_key: negative_key, positive_key: positive_key, key_threshold: key_threshold); + } + + public void emit_key_event() + { + if(negative_key == 0 && positive_key == 0) return; + + ulong last_update; + timer.elapsed(out last_update); + if(_value_sign == 0 && last_update >= Gamepad.KEY_UP_EMIT_TIMEOUT) + { + if(_pressed_sign < 0) Gamepad.emit_key_event(negative_key, false); + if(_pressed_sign > 0) Gamepad.emit_key_event(positive_key, false); + timer.stop(); + _value = 0; + _value_sign = 0; + _pressed_sign = 0; + _sign_changed = false; + return; + } + + if(!_sign_changed) return; + + if(_value_sign < 0) + { + Gamepad.emit_key_event(positive_key, false); + Gamepad.emit_key_event(negative_key, true); + _pressed_sign = -1; + } + else if(_value_sign > 0) + { + Gamepad.emit_key_event(negative_key, false); + Gamepad.emit_key_event(positive_key, true); + _pressed_sign = 1; + } + else + { + if(_pressed_sign < 0) Gamepad.emit_key_event(negative_key, false); + if(_pressed_sign > 0) Gamepad.emit_key_event(positive_key, false); + _pressed_sign = 0; + } + + _sign_changed = false; + timer.start(); + } + + public void reset() + { + value = 0; + emit_key_event(); + } + } + + public static void reset() + { + foreach(var button in Gamepad.Buttons.values) + { + button.reset(); + } + foreach(var axis in Gamepad.Axes.values) + { + axis.reset(); + } + } + + // hack, but works (on X11) + private static void emit_key_event(uint key_code, bool press) + { + if(key_code == 0) return; + foreach(var wnd in Gtk.Window.list_toplevels()) + { + if(wnd.is_active) + { + unowned X.Display xdisplay = (wnd.screen.get_display() as Gdk.X11.Display).get_xdisplay(); + XTest.fake_key_event(xdisplay, xdisplay.keysym_to_keycode((ulong) key_code), press, X.CURRENT_TIME); + Gamepad.ButtonPressed = true; + break; + } + } + } +} diff --git a/src/utils/Parser.vala b/src/utils/Parser.vala index c335d2cc..88721ed8 100644 --- a/src/utils/Parser.vala +++ b/src/utils/Parser.vala @@ -1,3 +1,21 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using GLib; using Gee; using Soup; @@ -6,49 +24,76 @@ namespace GameHub.Utils { public class Parser { - private static string load_file(string path, string file="") + private static Session? session = null; + + public static string load_file(string path, string file="") { - var full_path = FSUtils.expand(path, file); + var f = FSUtils.file(path, file); + if(!f.query_exists()) return ""; string data; try { - FileUtils.get_contents(full_path, out data); + FileUtils.get_contents(f.get_path(), out data); } catch(Error e) { - error(e.message); + warning(e.message); } return data; } - - private static string load_remote_file(string url, string method="GET", string? auth = null) + + private static Message? prepare_message(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { - var session = new Session(); + if(session == null) + { + session = new Session(); + session.timeout = 5; + session.max_conns = 256; + session.max_conns_per_host = 256; + } + var message = new Message(method, url); - + if(auth != null) { - var h = @"Bearer $(auth)"; - print("Authorization: %s\n", h); - message.request_headers.append("Authorization", h); + message.request_headers.append("Authorization", "Bearer " + auth); } - + + if(headers != null) + { + foreach(var header in headers.entries) + { + message.request_headers.append(header.key, header.value); + } + } + + if(data != null) + { + var multipart = new Multipart("multipart/form-data"); + foreach(var v in data.entries) + { + multipart.append_form_string(v.key, v.value); + } + multipart.to_message(message.request_headers, message.request_body); + } + + return message; + } + + public static string load_remote_file(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) + { + var message = prepare_message(url, method, auth, headers, data); + var status = session.send_message(message); if (status == 200) return (string) message.response_body.data; return ""; } - - private static async string load_remote_file_async(string url, string method="GET", string? auth = null) + + public static async string load_remote_file_async(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { var result = ""; - var session = new Session(); - var message = new Message(method, url); - - if(auth != null) - { - message.request_headers.append("Authorization", "Bearer " + auth); - } - + var message = prepare_message(url, method, auth, headers, data); + session.queue_message(message, (s, m) => { if(m.status_code == 200) result = (string) m.response_body.data; Idle.add(load_remote_file_async.callback); @@ -56,90 +101,157 @@ namespace GameHub.Utils yield; return result; } - - public static Json.Object parse_json(string json) + + public static Json.Node parse_json(string? json) { + if(json == null || json.length == 0) return new Json.Node(Json.NodeType.NULL); try { var parser = new Json.Parser(); parser.load_from_data(json); - return parser.get_root().get_object(); + return parser.get_root(); } catch(GLib.Error e) { - error(e.message); + warning(e.message); } - return new Json.Object(); + return new Json.Node(Json.NodeType.NULL); } - - public static Json.Object parse_vdf(string vdf) + + public static Json.Node parse_vdf(string vdf) { return parse_json(vdf_to_json(vdf)); } - - public static Json.Object parse_json_file(string path, string file="") + + public static Json.Node parse_json_file(string path, string file="") { return parse_json(load_file(path, file)); } - - public static Json.Object parse_vdf_file(string path, string file="") + + public static Json.Node parse_vdf_file(string path, string file="") { return parse_vdf(load_file(path, file)); } - - public static Json.Object parse_remote_json_file(string url, string method="GET", string? auth = null) + + public static Json.Node parse_remote_json_file(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { - return parse_json(load_remote_file(url, method, auth)); + return parse_json(load_remote_file(url, method, auth, headers, data)); } - - public static Json.Object parse_remote_vdf_file(string url, string method="GET", string? auth = null) + + public static Json.Node parse_remote_vdf_file(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { - return parse_vdf(load_remote_file(url, method, auth)); + return parse_vdf(load_remote_file(url, method, auth, headers, data)); } - - public static async Json.Object parse_remote_json_file_async(string url, string method="GET", string? auth = null) + + public static async Json.Node parse_remote_json_file_async(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { - return parse_json(yield load_remote_file_async(url, method, auth)); + return parse_json(yield load_remote_file_async(url, method, auth, headers, data)); } - - public static async Json.Object parse_remote_vdf_file_async(string url, string method="GET", string? auth = null) + + public static async Json.Node parse_remote_vdf_file_async(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) { - return parse_vdf(yield load_remote_file_async(url, method, auth)); + return parse_vdf(yield load_remote_file_async(url, method, auth, headers, data)); } - - public static Json.Object? json_object(Json.Object root, string[] keys) + + public static Json.Object? json_object(Json.Node? root, string[] keys) { - Json.Object? obj = root; - + if(root == null || root.get_node_type() != Json.NodeType.OBJECT) return null; + Json.Object? obj = root.get_object(); + foreach(var key in keys) { - if(obj != null && obj.has_member(key)) obj = obj.get_object_member(key); + if(obj != null && obj.has_member(key)) + { + var member = obj.get_member(key); + if(member != null && member.get_node_type() == Json.NodeType.OBJECT) + { + obj = member.get_object(); + } + else obj = null; + } else obj = null; - + if(obj == null) break; } - + return obj; } - + private static string vdf_to_json(string vdf_data) { var json = vdf_data; - + try { var nl_commas = new Regex("(\"|\\})(\\s*?\\r?\\n\\s*?\")"); var semicolons = new Regex("\"(\\s*?\\r?\\n?\\s*?(?:\"|\\{))"); - + json = nl_commas.replace(json, json.length, 0, "\\g<1>,\\g<2>"); json = semicolons.replace(json, json.length, 0, "\":\\g<1>"); } catch(Error e) { - error(e.message); + warning(e.message); } - + return "{" + json + "}"; } + + public static unowned Html.Doc* parse_html(string? html, string url) + { + if(html == null || html.length == 0) return null; + return Html.Doc.read_doc(html, url, null, Html.ParserOption.NOERROR | Html.ParserOption.NOWARNING | Html.ParserOption.RECOVER | Html.ParserOption.NONET); + } + + public static Html.Node* html_node(Html.Node* root, string[] tags) + { + if(root == null) return null; + var obj = root; + + foreach(var tag in tags) + { + if(obj != null) + { + obj = html_subnode(obj, tag); + } + else obj = null; + + if(obj == null) break; + } + + return obj; + } + + public static Html.Node* html_subnode(Xml.Node* root, string name) + { + for(var iter = root->children; iter != null; iter = iter->next) + { + if(iter->type == Xml.ElementType.ELEMENT_NODE) + { + if(iter->name == name) + { + return (Html.Node*) iter; + } + } + } + return null; + } + + public static Html.Doc* parse_html_file(string path, string file="") + { + return parse_html(load_file(path, file), "file://" + path); + } + + public static Html.Doc* parse_remote_html_file(string url, string method="GET", string? auth=null, HashMap? headers=null, HashMap? data=null) + { + return parse_html(load_remote_file(url, method, auth, headers, data), url); + } + + public static string xml_node_to_string(Xml.Node* node) + { + var buf = new Xml.Buffer(); + buf.node_dump(node->doc, node, 0, 1); + return buf.content(); + } } } diff --git a/src/utils/Settings.vala b/src/utils/Settings.vala new file mode 100644 index 00000000..b25253be --- /dev/null +++ b/src/utils/Settings.vala @@ -0,0 +1,203 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using GLib; +using Granite; + +namespace GameHub.Settings +{ + public enum WindowState + { + NORMAL = 0, MAXIMIZED = 1, FULLSCREEN = 2 + } + + public enum GamesView + { + GRID = 0, LIST = 1 + } + + public enum SortMode + { + NAME = 0, LAST_LAUNCH = 1, PLAYTIME = 2; + + public string name() + { + switch(this) + { + case SortMode.NAME: return C_("sort_mode", "By name"); + case SortMode.LAST_LAUNCH: return C_("sort_mode", "By last launch"); + case SortMode.PLAYTIME: return C_("sort_mode", "By playtime"); + } + assert_not_reached(); + } + + public string icon() + { + switch(this) + { + case SortMode.NAME: return "insert-text-symbolic"; + case SortMode.LAST_LAUNCH: return "document-open-recent-symbolic"; + case SortMode.PLAYTIME: return "preferences-system-time-symbolic"; + } + assert_not_reached(); + } + } + + public class SavedState: Granite.Services.Settings + { + public int window_width { get; set; } + public int window_height { get; set; } + public WindowState window_state { get; set; } + public int window_x { get; set; } + public int window_y { get; set; } + + public GamesView games_view { get; set; } + + public SortMode sort_mode { get; set; } + + public SavedState() + { + base(ProjectConfig.PROJECT_NAME + ".saved-state"); + } + + private static SavedState? instance; + public static unowned SavedState get_instance() + { + if(instance == null) + { + instance = new SavedState(); + } + return instance; + } + } + + public class UI: Granite.Services.Settings + { + public bool dark_theme { get; set; } + public bool compact_list { get; set; } + + public bool merge_games { get; set; } + + public bool show_unsupported_games { get; set; } + public bool use_compat { get; set; } + + public bool use_imported_tags { get; set; } + + public UI() + { + base(ProjectConfig.PROJECT_NAME + ".ui"); + } + + private static UI? instance; + public static unowned UI get_instance() + { + if(instance == null) + { + instance = new UI(); + } + return instance; + } + } + + namespace Auth + { + public class Steam: Granite.Services.Settings + { + public bool enabled { get; set; } + public bool authenticated { get; set; } + public string api_key { get; set; } + + public Steam() + { + base(ProjectConfig.PROJECT_NAME + ".auth.steam"); + } + + protected override void verify(string key) + { + switch(key) + { + case "api-key": + if(api_key.length != 32) + { + schema.reset("api-key"); + } + break; + } + } + + + private static Steam? instance; + public static unowned Steam get_instance() + { + if(instance == null) + { + instance = new Steam(); + } + return instance; + } + } + + public class GOG: Granite.Services.Settings + { + public bool enabled { get; set; } + public bool authenticated { get; set; } + public string access_token { get; set; } + public string refresh_token { get; set; } + + public GOG() + { + base(ProjectConfig.PROJECT_NAME + ".auth.gog"); + } + + private static GOG? instance; + public static unowned GOG get_instance() + { + if(instance == null) + { + instance = new GOG(); + } + return instance; + } + } + + public class Humble: Granite.Services.Settings + { + public bool enabled { get; set; } + public bool authenticated { get; set; } + public string access_token { get; set; } + + public bool load_trove_games { get; set; } + + public Humble() + { + base(ProjectConfig.PROJECT_NAME + ".auth.humble"); + } + + private static Humble? instance; + public static unowned Humble get_instance() + { + if(instance == null) + { + instance = new Humble(); + } + return instance; + } + } + } +} diff --git a/src/utils/Utils.vala b/src/utils/Utils.vala index 97389457..098539fc 100644 --- a/src/utils/Utils.vala +++ b/src/utils/Utils.vala @@ -1,3 +1,21 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + using Gtk; using Granite; @@ -6,39 +24,186 @@ namespace GameHub.Utils public delegate void Future(); public delegate void FutureBoolean(bool result); public delegate void FutureResult(T result); - - public static void open_uri(string uri, Window? parent = null) + public delegate void FutureResult2(T t, T2 t2); + + private class Worker + { + public string name; + public Future worker; + public Worker(string name, owned Future worker) + { + this.name = name; + this.worker = (owned) worker; + } + public void run() + { + bool dbg = !name.has_prefix("Merging-"); + if(dbg) debug("[Worker] %s started", name); + worker(); + if(dbg) debug("[Worker] %s finished", name); + } + } + + private static ThreadPool? threadpool = null; + + public static void open_uri(string uri) { try - { + { AppInfo.launch_default_for_uri(uri, null); } catch(Error e) { - error(e.message); + warning(e.message); } } - - public static string run(string cmd) + + public static string run(string[] cmd, string? dir=null, string[]? env=null, bool override_runtime=false, bool log=true) { string stdout; + + var cdir = dir ?? Environment.get_home_dir(); + var cenv = env ?? Environ.get(); + + #if FLATPAK + if(override_runtime && ProjectConfig.RUNTIME.length > 0) + { + cenv = Environ.set_variable(cenv, "LD_LIBRARY_PATH", ProjectConfig.RUNTIME); + } + string[] ccmd = { "flatpak-spawn", "--host" }; + foreach(var arg in cmd) + { + ccmd += arg; + } + #else + string[] ccmd = cmd; + #endif + + try + { + if(log) debug("[Utils.run] {'%s'}; dir: '%s'", string.joinv("' '", cmd), cdir); + Process.spawn_sync(cdir, ccmd, cenv, SpawnFlags.SEARCH_PATH, null, out stdout); + stdout = stdout.strip(); + if(log && stdout.length > 0) print(stdout + "\n"); + } + catch (Error e) + { + warning("[Utils.run] %s", e.message); + } + return stdout; + } + + public static async void run_async(string[] cmd, string? dir=null, string[]? env=null, bool override_runtime=false, bool wait=true, bool log=true) + { + Pid pid; + + var cdir = dir ?? Environment.get_home_dir(); + var cenv = env ?? Environ.get(); + + #if FLATPAK + if(override_runtime && ProjectConfig.RUNTIME.length > 0) + { + cenv = Environ.set_variable(cenv, "LD_LIBRARY_PATH", ProjectConfig.RUNTIME); + } + string[] ccmd = { "flatpak-spawn", "--host" }; + foreach(var arg in cmd) + { + ccmd += arg; + } + #else + string[] ccmd = cmd; + #endif + try { - Process.spawn_command_line_sync(cmd, out stdout); + if(log) debug("[Utils.run_async] Running {'%s'} in '%s'", string.joinv("' '", cmd), cdir); + Process.spawn_async(cdir, ccmd, cenv, SpawnFlags.SEARCH_PATH | SpawnFlags.STDERR_TO_DEV_NULL | SpawnFlags.DO_NOT_REAP_CHILD, null, out pid); + + ChildWatch.add(pid, (pid, status) => { + Process.close_pid(pid); + Idle.add(run_async.callback); + }); } catch (Error e) { - error(e.message); + warning("[Utils.run_async] %s", e.message); } + + if(wait) yield; + } + + public static async string run_thread(string[] cmd, string? dir=null, string[]? env=null, bool override_runtime=false, bool log=true) + { + string stdout = ""; + + Utils.thread("Utils.run_thread", () => { + stdout = Utils.run(cmd, dir, env, override_runtime, log); + Idle.add(run_thread.callback); + }); + + yield; return stdout; } - + + public static File? find_executable(string? name) + { + if(name == null || name.length == 0) return null; + var which = run({"which", name}); + if(which.length == 0 || !which.has_prefix("/")) + { + return null; + } + return File.new_for_path(which); + } + + public static void thread(string name, owned Future worker) + { + try + { + if(threadpool == null) + { + threadpool = new ThreadPool.with_owned_data(w => w.run(), -1, false); + } + threadpool.add(new Worker(name, (owned) worker)); + } + catch(Error e) + { + warning(e.message); + } + } + + public static string get_distro() + { + #if APPIMAGE + return "appimage"; + #elif FLATPAK + return "flatpak"; + #elif SNAP + return "snap"; + #else + if(distro != null) return distro; + distro = Utils.run({"bash", "-c", "lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om"}); + return distro; + #endif + } + + public static string get_language_name() + { + return Posix.nl_langinfo((Posix.NLItem) 786439); // _NL_IDENTIFICATION_LANGUAGE + } + public static bool is_package_installed(string package) { - var output = Utils.run("dpkg-query -W -f=${Status} " + package); + #if APPIMAGE || FLATPAK || SNAP + return false; + #elif PM_APT + var output = Utils.run({"dpkg-query", "-W", "-f=${Status}", package}); return "install ok installed" in output; + #else + return false; + #endif } - + public static async void sleep_async(uint interval, int priority = GLib.Priority.DEFAULT) { Timeout.add(interval, () => { @@ -47,4 +212,75 @@ namespace GameHub.Utils }, priority); yield; } + + public static string md5(string s) + { + return Checksum.compute_for_string(ChecksumType.MD5, s, s.length); + } + + public static async string? cache_image(string url, string prefix="remote") + { + if(url == null || url == "") return null; + var parts = url.split("?")[0].split("."); + var ext = parts.length > 1 ? parts[parts.length - 1] : null; + ext = ext != null && ext.length <= 6 ? "." + ext : null; + var hash = md5(url); + var remote = File.new_for_uri(url); + var cached = FSUtils.file(FSUtils.Paths.Cache.Images, @"$(prefix)_$(hash)$(ext)"); + try + { + if(!cached.query_exists()) + { + yield Downloader.download(remote, cached, null, false); + } + return cached.get_path(); + } + catch(IOError.EXISTS e){} + catch(Error e) + { + warning("Error caching `%s` in `%s`: %s", url, cached.get_path(), e.message); + } + return null; + } + + public static async void load_image(GameHub.UI.Widgets.AutoSizeImage image, string url, string prefix="remote") + { + var cached = yield cache_image(url, prefix); + try + { + image.set_source(cached != null ? new Gdk.Pixbuf.from_file(cached) : null); + } + catch(Error e){} + image.queue_draw(); + } + + private const string NAME_CHARS_TO_STRIP = "!@#$%^&*()-_+=:~`;?'\"<>,./\\|’“”„«»™℠®©"; + public static string strip_name(string name, string? keep=null) + { + if(name == null) return name; + var n = name.strip(); + if(n == "") return n; + unichar c; + for(int i = 0; NAME_CHARS_TO_STRIP.get_next_char(ref i, out c);) + { + if(keep != null && keep != "") + { + unichar k; + for(int j = 0; keep.get_next_char(ref j, out k);) + { + if(k == c) break; + } + if(k == c) continue; + } + n = n.replace(c.to_string(), ""); + } + try + { + n = new Regex(" {2,}").replace(n, n.length, 0, " "); + } + catch(Error e){} + return n.strip(); + } + + private static string? distro; } diff --git a/src/utils/downloader/Downloader.vala b/src/utils/downloader/Downloader.vala new file mode 100644 index 00000000..ff864050 --- /dev/null +++ b/src/utils/downloader/Downloader.vala @@ -0,0 +1,190 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GLib; + +namespace GameHub.Utils.Downloader +{ + public abstract class Downloader: Object + { + private static Downloader? downloader; + + public signal void download_started(Download download); + public signal void downloaded(Download download); + public signal void download_failed(Download download, Error error); + + public signal void dl_started(DownloadInfo info); + public signal void dl_ended(DownloadInfo info); + + public static Downloader? get_instance() + { + if(downloader == null) + { + downloader = new GameHub.Utils.Downloader.Soup.SoupDownloader(); + } + + return downloader; + } + + public abstract async File download(File remote, File local, DownloadInfo? info=null, bool preserve_filename=true) throws Error; + public abstract Download? get_download(File remote); + } + + public static async File download(File remote, File local, DownloadInfo? info=null, bool preserve_filename=true) throws Error + { + File result = local; + Error? error = null; + + var downloader = Downloader.get_instance(); + if(downloader == null) + { + return result; + } + + Utils.thread("Download-" + Utils.md5(remote.get_uri()), () => { + downloader.download.begin(remote, local, info, preserve_filename, (obj, res) => { + try + { + result = downloader.download.end(res); + } + catch(Error e) + { + error = e; + } + Idle.add(download.callback); + }); + }); + + yield; + + if(error != null) throw error; + + return result; + } + + public static Download? get_download(File remote) + { + var downloader = Downloader.get_instance(); + if(downloader == null) return null; + return downloader.get_download(remote); + } + + public static Downloader? get_instance() + { + return Downloader.get_instance(); + } + + public abstract class Download + { + public File remote; + public File local; + public File local_tmp; + + protected DownloadStatus _status = new DownloadStatus(); + public signal void status_change(DownloadStatus status); + + public DownloadStatus status + { + get { return _status; } + set { _status = value; status_change(_status); } + } + + public Download(File remote, File local, File local_tmp) + { + this.remote = remote; + this.local = local; + this.local_tmp = local_tmp; + } + + public abstract void cancel(); + } + + public abstract class PausableDownload: Download + { + public PausableDownload(File remote, File local, File local_tmp) + { + base(remote, local, local_tmp); + } + + public abstract void pause(); + public abstract void resume(); + } + + public class DownloadStatus + { + public DownloadState state; + + public int64 bytes_downloaded; + public int64 bytes_total; + + public DownloadStatus(DownloadState state=DownloadState.STARTING, int64 downloaded = -1, int64 total = -1) + { + this.state = state; + this.bytes_downloaded = downloaded; + this.bytes_total = total; + } + + public double progress + { + get { + return (double) bytes_downloaded / bytes_total; + } + } + + public string description + { + owned get + { + switch(state) + { + case DownloadState.STARTING: return C_("dl_status", "Starting download"); + case DownloadState.STARTED: return C_("dl_status", "Download started"); + case DownloadState.FINISHED: return C_("dl_status", "Download finished"); + case DownloadState.FAILED: return C_("dl_status", "Download failed"); + case DownloadState.DOWNLOADING: + return C_("dl_status", "Downloading: %d%% (%s / %s)").printf((int)(progress * 100), format_size(bytes_downloaded), format_size(bytes_total)); + case DownloadState.PAUSED: + return C_("dl_status", "Paused: %d%% (%s / %s)").printf((int)(progress * 100), format_size(bytes_downloaded), format_size(bytes_total)); + } + return C_("dl_status", "Download cancelled"); + } + } + } + + public enum DownloadState + { + STARTING, STARTED, DOWNLOADING, FINISHED, PAUSED, CANCELLED, FAILED; + } + + public class DownloadInfo: Object + { + public string name { get; construct; } + public string description { get; construct; } + public string? icon { get; construct; } + public string? icon_name { get; construct; } + public string? type_icon { get; construct; } + public string? type_icon_name { get; construct; } + + public Download? download { get; set; } + + public DownloadInfo(string name, string description, string? icon=null, string? icon_name=null, string? type_icon=null, string? type_icon_name=null) + { + Object(name: name, description: description, icon: icon, icon_name: icon_name, type_icon: type_icon, type_icon_name: type_icon_name); + } + } +} diff --git a/src/utils/downloader/SoupDownloader.vala b/src/utils/downloader/SoupDownloader.vala new file mode 100644 index 00000000..96c94038 --- /dev/null +++ b/src/utils/downloader/SoupDownloader.vala @@ -0,0 +1,341 @@ +/* +This file is part of GameHub. +Copyright (C) 2018 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GLib; +using Soup; + +using GameHub.Utils.Downloader; + +namespace GameHub.Utils.Downloader.Soup +{ + public class SoupDownloader: Downloader + { + private Session session; + + private HashTable downloads; + private HashTable dl_info; + + private static string[] supported_schemes = { "http", "https" }; + + public SoupDownloader() + { + downloads = new HashTable(str_hash, str_equal); + dl_info = new HashTable(str_hash, str_equal); + session = new Session(); + session.max_conns = 32; + session.max_conns_per_host = 16; + } + + public override Download? get_download(File remote) + { + return downloads.get(remote.get_uri()); + } + + public override async File download(File remote, File local, DownloadInfo? info=null, bool preserve_filename=true) throws Error + { + var uri = remote.get_uri(); + SoupDownload download = downloads.get(uri); + + if(download != null) return yield await_download(download); + + if(local.query_exists()) + { + debug("[SoupDownloader] '%s' is already downloaded", uri); + return local; + } + + var tmp = File.new_for_path(local.get_path() + "~"); + + download = new SoupDownload(remote, local, tmp); + download.session = session; + downloads.set(uri, download); + + download_started(download); + + if(info != null) + { + info.download = download; + dl_info.set(uri, info); + dl_started(info); + } + + debug("[SoupDownloader] Downloading '%s'...", uri); + + try + { + if(remote.get_uri_scheme() in supported_schemes) + yield download_from_http(download, preserve_filename); + else + yield download_from_filesystem(download); + } + catch(IOError.CANCELLED error) + { + download.status = new DownloadStatus(DownloadState.CANCELLED); + if(info != null) dl_ended(info); + throw error; + } + catch(Error error) + { + download.status = new DownloadStatus(DownloadState.FAILED); + download_failed(download, error); + if(info != null) dl_ended(info); + throw error; + } + finally + { + downloads.remove(uri); + dl_info.remove(uri); + } + + if(download.local_tmp.query_exists()) + { + download.local_tmp.move(download.local, FileCopyFlags.OVERWRITE); + } + + debug("[SoupDownloader] Downloaded '%s'", uri); + + downloaded(download); + if(info != null) dl_ended(info); + + return download.local; + } + + private async void download_from_http(SoupDownload download, bool preserve_filename=true) throws Error + { + var msg = new Message("GET", download.remote.get_uri()); + msg.response_body.set_accumulate(false); + + download.session = session; + download.message = msg; + + #if !FLATPAK + var address = msg.get_address(); + var connectable = new NetworkAddress(address.name, (uint16) address.port); + var network_monitor = NetworkMonitor.get_default(); + if(!(yield network_monitor.can_reach_async(connectable))) + throw new IOError.HOST_UNREACHABLE("Failed to reach host"); + #endif + + GLib.Error? err = null; + + FileOutputStream? local_stream = null; + + int64 dl_bytes = 0; + int64 dl_bytes_total = 0; + + #if SOUP_2_60 + int64 resume_from = 0; + var resume_dl = false; + + if(download.local_tmp.get_basename().has_suffix("~") && download.local_tmp.query_exists()) + { + var info = yield download.local_tmp.query_info_async(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); + resume_from = info.get_size(); + if(resume_from > 0) + { + resume_dl = true; + msg.request_headers.set_range(resume_from, -1); + debug(@"[SoupDownloader] Download part found, size: $(resume_from)"); + } + } + #endif + + msg.got_headers.connect(() => { + dl_bytes_total = msg.response_headers.get_content_length(); + debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); + try + { + if(preserve_filename) + { + string filename = null; + string disposition = null; + HashTable dparams = null; + + if(msg.response_headers.get_content_disposition(out disposition, out dparams)) + { + if(disposition == "attachment" && dparams != null) + { + filename = dparams.get("filename"); + if(filename != null) + { + debug(@"[SoupDownloader] Content-Disposition: filename=%s", filename); + } + } + } + + if(filename == null) + { + filename = download.remote.get_basename(); + } + + if(filename != null) + { + download.local = download.local.get_parent().get_child(filename); + if(download.local.query_exists()) + { + debug(@"[SoupDownloader] '%s' exists", download.local.get_path()); + var info = download.local.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); + if(info.get_size() == dl_bytes_total) + { + session.cancel_message(msg, Status.OK); + return; + } + } + debug(@"[SoupDownloader] Downloading to '%s'", download.local.get_path()); + } + } + + #if SOUP_2_60 + int64 rstart = -1, rend = -1; + if(resume_dl && msg.response_headers.get_content_range(out rstart, out rend, out dl_bytes_total)) + { + debug(@"[SoupDownloader] Content-Range is supported($(rstart)-$(rend)), resuming from $(resume_from)"); + debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); + dl_bytes = resume_from; + local_stream = download.local_tmp.append_to(FileCreateFlags.NONE); + } + else + #endif + { + local_stream = download.local_tmp.replace(null, false, FileCreateFlags.REPLACE_DESTINATION); + } + } + catch(Error e) + { + warning(e.message); + } + }); + + msg.got_chunk.connect((msg, chunk) => { + if(session.would_redirect(msg) || local_stream == null) return; + + dl_bytes += chunk.length; + try + { + local_stream.write(chunk.data); + download.status = new DownloadStatus(DownloadState.DOWNLOADING, dl_bytes, dl_bytes_total); + } + catch(Error e) + { + err = e; + session.cancel_message(msg, Status.CANCELLED); + } + }); + + session.queue_message(msg, (session, msg) => { + download_from_http.callback(); + }); + + yield; + + if(local_stream == null) return; + + yield local_stream.close_async(Priority.DEFAULT); + + if(msg.status_code != Status.OK && msg.status_code != Status.PARTIAL_CONTENT) + { + if(msg.status_code == Status.CANCELLED) + { + throw new IOError.CANCELLED("Download cancelled by user"); + } + + if(err == null) + err = new GLib.Error(http_error_quark(), (int) msg.status_code, msg.reason_phrase); + + throw err; + } + } + + private async File? await_download(SoupDownload download) throws Error + { + File downloaded_file = null; + Error download_error = null; + + SourceFunc callback = await_download.callback; + var downloaded_id = downloaded.connect((downloader, downloaded) => { + if(downloaded.remote.get_uri() != download.remote.get_uri()) return; + downloaded_file = downloaded.local_tmp; + callback(); + }); + var downloaded_failed_id = download_failed.connect((downloader, failed_download, error) => { + if(failed_download.remote.get_uri() != download.remote.get_uri()) return; + download_error = error; + callback(); + }); + + yield; + + disconnect(downloaded_id); + disconnect(downloaded_failed_id); + + if(download_error != null) throw download_error; + + return downloaded_file; + } + + private async void download_from_filesystem(SoupDownload download) throws GLib.Error + { + try + { + debug("[SoupDownloader] Copying '%s' to '%s'", download.remote.get_path(), download.local_tmp.get_path()); + yield download.remote.copy_async( + download.local_tmp, + FileCopyFlags.OVERWRITE, + Priority.DEFAULT, + null, + (current, total) => { download.status = new DownloadStatus(DownloadState.DOWNLOADING, current, total); }); + } + catch(IOError.EXISTS error){} + } + } + + public class SoupDownload: PausableDownload + { + public Session? session; + public Message? message; + + public SoupDownload(File remote, File local, File local_tmp) + { + base(remote, local, local_tmp); + } + + public override void pause() + { + if(session != null && message != null && _status.state == DownloadState.DOWNLOADING) + { + session.pause_message(message); + _status.state = DownloadState.PAUSED; + status_change(_status); + } + } + public override void resume() + { + if(session != null && message != null && _status.state == DownloadState.PAUSED) + { + session.unpause_message(message); + } + } + public override void cancel() + { + if(session != null && message != null) + { + session.cancel_message(message, Status.CANCELLED); + } + } + } +}