diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..8d86e4518e450377768d1de80adafba87984a148 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.metadata b/.metadata deleted file mode 100644 index 182cccafd96b59814f686d06695a1a60af003e8d..0000000000000000000000000000000000000000 --- a/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 78910062997c3a836feee883712c241a5fd22983 - channel: stable - -project_type: app diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index f288702d2fa16d3cdf0035b15a9fcbc552cd88e7..0000000000000000000000000000000000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU 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 <https://www.gnu.org/licenses/>. - -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: - - <program> Copyright (C) <year> <name of author> - 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 -<https://www.gnu.org/licenses/>. - - 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 -<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/README.md b/README.md index a0c89b96fa7cf70ac3dbc38d0b99b5b97cfd98b6..ed4d38e96c23004b6a210a3e61c235df782eb510 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,2 @@ Hangman -======== -GNU General Public License (GPLv3). diff --git a/android/app/build.gradle b/android/app/build.gradle index 148eac42d6cdd4256a3201f31c294ca3b64a2759..6399b0b1a7236cedb384ced2332873e5f4988fb1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 namespace "org.benoitharrault.hangman" defaultConfig { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a68012e7b467604dd3a36fe3447b18866219dd26..39852e853e9357f8bfcd654b5bdf7a64e6ee2f62 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.benoitharrault.hangman"> - <!-- io.flutter.app.FlutterApplication is an android.app.Application that - calls FlutterMain.startInitialization(this); in its onCreate method. - In most cases you can leave this as-is, but you if you want to provide - additional functionality it is fine to subclass or reimplement - FlutterApplication and put your custom class here. --> <uses-permission android:name="android.permission.INTERNET"/> <application android:label="Hangman" @@ -16,19 +11,10 @@ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> - <!-- Specifies an Android theme to apply to this Activity as soon as - the Android process has started. This theme is visible to the user - while the Flutter UI initializes. After that, this theme continues - to determine the Window background behind the Flutter UI. --> <meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" /> - <!-- Displays an Android View that continues showing the launch screen - Drawable until Flutter paints its first frame, then this splash - screen fades out. A splash screen is useful to avoid any visual - gap between the end of Android's launch screen and the painting of - Flutter's first frame. --> <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background" @@ -38,8 +24,6 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> - <!-- Don't delete the meta-data below. - This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" android:value="2" /> diff --git a/android/app/src/main/java/com/hangman/MainActivity.java b/android/app/src/main/java/org/benoitharrault/hangman/MainActivity.java similarity index 100% rename from android/app/src/main/java/com/hangman/MainActivity.java rename to android/app/src/main/java/org/benoitharrault/hangman/MainActivity.java diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..428eb361622fc80a398549107f435d3608d8cbe8 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="?android:colorBackground" /> + + <item> + <bitmap + android:gravity="center" + android:src="@mipmap/launch_image" /> + </item> +</layer-list> diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000000000000000000000000000000000..c6e7031f33f4c950119a059603cf058d16983dd3 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> + <item name="android:windowBackground">@drawable/launch_background</item> + </style> + <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> + <item name="android:windowBackground">?android:colorBackground</item> + </style> +</resources> diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index d1dab0397ac46cebc1617cc6dace23e3a41ee3a8..ff81bae863800e62cfd23ccf572c4ba607128496 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> + <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar"> <item name="android:windowBackground">@drawable/launch_background</item> </style> - <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"> - <item name="android:windowBackground">@android:color/white</item> + <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> + <item name="android:windowBackground">?android:colorBackground</item> </style> </resources> diff --git a/android/gradle.properties b/android/gradle.properties index f522c400d72b2afc43382fa45d82adf1ac156317..7f924def6fbe9a6322f047c8511363349ba0baf1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=1.2.16 -app.versionCode=27 +app.versionName=1.2.17 +app.versionCode=28 diff --git a/assets/files/word-list-fr.json b/assets/files/word-list-fr.json deleted file mode 100644 index 6892b65ca48889cf8c0ffd276780cbbaa76eaf31..0000000000000000000000000000000000000000 --- a/assets/files/word-list-fr.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "words-list": [ - { - "category": "ANIMAUX", - "clue": "Animal", - "words": [ - "ANACONDA", - "AUTRUCHE", - "BARRACUDA", - "BOUQUETIN", - "COCCINELLE", - "CROCODILE", - "DROMADAIRE", - "ELEPHANT", - "ESCARGOT", - "FOURMILIER", - "GRENOUILLE", - "HIPPOCAMPE", - "HIPPOPOTAME", - "KANGOUROU", - "LIBELLULE", - "PERROQUET", - "PIPISTRELLE", - "RHINOCEROS", - "SAUTERELLE", - "TARENTULE" - ] - }, - { - "category": "FRUITS", - "clue": "Fruit", - "words": [ - "AUBERGINE", - "BETTERAVE", - "CITROUILLE", - "CONCOMBRE", - "FRAMBOISE", - "GROSEILLE", - "MANDARINE", - "MIRABELLE", - "MYRTILLE", - "PAMPLEMOUSSE" - ] - }, - { - "category": "METIERS", - "clue": "Métier", - "words": [ - "AGRICULTEUR", - "ARCHEOLOGUE", - "ARCHITECTE", - "ASTRONAUTE", - "BIJOUTIER", - "BIOLOGISTE", - "CHARCUTIER", - "CHARPENTIER", - "CUISINIER", - "ELECTRICIEN", - "HORTICULTEUR", - "INFIRMIER", - "MECANICIEN", - "MENUISIER", - "METEOROLOGUE", - "PHOTOGRAPHE", - "PROFESSEUR", - "STANDARDISTE", - "VETERINAIRE", - "VOLCANOLOGUE" - ] - }, - { - "category": "GEOGRAPHIE", - "clue": "Géographie", - "words": [ - "ALLEMAGNE", - "ANTARCTIQUE", - "ARGENTINE", - "ATLANTIQUE", - "AUSTRALIE", - "EMBOUCHURE", - "HEMISPHERE", - "HYDROGRAPHIE", - "KILIMANDJARO", - "LUXEMBOURG", - "MADAGASCAR", - "MEDITERRANEE", - "MISSISSIPPI", - "NORMANDIE", - "PACIFIQUE", - "PLANISPHERE", - "STRASBOURG", - "SUPERFICIE", - "VENEZUELA", - "WASHINGTON" - ] - }, - { - "category": "COULEURS", - "clue": "Couleur", - "words": [ - "ROUGE", - "BLEU", - "VERT", - "JAUNE", - "VIOLET", - "ORANGE", - "MARRON", - "NOIR", - "BLANC", - "TURQUOISE", - "BEIGE", - "ROSE" - ] - }, - { - "category": "FLEURS", - "clue": "Fleur", - "words": [ - "ROSE", - "PIVOINE", - "TULIPE", - "JONQUILLE", - "CACTUS" - ] - }, - { - "category": "SPORTS", - "clue": "Sport ou jeu", - "words": [ - "GYMNASTIQUE", - "FOOTBALL", - "HANDBALL", - "COURSE", - "CYCLISME", - "RANDONNEE" - ] - }, - { - "category": "ALIMENTS", - "clue": "Aliment ou plat", - "words": [ - "FROMAGE", - "PIZZA", - "SAUCISSON", - "JAMBON", - "SALAMI", - "PAELLA", - "PATES", - "SALADE", - "SOUPE", - "CHOCOLAT", - "OEUF", - "CREME", - "LAIT", - "CORNICHON", - "FLAN", - "TARTE", - "PUREE", - "SAUMON", - "SANDWICH" - ] - }, - { - "category": "VEHICULE", - "clue": "Véhicule ou moyen de transport", - "words": [ - "VOITURE", - "MOTO", - "VELO", - "TRAIN", - "BATEAU", - "AVION", - "HELICOPTERE", - "AUTOBUS", - "CAR", - "TRAINEAU", - "FUSEE", - "VOILIER", - "PAQUEBOT", - "METRO", - "SOUS-MARIN", - "CAMION", - "TRACTEUR", - "KAYAK" - ] - } - ] -} diff --git a/assets/fonts/Nunito-Bold.ttf b/assets/fonts/Nunito-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6519feb781449ebe0015cbc74dfd9e13110fbba9 Binary files /dev/null and b/assets/fonts/Nunito-Bold.ttf differ diff --git a/assets/fonts/Nunito-Light.ttf b/assets/fonts/Nunito-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8a0736c41cd6c2a1225d356bf274de1d0afc3497 Binary files /dev/null and b/assets/fonts/Nunito-Light.ttf differ diff --git a/assets/fonts/Nunito-Medium.ttf b/assets/fonts/Nunito-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..88fccdc0638b6f5d6ac49d9d269dc3d518618ad1 Binary files /dev/null and b/assets/fonts/Nunito-Medium.ttf differ diff --git a/assets/fonts/Nunito-Regular.ttf b/assets/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7b8375a896ef0cd8e06730a78c84532b377e784 Binary files /dev/null and b/assets/fonts/Nunito-Regular.ttf differ diff --git a/assets/images/gameover.png b/assets/images/gameover.png deleted file mode 100644 index 97e98b39d94c4934e1a731d6b053b56bf7de0ed8..0000000000000000000000000000000000000000 Binary files a/assets/images/gameover.png and /dev/null differ diff --git a/assets/images/img1.png b/assets/images/img1.png deleted file mode 100644 index 1cdc24f0f8c88ecf576101517462b9b8c9ea4767..0000000000000000000000000000000000000000 Binary files a/assets/images/img1.png and /dev/null differ diff --git a/assets/images/img2.png b/assets/images/img2.png deleted file mode 100644 index 7ee894b8b2e5847c374f398ef376ce7481838129..0000000000000000000000000000000000000000 Binary files a/assets/images/img2.png and /dev/null differ diff --git a/assets/images/img3.png b/assets/images/img3.png deleted file mode 100644 index cf02d9bd0a49e9c5f7ab65bb763e14cb02da05e8..0000000000000000000000000000000000000000 Binary files a/assets/images/img3.png and /dev/null differ diff --git a/assets/images/img4.png b/assets/images/img4.png deleted file mode 100644 index 1017ccb260a995e1099335f8e3d737684200fa91..0000000000000000000000000000000000000000 Binary files a/assets/images/img4.png and /dev/null differ diff --git a/assets/images/img5.png b/assets/images/img5.png deleted file mode 100644 index bd09508eb49a7ac5e099826804f458161284f43d..0000000000000000000000000000000000000000 Binary files a/assets/images/img5.png and /dev/null differ diff --git a/assets/images/img6.png b/assets/images/img6.png deleted file mode 100644 index 4f9d5b119f47e5db3ddcfc4f3fa97bd7e6d3d9c6..0000000000000000000000000000000000000000 Binary files a/assets/images/img6.png and /dev/null differ diff --git a/assets/images/img7.png b/assets/images/img7.png deleted file mode 100644 index ceadc43ae13aed5d45c7e65d8dff823753acb207..0000000000000000000000000000000000000000 Binary files a/assets/images/img7.png and /dev/null differ diff --git a/assets/images/img8.png b/assets/images/img8.png deleted file mode 100644 index 9b7b8db3a3cebba376ba7e7f1b40a7622eaee009..0000000000000000000000000000000000000000 Binary files a/assets/images/img8.png and /dev/null differ diff --git a/assets/images/img9.png b/assets/images/img9.png deleted file mode 100644 index 9d6ec80ac63304397349443881eb17fe319fab91..0000000000000000000000000000000000000000 Binary files a/assets/images/img9.png and /dev/null differ diff --git a/assets/skins/default_img1.png b/assets/skins/default_img1.png new file mode 100644 index 0000000000000000000000000000000000000000..9271f8beb8f9532818341ceb75d8cbb2ed9aa481 Binary files /dev/null and b/assets/skins/default_img1.png differ diff --git a/assets/skins/default_img2.png b/assets/skins/default_img2.png new file mode 100644 index 0000000000000000000000000000000000000000..82cb8dcb2701c6bcb473975d5cb1a6dcc0d2aaf7 Binary files /dev/null and b/assets/skins/default_img2.png differ diff --git a/assets/skins/default_img3.png b/assets/skins/default_img3.png new file mode 100644 index 0000000000000000000000000000000000000000..9aabea90a3e180829f609ce0da4dfb40b4762896 Binary files /dev/null and b/assets/skins/default_img3.png differ diff --git a/assets/skins/default_img4.png b/assets/skins/default_img4.png new file mode 100644 index 0000000000000000000000000000000000000000..5de0b9da5b21f9ba50097b865a4b051431d56c67 Binary files /dev/null and b/assets/skins/default_img4.png differ diff --git a/assets/skins/default_img5.png b/assets/skins/default_img5.png new file mode 100644 index 0000000000000000000000000000000000000000..9aa5c5cefe1d560ed2991dc14d92985c86c4b14d Binary files /dev/null and b/assets/skins/default_img5.png differ diff --git a/assets/skins/default_img6.png b/assets/skins/default_img6.png new file mode 100644 index 0000000000000000000000000000000000000000..39ba063999584c5cbeb3b42dc4afc3fbccb84256 Binary files /dev/null and b/assets/skins/default_img6.png differ diff --git a/assets/skins/default_img7.png b/assets/skins/default_img7.png new file mode 100644 index 0000000000000000000000000000000000000000..3cfef2247142d6683dc2512c6fedc7e3d57dd4d0 Binary files /dev/null and b/assets/skins/default_img7.png differ diff --git a/assets/skins/default_img8.png b/assets/skins/default_img8.png new file mode 100644 index 0000000000000000000000000000000000000000..de0079db4875944a56288f5b0d631389a0116afe Binary files /dev/null and b/assets/skins/default_img8.png differ diff --git a/assets/skins/default_img9.png b/assets/skins/default_img9.png new file mode 100644 index 0000000000000000000000000000000000000000..c96a2f9afef971f07dde3b403edc6a3d6e09185f Binary files /dev/null and b/assets/skins/default_img9.png differ diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..2517d964c4ce39900f58d0e80073c4f17cdb5d2f --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,12 @@ +{ + "app_name": "Hangman", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "Informations", + "about_content": "Hangman", + "about_version": "Version: {version}", + + "": "" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..c10b7472dbf48d085a967f92dacef11f26119ab1 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,12 @@ +{ + "app_name": "Le Pendu", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Le Pendu.", + "about_version": "Version : {version}", + + "": "" +} diff --git a/assets/ui/button_back.png b/assets/ui/button_back.png new file mode 100644 index 0000000000000000000000000000000000000000..cc48ffb1dbb653d9a996f139dfbe02969724bfa5 Binary files /dev/null and b/assets/ui/button_back.png differ diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d Binary files /dev/null and b/assets/ui/button_resume_game.png differ diff --git a/assets/ui/button_start.png b/assets/ui/button_start.png new file mode 100644 index 0000000000000000000000000000000000000000..6845e2f5c21598ab61f1684d2075aeec0334bf23 Binary files /dev/null and b/assets/ui/button_start.png differ diff --git a/assets/ui/game_fail.png b/assets/ui/game_fail.png new file mode 100644 index 0000000000000000000000000000000000000000..93f2801f9d6bb2ce508e1293cd64d6ff2e9970ec Binary files /dev/null and b/assets/ui/game_fail.png differ diff --git a/assets/ui/game_win.png b/assets/ui/game_win.png new file mode 100644 index 0000000000000000000000000000000000000000..876334279c1711b349a62131a33607eecf924eb6 Binary files /dev/null and b/assets/ui/game_win.png differ diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/ui/placeholder.png differ diff --git a/fdroid_metadata.yml b/fdroid_metadata.yml index 5d0a98c47ef8d0ab8c94de5a88a1aee1d29acf68..4152397802a427fa7b6a5b806f9c76549d962a47 100644 --- a/fdroid_metadata.yml +++ b/fdroid_metadata.yml @@ -5,9 +5,9 @@ Name: Hangman Game AutoName: Hangman License: GPL-3.0-only WebSite: 'https://git.harrault.fr/android/hangman' -SourceCode: https://git.harrault.fr/android/hangman -IssueTracker: https://git.harrault.fr/android/hangman/issues -Changelog: https://git.harrault.fr/android/hangman/-/tags +SourceCode: 'https://git.harrault.fr/android/hangman' +IssueTracker: 'https://git.harrault.fr/android/hangman/issues' +Changelog: 'https://git.harrault.fr/android/hangman/-/tags' Summary: '' Description: |- diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..0000000000000000000000000000000000000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 442d9132ea32808ad980df4bd233b359f76341a7..0000000000000000000000000000000000000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 4f906e0c811fc9e230eb44819f509cd0627f2600..0000000000000000000000000000000000000000 --- a/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index ac1b06f93825db68fb0c0b5150917f340eaa5d02..0000000000000000000000000000000000000000 --- a/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh deleted file mode 100755 index 218080d1eb12952690ba1e182a468f6e3cf03d65..0000000000000000000000000000000000000000 --- a/icons/build_game_icons.sh +++ /dev/null @@ -1,98 +0,0 @@ -#! /bin/bash - -# Check dependencies -command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } -command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } -command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } - -CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" -ASSETS_DIR="${BASE_DIR}/assets" - -OPTIPNG_OPTIONS="-preserve -quiet -o7" -ICON_SIZE=192 - -####################################################### - -# Game images -AVAILABLE_GAME_IMAGES=" -" - -# Settings images -AVAILABLES_GAME_SETTINGS=" -" - -####################################################### - -# optimize svg -function optimize_svg() { - SOURCE="$1" - - cp ${SOURCE} ${SOURCE}.tmp - scour \ - --remove-descriptive-elements \ - --enable-id-stripping \ - --enable-viewboxing \ - --enable-comment-stripping \ - --nindent=4 \ - --quiet \ - -i ${SOURCE}.tmp \ - -o ${SOURCE} - rm ${SOURCE}.tmp -} - -# build icons -function build_icon() { - SOURCE="$1" - TARGET="$2" - - echo "Building ${TARGET}" - - if [ ! -f "${SOURCE}" ]; then - echo "Missing file: ${SOURCE}" - exit 1 - fi - - optimize_svg "${SOURCE}" - - inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET} \ - ${SOURCE} - - optipng ${OPTIPNG_OPTIONS} ${TARGET} -} - -function build_settings_icons() { - INPUT_STRING="$1" - - SETTING_NAME="$(echo "${INPUT_STRING}" | cut -d":" -f1)" - SETTING_VALUES="$(echo "${INPUT_STRING}" | cut -d":" -f2 | tr "," " ")" - - for SETTING_VALUE in ${SETTING_VALUES} - do - SETTING_CODE="${SETTING_NAME}_${SETTING_VALUE}" - build_icon ${CURRENT_DIR}/${SETTING_CODE}.svg ${ASSETS_DIR}/icons/${SETTING_CODE}.png - done -} - -####################################################### - -# Create output folders -mkdir -p ${ASSETS_DIR}/icons - -# Delete existing generated images -find ${ASSETS_DIR}/icons -type f -name "*.png" -delete - -# build game images -for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} -do - build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png -done - -# build settings images -for GAME_SETTING in ${AVAILABLES_GAME_SETTINGS} -do - build_settings_icons "${GAME_SETTING}" -done diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..f1f3a0c50240191264148f9ecf212227b1e99caf --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,49 @@ +import 'package:hangman/utils/tools.dart'; + +class DefaultGameSettings { + // available game parameters codes + static const String parameterCodeGameMode = 'gameMode'; + static const String parameterCodeGameLevel = 'gameLevel'; + static const List<String> availableParameters = [ + parameterCodeGameMode, + parameterCodeGameLevel, + ]; + + // game mode: available values + static const String gameModeValueOnline = 'online'; + static const String gameModeValueOffline = 'offline'; + static const List<String> allowedGameModeValues = [ + gameModeValueOnline, + gameModeValueOffline, + ]; + // game mode: default value + static const String defaultGameModeValue = gameModeValueOffline; + + // game level: available values + static const String gameLevelValueEasy = 'easy'; + static const String gameLevelValueHard = 'hard'; + static const List<String> allowedGameLevelValues = [ + gameLevelValueEasy, + gameLevelValueHard, + ]; + // game level: default value + static const String defaultGameLevelValue = gameLevelValueEasy; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeGameMode: + return DefaultGameSettings.allowedGameModeValues; + case parameterCodeGameLevel: + return DefaultGameSettings.allowedGameLevelValues; + } + + printlog('Did not find any available value for game parameter "$parameterCode".'); + return []; + } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; +} diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..678b88053acaf959db2266ce3fee9c026ef8d898 --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,33 @@ +import 'package:hangman/utils/tools.dart'; + +class DefaultGlobalSettings { + // available global parameters codes + static const String parameterCodeSkin = 'skin'; + static const List<String> availableParameters = [ + parameterCodeSkin, + ]; + + // skin: available values + static const String skinValueDefault = 'default'; + static const List<String> allowedSkinValues = [ + skinValueDefault, + ]; + // skin: default value + static const String defaultSkinValue = skinValueDefault; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeSkin: + return DefaultGlobalSettings.allowedSkinValues; + } + + printlog('Did not find any available value for global parameter "$parameterCode".'); + return []; + } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; +} diff --git a/lib/config/game_colors.dart b/lib/config/game_colors.dart new file mode 100644 index 0000000000000000000000000000000000000000..b42ea3f797cd70440c37e6f462631593323c8f51 --- /dev/null +++ b/lib/config/game_colors.dart @@ -0,0 +1,6 @@ +import 'dart:ui'; + +class GameColors { + static const Color boardColor = Color(0xff004D00); + static const Color keyColor = Color(0xFFD59500); +} diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..d59a4278e25e81b369d2eb12cd4cd055ace5fce0 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:hangman/ui/screens/page_about.dart'; +import 'package:hangman/ui/screens/page_game.dart'; +import 'package:hangman/ui/screens/page_settings.dart'; + +class MenuItem { + final Icon icon; + final Widget page; + + const MenuItem({ + required this.icon, + required this.page, + }); +} + +class Menu { + static const indexGame = 0; + static const menuItemGame = MenuItem( + icon: Icon(UniconsLine.home), + page: PageGame(), + ); + + static const indexSettings = 1; + static const menuItemSettings = MenuItem( + icon: Icon(UniconsLine.setting), + page: PageSettings(), + ); + + static const indexAbout = 2; + static const menuItemAbout = MenuItem( + icon: Icon(UniconsLine.info_circle), + page: PageAbout(), + ); + + static Map<int, MenuItem> items = { + indexGame: menuItemGame, + indexSettings: menuItemSettings, + indexAbout: menuItemAbout, + }; + + static bool isIndexAllowed(int pageIndex) { + return items.keys.contains(pageIndex); + } + + static Widget getPageWidget(int pageIndex) { + return items[pageIndex]?.page ?? menuItemGame.page; + } + + static int itemsCount = Menu.items.length; +} diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; + +/// Colors from Tailwind CSS (v3.0) - June 2022 +/// +/// https://tailwindcss.com/docs/customizing-colors + +const int _primaryColor = 0xFF6366F1; +const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{ + 50: Color(0xFFEEF2FF), // indigo-50 + 100: Color(0xFFE0E7FF), // indigo-100 + 200: Color(0xFFC7D2FE), // indigo-200 + 300: Color(0xFFA5B4FC), // indigo-300 + 400: Color(0xFF818CF8), // indigo-400 + 500: Color(_primaryColor), // indigo-500 + 600: Color(0xFF4F46E5), // indigo-600 + 700: Color(0xFF4338CA), // indigo-700 + 800: Color(0xFF3730A3), // indigo-800 + 900: Color(0xFF312E81), // indigo-900 +}); + +const int _textColor = 0xFF64748B; +const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{ + 50: Color(0xFFF8FAFC), // slate-50 + 100: Color(0xFFF1F5F9), // slate-100 + 200: Color(0xFFE2E8F0), // slate-200 + 300: Color(0xFFCBD5E1), // slate-300 + 400: Color(0xFF94A3B8), // slate-400 + 500: Color(_textColor), // slate-500 + 600: Color(0xFF475569), // slate-600 + 700: Color(0xFF334155), // slate-700 + 800: Color(0xFF1E293B), // slate-800 + 900: Color(0xFF0F172A), // slate-900 +}); + +const Color errorColor = Color(0xFFDC2626); // red-600 + +final ColorScheme lightColorScheme = ColorScheme.light( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade500, + surface: textSwatch.shade50, + surfaceContainerHighest: Colors.white, + shadow: textSwatch.shade900.withOpacity(.1), +); + +final ColorScheme darkColorScheme = ColorScheme.dark( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade300, + surface: const Color(0xFF262630), + surfaceContainerHighest: const Color(0xFF282832), + shadow: textSwatch.shade900.withOpacity(.2), +); + +final ThemeData lightTheme = ThemeData( + colorScheme: lightColorScheme, + fontFamily: 'Nunito', + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + ), +); + +final ThemeData darkTheme = lightTheme.copyWith( + colorScheme: darkColorScheme, + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + ), +); diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..3aa1c51b6742b9d23085faa15a06d45ad17d8538 --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,148 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hangman/data/fetch_data_helper.dart'; +import 'package:hangman/models/game/picked_word.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/models/settings/settings_game.dart'; +import 'package:hangman/models/settings/settings_global.dart'; + +part 'game_state.dart'; + +class GameCubit extends HydratedCubit<GameState> { + GameCubit() + : super(GameState( + currentGame: Game.createNull(), + )); + + void updateState(Game game) { + emit(GameState( + currentGame: game, + )); + } + + void refresh() { + final Game game = Game( + // Settings + gameSettings: state.currentGame.gameSettings, + globalSettings: state.currentGame.globalSettings, + // State + isRunning: state.currentGame.isRunning, + isStarted: state.currentGame.isStarted, + isFinished: state.currentGame.isFinished, + animationInProgress: state.currentGame.animationInProgress, + gettingSecretWord: state.currentGame.gettingSecretWord, + // Base data + secretWord: state.currentGame.secretWord, + clue: state.currentGame.clue, + // Game data + hiddenWord: state.currentGame.hiddenWord, + usedLetters: state.currentGame.usedLetters, + errorsCount: state.currentGame.errorsCount, + isClueDisplayed: state.currentGame.isClueDisplayed, + ); + // game.dump(); + + updateState(game); + } + + void startNewGame({ + required GameSettings gameSettings, + required GlobalSettings globalSettings, + }) { + final Game newGame = Game.createNew( + // Settings + gameSettings: gameSettings, + globalSettings: globalSettings, + ); + + updateState(newGame); + refresh(); + + FetchDataHelper().pickRandomWord(gameSettings).then((PickedWord pickedWord) { + state.currentGame.gettingSecretWord = false; + state.currentGame.secretWord = pickedWord.word; + state.currentGame.clue = pickedWord.clue; + state.currentGame.hiddenWord = List<String>.generate(pickedWord.word.length, (i) => '_'); + state.currentGame.dump(); + + refresh(); + }); + } + + void quitGame() { + state.currentGame.isRunning = false; + refresh(); + } + + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; + refresh(); + } + + void submitLetter(String key) { + if (!state.currentGame.usedLetters.contains(key)) { + updateUsedLetters(key); + + if (state.currentGame.secretWord.contains(key)) { + for (int index = 0; index < state.currentGame.secretWord.length; index++) { + if (key == state.currentGame.secretWord[index]) { + updateHiddenWord(index, key); + } + } + } else { + incrementErrorsCount(); + } + } + + if ((state.currentGame.hiddenWordAsString == state.currentGame.secretWord) || + (state.currentGame.errorsCount == 8)) { + state.currentGame.isFinished = true; + } + + refresh(); + } + + void updateUsedLetters(String key) { + state.currentGame.usedLetters.add(key); + refresh(); + } + + void updateHiddenWord(int index, String letter) { + state.currentGame.hiddenWord[index] = letter; + refresh(); + } + + void incrementErrorsCount() { + state.currentGame.errorsCount++; + refresh(); + } + + void toggleClueDisplay() { + state.currentGame.isClueDisplayed = !state.currentGame.isClueDisplayed; + refresh(); + } + + @override + GameState? fromJson(Map<String, dynamic> json) { + final Game currentGame = json['currentGame'] as Game; + + return GameState( + currentGame: currentGame, + ); + } + + @override + Map<String, dynamic>? toJson(GameState state) { + return <String, dynamic>{ + 'currentGame': state.currentGame.toJson(), + }; + } +} diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..00e211668c3269255926939324355792abd61c41 --- /dev/null +++ b/lib/cubit/game_state.dart @@ -0,0 +1,15 @@ +part of 'game_cubit.dart'; + +@immutable +class GameState extends Equatable { + const GameState({ + required this.currentGame, + }); + + final Game currentGame; + + @override + List<dynamic> get props => <dynamic>[ + currentGame, + ]; +} diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..85303abe21a8dae2319d3e3c75ed7afb1574d143 --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:hangman/config/menu.dart'; + +class NavCubit extends HydratedCubit<int> { + NavCubit() : super(0); + + void updateIndex(int index) { + if (Menu.isIndexAllowed(index)) { + emit(index); + } else { + goToGamePage(); + } + } + + void goToGamePage() { + emit(Menu.indexGame); + } + + void goToSettingsPage() { + emit(Menu.indexSettings); + } + + void goToAboutPage() { + emit(Menu.indexAbout); + } + + @override + int fromJson(Map<String, dynamic> json) { + return Menu.indexGame; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'pageIndex': state}; + } +} diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..09144f1439b220798c221dbaf6af946a06e77a29 --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:hangman/config/default_game_settings.dart'; +import 'package:hangman/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? gameMode, + String? gameLevel, + }) { + emit( + GameSettingsState( + settings: GameSettings( + gameMode: gameMode ?? state.settings.gameMode, + gameLevel: gameLevel ?? state.settings.gameLevel, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeGameMode: + return GameSettings.getGameModeValueFromUnsafe(state.settings.gameMode); + case DefaultGameSettings.parameterCodeGameLevel: + return GameSettings.getGameLevelValueFromUnsafe(state.settings.gameLevel); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String gameMode = code == DefaultGameSettings.parameterCodeGameMode + ? value + : getParameterValue(DefaultGameSettings.parameterCodeGameMode); + final String gameLevel = code == DefaultGameSettings.parameterCodeGameLevel + ? value + : getParameterValue(DefaultGameSettings.parameterCodeGameLevel); + + setValues( + gameMode: gameMode, + gameLevel: gameLevel, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String gameMode = json[DefaultGameSettings.parameterCodeGameMode] as String; + final String gameLevel = json[DefaultGameSettings.parameterCodeGameLevel] as String; + + return GameSettingsState( + settings: GameSettings( + gameMode: gameMode, + gameLevel: gameLevel, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: state.settings.gameMode, + DefaultGameSettings.parameterCodeGameLevel: state.settings.gameLevel, + }; + } +} diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed --- /dev/null +++ b/lib/cubit/settings_game_state.dart @@ -0,0 +1,15 @@ +part of 'settings_game_cubit.dart'; + +@immutable +class GameSettingsState extends Equatable { + const GameSettingsState({ + required this.settings, + }); + + final GameSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..091dd900589b74458d55241d7ce97cdbb62f211b --- /dev/null +++ b/lib/cubit/settings_global_cubit.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:hangman/config/default_global_settings.dart'; +import 'package:hangman/models/settings/settings_global.dart'; + +part 'settings_global_state.dart'; + +class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { + GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); + + void setValues({ + String? skin, + }) { + emit( + GlobalSettingsState( + settings: GlobalSettings( + skin: skin ?? state.settings.skin, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGlobalSettings.parameterCodeSkin: + return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin); + } + return ''; + } + + void setParameterValue(String code, String value) { + final String skin = (code == DefaultGlobalSettings.parameterCodeSkin) + ? value + : getParameterValue(DefaultGlobalSettings.parameterCodeSkin); + + setValues( + skin: skin, + ); + } + + @override + GlobalSettingsState? fromJson(Map<String, dynamic> json) { + final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String; + + return GlobalSettingsState( + settings: GlobalSettings( + skin: skin, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GlobalSettingsState state) { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: state.settings.skin, + }; + } +} diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24 --- /dev/null +++ b/lib/cubit/settings_global_state.dart @@ -0,0 +1,15 @@ +part of 'settings_global_cubit.dart'; + +@immutable +class GlobalSettingsState extends Equatable { + const GlobalSettingsState({ + required this.settings, + }); + + final GlobalSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..b793e895dbb0c672d451cd403e0036c3d9ac9b42 --- /dev/null +++ b/lib/cubit/theme_cubit.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +part 'theme_state.dart'; + +class ThemeCubit extends HydratedCubit<ThemeModeState> { + ThemeCubit() : super(const ThemeModeState()); + + void getTheme(ThemeModeState state) { + emit(state); + } + + @override + ThemeModeState? fromJson(Map<String, dynamic> json) { + switch (json['themeMode']) { + case 'ThemeMode.dark': + return const ThemeModeState(themeMode: ThemeMode.dark); + case 'ThemeMode.light': + return const ThemeModeState(themeMode: ThemeMode.light); + case 'ThemeMode.system': + default: + return const ThemeModeState(themeMode: ThemeMode.system); + } + } + + @override + Map<String, String>? toJson(ThemeModeState state) { + return <String, String>{'themeMode': state.themeMode.toString()}; + } +} diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..e479a50f12fe72a35a1fd1722ff72afbb692a136 --- /dev/null +++ b/lib/cubit/theme_state.dart @@ -0,0 +1,15 @@ +part of 'theme_cubit.dart'; + +@immutable +class ThemeModeState extends Equatable { + const ThemeModeState({ + this.themeMode, + }); + + final ThemeMode? themeMode; + + @override + List<Object?> get props => <Object?>[ + themeMode, + ]; +} diff --git a/lib/data/dictionary_french_easy.dart b/lib/data/dictionary_french_easy.dart new file mode 100644 index 0000000000000000000000000000000000000000..f9c646bfc13bdd4ed2ffb91a1cfa378f5f01f172 --- /dev/null +++ b/lib/data/dictionary_french_easy.dart @@ -0,0 +1,193 @@ +const List<dynamic> dictionaryFrenchEasy = [ + { + "category": "ANIMAUX", + "clue": "Animal", + "words": [ + "ANACONDA", + "AUTRUCHE", + "BARRACUDA", + "BOUQUETIN", + "COCCINELLE", + "CROCODILE", + "DROMADAIRE", + "ELEPHANT", + "ESCARGOT", + "FOURMILIER", + "GRENOUILLE", + "HIPPOCAMPE", + "HIPPOPOTAME", + "KANGOUROU", + "LIBELLULE", + "PERROQUET", + "PIPISTRELLE", + "RHINOCEROS", + "SAUTERELLE", + "TARENTULE" + ] + }, + { + "category": "FRUITS", + "clue": "Fruit", + "words": [ + "AUBERGINE", + "BETTERAVE", + "CITROUILLE", + "CONCOMBRE", + "FRAMBOISE", + "GROSEILLE", + "MANDARINE", + "MIRABELLE", + "MYRTILLE", + "PAMPLEMOUSSE" + ] + }, + { + "category": "METIERS", + "clue": "Métier", + "words": [ + "AGRICULTEUR", + "ARCHEOLOGUE", + "ARCHITECTE", + "ASTRONAUTE", + "BIJOUTIER", + "BIOLOGISTE", + "CHARCUTIER", + "CHARPENTIER", + "CUISINIER", + "ELECTRICIEN", + "HORTICULTEUR", + "INFIRMIER", + "MECANICIEN", + "MENUISIER", + "METEOROLOGUE", + "PHOTOGRAPHE", + "PROFESSEUR", + "STANDARDISTE", + "VETERINAIRE", + "VOLCANOLOGUE" + ] + }, + { + "category": "GEOGRAPHIE", + "clue": "Géographie", + "words": [ + "ALLEMAGNE", + "ANTARCTIQUE", + "ARGENTINE", + "ATLANTIQUE", + "AUSTRALIE", + "EMBOUCHURE", + "HEMISPHERE", + "HYDROGRAPHIE", + "KILIMANDJARO", + "LUXEMBOURG", + "MADAGASCAR", + "MEDITERRANEE", + "MISSISSIPPI", + "NORMANDIE", + "PACIFIQUE", + "PLANISPHERE", + "STRASBOURG", + "SUPERFICIE", + "VENEZUELA", + "WASHINGTON" + ] + }, + { + "category": "COULEURS", + "clue": "Couleur", + "words": [ + "ROUGE", + "BLEU", + "VERT", + "JAUNE", + "VIOLET", + "ORANGE", + "MARRON", + "NOIR", + "BLANC", + "TURQUOISE", + "BEIGE", + "ROSE" + ] + }, + { + "category": "FLEURS", + "clue": "Fleur", + "words": [ + "ROSE", + "PIVOINE", + "TULIPE", + "JONQUILLE", + "CACTUS", + "PETUNIA", + "COQUELICOT", + "JACINTHE" + ] + }, + { + "category": "SPORTS", + "clue": "Sport ou jeu", + "words": [ + "GYMNASTIQUE", + "FOOTBALL", + "HANDBALL", + "COURSE", + "CYCLISME", + "RANDONNEE", + "NATATION", + "KARATE", + "ESCRIME", + "EQUITATION" + ] + }, + { + "category": "ALIMENTS", + "clue": "Aliment ou plat", + "words": [ + "FROMAGE", + "PIZZA", + "SAUCISSON", + "JAMBON", + "SALAMI", + "PAELLA", + "PATES", + "SALADE", + "SOUPE", + "CHOCOLAT", + "OEUF", + "CREME", + "LAIT", + "CORNICHON", + "FLAN", + "TARTE", + "PUREE", + "SAUMON", + "SANDWICH" + ] + }, + { + "category": "VEHICULE", + "clue": "Véhicule ou moyen de transport", + "words": [ + "VOITURE", + "MOTO", + "VELO", + "TRAIN", + "BATEAU", + "AVION", + "HELICOPTERE", + "AUTOBUS", + "CAR", + "TRAINEAU", + "FUSEE", + "VOILIER", + "PAQUEBOT", + "METRO", + "SOUS-MARIN", + "CAMION", + "TRACTEUR", + "KAYAK" + ] + } +]; diff --git a/lib/words/list_french_words.dart b/lib/data/dictionary_french_hard.dart similarity index 99% rename from lib/words/list_french_words.dart rename to lib/data/dictionary_french_hard.dart index 26f77025aef6bd99628169924d664f0526b983e5..fcd2bf0035517b9bb7320697cbe048aa58090434 100644 --- a/lib/words/list_french_words.dart +++ b/lib/data/dictionary_french_hard.dart @@ -1,5 +1,5 @@ /// The list of french words, ~340,000 words in total -const List<String> listFrenchWords = [ +const List<String> dictionaryFrench = [ 'a', 'à', 'abaissa', diff --git a/lib/data/fetch_data_helper.dart b/lib/data/fetch_data_helper.dart new file mode 100644 index 0000000000000000000000000000000000000000..8ef7ca5f22e430b1a9904eacc303fc432fe2509e --- /dev/null +++ b/lib/data/fetch_data_helper.dart @@ -0,0 +1,116 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' show Random; + +import 'package:diacritic/diacritic.dart'; +import 'package:hangman/data/dictionary_french_easy.dart'; +import 'package:hangman/data/dictionary_french_hard.dart'; +import 'package:hangman/models/game/picked_word.dart'; +import 'package:html/parser.dart'; +import 'package:html/dom.dart'; +import 'package:http/http.dart' as http; + +import 'package:hangman/config/default_game_settings.dart'; +import 'package:hangman/models/settings/settings_game.dart'; + +class FetchDataHelper { + FetchDataHelper(); + + Future<PickedWord> pickRandomWord(GameSettings gameSettings) async { + switch (gameSettings.gameMode) { + case DefaultGameSettings.gameModeValueOffline: + return Future(() => pickRandomOfflineWord(gameSettings)); + case DefaultGameSettings.gameModeValueOnline: + return await pickRandomOnlineWord(gameSettings); + } + + return PickedWord.none; + } + + PickedWord pickRandomOfflineWord(GameSettings gameSettings) { + switch (gameSettings.gameLevel) { + case DefaultGameSettings.gameLevelValueEasy: + return wordFromLocalEasyDictionary(); + case DefaultGameSettings.gameLevelValueHard: + return wordFromLocalFullDictionary(); + } + + return PickedWord.none; + } + + Future<PickedWord> pickRandomOnlineWord(GameSettings gameSettings) { + const baseUrl = 'https://www.palabrasaleatorias.com/mots-aleatoires.php'; + switch (gameSettings.gameLevel) { + case DefaultGameSettings.gameLevelValueEasy: + return wordFromWeb('$baseUrl?fs=1&fs2=0&Submit=Nouveau+mot'); + case DefaultGameSettings.gameLevelValueHard: + return wordFromWeb('$baseUrl?fs=1&fs2=1&Submit=Nouveau+mot'); + } + return Future(() => PickedWord.none); + } + + Future<PickedWord> wordFromWeb(String url) async { + try { + final response = await http.Client().get(Uri.parse(url)); + if (response.statusCode == 200) { + final html = parse(utf8.decode(response.bodyBytes)); + Element element = html.getElementsByTagName('div').last; + String word = clean(element.text); + if (word == '') { + throw Exception(); + } + return PickedWord( + word: word, + clue: '', + ); + } else { + throw Exception(); + } + } catch (e) { + return PickedWord.none; + } + } + + PickedWord wordFromLocalFullDictionary() { + final String rawWord = dictionaryFrench[Random().nextInt(dictionaryFrench.length)]; + + return PickedWord( + word: clean(rawWord), + clue: '', + ); + } + + PickedWord wordFromLocalEasyDictionary() { + final Map<String, dynamic> randomCategory = + dictionaryFrenchEasy[Random().nextInt(dictionaryFrenchEasy.length)]; + + final clue = randomCategory['clue']; + final words = randomCategory['words'] as List<String>; + + final word = words[Random().nextInt(words.length)]; + + return PickedWord( + word: clean(word), + clue: clue, + ); + } + + String clean(String wordCandidate) { + String word = wordCandidate.trim(); + + if (word.contains(' ')) { + return ''; + } + + word = removeDiacritics(word); + word = word.toUpperCase(); + word = word.replaceAll(RegExp('[0-9]'), ''); + word = word.replaceAll(RegExp('[^A-Z]'), ''); + + if (word.length < 3 || word.length > 12) { + return ''; + } + + return word; + } +} diff --git a/lib/main.dart b/lib/main.dart index a96452ba967bda7e898b0a3db2cc239d166e977b..dbcc7a7bbe21aa82bc61e254512d60b074cab448 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,38 +1,113 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:hangman/config/default_global_settings.dart'; +import 'package:hangman/config/theme.dart'; +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/cubit/nav_cubit.dart'; +import 'package:hangman/cubit/settings_game_cubit.dart'; +import 'package:hangman/cubit/settings_global_cubit.dart'; +import 'package:hangman/cubit/theme_cubit.dart'; +import 'package:hangman/ui/skeleton.dart'; -import 'package:hangman/provider/data.dart'; -import 'package:hangman/screens/game.dart'; -import 'package:hangman/screens/home.dart'; -import 'package:hangman/screens/scores.dart'; -import 'package:hangman/utils/constants.dart'; +void main() async { + // Initialize packages + WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + final Directory tmpDir = await getTemporaryDirectory(); + Hive.init(tmpDir.toString()); + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: tmpDir, + ); -void main() => runApp(const Hangman()); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) + .then((value) => runApp(EasyLocalization( + path: 'assets/translations', + supportedLocales: const <Locale>[ + Locale('en'), + Locale('fr'), + ], + fallbackLocale: const Locale('en'), + useFallbackTranslations: true, + child: const MyApp(), + ))); +} -class Hangman extends StatelessWidget { - const Hangman({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (BuildContext context) => Data(), - child: Consumer<Data>( - builder: (context, data, child) { + final List<String> assets = getImagesAssets(); + for (String asset in assets) { + precacheImage(AssetImage(asset), context); + } + + return MultiBlocProvider( + providers: [ + BlocProvider<NavCubit>(create: (context) => NavCubit()), + BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()), + BlocProvider<GameCubit>(create: (context) => GameCubit()), + BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()), + BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()), + ], + child: BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { return MaterialApp( + title: 'Hangman', + home: const SkeletonScreen(), + + // Theme stuff + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state.themeMode, + + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, debugShowCheckedModeBanner: false, - theme: ThemeData( - primaryColor: const Color(darkGreen), - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const Home(), - routes: { - Home.id: (context) => const Home(), - Game.id: (context) => const Game(), - Scores.id: (context) => const Scores(), - }, ); }, ), ); } + + List<String> getImagesAssets() { + final List<String> assets = []; + + const List<String> gameImages = [ + 'button_back', + 'button_delete_saved_game', + 'button_resume_game', + 'button_start', + 'game_fail', + 'game_win', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } + + final List<String> skinImages = []; + for (int value = 1; value <= 9; value++) { + skinImages.add('img$value'); + } + + for (String skin in DefaultGlobalSettings.allowedSkinValues) { + for (String image in skinImages) { + assets.add('assets/skins/${skin}_$image.png'); + } + } + + return assets; + } } diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..bff643d2f87b809edc6dd160a47bb9d4ce8a5b6e --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,150 @@ +import 'package:hangman/models/settings/settings_game.dart'; +import 'package:hangman/models/settings/settings_global.dart'; +import 'package:hangman/utils/tools.dart'; + +typedef MovingTile = String; +typedef Player = String; +typedef ConflictsCount = List<List<int>>; +typedef AnimatedBoard = List<List<bool>>; +typedef AnimatedBoardSequence = List<AnimatedBoard>; +typedef Word = String; + +class Game { + Game({ + // Settings + required this.gameSettings, + required this.globalSettings, + + // State + this.isRunning = false, + this.isStarted = false, + this.isFinished = false, + this.animationInProgress = false, + this.gettingSecretWord = false, + + // Base data + required this.secretWord, + required this.clue, + + // Game data + required this.hiddenWord, + required this.usedLetters, + this.errorsCount = 0, + this.isClueDisplayed = false, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + bool gettingSecretWord; + + // Base data + String secretWord; + String clue; + + // Game data + List<String> hiddenWord = []; + List<String> usedLetters = []; + int errorsCount; + bool isClueDisplayed; + + factory Game.createNull() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + // Base data + secretWord: '', + clue: '', + // Game data + hiddenWord: [], + usedLetters: [], + ); + } + + factory Game.createNew({ + required GameSettings? gameSettings, + required GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + gettingSecretWord: true, + // Base data + secretWord: '', + clue: '', + // Game data + hiddenWord: [], + usedLetters: [], + ); + } + + String get hiddenWordAsString => hiddenWord.join(); + + bool get canBeResumed => isStarted && !isFinished; + bool get gameWon => isFinished && (hiddenWordAsString == secretWord); + + void dump() { + printlog(''); + printlog('## Current game dump:'); + printlog(''); + printlog('$Game:'); + printlog(' Settings'); + gameSettings.dump(); + globalSettings.dump(); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' gettingSecretWord: $gettingSecretWord'); + printlog(' Base data'); + printlog(' secretWord: $secretWord'); + printlog(' clue: $clue'); + printlog(' Game data'); + printlog(' hiddenWord: $hiddenWord'); + printlog(' usedLetters: $usedLetters'); + printlog(' errorsCount: $errorsCount'); + printlog(' isClueDisplayed: $isClueDisplayed'); + printlog(''); + } + + @override + String toString() { + return '$Game(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + // Settings + 'gameSettings': gameSettings.toJson(), + 'globalSettings': globalSettings.toJson(), + // State + 'isRunning': isRunning, + 'isStarted': isStarted, + 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + 'gettingSecretWord': gettingSecretWord, + // Base data + 'secretWord': secretWord, + 'clue': clue, + // Game data + 'hiddenWord': hiddenWord, + 'usedLetters': usedLetters, + 'errorsCount': errorsCount, + 'isClueDisplayed': isClueDisplayed, + }; + } +} diff --git a/lib/models/game/picked_word.dart b/lib/models/game/picked_word.dart new file mode 100644 index 0000000000000000000000000000000000000000..890e2ae58cba347555e3d2cf9e0e4bd8c6321428 --- /dev/null +++ b/lib/models/game/picked_word.dart @@ -0,0 +1,35 @@ +import 'package:hangman/utils/tools.dart'; + +class PickedWord { + const PickedWord({ + required this.word, + required this.clue, + }); + + final String word; + final String clue; + + static const PickedWord none = PickedWord( + word: '', + clue: '', + ); + + void dump() { + printlog('$PickedWord:'); + printlog(' word: $word'); + printlog(' clue: $clue'); + printlog(''); + } + + @override + String toString() { + return '$PickedWord(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'word': word, + 'clue': clue, + }; + } +} diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..4c0f947021504509c75a39b8f5487a45982cce46 --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,54 @@ +import 'package:hangman/config/default_game_settings.dart'; +import 'package:hangman/utils/tools.dart'; + +class GameSettings { + final String gameMode; + final String gameLevel; + + GameSettings({ + required this.gameMode, + required this.gameLevel, + }); + + static String getGameModeValueFromUnsafe(String gameMode) { + if (DefaultGameSettings.allowedGameModeValues.contains(gameMode)) { + return gameMode; + } + + return DefaultGameSettings.defaultGameModeValue; + } + + static String getGameLevelValueFromUnsafe(String gameLevel) { + if (DefaultGameSettings.allowedGameLevelValues.contains(gameLevel)) { + return gameLevel; + } + + return DefaultGameSettings.defaultGameLevelValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + gameMode: DefaultGameSettings.defaultGameModeValue, + gameLevel: DefaultGameSettings.defaultGameLevelValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeGameMode}: $gameMode'); + printlog(' ${DefaultGameSettings.parameterCodeGameLevel}: $gameLevel'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameMode: gameMode, + DefaultGameSettings.parameterCodeGameLevel: gameLevel, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..d9c3326f4bbb863be20bd606167a4dd0d7f73335 --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:hangman/config/default_global_settings.dart'; +import 'package:hangman/utils/tools.dart'; + +class GlobalSettings { + String skin; + + GlobalSettings({ + required this.skin, + }); + + static String getSkinValueFromUnsafe(String skin) { + if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) { + return skin; + } + + return DefaultGlobalSettings.defaultSkinValue; + } + + factory GlobalSettings.createDefault() { + return GlobalSettings( + skin: DefaultGlobalSettings.defaultSkinValue, + ); + } + + void dump() { + printlog('$GlobalSettings:'); + printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin'); + printlog(''); + } + + @override + String toString() { + return '$GlobalSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: skin, + }; + } +} diff --git a/lib/provider/data.dart b/lib/provider/data.dart deleted file mode 100644 index 1bbdf069f604961f1cdf861ee15f980afd372866..0000000000000000000000000000000000000000 --- a/lib/provider/data.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:flutter/foundation.dart'; - -import 'package:hangman/utils/constants.dart'; -import 'package:hangman/utils/shared_prefs.dart'; - -class Data extends ChangeNotifier { - // settings - final SharedPrefs _sharedPrefs = SharedPrefs(); - - // screen settings - bool _gameModeValue = false; - String _levelValue = defaultLevel; - - // randomization - String _secretWord = ''; - bool _searching = false; - String _clue = ''; - List<String> _hiddenWord = []; - List<String> _usedLetters = []; - - bool get searching => _searching; - - set searching(bool value) { - _searching = value; - notifyListeners(); - } - - // scores - int _errors = 0; - int _victoryCount = 0; - int _defeatCount = 0; - - Data() { - _getPrefs(); - } - - void _getPrefs() async { - await _sharedPrefs.init(); - - _gameModeValue = onlineGameMode.keys.firstWhere( - (k) => onlineGameMode[k]?.contains(_sharedPrefs.level) ?? false, - orElse: () => false); - - _levelValue = onlineGameMode[_gameModeValue]?.contains(_sharedPrefs.level) ?? false - ? _sharedPrefs.level - : onlineGameMode[_gameModeValue]?.first ?? ''; - - _victoryCount = _sharedPrefs.victoryCount; - _defeatCount = _sharedPrefs.defeatCount; - notifyListeners(); - } - - bool get gameModePref => _sharedPrefs.gameMode; - String get levelPref => (_sharedPrefs.level != '') - ? _sharedPrefs.level - : onlineGameMode[gameModePref]?.first ?? ''; - - void resetValues() => _getPrefs(); - - set setPrefGameMode(bool prefGameMode) { - _sharedPrefs.gameMode = prefGameMode; - notifyListeners(); - } - - set setPrefLevel(String prefLevel) { - _sharedPrefs.level = prefLevel; - notifyListeners(); - } - - bool get gameModeValue => _gameModeValue; - - set updateGameMode(bool value) { - _gameModeValue = value; - setPrefGameMode = gameModeValue; - updateLevel = onlineGameMode[value]?.first ?? ''; - notifyListeners(); - } - - String get levelValue => _levelValue; - - set updateLevel(String value) { - _levelValue = value; - setPrefLevel = levelValue; - notifyListeners(); - } - - String get secretWord => _secretWord; - - set updateSecretWord(String value) { - _secretWord = value; - _hiddenWord = List<String>.generate(value.length, (i) => '_'); - notifyListeners(); - } - - String get clue => _clue; - - set updateClue(String value) { - _clue = value; - notifyListeners(); - } - - String get hiddenWord => _hiddenWord.join(); - - void updateHiddenWord(int index, String letter) { - _hiddenWord[index] = letter; - notifyListeners(); - } - - List<String> get usedLetters => _usedLetters; - - void updateUsedLetters(String key) { - _usedLetters.add(key); - notifyListeners(); - } - - void resetUsedLetters() { - _usedLetters.clear(); - notifyListeners(); - } - - int get errors => _errors; - - void addError() { - _errors++; - notifyListeners(); - } - - void resetSuccessAndErrors() { - _errors = 0; - notifyListeners(); - } - - int get victoryCount => _victoryCount; - int get defeatCount => _defeatCount; - - void addVictory() { - _victoryCount++; - _sharedPrefs.victoryCount = _victoryCount; - notifyListeners(); - } - - void addDefeat() { - _defeatCount++; - _sharedPrefs.defeatCount = _defeatCount; - notifyListeners(); - } - - void resetScores() { - _victoryCount = 0; - _defeatCount = 0; - notifyListeners(); - } - - void resetGame() { - _errors = 0; - _secretWord = ''; - _clue = ''; - _hiddenWord = []; - _usedLetters = []; - _usedLetters.clear(); - notifyListeners(); - } -} diff --git a/lib/screens/game.dart b/lib/screens/game.dart deleted file mode 100644 index 65f2f42877086a726a9e663261cda9d88f2a2404..0000000000000000000000000000000000000000 --- a/lib/screens/game.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:hangman/provider/data.dart'; -import 'package:hangman/utils/constants.dart'; -import 'package:hangman/utils/random_pick.dart'; -import 'package:hangman/widgets/letters.dart'; - -class Game extends StatelessWidget { - const Game({super.key}); - - static const String id = 'game'; - - Future<void> pickWord(BuildContext context, Data myProvider) async { - myProvider.searching = true; - RandomPick randompick; - int attempts = 0; - do { - randompick = RandomPick(myProvider.levelPref); - await randompick.init(); - if (randompick.word != '') { - myProvider.updateSecretWord = randompick.word; - myProvider.resetSuccessAndErrors(); - myProvider.resetUsedLetters(); - if (myProvider.levelPref == defaultLevel) { - myProvider.updateClue = randompick.clue; - } - myProvider.searching = false; - break; - } - attempts++; - } while (attempts < 3); - } - - @override - Widget build(BuildContext context) { - Orientation orientation = MediaQuery.of(context).orientation; - Data myProvider = Provider.of<Data>(context); - - return Scaffold( - backgroundColor: const Color(board), - floatingActionButton: myProvider.levelPref == defaultLevel - ? FloatingActionButton( - foregroundColor: Colors.white, - backgroundColor: Colors.transparent, - elevation: 0.0, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Indice'), - content: Text(myProvider.clue), - actions: <Widget>[ - TextButton( - child: const Text('Revenir au jeu'), - onPressed: () => Navigator.of(context).pop(), - ) - ], - ); - }, - ); - }, - child: const Icon(Icons.help_outline), - ) - : null, - floatingActionButtonLocation: orientation == Orientation.portrait - ? FloatingActionButtonLocation.endTop - : FloatingActionButtonLocation.centerTop, - body: orientation == Orientation.portrait - ? const Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Padding( - padding: EdgeInsets.only(top: 60.0, left: 30.0, right: 30.0, bottom: 10.0), - child: ImgGallow(), - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 30.0), - child: HiddenWord(), - ), - LetterButtons(), - ], - ) - : const Row( - children: [ - Expanded( - flex: 2, - child: Column( - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.only(top: 40.0), - child: ImgGallow(), - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0), - child: HiddenWord(), - ), - ], - ), - ), - Expanded(child: LetterButtons()), - ], - ), - ); - } -} - -class ImgGallow extends StatelessWidget { - const ImgGallow({super.key}); - @override - Widget build(BuildContext context) { - Data myProvider = Provider.of<Data>(context); - return Stack( - children: [ - for (int error = 0; error < 9; error++) - AnimatedOpacity( - opacity: myProvider.errors >= error ? 1.0 : 0.0, - duration: Duration(milliseconds: myProvider.errors >= error ? 800 : 0), - child: Image.asset('assets/images/img${error + 1}.png'), - ) - ], - ); - } -} - -class HiddenWord extends StatelessWidget { - const HiddenWord({super.key}); - @override - Widget build(BuildContext context) { - Data myProvider = Provider.of<Data>(context); - return FittedBox( - child: Text( - (myProvider.hiddenWord.isEmpty || myProvider.hiddenWord == '') - ? 'UNEXPECTED ERROR' - : myProvider.hiddenWord, - style: const TextStyle( - fontFamily: 'Tiza', - color: Colors.white, - fontSize: 34.0, - letterSpacing: 10.0, - ), - ), - ); - } -} diff --git a/lib/screens/home.dart b/lib/screens/home.dart deleted file mode 100644 index cde5145e16263914003cb4307cb813a20fd45d48..0000000000000000000000000000000000000000 --- a/lib/screens/home.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:hangman/provider/data.dart'; -import 'package:hangman/screens/game.dart'; -import 'package:hangman/utils/constants.dart'; -import 'package:hangman/widgets/dialog_fetch_error.dart'; -import 'package:hangman/widgets/my_app_bar.dart'; - -class Home extends StatelessWidget { - const Home({super.key}); - - static const String id = 'home'; - - @override - Widget build(BuildContext context) { - final Data myProvider = Provider.of<Data>(context); - - void errorWord(context) { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: const Text('Erreur inattendue'), - content: const Text('Erreur inattendue à la récupération d\'un mot aléatoire.\n' - 'Installer une nouvelle version de l\'application pourrait corriger cette anomalie.'), - actions: <Widget>[ - TextButton( - child: const Text('Fermer'), - onPressed: () => Navigator.of(context).pop(), - ) - ], - ), - ); - } - - return Scaffold( - appBar: MyAppBar(appBar: AppBar()), - body: Builder( - builder: (context) => Center( - child: myProvider.searching == true - ? PopScope( - onPopInvoked: (didPop) {}, - child: const Center( - child: CircularProgressIndicator(), - ), - ) - : SingleChildScrollView( - padding: const EdgeInsets.all(15.0), - child: SizedBox( - height: MediaQuery.of(context).size.height / 1.25, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), - child: FittedBox( - fit: BoxFit.fitWidth, - child: Text( - 'LE PENDU', - style: TextStyle( - fontFamily: 'Tiza', - fontSize: 28.0, - color: Colors.grey[700], - ), - ), - ), - ), - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Mode en ligne'), - Switch( - value: myProvider.gameModeValue, - onChanged: (bool value) => myProvider.updateGameMode = value, - activeTrackColor: const Color(board), - activeColor: Colors.greenAccent[400], - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Niveau'), - DropdownButton<String>( - value: myProvider.levelValue != '' - ? myProvider.levelValue - : onlineGameMode[myProvider.gameModeValue]?.first, - items: onlineGameMode[myProvider.gameModeValue] - ?.map<DropdownMenuItem<String>>((String value) { - return DropdownMenuItem<String>( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? value) => - myProvider.updateLevel = value ?? '', - ), - ], - ), - ], - ), - TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: const Color(board), - padding: const EdgeInsets.all(20.0), - ), - onPressed: () async { - myProvider.resetGame(); - myProvider.searching = true; - bool control = true; - await const Game().pickWord(context, myProvider); - if (myProvider.secretWord == '' || myProvider.hiddenWord == '') { - control = false; - if (!context.mounted) return; - var response = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DialogFetchError(), - ), - ); - if (response == false) { - myProvider.searching = false; - myProvider.resetGame(); - } else { - myProvider.setPrefGameMode = false; - myProvider.setPrefLevel = defaultLevel; - if (!context.mounted) return; - await const Game().pickWord(context, myProvider); - control = true; - } - } - - if (myProvider.secretWord == 'UNEXPECTED ERROR') { - control = false; - myProvider.resetGame(); - if (!context.mounted) return; - errorWord(context); - } - - if (control) { - if (!context.mounted) return; - Navigator.pushNamed(context, Game.id) - .then((value) => myProvider.searching = false); - } - }, - icon: const Icon( - Icons.check_box, - color: Colors.white, - size: 60.0, - ), - label: Column( - children: [ - const Text( - 'JOUER', - style: TextStyle( - fontSize: 22.0, - letterSpacing: 2.0, - ), - ), - Text( - 'Mode de jeu: ${myProvider.levelPref}', - style: const TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w300, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/screens/scores.dart b/lib/screens/scores.dart deleted file mode 100644 index 2a26025537843ac522bb08fbc6a176ab2e029737..0000000000000000000000000000000000000000 --- a/lib/screens/scores.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:hangman/provider/data.dart'; -import 'package:hangman/utils/constants.dart'; - -class Scores extends StatelessWidget { - const Scores({super.key}); - - static const String id = 'scores'; - - @override - Widget build(BuildContext context) { - final Data myProvider = Provider.of<Data>(context); - - return Scaffold( - backgroundColor: const Color(board), - appBar: AppBar( - title: const Text('Scores'), - actions: [ - IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () => myProvider.resetScores(), - ), - ], - ), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Flexible( - child: FractionallySizedBox(heightFactor: 0.6), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: FittedBox( - fit: BoxFit.fitWidth, - child: Text( - 'SCORES', - style: TextStyle( - fontFamily: 'Tiza', - fontSize: 28.0, - color: Colors.grey[300], - ), - ), - ), - ), - const Flexible(child: FractionallySizedBox(heightFactor: 0.2)), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CardBox( - Icon( - Icons.emoji_events, - color: Colors.grey[200], - size: 60.0, - ), - '${myProvider.victoryCount}', - ), - CardBox( - ImageIcon( - const AssetImage('assets/images/gameover.png'), - color: Colors.grey[200], - size: 60.0, - ), - '${myProvider.defeatCount}', - ), - ], - ), - ], - ), - ), - ); - } -} - -class CardBox extends StatelessWidget { - final Widget imagen; - final String text; - - const CardBox(this.imagen, this.text, {super.key}); - - @override - Widget build(BuildContext context) { - return Flexible( - child: Card( - elevation: 10.0, - margin: const EdgeInsets.symmetric(horizontal: 10.0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - color: const Color(darkGreen), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(10.0), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.grey, - width: 0.5, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: imagen, - ), - ), - Container( - padding: const EdgeInsets.all(20.0), - child: Text( - text, - style: TextStyle( - fontFamily: 'Tiza', - fontSize: 20.0, - color: Colors.grey[300], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart new file mode 100644 index 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad --- /dev/null +++ b/lib/ui/helpers/app_titles.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ); + } +} + +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/helpers/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..6d8c7f77e10191464866ebaac4b5ed3a856f0731 --- /dev/null +++ b/lib/ui/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:hangman/utils/color_extensions.dart'; + +class OutlinedText extends StatelessWidget { + const OutlinedText({ + super.key, + required this.text, + required this.fontSize, + required this.textColor, + this.outlineColor, + }); + + final String text; + final double fontSize; + final Color textColor; + final Color? outlineColor; + + @override + Widget build(BuildContext context) { + final double delta = fontSize / 30; + + return Text( + text, + style: TextStyle( + inherit: true, + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: textColor, + shadows: [ + Shadow( + offset: Offset(-delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(-delta, delta), + color: outlineColor ?? textColor.darken(), + ), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..0cb2223e8fef4363513c721a81f6e2bce3b6dbe4 --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hangman/config/game_colors.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/ui/widgets/game/game_answer.dart'; +import 'package:hangman/ui/widgets/game/game_clue.dart'; +import 'package:hangman/ui/widgets/game/game_hidden_word.dart'; +import 'package:hangman/ui/widgets/game/game_loading.dart'; +import 'package:hangman/ui/widgets/game/game_something_went_wrong.dart'; +import 'package:hangman/ui/widgets/game/game_virtual_keyboard.dart'; +import 'package:hangman/ui/widgets/game/game_end.dart'; +import 'package:hangman/ui/widgets/game/game_top.dart'; + +class GameLayout extends StatelessWidget { + const GameLayout({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + if (currentGame.gettingSecretWord) { + return const GameLoadingIndicatorWidget(); + } + + if (currentGame.secretWord == '') { + return const GameSomethingWentWrongWidget(); + } + + return Container( + alignment: AlignmentDirectional.topCenter, + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + color: GameColors.boardColor, + alignment: AlignmentDirectional.topCenter, + padding: const EdgeInsets.all(12), + child: const Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 24), + GameTopWidget(), + SizedBox(height: 24), + GameHiddenWordWidget(), + SizedBox(height: 24), + ], + ), + ), + currentGame.clue != '' ? const GameDisplayClueWidget() : const SizedBox.shrink(), + currentGame.isFinished && !currentGame.gameWon + ? const GameDisplayAnswerWidget() + : const SizedBox.shrink(), + const Expanded(child: SizedBox.shrink()), + currentGame.isFinished + ? const GameEndWidget() + : const GameVirtualKeyboardWidget(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..7022e8339ac09d105ce5dc546e0de0a423fef15c --- /dev/null +++ b/lib/ui/layouts/parameters_layout.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/config/default_game_settings.dart'; +import 'package:hangman/config/default_global_settings.dart'; +import 'package:hangman/cubit/settings_game_cubit.dart'; +import 'package:hangman/cubit/settings_global_cubit.dart'; +import 'package:hangman/ui/parameters/parameter_painter.dart'; +import 'package:hangman/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:hangman/ui/widgets/actions/button_game_start_new.dart'; +import 'package:hangman/ui/widgets/actions/button_resume_saved_game.dart'; +import 'package:hangman/ui/parameters/parameter_image.dart'; + +class ParametersLayout extends StatelessWidget { + const ParametersLayout({super.key, required this.canResume}); + + final bool canResume; + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Game settings + for (String code in DefaultGameSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: false, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + lines.add(SizedBox(height: separatorHeight)); + + if (canResume == false) { + // Start new game + lines.add(const Expanded( + child: StartNewGameButton(), + )); + } else { + // Resume game + lines.add(const Expanded( + child: ResumeSavedGameButton(), + )); + // Delete saved game + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 4, + child: const DeleteSavedGameButton(), + )); + } + + lines.add(SizedBox(height: separatorHeight)); + + // Global settings + for (String code in DefaultGlobalSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: true, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + return Column( + children: lines, + ); + } + + List<Widget> buildParametersLine({ + required String code, + required bool isGlobal, + }) { + final List<Widget> parameterButtons = []; + + final List<String> availableValues = isGlobal + ? DefaultGlobalSettings.getAvailableValues(code) + : DefaultGameSettings.getAvailableValues(code); + + if (availableValues.length <= 1) { + return []; + } + + for (String value in availableValues) { + final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + final GameSettingsCubit gameSettingsCubit = + BlocProvider.of<GameSettingsCubit>(context); + final GlobalSettingsCubit globalSettingsCubit = + BlocProvider.of<GlobalSettingsCubit>(context); + + final String currentValue = isGlobal + ? globalSettingsCubit.getParameterValue(code) + : gameSettingsCubit.getParameterValue(code); + + final bool isActive = (value == currentValue); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / availableValues.length - 26; + + final bool displayedWithAssets = + DefaultGlobalSettings.displayedWithAssets.contains(code) || + DefaultGameSettings.displayedWithAssets.contains(code); + + return TextButton( + child: Container( + child: displayedWithAssets + ? SizedBox.square( + dimension: itemWidth, + child: ParameterImage( + code: code, + value: value, + isSelected: isActive, + ), + ) + : CustomPaint( + size: Size(itemWidth, itemWidth), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + isSelected: isActive, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ), + isComplex: true, + ), + ), + onPressed: () { + isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value); + }, + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart new file mode 100644 index 0000000000000000000000000000000000000000..fc4b576f85b01158b74548400d11a4d027c57fbe --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..995ac7a3f1fd63bcd42b034803a9f555f5958bed --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,203 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:hangman/config/default_game_settings.dart'; +import 'package:hangman/models/settings/settings_game.dart'; +import 'package:hangman/models/settings/settings_global.dart'; +import 'package:hangman/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.isSelected, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final String value; + final bool isSelected; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + const Color borderColorEnabled = Colors.blue; + const Color borderColorDisabled = Colors.white; + + // "enabled/disabled" border + final paint = Paint(); + paint.style = PaintingStyle.stroke; + paint.color = isSelected ? borderColorEnabled : borderColorDisabled; + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 10; + canvas.drawRect( + Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint); + + // content + switch (code) { + case DefaultGameSettings.parameterCodeGameMode: + paintGameModeParameterItem(value, canvas, canvasSize); + break; + case DefaultGameSettings.parameterCodeGameLevel: + paintGameLevelParameterItem(value, canvas, canvasSize); + break; + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(value, canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple block with text + void paintUnknownParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + paint.color = Colors.grey; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + final textSpan = TextSpan( + text: '?\n$value', + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } + + void paintGameModeParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + const Color backgroundColor = Colors.grey; + String text = ''; + + switch (value) { + case DefaultGameSettings.gameModeValueOnline: + text = '🌐'; + break; + case DefaultGameSettings.gameModeValueOffline: + text = '📱'; + break; + default: + printlog('Wrong value for gameMode parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Colored background + paint.color = backgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + // centered text value + final textSpan = TextSpan( + text: text, + style: TextStyle( + color: Colors.black, + fontSize: size / 2, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } + + void paintGameLevelParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + Color backgroundColor = Colors.grey; + String text = ''; + + switch (value) { + case DefaultGameSettings.gameLevelValueEasy: + backgroundColor = Colors.green; + text = '🧒'; + break; + case DefaultGameSettings.gameLevelValueHard: + backgroundColor = Colors.orange; + text = '🧑🎓'; + break; + default: + printlog('Wrong value for gameLevel parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Colored background + paint.color = backgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + // centered text value + final textSpan = TextSpan( + text: text, + style: TextStyle( + color: Colors.black, + fontSize: size / 2, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } +} diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart new file mode 100644 index 0000000000000000000000000000000000000000..f1d171f40eb516cde51a56207eec12e7c0d52d01 --- /dev/null +++ b/lib/ui/screens/page_about.dart @@ -0,0 +1,41 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'package:hangman/ui/helpers/app_titles.dart'; + +class PageAbout extends StatelessWidget { + const PageAbout({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + const SizedBox(height: 8), + const AppTitle(text: 'about_title'), + const Text('about_content').tr(), + FutureBuilder<PackageInfo>( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return const Text('about_version').tr( + namedArgs: { + 'version': snapshot.data!.version, + }, + ); + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..9d0bee2f20118c5c8551d9ab1223bfa7f8ead34d --- /dev/null +++ b/lib/ui/screens/page_game.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/ui/layouts/parameters_layout.dart'; +import 'package:hangman/ui/layouts/game_layout.dart'; + +class PageGame extends StatelessWidget { + const PageGame({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return currentGame.isRunning + ? const GameLayout() + : ParametersLayout(canResume: currentGame.canBeResumed); + }, + ); + } +} diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..fe20de3a220e09a4c9d3033367392cab797ec9b1 --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:hangman/ui/helpers/app_titles.dart'; +import 'package:hangman/ui/settings/settings_form.dart'; + +class PageSettings extends StatelessWidget { + const PageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppTitle(text: 'settings_title'), + SizedBox(height: 8), + SettingsForm(), + ], + ), + ); + } +} diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..e79a6be64ff97787f6d52ccf9ae25e11006aa52f --- /dev/null +++ b/lib/ui/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:hangman/ui/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/settings/theme_card.dart b/lib/ui/settings/theme_card.dart new file mode 100644 index 0000000000000000000000000000000000000000..c5efb0be8249094bd49fde8e3b3a31ca3346d98e --- /dev/null +++ b/lib/ui/settings/theme_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/theme_cubit.dart'; + +class ThemeCard extends StatelessWidget { + const ThemeCard({ + super.key, + required this.mode, + required this.icon, + }); + + final IconData icon; + final ThemeMode mode; + + @override + Widget build(BuildContext context) { + return BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: state.themeMode == mode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(5), + child: InkWell( + onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( + ThemeModeState(themeMode: mode), + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + icon, + size: 32, + color: state.themeMode != mode + ? Theme.of(context).colorScheme.primary + : Colors.white, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart new file mode 100644 index 0000000000000000000000000000000000000000..87fd74cf99a82a412190aa7d3284f00f95ecc2e5 --- /dev/null +++ b/lib/ui/skeleton.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/config/menu.dart'; +import 'package:hangman/cubit/nav_cubit.dart'; +import 'package:hangman/ui/widgets/global_app_bar.dart'; + +class SkeletonScreen extends StatelessWidget { + const SkeletonScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const GlobalAppBar(), + extendBodyBehindAppBar: false, + body: Material( + color: Theme.of(context).colorScheme.surface, + child: BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + return Padding( + padding: const EdgeInsets.only( + top: 8, + left: 2, + right: 2, + ), + child: Menu.getPageWidget(pageIndex), + ); + }, + ), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + ); + } +} diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..9ea5a65b330bf4ac1b51e63f126ea6464dadc364 --- /dev/null +++ b/lib/ui/widgets/actions/button_delete_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart new file mode 100644 index 0000000000000000000000000000000000000000..6b2978245dbaa01be2b74e47b876683dbf445ca5 --- /dev/null +++ b/lib/ui/widgets/actions/button_game_quit.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; + +class QuitGameButton extends StatelessWidget { + const QuitGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart new file mode 100644 index 0000000000000000000000000000000000000000..1c8af9a5ab3dcbcc29b0fa392f33d57191ace346 --- /dev/null +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/cubit/settings_game_cubit.dart'; +import 'package:hangman/cubit/settings_global_cubit.dart'; + +class StartNewGameButton extends StatelessWidget { + const StartNewGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_start.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..2660baa90f8b3777564c116b768cf4eccee10ef8 --- /dev/null +++ b/lib/ui/widgets/actions/button_resume_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; + +class ResumeSavedGameButton extends StatelessWidget { + const ResumeSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_resume_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).resumeSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_answer.dart b/lib/ui/widgets/game/game_answer.dart new file mode 100644 index 0000000000000000000000000000000000000000..5d76196a6e65b9482fef9a118b7c7e27e450157a --- /dev/null +++ b/lib/ui/widgets/game/game_answer.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; + +class GameDisplayAnswerWidget extends StatelessWidget { + const GameDisplayAnswerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Padding( + padding: const EdgeInsets.all(8), + child: FittedBox( + child: Text( + currentGame.secretWord, + style: const TextStyle( + fontFamily: 'Tiza', + fontSize: 36.0, + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_clue.dart b/lib/ui/widgets/game/game_clue.dart new file mode 100644 index 0000000000000000000000000000000000000000..0fe839f8eb8de116592a9072348fbe74e4caf276 --- /dev/null +++ b/lib/ui/widgets/game/game_clue.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; + +class GameDisplayClueWidget extends StatelessWidget { + const GameDisplayClueWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return TextButton( + onPressed: () { + BlocProvider.of<GameCubit>(context).toggleClueDisplay(); + }, + child: FittedBox( + child: Text( + currentGame.isClueDisplayed ? currentGame.clue : '?', + style: const TextStyle( + fontFamily: 'Tiza', + color: Colors.black, + fontSize: 24.0, + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_end.dart b/lib/ui/widgets/game/game_end.dart new file mode 100644 index 0000000000000000000000000000000000000000..acf9c8209447473a381152921cafd55f32630107 --- /dev/null +++ b/lib/ui/widgets/game/game_end.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/ui/widgets/actions/button_game_quit.dart'; + +class GameEndWidget extends StatelessWidget { + const GameEndWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final Image decorationImage = Image( + image: AssetImage( + currentGame.gameWon ? 'assets/ui/game_win.png' : 'assets/ui/game_fail.png'), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [decorationImage], + ), + Column( + children: [ + currentGame.animationInProgress + ? decorationImage + : const QuitGameButton() + ], + ), + Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_hidden_word.dart b/lib/ui/widgets/game/game_hidden_word.dart new file mode 100644 index 0000000000000000000000000000000000000000..0cd443e5b6bf9813b5c44866d296e114480238c1 --- /dev/null +++ b/lib/ui/widgets/game/game_hidden_word.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; + +class GameHiddenWordWidget extends StatelessWidget { + const GameHiddenWordWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return FittedBox( + child: Text( + currentGame.hiddenWordAsString, + style: const TextStyle( + fontFamily: 'Tiza', + color: Colors.white, + fontSize: 34.0, + letterSpacing: 10.0, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/game/game_loading.dart b/lib/ui/widgets/game/game_loading.dart new file mode 100644 index 0000000000000000000000000000000000000000..c1f01b4f362df6aaf820d382c377e978a0d0221f --- /dev/null +++ b/lib/ui/widgets/game/game_loading.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class GameLoadingIndicatorWidget extends StatelessWidget { + const GameLoadingIndicatorWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } +} diff --git a/lib/ui/widgets/game/game_something_went_wrong.dart b/lib/ui/widgets/game/game_something_went_wrong.dart new file mode 100644 index 0000000000000000000000000000000000000000..407173152096569dc611412e3ad75c995295cae8 --- /dev/null +++ b/lib/ui/widgets/game/game_something_went_wrong.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class GameSomethingWentWrongWidget extends StatelessWidget { + const GameSomethingWentWrongWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Text("Something went wrong..."), + ); + } +} diff --git a/lib/ui/widgets/game/game_top.dart b/lib/ui/widgets/game/game_top.dart new file mode 100644 index 0000000000000000000000000000000000000000..9e5bb63e1617ea148664247d1ffbb480b19fb7ef --- /dev/null +++ b/lib/ui/widgets/game/game_top.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; + +class GameTopWidget extends StatelessWidget { + const GameTopWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + return Stack( + children: [ + for (int error = 0; error < 9; error++) + AnimatedOpacity( + opacity: currentGame.errorsCount >= error ? 1.0 : 0.0, + duration: Duration(milliseconds: currentGame.errorsCount >= error ? 500 : 0), + child: Image.asset('assets/skins/default_img${error + 1}.png'), + ) + ], + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_virtual_keyboard.dart b/lib/ui/widgets/game/game_virtual_keyboard.dart new file mode 100644 index 0000000000000000000000000000000000000000..02c028832b9ef86ed0e468a309ce4dd665b1c29d --- /dev/null +++ b/lib/ui/widgets/game/game_virtual_keyboard.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/ui/widgets/game/game_virtual_keyboard_key.dart'; + +class GameVirtualKeyboardWidget extends StatelessWidget { + const GameVirtualKeyboardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final List<List<String>> keys = [ + ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], + ['Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M'], + [' ', ' ', 'W', 'X', 'C', 'V', 'B', 'N', ' ', ' '], + ]; + + List<TableRow> tableRows = []; + for (List<String> row in keys) { + List<TableCell> tableCells = []; + for (String key in row) { + tableCells.add(TableCell( + child: GameVirtualKeyboardKeyWidget( + caption: key, + enabled: !currentGame.usedLetters.contains(key), + ), + )); + } + tableRows.add(TableRow(children: tableCells)); + } + + return SizedBox( + height: 150, + width: double.maxFinite, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2), + padding: const EdgeInsets.all(2), + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: tableRows, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/game_virtual_keyboard_key.dart b/lib/ui/widgets/game/game_virtual_keyboard_key.dart new file mode 100644 index 0000000000000000000000000000000000000000..1f6b3f0945f951d47126061bf8b03dcdd3d75ae8 --- /dev/null +++ b/lib/ui/widgets/game/game_virtual_keyboard_key.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hangman/config/game_colors.dart'; + +import 'package:hangman/cubit/game_cubit.dart'; + +class GameVirtualKeyboardKeyWidget extends StatelessWidget { + const GameVirtualKeyboardKeyWidget({ + super.key, + required this.caption, + required this.enabled, + }); + + final String caption; + final bool enabled; + + @override + Widget build(BuildContext context) { + final Color keyColor = enabled ? Colors.black : GameColors.keyColor; + + if (caption == ' ') { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.all(2), + child: AspectRatio( + aspectRatio: 1, + child: TextButton( + style: TextButton.styleFrom( + padding: const EdgeInsets.all(0), + backgroundColor: GameColors.keyColor, + ), + child: Text( + caption, + style: TextStyle( + color: keyColor, + fontSize: 24.0, + fontWeight: FontWeight.w800, + ), + textAlign: TextAlign.center, + ), + onPressed: () { + if (caption != ' ') { + BlocProvider.of<GameCubit>(context).submitLetter(caption); + } + }, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..8e56835db33475c88241a4711556b9a51c3145c3 --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hangman/config/menu.dart'; +import 'package:hangman/cubit/game_cubit.dart'; +import 'package:hangman/cubit/nav_cubit.dart'; +import 'package:hangman/models/game/game.dart'; +import 'package:hangman/ui/helpers/app_titles.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + final Game currentGame = gameState.currentGame; + + final List<Widget> menuActions = []; + + if (currentGame.isRunning && !currentGame.isFinished) { + menuActions.add(TextButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () {}, + onLongPress: () { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + gameCubit.quitGame(); + }, + )); + } else { + if (pageIndex == Menu.indexGame) { + // go to Settings page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToSettingsPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemSettings.icon, + )); + + // go to About page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToAboutPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemAbout.icon, + )); + } else { + // back to Home page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToGamePage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemGame.icon, + )); + } + } + + return AppBar( + title: const AppHeader(text: 'app_name'), + actions: menuActions, + ); + }, + ); + }, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart new file mode 100644 index 0000000000000000000000000000000000000000..4e55e338f0d3ed98b233d1ef887b7b3e17e29d97 --- /dev/null +++ b/lib/utils/color_extensions.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + alpha, + (red * value).round(), + (green * value).round(), + (blue * value).round(), + ); + } + + Color lighten([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = percent / 100; + return Color.fromARGB( + alpha, + (red + ((255 - red) * value)).round(), + (green + ((255 - green) * value)).round(), + (blue + ((255 - blue) * value)).round(), + ); + } + + Color avg(Color other) { + final red = (this.red + other.red) ~/ 2; + final green = (this.green + other.green) ~/ 2; + final blue = (this.blue + other.blue) ~/ 2; + final alpha = (this.alpha + other.alpha) ~/ 2; + return Color.fromARGB(alpha, red, green, blue); + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart deleted file mode 100644 index ef2ab77fd46920fe1e4314140213158617e6379a..0000000000000000000000000000000000000000 --- a/lib/utils/constants.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -const String letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -const Map<bool, List<String>> onlineGameMode = { - false: ['Thèmes', 'Expert'], - true: ['Junior', 'Avancé'], -}; - -const String defaultLevel = 'Thèmes'; - -// Colors -const int darkGreen = 0xff013220; -const int board = 0xff004D00; -const int accent = 0xffD81B60; - -// GameOver -enum GameOver { victory, defeat } - -extension GameOverExtension on GameOver { - static const titles = { - GameOver.victory: 'Victoire !', - GameOver.defeat: 'Pendu !', - }; - - String get title => titles[this] ?? '?'; - - static const icons = { - GameOver.victory: Icons.military_tech, - GameOver.defeat: Icons.gavel, - }; - - IconData get icon => icons[this] ?? Icons.error; -} - -const victory = GameOver.victory; -const defeat = GameOver.defeat; diff --git a/lib/utils/random_pick.dart b/lib/utils/random_pick.dart deleted file mode 100644 index 668d5be94e8b4816f634c0ba91be6836736496ef..0000000000000000000000000000000000000000 --- a/lib/utils/random_pick.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math' show Random; - -import 'package:diacritic/diacritic.dart'; -import 'package:flutter/services.dart'; -import 'package:html/parser.dart'; -import 'package:html/dom.dart'; -import 'package:http/http.dart' as http; - -import 'package:hangman/words/list_french_words.dart'; - -class RandomPick { - final String level; - RandomPick(this.level); - - String _url = ''; - String _word = ''; - String _clue = ''; - final random = Random(); - - init() async { - switch (level) { - case 'Thèmes': - await wordFromLocal(); - break; - case 'Avancé': - _url = - 'https://www.palabrasaleatorias.com/mots-aleatoires.php?fs=1&fs2=0&Submit=Nouveau+mot'; - await wordFromWeb(_url); - break; - case 'Junior': - _url = - 'https://www.palabrasaleatorias.com/mots-aleatoires.php?fs=1&fs2=1&Submit=Nouveau+mot'; - await wordFromWeb(_url); - break; - case 'Expert': - await wordFromPackage(); - break; - default: - await wordFromLocal(); - } - } - - Future<void> wordFromWeb(String url) async { - try { - var response = await http.Client().get(Uri.parse(url)); - if (response.statusCode == 200) { - var html = parse(utf8.decode(response.bodyBytes)); - Element element = html.getElementsByTagName('div').last; - String word = _validate(element.text); - if (word == '') { - throw Exception(); - } - _word = word; - } else { - throw Exception(); - } - } catch (e) { - _word = ''; - } - } - - String _validate(String word) { - String wordToValidate = word.trim(); - if (wordToValidate.contains(' ')) { - return ''; - } - wordToValidate = removeDiacritics(wordToValidate); - wordToValidate = wordToValidate.toUpperCase(); - wordToValidate = wordToValidate.replaceAll(RegExp('[0-9]'), ''); - wordToValidate = wordToValidate.replaceAll(RegExp('[^A-Z]'), ''); - if (wordToValidate.length < 3 || wordToValidate.length > 12) { - return ''; - } - return wordToValidate; - } - - Future _waitList() => Future(() { - final completer = Completer(); - int indexRandom = random.nextInt(listFrenchWords.length); - completer.complete(listFrenchWords.sublist(indexRandom, indexRandom + 1).join('\n')); - return completer.future; - }); - - wordFromPackage() async { - var wordList = await _waitList(); - var word = _validate(wordList); - _word = word; - } - - Future<void> wordFromLocal() async { - String jsonString; - try { - jsonString = await rootBundle.loadString('assets/files/word-list-fr.json'); - final jsonResponse = await json.decode(jsonString); - var wordList = jsonResponse[jsonResponse.keys.toList().join()]; - int randomCategoryIndex = random.nextInt(wordList.length); - _clue = wordList[randomCategoryIndex]['clue']; - List<String> words = []; - for (var word in wordList[randomCategoryIndex]['words']) { - words.add(word); - } - String word = words[random.nextInt(words.length)]; - if (word != '') { - _word = word; - } else { - _word = 'UNEXPECTED ERROR'; - } - } catch (e) { - _word = 'UNEXPECTED ERROR'; - } - } - - String get word => _word; - - String get clue => _clue; -} diff --git a/lib/utils/shared_prefs.dart b/lib/utils/shared_prefs.dart deleted file mode 100644 index fff13bf5d21b6797fa5c8673d74bf837ba0c583e..0000000000000000000000000000000000000000 --- a/lib/utils/shared_prefs.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class SharedPrefs { - static SharedPreferences? _sharedPrefs; - static const String _prefsGameMode = 'gameMode'; - static const String _prefsLevel = 'level'; - static const String _prefsVictoryCount = 'victoryCount'; - static const String _prefsDefeatCount = 'defeatCount'; - - init() async { - _sharedPrefs ??= await SharedPreferences.getInstance(); - } - - bool get gameMode => _sharedPrefs?.getBool(_prefsGameMode) ?? false; - - set gameMode(bool value) => _sharedPrefs?.setBool(_prefsGameMode, value); - - String get level => _sharedPrefs?.getString(_prefsLevel) ?? ''; - - set level(String value) => _sharedPrefs?.setString(_prefsLevel, value); - - int get victoryCount => _sharedPrefs?.getInt(_prefsVictoryCount) ?? 0; - - set victoryCount(int value) => _sharedPrefs?.setInt(_prefsVictoryCount, value); - - int get defeatCount => _sharedPrefs?.getInt(_prefsDefeatCount) ?? 0; - - set defeatCount(int value) => _sharedPrefs?.setInt(_prefsDefeatCount, value); -} diff --git a/lib/widgets/dialog_fetch_error.dart b/lib/widgets/dialog_fetch_error.dart deleted file mode 100644 index 45f235d2bc4903b3471457d9ae8e575d2f3348bb..0000000000000000000000000000000000000000 --- a/lib/widgets/dialog_fetch_error.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hangman/utils/constants.dart'; -import 'package:flutter/material.dart'; - -class DialogFetchError extends StatelessWidget { - const DialogFetchError({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(board), - body: PopScope( - onPopInvoked: (didPop) {}, - child: AlertDialog( - title: const Text('Connexion impossible'), - content: const Text('Impossible de récupérer un mot aléatoire. ' - 'La connexion internet est peut-être inaccessible.\n' - 'Changer vers un mode de jeu hors-ligne ?'), - actions: [ - TextButton( - child: const Text('REVENIR'), - onPressed: () => Navigator.pop(context, false), - ), - TextButton( - child: const Text('ACCEPTER'), - onPressed: () => Navigator.pop(context, true), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/dialog_gameover.dart b/lib/widgets/dialog_gameover.dart deleted file mode 100644 index 96f336faa7ef4c731c4efc4e4f16ac171e51549e..0000000000000000000000000000000000000000 --- a/lib/widgets/dialog_gameover.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:hangman/provider/data.dart'; -import 'package:hangman/screens/game.dart'; -import 'package:hangman/screens/home.dart'; -import 'package:hangman/utils/constants.dart'; - -class DialogGameOver extends StatelessWidget { - final GameOver gameOver; - const DialogGameOver(this.gameOver, {super.key}); - - @override - Widget build(BuildContext context) { - final Data myProvider = Provider.of<Data>(context); - - return Scaffold( - backgroundColor: Colors.transparent, - body: myProvider.searching == true - ? const Center(child: CircularProgressIndicator()) - : PopScope( - onPopInvoked: (didPop) {}, - child: AlertDialog( - title: Row( - children: [ - Icon(gameOver.icon), - const SizedBox(width: 10.0), - Text(gameOver.title), - ], - ), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (gameOver == GameOver.defeat) - Text('Le mot secret était ${myProvider.secretWord}'), - const Text('Rejouer ?'), - ], - ), - ), - actions: [ - TextButton( - child: const Text('QUITTER'), - onPressed: () { - Navigator.pushNamed(context, Home.id); - }, - ), - TextButton( - child: const Text('REJOUER'), - onPressed: () async { - myProvider.resetSuccessAndErrors(); - await const Game().pickWord(context, myProvider); - if (!context.mounted) return; - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/letters.dart b/lib/widgets/letters.dart deleted file mode 100644 index eb567e5a6d65d636b38c39d65fabe8d12068e7e5..0000000000000000000000000000000000000000 --- a/lib/widgets/letters.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:hangman/provider/data.dart'; -import 'package:hangman/utils/constants.dart'; -import 'package:hangman/widgets/dialog_gameover.dart'; - -class LetterButtons extends StatelessWidget { - const LetterButtons({super.key}); - - @override - Widget build(BuildContext context) { - final Orientation orientation = MediaQuery.of(context).orientation; - var size = MediaQuery.of(context).size; - final double paddingTop = MediaQuery.of(context).padding.top; - final double itemHeight = (size.height - paddingTop) / 2; - final double itemWidth = size.width / 2; - - final Data myProvider = Provider.of<Data>(context); - - final List<String> lettersList = letters.split(''); - final List<Widget> keys = []; - - for (var key in lettersList) { - keys.add( - Padding( - padding: const EdgeInsets.all(2.0), - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.grey, - foregroundColor: Colors.white, - shadowColor: const Color(accent), - ), - onPressed: myProvider.usedLetters.contains(key) - ? null - : () async { - myProvider.updateUsedLetters(key); - if (myProvider.secretWord.contains(key)) { - for (int index = 0; index < myProvider.secretWord.length; index++) { - if (key == myProvider.secretWord[index]) { - myProvider.updateHiddenWord(index, key); - } - } - - if (myProvider.hiddenWord == myProvider.secretWord) { - myProvider.addVictory(); - showDialog( - context: context, - builder: (context) => const DialogGameOver(victory), - ); - } - } else { - myProvider.addError(); - if (myProvider.errors == 8) { - await Future.delayed(const Duration(milliseconds: 900)); //???? - myProvider.addDefeat(); - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) => const DialogGameOver(defeat), - ); - } - } - }, - child: Text( - key, - textAlign: TextAlign.center, - ), - ), - ), - ); - } - - return Container( - alignment: - orientation == Orientation.portrait ? Alignment.bottomCenter : Alignment.center, - color: const Color(darkGreen), - padding: const EdgeInsets.all(10.0), - margin: EdgeInsets.only(top: orientation == Orientation.portrait ? 0.0 : paddingTop), - child: GridView.count( - padding: EdgeInsets.zero, - shrinkWrap: true, - crossAxisCount: orientation == Orientation.portrait ? 9 : 3, - childAspectRatio: - orientation == Orientation.portrait ? 1 / 1 : (itemWidth / itemHeight), - children: keys, - ), - ); - } -} diff --git a/lib/widgets/my_app_bar.dart b/lib/widgets/my_app_bar.dart deleted file mode 100644 index d25c46f6a6f566551680441f58c8b801cd524c0d..0000000000000000000000000000000000000000 --- a/lib/widgets/my_app_bar.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show SystemNavigator; - -import 'package:hangman/screens/scores.dart'; - -class MyAppBar extends StatelessWidget implements PreferredSizeWidget { - const MyAppBar({super.key, this.appBar}); - - final AppBar? appBar; - - @override - Size get preferredSize => Size.fromHeight(appBar?.preferredSize.height ?? 0.0); - - @override - Widget build(BuildContext context) { - return AppBar( - title: const Text('Hangman'), - automaticallyImplyLeading: false, - actions: [ - PopupMenuButton<String>( - onSelected: (String value) { - switch (value) { - case 'Quitter': - SystemNavigator.pop(); - break; - case 'Scores': - Navigator.pushNamed(context, Scores.id); - break; - } - }, - itemBuilder: (BuildContext context) { - return {'Scores', 'Quitter'}.map((String choice) { - return PopupMenuItem<String>( - value: choice, - child: Text(choice), - ); - }).toList(); - }, - ), - ], - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 2599bd07dcef5abeda4ed10e2309ee446443f091..bbf060c1d2f00d1a8f8bdb1b07ec681da40180ec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -9,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -17,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" collection: dependency: transitive description: @@ -25,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" csslib: dependency: transitive description: @@ -41,14 +73,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.5" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -62,19 +118,40 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" html: dependency: "direct main" description: @@ -87,10 +164,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_parser: dependency: transitive description: @@ -99,30 +176,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c + url: "https://pub.dev" + source: hosted + version: "9.1.5" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" nested: dependency: transitive description: @@ -131,6 +224,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + url: "https://pub.dev" + source: hosted + version: "8.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: transitive description: @@ -139,6 +248,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -159,18 +292,18 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -180,7 +313,7 @@ packages: source: hosted version: "2.1.8" provider: - dependency: "direct main" + dependency: transitive description: name: provider sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c @@ -188,61 +321,61 @@ packages: source: hosted version: "6.1.2" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -260,10 +393,18 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "3.2.0" term_glyph: dependency: transitive description: @@ -280,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: "1cca7462df18ff191b7e41b52f747d08854916531d1d7ab7cec0552095995206" + url: "https://pub.dev" + source: hosted + version: "2.1.2" vector_math: dependency: transitive description: @@ -292,18 +441,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "1.0.0" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.4" xdg_directories: dependency: transitive description: @@ -313,5 +462,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 30bab3294dae8c01f991a1c8c76d4d08a0fd9d9d..344a91808baf139742e0939f363f0a02e9b52a28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,29 +1,54 @@ name: hangman description: Hangman game, have fun with words and letters! -publish_to: 'none' -version: 1.2.16+27 + +publish_to: "none" + +version: 1.2.17+28 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter - provider: ^6.0.5 - shared_preferences: ^2.2.1 + + # base + easy_localization: ^3.0.1 + equatable: ^2.0.5 + flutter_bloc: ^8.1.1 + hive: ^2.2.3 + hydrated_bloc: ^9.0.0 + package_info_plus: ^8.0.0 + path_provider: ^2.0.11 + unicons: ^2.1.1 + + # specific + diacritic: ^0.1.4 html: ^0.15.4 http: ^1.1.0 - diacritic: ^0.1.4 dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 flutter: uses-material-design: true assets: - - assets/images/ - - assets/files/ + - assets/skins/ + - assets/translations/ + - assets/ui/ + fonts: - family: Tiza fonts: - asset: assets/fonts/tiza.ttf + - family: Nunito + fonts: + - asset: assets/fonts/Nunito-Bold.ttf + weight: 700 + - asset: assets/fonts/Nunito-Medium.ttf + weight: 500 + - asset: assets/fonts/Nunito-Regular.ttf + weight: 400 + - asset: assets/fonts/Nunito-Light.ttf + weight: 300 + diff --git a/icons/build_application_icons.sh b/resources/app/build_application_resources.sh similarity index 98% rename from icons/build_application_icons.sh rename to resources/app/build_application_resources.sh index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755 --- a/icons/build_application_icons.sh +++ b/resources/app/build_application_resources.sh @@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" SOURCE_ICON="${CURRENT_DIR}/icon.svg" SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg similarity index 100% rename from icons/featureGraphic.svg rename to resources/app/featureGraphic.svg diff --git a/icons/icon.svg b/resources/app/icon.svg similarity index 100% rename from icons/icon.svg rename to resources/app/icon.svg diff --git a/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..45d0d745ed6c0c15fdd3c876a415c0c3ba428248 --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +${CURRENT_DIR}/app/build_application_resources.sh +${CURRENT_DIR}/ui/build_ui_resources.sh +${CURRENT_DIR}/data/build_words_lists.sh diff --git a/resources/data/build_words_lists.sh b/resources/data/build_words_lists.sh new file mode 100755 index 0000000000000000000000000000000000000000..927331a3a23b0918a032f80375e4886b6d0733dd --- /dev/null +++ b/resources/data/build_words_lists.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +command -v jq >/dev/null 2>&1 || { echo >&2 "I require jq (json parser) but it's not installed. Aborting."; exit 1; } + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" + +# JSON date file for frean "easy" words +DICTIONARY_JSON_FILE="${CURRENT_DIR}/dictionary-fr-easy.json" +if [ ! -f "${DICTIONARY_JSON_FILE}" ]; then + echo "File ${DICTIONARY_JSON_FILE} not found. Abort." + exit 1 +fi +# DART source code file to inject data into +GAME_DATA_DART_FILE="${BASE_DIR}/lib/data/dictionary_french_easy.dart" +if [ ! -f "${GAME_DATA_DART_FILE}" ]; then + echo "File ${GAME_DATA_DART_FILE} not found. Abort." + exit 1 +fi + +echo "Injecting json file ${DICTIONARY_JSON_FILE} in ${GAME_DATA_DART_FILE}..." +echo "const List<dynamic> dictionaryFrenchEasy = $(cat "${DICTIONARY_JSON_FILE}");" >"${GAME_DATA_DART_FILE}" + +echo "Formatting dart source code file..." +dart format "${GAME_DATA_DART_FILE}" + +echo "done." diff --git a/resources/data/dictionary-fr-easy.json b/resources/data/dictionary-fr-easy.json new file mode 100644 index 0000000000000000000000000000000000000000..c7e2eef36f433a9120637fbf17cb84edb0d15421 --- /dev/null +++ b/resources/data/dictionary-fr-easy.json @@ -0,0 +1,193 @@ +[ + { + "category": "ANIMAUX", + "clue": "Animal", + "words": [ + "ANACONDA", + "AUTRUCHE", + "BARRACUDA", + "BOUQUETIN", + "COCCINELLE", + "CROCODILE", + "DROMADAIRE", + "ELEPHANT", + "ESCARGOT", + "FOURMILIER", + "GRENOUILLE", + "HIPPOCAMPE", + "HIPPOPOTAME", + "KANGOUROU", + "LIBELLULE", + "PERROQUET", + "PIPISTRELLE", + "RHINOCEROS", + "SAUTERELLE", + "TARENTULE" + ] + }, + { + "category": "FRUITS", + "clue": "Fruit", + "words": [ + "AUBERGINE", + "BETTERAVE", + "CITROUILLE", + "CONCOMBRE", + "FRAMBOISE", + "GROSEILLE", + "MANDARINE", + "MIRABELLE", + "MYRTILLE", + "PAMPLEMOUSSE" + ] + }, + { + "category": "METIERS", + "clue": "Métier", + "words": [ + "AGRICULTEUR", + "ARCHEOLOGUE", + "ARCHITECTE", + "ASTRONAUTE", + "BIJOUTIER", + "BIOLOGISTE", + "CHARCUTIER", + "CHARPENTIER", + "CUISINIER", + "ELECTRICIEN", + "HORTICULTEUR", + "INFIRMIER", + "MECANICIEN", + "MENUISIER", + "METEOROLOGUE", + "PHOTOGRAPHE", + "PROFESSEUR", + "STANDARDISTE", + "VETERINAIRE", + "VOLCANOLOGUE" + ] + }, + { + "category": "GEOGRAPHIE", + "clue": "Géographie", + "words": [ + "ALLEMAGNE", + "ANTARCTIQUE", + "ARGENTINE", + "ATLANTIQUE", + "AUSTRALIE", + "EMBOUCHURE", + "HEMISPHERE", + "HYDROGRAPHIE", + "KILIMANDJARO", + "LUXEMBOURG", + "MADAGASCAR", + "MEDITERRANEE", + "MISSISSIPPI", + "NORMANDIE", + "PACIFIQUE", + "PLANISPHERE", + "STRASBOURG", + "SUPERFICIE", + "VENEZUELA", + "WASHINGTON" + ] + }, + { + "category": "COULEURS", + "clue": "Couleur", + "words": [ + "ROUGE", + "BLEU", + "VERT", + "JAUNE", + "VIOLET", + "ORANGE", + "MARRON", + "NOIR", + "BLANC", + "TURQUOISE", + "BEIGE", + "ROSE" + ] + }, + { + "category": "FLEURS", + "clue": "Fleur", + "words": [ + "ROSE", + "PIVOINE", + "TULIPE", + "JONQUILLE", + "CACTUS", + "PETUNIA", + "COQUELICOT", + "JACINTHE" + ] + }, + { + "category": "SPORTS", + "clue": "Sport ou jeu", + "words": [ + "GYMNASTIQUE", + "FOOTBALL", + "HANDBALL", + "COURSE", + "CYCLISME", + "RANDONNEE", + "NATATION", + "KARATE", + "ESCRIME", + "EQUITATION" + ] + }, + { + "category": "ALIMENTS", + "clue": "Aliment ou plat", + "words": [ + "FROMAGE", + "PIZZA", + "SAUCISSON", + "JAMBON", + "SALAMI", + "PAELLA", + "PATES", + "SALADE", + "SOUPE", + "CHOCOLAT", + "OEUF", + "CREME", + "LAIT", + "CORNICHON", + "FLAN", + "TARTE", + "PUREE", + "SAUMON", + "SANDWICH" + ] + }, + { + "category": "VEHICULE", + "clue": "Véhicule ou moyen de transport", + "words": [ + "VOITURE", + "MOTO", + "VELO", + "TRAIN", + "BATEAU", + "AVION", + "HELICOPTERE", + "AUTOBUS", + "CAR", + "TRAINEAU", + "FUSEE", + "VOILIER", + "PAQUEBOT", + "METRO", + "SOUS-MARIN", + "CAMION", + "TRACTEUR", + "KAYAK" + ] + } +] diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..2375593637ee4aa2198bceffffa4ba668358dc05 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,141 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } +command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } +command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" +ASSETS_DIR="${BASE_DIR}/assets" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" +IMAGE_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_SVG_IMAGES="" +AVAILABLE_GAME_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_SVG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" + AVAILABLE_GAME_PNG_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" +fi + +# Skins (subfolders found in `skins` folder) +AVAILABLE_SKINS="" +if [ -d "${CURRENT_DIR}/skins" ]; then + AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')" +fi + +# Images per skin (svg files found recursively in `skins` folder and subfolders) +SKIN_SVG_IMAGES="" +SKIN_PNG_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_SVG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" + SKIN_PNG_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.png" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" +fi + +####################################################### + +# optimize svg +function optimize_svg() { + SOURCE="$1" + + cp ${SOURCE} ${SOURCE}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SOURCE}.tmp \ + -o ${SOURCE} + rm ${SOURCE}.tmp +} + +# build png from svg +function build_svg_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + optimize_svg "${SOURCE}" + + mkdir -p "$(dirname "${TARGET}")" + + inkscape \ + --export-width=${IMAGE_SIZE} \ + --export-height=${IMAGE_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +# build png from png +function build_png_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + mkdir -p "$(dirname "${TARGET}")" + + convert -resize ${IMAGE_SIZE}x${IMAGE_SIZE} "${SOURCE}" "${TARGET}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_images_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_SVG_IMAGE in ${SKIN_SVG_IMAGES} + do + build_svg_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_SVG_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_SVG_IMAGE}.png + done + for SKIN_PNG_IMAGE in ${SKIN_PNG_IMAGES} + do + build_png_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_PNG_IMAGE}.png ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_PNG_IMAGE}.png + done +} + +####################################################### + +# Delete existing generated images +if [ -d "${ASSETS_DIR}/ui" ]; then + find ${ASSETS_DIR}/ui -type f -name "*.png" -delete +fi +if [ -d "${ASSETS_DIR}/skins" ]; then + find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +fi + +# build game images +for GAME_SVG_IMAGE in ${AVAILABLE_GAME_SVG_IMAGES} +do + build_svg_image ${CURRENT_DIR}/images/${GAME_SVG_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_SVG_IMAGE}.png +done +for GAME_PNG_IMAGE in ${AVAILABLE_GAME_PNG_IMAGES} +do + build_png_image ${CURRENT_DIR}/images/${GAME_PNG_IMAGE}.png ${ASSETS_DIR}/ui/${GAME_PNG_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS} +do + build_images_for_skin "${SKIN}" +done + diff --git a/resources/ui/images/button_back.svg b/resources/ui/images/button_back.svg new file mode 100644 index 0000000000000000000000000000000000000000..2622a578dba53ce582afabfc587c2a85a1fb6eaa --- /dev/null +++ b/resources/ui/images/button_back.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..ac7eefef476f761903fe781b8c86d0c94323550a --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ad8b64202d0e70f898c16c520e756fe8a934add --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/resources/ui/images/button_start.svg b/resources/ui/images/button_start.svg new file mode 100644 index 0000000000000000000000000000000000000000..e9d49d2172b9a0305db82779971e3c1e12f34a70 --- /dev/null +++ b/resources/ui/images/button_start.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> diff --git a/resources/ui/images/game_fail.svg b/resources/ui/images/game_fail.svg new file mode 100644 index 0000000000000000000000000000000000000000..2922fd7adc2bd2e813836c728f095376c73d4143 --- /dev/null +++ b/resources/ui/images/game_fail.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#d11717" stroke="#fff" stroke-width=".238"/><path d="m71.624 59.304c3.5089 3.5089 3.5089 9.0561 0 12.565-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034l-12.452-12.452-12.452 12.452c-1.6976 1.6976-3.9623 2.6034-6.2261 2.6034s-4.5275-0.90569-6.2261-2.6034c-3.5089-3.5089-3.5089-9.0561 0-12.565l12.452-12.452-12.452-12.452c-3.5089-3.5089-3.5089-9.0561 0-12.565s9.0561-3.5089 12.565 0l12.452 12.452 12.452-12.452c3.5089-3.5089 9.0561-3.5089 12.565 0s3.5089 9.0561 0 12.565l-12.452 12.452z" fill="#e7e7e7" stroke-width=".20213"/></svg> diff --git a/resources/ui/images/game_win.svg b/resources/ui/images/game_win.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe20923864d0c5d39168eced03038b65106a596b --- /dev/null +++ b/resources/ui/images/game_win.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.17604 0 0 .17604 7.9341 1.7716)"><path d="m101.92 496.35c-1.8555 0-3.7109-0.69532-5.1484-2.0898-2.9297-2.8438-3-7.5234-0.15234-10.453l9.1875-9.4648c2.8438-2.9297 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4648c-1.4492 1.4961-3.375 2.2461-5.3047 2.2461z" fill="#ff4e61"/><path d="m201.65 133.26c-1.8516 0-3.7109-0.69531-5.1445-2.0898-2.9297-2.8438-3-7.5234-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5195-3 10.449-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#ff4e61"/><path d="m413.8 100.39c-1.8555 0-3.7109-0.69141-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9297 2.8398 3 7.5234 0.15625 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m413.8 463.77c-1.8555 0-3.7109-0.69532-5.1484-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4688c2.8438-2.9258 7.5234-3 10.453-0.15625s3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m63.07 112.91c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1914-9.4687c2.8438-2.9258 7.5234-2.9961 10.453-0.15234 2.9258 2.8438 2.9961 7.5234 0.15234 10.449l-9.1914 9.4688c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m12.309 278.82c-1.8516 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l9.1875-9.4688c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1914 9.4688c-1.4453 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#2dc471"/><path d="m216.29 278.49-23.996 12.996c-6.2226 3.3711-13.496-2.0742-12.309-9.2148l4.582-27.523c0.47266-2.8359-0.4375-5.7266-2.4375-7.7344l-19.414-19.496c-5.0352-5.0547-2.2578-13.863 4.7031-14.906l26.824-4.0156c2.7656-0.41407 5.1524-2.1992 6.3867-4.7812l12-25.043c3.1133-6.4922 12.102-6.4922 15.215 0l11.996 25.043c1.2383 2.582 3.625 4.3672 6.3867 4.7812l26.828 4.0156c6.957 1.043 9.7344 9.8516 4.6992 14.906l-19.41 19.496c-2 2.0078-2.9141 4.8984-2.4414 7.7344l4.582 27.523c1.1914 7.1406-6.082 12.586-12.305 9.2148l-23.996-12.996c-2.4727-1.3398-5.4258-1.3398-7.8945 0z" fill="#ffd02f"/><path d="m220.24 512c-4.082 0-7.3906-3.3086-7.3906-7.3906v-115.59c0-4.082 3.3086-7.3945 7.3906-7.3945s7.3906 3.3125 7.3906 7.3945v115.59c0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#5c73bc"/><path d="m220.3 357.42h-0.11328c-4.082 0-7.3945-3.3125-7.3945-7.3945s3.3086-7.3906 7.3945-7.3906h0.11328c4.082 0 7.3906 3.3086 7.3906 7.3906s-3.3086 7.3945-7.3906 7.3945z" fill="#5c73bc"/><path d="m220.3 332h-0.14838c-4.082-0.0156-7.375-3.3398-7.3594-7.4219 0.0195-4.0742 3.3242-7.3594 7.3906-7.3594h0.14848c4.082 0.0156 7.375 3.3398 7.3594 7.4219-0.0156 4.0703-3.3242 7.3594-7.3906 7.3594z" fill="#fa0"/><path d="m87.234 230.89c-1.9297 0-3.8555-0.75-5.3047-2.2422l-79.34-81.738c-2.8438-2.9297-2.7773-7.6094 0.15234-10.449 2.9297-2.8438 7.6094-2.7734 10.453 0.15235l79.344 81.738c2.8438 2.9258 2.7734 7.6094-0.15625 10.449-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#ff4e61"/><path d="m113.95 258.5c-1.8633 0-3.7266-0.69922-5.1641-2.1055-2.9219-2.8516-2.9766-7.5312-0.125-10.453l0.082-0.082c2.8516-2.918 7.5312-2.9766 10.453-0.12109 2.9219 2.8516 2.9766 7.5312 0.12109 10.453l-0.0781 0.082c-1.4492 1.4805-3.3672 2.2266-5.2891 2.2266z" fill="#fa0"/><path d="m131.4 276.48c-1.8555 0-3.7109-0.69531-5.1484-2.0898-2.9258-2.8438-2.9961-7.5234-0.15235-10.449l0.0781-0.0859c2.8476-2.9297 7.5273-2.9961 10.453-0.15235 2.9297 2.8438 3 7.5234 0.15625 10.453l-0.082 0.082c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#5c73bc"/><path d="m353.24 227.99c-1.8555 0-3.7109-0.69141-5.1445-2.0859-2.9297-2.8438-3-7.5234-0.15625-10.453l79.34-81.734c2.8438-2.9297 7.5234-3 10.453-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-79.344 81.734c-1.4492 1.4922-3.375 2.2422-5.3047 2.2422z" fill="#fa0"/><path d="m326.52 255.6c-1.9141 0-3.8242-0.73828-5.2695-2.2109l-0.082-0.082c-2.8633-2.9141-2.8203-7.5938 0.0899-10.453 2.9141-2.8633 7.5938-2.8203 10.453 0.0898l0.082 0.082c2.8594 2.9141 2.8203 7.5938-0.0937 10.453-1.4375 1.4141-3.3086 2.1211-5.1797 2.1211z" fill="#ff4e61"/><path d="m309.07 273.58c-1.9297 0-3.8555-0.75-5.3047-2.2422l-0.082-0.082c-2.8398-2.9297-2.7734-7.6094 0.15625-10.453s7.6094-2.7734 10.453 0.15234l0.082 0.082c2.8398 2.9297 2.7734 7.6094-0.15625 10.453-1.4375 1.3945-3.293 2.0898-5.1484 2.0898z" fill="#fa0"/><path d="m300.65 116.69c-1.2422 0-2.5-0.3125-3.6523-0.97266-3.5469-2.0234-4.7812-6.5391-2.7578-10.082l56.863-99.652c2.0234-3.543 6.5352-4.7773 10.082-2.7539 3.5469 2.0234 4.7812 6.5391 2.7578 10.082l-56.863 99.652c-1.3633 2.3867-3.8594 3.7266-6.4297 3.7266z" fill="#62d38f"/><path d="m281.52 150.33c-1.293 0-2.5977-0.33593-3.7891-1.0469l-0.0976-0.0586c-3.5-2.0938-4.6445-6.6328-2.5469-10.137 2.0938-3.5078 6.6328-4.6445 10.137-2.5508l0.0977 0.0586c3.5039 2.0938 4.6445 6.6328 2.5508 10.137-1.3867 2.3164-3.8359 3.5976-6.3516 3.5976z" fill="#fa0"/><path d="m269.02 172.25c-1.3008 0-2.6172-0.34375-3.8086-1.0625l-0.0977-0.0586c-3.4961-2.1094-4.6211-6.6523-2.5156-10.148 2.1094-3.4961 6.6523-4.6172 10.148-2.5117l0.0976 0.0586c3.4961 2.1094 4.6211 6.6523 2.5117 10.148-1.3867 2.3008-3.832 3.5742-6.3359 3.5742z" fill="#2dc471"/><path d="m139.96 116.69c-2.5703 0-5.0664-1.3398-6.4297-3.7305l-56.863-99.648c-2.0234-3.5469-0.78906-8.0586 2.7539-10.082 3.5469-2.0234 8.0625-0.79297 10.086 2.7539l56.863 99.648c2.0234 3.5469 0.78906 8.0625-2.7539 10.086-1.1562 0.66016-2.4141 0.97266-3.6562 0.97266z" fill="#5c73bc"/><path d="m159.09 150.33c-2.5078 0-4.957-1.2773-6.3438-3.582-2.1016-3.5-0.96875-8.043 2.5273-10.145l0.10157-0.0586c3.5-2.1016 8.0391-0.97266 10.141 2.5273 2.1055 3.5 0.97266 8.0391-2.5273 10.145l-0.0977 0.0586c-1.1914 0.71484-2.5039 1.0547-3.8008 1.0547z" fill="#ff4e61"/><path d="m171.6 172.25c-2.5 0-4.9375-1.2656-6.3281-3.5625-2.1172-3.4922-1-8.0352 2.4883-10.152l0.0977-0.0586c3.4961-2.1133 8.0391-1 10.156 2.4922 2.1133 3.4922 1 8.0352-2.4922 10.152l-0.0977 0.0586c-1.1992 0.72656-2.5195 1.0703-3.8242 1.0703z" fill="#fa0"/><path d="m402.14 357.28-15.523 11.602c-4.0234 3.0117-9.6523-0.043-9.5234-5.1641l0.5039-19.75c0.0508-2.0352-0.87109-3.9648-2.4688-5.1641l-15.508-11.621c-4.0234-3.0156-2.9453-9.4726 1.8242-10.93l18.391-5.6094c1.8906-0.57812 3.3906-2.082 4-4.0156l5.9375-18.785c1.5391-4.875 7.8359-5.8125 10.652-1.5898l10.863 16.285c1.1211 1.6758 2.9688 2.6797 4.9414 2.6797l19.18 0.0117c4.9766 4e-3 7.7891 5.8828 4.7578 9.9492l-11.676 15.672c-1.2031 1.6172-1.5586 3.7383-0.94922 5.6719l5.918 18.797c1.5312 4.875-3.0273 9.4453-7.7148 7.7344l-18.078-6.5977c-1.8594-0.67969-3.9258-0.37109-5.5273 0.82422z" fill="#ffd02f"/><path d="m261.51 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-57.23 22.832-95.922 41.984-118.3 20.828-24.332 41.613-35.023 42.488-35.469 3.6406-1.8477 8.0898-0.39063 9.9336 3.2539 1.8438 3.6367 0.39453 8.0781-3.2422 9.9297-0.3125 0.16016-19.5 10.164-38.367 32.395-25.227 29.719-38.016 66.121-38.016 108.2 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#ff4e61"/><path d="m102.86 397.35 11.766 15.605c3.0547 4.0469 9.2852 2.7305 10.547-2.2266l4.8633-19.113c0.5-1.9648 1.9102-3.5547 3.7695-4.2461l18.039-6.707c4.6797-1.7383 5.3906-8.25 1.207-11.016l-16.141-10.672c-1.6602-1.1016-2.6914-2.9726-2.7578-5.0039l-0.61719-19.75c-0.15625-5.1211-5.9492-7.832-9.7969-4.5859l-14.84 12.516c-1.5312 1.2891-3.5781 1.7227-5.4726 1.1562l-18.422-5.5c-4.7773-1.4258-9.0703 3.4102-7.2617 8.1836l6.9688 18.41c0.71875 1.8945 0.48438 4.0352-0.625 5.7188l-10.77 16.348c-2.793 4.2422 0.34375 9.9375 5.3125 9.6445l19.145-1.1406c1.9727-0.11719 3.875 0.77343 5.0859 2.3789z" fill="#ffd02f"/><path d="m179.02 512c-4.082 0-7.3906-3.3086-7.3906-7.3906 0-30.059-6.6797-57.559-19.852-81.734-1.9531-3.5859-0.62891-8.0742 2.957-10.027 3.5859-1.9531 8.0742-0.62891 10.027 2.9531 14.363 26.375 21.648 56.254 21.648 88.809 0 4.082-3.3086 7.3906-7.3906 7.3906z" fill="#fa0"/><path d="m268.93 55.898c0-11.285-8.8828-20.434-19.836-20.434-10.957 0-19.836 9.1484-19.836 20.434 0 11.285 8.8789 20.438 19.836 20.438 10.953 0 19.836-9.1523 19.836-20.438z" fill="#ffd02f"/><path d="m373.08 446.81c0-11.285-8.8789-20.434-19.832-20.434-10.957 0-19.836 9.1484-19.836 20.434s8.8789 20.434 19.836 20.434c10.953 0 19.832-9.1484 19.832-20.434z" fill="#5c73bc"/><path d="m44.129 450.86c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387 0 9.0547 7.1211 16.391 15.906 16.391 8.7891 0 15.91-7.3359 15.91-16.391z" fill="#62d38f"/><path d="m88.172 288.35c0-9.0508-7.1211-16.387-15.91-16.387-8.7852 0-15.906 7.3359-15.906 16.387s7.1211 16.391 15.906 16.391c8.7891 0 15.91-7.3398 15.91-16.391z" fill="#5c73bc"/><g fill="#ff4e61"><path d="m210.84 16.391c0-9.0547-7.1211-16.391-15.906-16.391-8.7891 0-15.91 7.3359-15.91 16.391 0 9.0508 7.1211 16.387 15.91 16.387 8.7852 0 15.906-7.3359 15.906-16.387z"/><path d="m365.23 152.88c0-9.0508-7.125-16.391-15.91-16.391-8.7852 0-15.91 7.3398-15.91 16.391s7.125 16.387 15.91 16.387c8.7852 0 15.91-7.3359 15.91-16.387z"/><path d="m139.96 32.746c-1.8555 0-3.7109-0.69141-5.1484-2.0898-2.9297-2.8438-3-7.5195-0.15625-10.449l9.1914-9.4688c2.8438-2.9297 7.5234-3 10.449-0.15625 2.9297 2.8438 3 7.5234 0.15625 10.453l-9.1875 9.4688c-1.4492 1.4922-3.3789 2.2422-5.3047 2.2422z"/></g></g></svg> diff --git a/resources/ui/images/placeholder.svg b/resources/ui/images/placeholder.svg new file mode 100644 index 0000000000000000000000000000000000000000..23ace81fbb82a8409cc0710c0f7bddd6381f7256 --- /dev/null +++ b/resources/ui/images/placeholder.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/> diff --git a/resources/ui/skins/default/img1.png b/resources/ui/skins/default/img1.png new file mode 100644 index 0000000000000000000000000000000000000000..4d3ba948e29a0c2ce6a4a3ca7dbf8f1cd28fa590 Binary files /dev/null and b/resources/ui/skins/default/img1.png differ diff --git a/resources/ui/skins/default/img2.png b/resources/ui/skins/default/img2.png new file mode 100644 index 0000000000000000000000000000000000000000..97b41d5a09f1a2e8daab9dccb3183bdb6d0a687f Binary files /dev/null and b/resources/ui/skins/default/img2.png differ diff --git a/resources/ui/skins/default/img3.png b/resources/ui/skins/default/img3.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c4bc5cf6f1e56e9f9b0bf593a767791bc2b6c4 Binary files /dev/null and b/resources/ui/skins/default/img3.png differ diff --git a/resources/ui/skins/default/img4.png b/resources/ui/skins/default/img4.png new file mode 100644 index 0000000000000000000000000000000000000000..748b6db01a0bf8cf9cfb804ba81f855dcd6e7261 Binary files /dev/null and b/resources/ui/skins/default/img4.png differ diff --git a/resources/ui/skins/default/img5.png b/resources/ui/skins/default/img5.png new file mode 100644 index 0000000000000000000000000000000000000000..76fa28abf8807bc6dd3a6f8801c5be752bdbea0d Binary files /dev/null and b/resources/ui/skins/default/img5.png differ diff --git a/resources/ui/skins/default/img6.png b/resources/ui/skins/default/img6.png new file mode 100644 index 0000000000000000000000000000000000000000..7eebdc2836c7e4244fe6b027c6306e215593dc8e Binary files /dev/null and b/resources/ui/skins/default/img6.png differ diff --git a/resources/ui/skins/default/img7.png b/resources/ui/skins/default/img7.png new file mode 100644 index 0000000000000000000000000000000000000000..a01af2aebf11ba513a9fdd024a8991af94b3f0d8 Binary files /dev/null and b/resources/ui/skins/default/img7.png differ diff --git a/resources/ui/skins/default/img8.png b/resources/ui/skins/default/img8.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac728e19c25619218574700b1ae745e43bc8159 Binary files /dev/null and b/resources/ui/skins/default/img8.png differ diff --git a/resources/ui/skins/default/img9.png b/resources/ui/skins/default/img9.png new file mode 100644 index 0000000000000000000000000000000000000000..2dabefffaf8c8193ca419976969a2eed3e3416cd Binary files /dev/null and b/resources/ui/skins/default/img9.png differ